Skip to content

fix(app/sandbox): fix marketplace install and billing profile provisioning#4505

Open
vsengar-79 wants to merge 3 commits into
openmeterio:mainfrom
vsengar-79:sandbox-fix
Open

fix(app/sandbox): fix marketplace install and billing profile provisioning#4505
vsengar-79 wants to merge 3 commits into
openmeterio:mainfrom
vsengar-79:sandbox-fix

Conversation

@vsengar-79

@vsengar-79 vsengar-79 commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Fix POST /api/v1/marketplace/listings/sandbox/install returning 400 "does not support this installation method": the sandbox Factory advertised no_credentials_required in its marketplace listing but only implemented AppFactoryInstallWithAPIKey. Added the missing InstallApp method and inlined the logic directly, removing the now-redundant InstallAppWithAPIKey.

  • Fix createBillingProfile being a no-op for sandbox: the marketplace install handler had a // TODO: Implement sandbox billing profile creation stub that always returned nil. Replaced it with a call to ProvisionDefaultBillingProfile, matching what server startup does for the default namespace.

Together these two fixes make POST /api/v1/marketplace/listings/sandbox/install with {"createBillingProfile": true} fully self-contained — one call installs the app and provisions the default billing profile.

Summary by CodeRabbit

  • New Features
    • Sandbox apps now provision a default billing profile during install, including capabilities for tax calculation, invoicing customers, and collecting payments.
  • Refactor
    • Sandbox app installation now validates input, checks for an existing sandbox app in the target namespace, returns a conflict if one is present, and otherwise returns the newly created app instance.

@vsengar-79 vsengar-79 requested a review from a team as a code owner June 11, 2026 07:27
@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cf148a2c-04d7-4588-8f26-fedf6fe1d969

📥 Commits

Reviewing files that changed from the base of the PR and between 286679e and 26e89a8.

📒 Files selected for processing (2)
  • openmeter/app/httpdriver/marketplace.go
  • openmeter/app/sandbox/app.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • openmeter/app/httpdriver/marketplace.go
  • openmeter/app/sandbox/app.go

📝 Walkthrough

Walkthrough

Replaces the Sandbox install entry with Factory.InstallApp (pre-flight namespace check) and implements sandbox billing profile creation to provision a default profile and return standard capability types.

Changes

Sandbox App Installation Flow

Layer / File(s) Summary
InstallApp entry and pre-flight namespace check
openmeter/app/sandbox/app.go
Introduces Factory.InstallApp, adds lo import, validates install input, lists existing sandbox apps in the target namespace, and returns a conflict if one exists.
Sandbox billing profile provisioning
openmeter/app/httpdriver/marketplace.go
Implements createBillingProfile for sandbox apps by deriving the namespace from the installed app ID, calling billingService.ProvisionDefaultBillingProfile, and returning capability types: calculate tax, invoice customers, collect payments.

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main changes: fixing the broken sandbox app install endpoint and implementing billing profile provisioning.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

Review ran into problems

🔥 Problems

Stopped waiting for pipeline failures after 30000ms. One of your pipelines takes longer than our 30000ms fetch window to run, so review may not consider pipeline-failure results for inline comments if any failures occurred after the fetch window. Increase the timeout if you want to wait longer or run a @coderabbit review after the pipeline has finished.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@openmeter/app/sandbox/app.go`:
- Around line 235-237: The code indexes existing.Items[0] after checking
existing.TotalCount > 0 which can still panic if Items is empty; update the
guard in the create/install flow (the block that returns
models.NewGenericConflictError) to verify len(existing.Items) > 0 before
accessing existing.Items[0]; if Items is empty but TotalCount > 0, fall back to
a safe error message (e.g., use existing.GetName() only when present or include
the conflicting app name unknown) so you never index Items[0] without confirming
its length.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a3771f68-24f2-446e-9ab5-d31be412fbe6

📥 Commits

Reviewing files that changed from the base of the PR and between a492aed and 836f49c.

📒 Files selected for processing (1)
  • openmeter/app/sandbox/app.go

Comment thread openmeter/app/sandbox/app.go
@tothandras

Copy link
Copy Markdown
Contributor

Sandbox apps are auto-provisioned. It shouldn't be available for installs.

@vsengar-79

vsengar-79 commented Jun 15, 2026

Copy link
Copy Markdown
Contributor Author

Sandbox apps are auto-provisioned. It shouldn't be available for installs.

Hi @tothandras, You're right that it's auto-provisioned, but right now, the marketplace listing still publicly advertises a 'No Credentials' install. The endpoint was just completely broken (returning a 400) because the backend code was written for allowing API key installs, even when the API key wasn't even validated. And the error message was also very confusing stating that this installation method wasn't supported, while listing the same installation method in the 'Supported methods'.

Keeping your opinion in mind, I already wrote a check to stop the app from being installed when it already is (which is the case for 99.9% percent of instances). This is basically just implementing dead code and fixing confusing mess. If we shouldn't allow manual installs at all, should I change this PR to remove the installation capability from the sandbox marketplace listing entirely?

This PR was made to keep in mind that the Sandbox app is provisioned for the set default namespace. If a user wrote some code to allow dynamic namespace support, this would just make it easy for them too to initiate the Sandbox app for subscriptions/features. And this would make no negative effect on already existing instances due to the duplicate check. Currently, multiple sandbox apps are being allowed to install which isn't useful for anything.

@greptile-apps

greptile-apps Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes two related bugs in the sandbox app marketplace flow. First, the sandbox Factory now implements InstallApp (matching its advertised no_credentials_required install method), replacing the defunct InstallAppWithAPIKey method that caused a 400 for every install request. Second, the createBillingProfile handler for sandbox now calls ProvisionDefaultBillingProfile instead of silently returning nil, enabling a full install-and-provision in a single API call.

  • openmeter/app/sandbox/app.go: Renames InstallAppWithAPIKey to InstallApp and adds a singleton guard — checking for an existing sandbox app and returning a 409 conflict — before calling CreateApp.
  • openmeter/app/httpdriver/marketplace.go: Replaces the // TODO: Implement sandbox billing profile creation stub with a ProvisionDefaultBillingProfile call and returns all three capability types on success.

Confidence Score: 5/5

  • Safe to merge; the two targeted fixes are correct and self-contained for the common install path.
  • Both changes are narrow and well-scoped. The sandbox singleton guard correctly returns a conflict when an app already exists, and ProvisionDefaultBillingProfile idempotently provisions the default profile only when one is absent. The one edge case — a misleading capability list in the response when an existing non-sandbox billing profile prevents provisioning — affects only a non-standard setup and carries no functional impact on billing operations.
  • No files require special attention beyond the minor response-accuracy edge case noted in marketplace.go.

Important Files Changed

Filename Overview
openmeter/app/sandbox/app.go Replaces InstallAppWithAPIKey with InstallApp to match the no_credentials_required install method; adds singleton check using ListApps+TotalCount before CreateApp.
openmeter/app/httpdriver/marketplace.go Replaces the no-op TODO stub for sandbox billing profile with a real ProvisionDefaultBillingProfile call; always returns all 3 capability types regardless of whether the provision was a no-op.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Client
    participant Handler as MarketplaceAppInstall Handler
    participant AppService as App Service / Adapter
    participant SandboxFactory as Sandbox Factory
    participant BillingService as Billing Service

    Client->>Handler: POST /marketplace/listings/sandbox/install
    Handler->>AppService: InstallMarketplaceListing(ctx, input)
    AppService->>AppService: transaction.Run(ctx, ...)
    AppService->>SandboxFactory: InstallApp(ctx, input)
    SandboxFactory->>AppService: "ListApps(namespace, type=sandbox)"
    AppService-->>SandboxFactory: existing apps
    alt sandbox already exists
        SandboxFactory-->>AppService: GenericConflictError(409)
        AppService-->>Handler: error
        Handler-->>Client: 409 Conflict
    else no sandbox exists
        SandboxFactory->>AppService: "CreateApp(namespace, name, type=sandbox)"
        AppService-->>SandboxFactory: appBase
        SandboxFactory-->>AppService: App
        AppService-->>Handler: installedApp (tx committed)
    end

    Handler->>BillingService: createBillingProfile(ctx, installedApp)
    BillingService->>BillingService: ProvisionDefaultBillingProfile(ctx, namespace)
    BillingService->>BillingService: GetDefaultProfile(namespace)
    alt default profile already exists
        BillingService-->>Handler: nil (no-op)
    else no default profile
        BillingService->>AppService: "ListApps(namespace, type=sandbox)"
        AppService-->>BillingService: [sandboxApp]
        BillingService->>BillingService: "CreateProfile(default=true, apps=sandboxApp)"
        BillingService-->>Handler: nil
    end
    Handler-->>Client: "200 OK {app, defaultForCapabilityTypes: [CalculateTax, InvoiceCustomers, CollectPayments]}"
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Client
    participant Handler as MarketplaceAppInstall Handler
    participant AppService as App Service / Adapter
    participant SandboxFactory as Sandbox Factory
    participant BillingService as Billing Service

    Client->>Handler: POST /marketplace/listings/sandbox/install
    Handler->>AppService: InstallMarketplaceListing(ctx, input)
    AppService->>AppService: transaction.Run(ctx, ...)
    AppService->>SandboxFactory: InstallApp(ctx, input)
    SandboxFactory->>AppService: "ListApps(namespace, type=sandbox)"
    AppService-->>SandboxFactory: existing apps
    alt sandbox already exists
        SandboxFactory-->>AppService: GenericConflictError(409)
        AppService-->>Handler: error
        Handler-->>Client: 409 Conflict
    else no sandbox exists
        SandboxFactory->>AppService: "CreateApp(namespace, name, type=sandbox)"
        AppService-->>SandboxFactory: appBase
        SandboxFactory-->>AppService: App
        AppService-->>Handler: installedApp (tx committed)
    end

    Handler->>BillingService: createBillingProfile(ctx, installedApp)
    BillingService->>BillingService: ProvisionDefaultBillingProfile(ctx, namespace)
    BillingService->>BillingService: GetDefaultProfile(namespace)
    alt default profile already exists
        BillingService-->>Handler: nil (no-op)
    else no default profile
        BillingService->>AppService: "ListApps(namespace, type=sandbox)"
        AppService-->>BillingService: [sandboxApp]
        BillingService->>BillingService: "CreateProfile(default=true, apps=sandboxApp)"
        BillingService-->>Handler: nil
    end
    Handler-->>Client: "200 OK {app, defaultForCapabilityTypes: [CalculateTax, InvoiceCustomers, CollectPayments]}"
Loading

Reviews (2): Last reviewed commit: "Implement singleton check for sandbox ap..." | Re-trigger Greptile

Comment on lines +225 to +237
// Sandbox is a singleton per namespace — only one instance makes sense since all
// instances are functionally identical (no credentials, no external state).
existing, err := a.appService.ListApps(ctx, app.ListAppInput{
Namespace: input.Namespace,
Type: lo.ToPtr(app.AppTypeSandbox),
})
if err != nil {
return nil, fmt.Errorf("failed to list sandbox apps: %w", err)
}

if existing.TotalCount > 0 {
return nil, models.NewGenericConflictError(fmt.Errorf("sandbox app: %s already exists", existing.Items[0].GetName()))
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Non-atomic singleton check (TOCTOU)

The list-then-create pattern is not atomic: two concurrent InstallApp calls that both arrive when no sandbox app exists will both pass the TotalCount > 0 guard and each call CreateApp, producing two sandbox instances despite the comment stating this is a singleton. Because the check and creation are separate DB operations with no unique constraint or advisory lock, the race window is small but real. A database-level unique partial index on (namespace, type) — or wrapping the check and insert in the same transaction — would make the guarantee reliable.

Fix in Claude Code

@turip turip left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

@vsengar-79

Copy link
Copy Markdown
Contributor Author

@turip, should I do anything about the failing checks or can you merge it regardless?

Signed-off-by: vsengar-79 <146079793+vsengar-79@users.noreply.github.com>
Implemented sandbox billing profile creation by provisioning a default billing profile and returning relevant app capabilities.

Signed-off-by: vsengar-79 <146079793+vsengar-79@users.noreply.github.com>
Add singleton check for sandbox app creation to prevent duplicates.

Signed-off-by: vsengar-79 <146079793+vsengar-79@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants