Skip to content

feat(sandbox): plumb per-account gitUser into POST /api/sandbox#534

Merged
sweetmantech merged 1 commit intotestfrom
feat/sandbox-git-user-resolver
May 8, 2026
Merged

feat(sandbox): plumb per-account gitUser into POST /api/sandbox#534
sweetmantech merged 1 commit intotestfrom
feat/sandbox-git-user-resolver

Conversation

@sweetmantech
Copy link
Copy Markdown
Contributor

@sweetmantech sweetmantech commented May 7, 2026

Summary

  • Adds `resolveGitUser(accountId)` that derives a `{ name, email }` pair from the `accounts` + `account_emails` tables, with no-PII fallbacks (`recoupable-{prefix}` / `{accountId}@users.noreply.recoupable.com`).
  • Wires it into `createSandboxHandler` so each `POST /api/sandbox` forwards `gitUser` to `connectSandbox`. The Vercel sandbox runtime then applies `git config user.name` / `user.email` inside the workspace — controlling commit authorship per user.
  • Closes the only commit-attribution gap in the open-agents → api cutover. The push credential (`getServiceGithubToken`) is unrelated and remains a single hardcoded service token.

TDD: red `resolveGitUser` test (5 cases) → green; red `createSandboxHandler` integration test → green.

Test plan

  • `pnpm test` — 2585 / 2585 pass
  • `pnpm lint:check` — clean
  • `npx tsc --noEmit` — clean for changed files
  • Smoke: provision a sandbox via `POST /api/sandbox`, shell in, run `git config user.name && git config user.email`, confirm they match the resolved values

🤖 Generated with Claude Code


Summary by cubic

Passes a per-account Git user (name and email) into sandbox creation so commits are authored by the correct user. Adds a resolver that pulls values from accounts and account_emails with stable no-PII fallbacks.

  • New Features
    • Added resolveGitUser(accountId) which uses accounts.name and account_emails.email, falling back to recoupable-{accountId.slice(0,8)} and {accountId}@users.noreply.recoupable.com.
    • POST /api/sandbox now forwards gitUser to connectSandbox to set git config user.name/user.email; push credentials via getServiceGithubToken are unchanged.
    • Added tests for the resolver (5 cases) and a handler integration test.

Written for commit bd4e946. Summary will update on new commits.

Summary by CodeRabbit

  • New Features
    • Sandboxes now configure commits with account-specific user information (name and email), ensuring proper commit attribution and author identity resolution within the environment.

Open-agents passes a per-user `gitUser = { name, email }` to
`connectSandbox`, which the sandbox runtime applies via
`git config user.name` / `user.email` so commit objects carry that
identity. The push credential (a single hardcoded service GitHub
token) is unrelated — `gitUser` is purely about commit *authorship*.

The api was the lone gap: it never resolved a `gitUser`, so commits
made inside an api-provisioned sandbox would either fail (no author)
or fall back to whatever was baked into the snapshot image.

Adds `resolveGitUser(accountId)`:
  - looks up `accounts.name` for the display name
  - looks up `account_emails.email` for the address
  - falls back to `recoupable-{accountId.slice(0,8)}` /
    `{accountId}@users.noreply.recoupable.com` when either is missing
  - returns `{ name, email }` ready to forward into `connectSandbox`

Wires it into `createSandboxHandler` so each request derives its
gitUser from the validated `auth.accountId`.

TDD: red test for `resolveGitUser` (5 cases — populated values,
missing name fallback, missing email fallback, both-missing, null
email row), then green; red test for handler integration, then green.

Tests 2585 / 2585 pass. Lint + tsc clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 7, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds account-specific git user identity resolution for sandboxes. A new resolveGitUser function fetches account name and email with synthetic fallbacks, and integrates into the sandbox handler to control commit author attribution within the sandbox environment.

Changes

Git Identity Resolution

Layer / File(s) Summary
Data Contract
lib/sandbox/resolveGitUser.ts
GitUser interface defines name and email fields for git configuration.
Git User Resolution
lib/sandbox/resolveGitUser.ts
resolveGitUser(accountId) concurrently fetches account and email rows, derives name with recoupable-<first8> fallback and email with <accountId>@users.noreply.recoupable.com fallback, returning the resolved identity.
Sandbox Handler Integration
lib/sandbox/createSandboxHandler.ts
Handler imports resolveGitUser, resolves account-scoped git identity before sandbox connection, and passes it to connectSandbox options for commit author attribution.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🌳 Each commit now bears its author's name,
No more synthetic ghosts to claim the frame,
Account and email dance in parallel,
Fallbacks stand ready should details fail,
Your sandbox commits wear your identity well! 🎭

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Solid & Clean Code ⚠️ Warning Two violations: resolveGitUser call (line 90) lacks error handling outside try/catch; email validation (line 33) doesn't trim, risking invalid whitespace-only emails. Both noted in review comments. Wrap resolveGitUser in try/catch with fallback. Add trim() to email validation check and test whitespace edge case.
✅ Passed checks (2 passed)
Check name Status Explanation
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/sandbox-git-user-resolver

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.

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 7, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
api Ready Ready Preview May 7, 2026 11:39pm

Request Review

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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 `@lib/sandbox/createSandboxHandler.ts`:
- Around line 86-91: The call to resolveGitUser is currently executed outside
the provisioning error handling path, so any exception it throws bypasses the
structured error response; wrap the resolveGitUser call in the same try/catch
used for provisioning (or add a dedicated try/catch immediately around
resolveGitUser) inside createSandboxHandler, catch and convert failures into the
same structured error response/return used by the provisioning flow (same shape
and logging), and only proceed to call provisionSandbox (or the provisioning
logic) when resolveGitUser succeeds.

In `@lib/sandbox/resolveGitUser.ts`:
- Around line 33-37: The selected email may be whitespace-only; update the logic
that computes email (using emails, emailRow, accountId, and name) to trim and
validate the found email before using it: derive a trimmedEmail from
emailRow?.email, check that trimmedEmail is a non-empty string, and only then
use it; otherwise fall back to the default
`${accountId}@users.noreply.recoupable.com`. Ensure the existing name fallback
using account?.name?.trim() remains unchanged.
🪄 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: b4985606-2570-4f8a-a276-99999532aa16

📥 Commits

Reviewing files that changed from the base of the PR and between 12ec76e and bd4e946.

⛔ Files ignored due to path filters (2)
  • lib/sandbox/__tests__/createSandboxHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/sandbox/__tests__/resolveGitUser.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (2)
  • lib/sandbox/createSandboxHandler.ts
  • lib/sandbox/resolveGitUser.ts

Comment on lines +86 to +91
// Per-account `gitUser` controls commit authorship inside the sandbox
// (`git config user.name` / `user.email`). The push credential is a
// separate hardcoded service token — `gitUser` is purely about who
// each commit object is *authored* by.
const gitUser = await resolveGitUser(auth.accountId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard resolveGitUser failures before provisioning.

At Line 90, resolveGitUser is outside the provisioning try/catch. If account/email lookup throws, this request skips your structured error response path.

Proposed fix
-  const gitUser = await resolveGitUser(auth.accountId);
+  let gitUser: { name: string; email: string };
+  try {
+    gitUser = await resolveGitUser(auth.accountId);
+  } catch (error) {
+    console.error("[createSandboxHandler] resolveGitUser failed:", error);
+    gitUser = {
+      name: `recoupable-${auth.accountId.slice(0, 8)}`,
+      email: `${auth.accountId}@users.noreply.recoupable.com`,
+    };
+  }

As per coding guidelines, “Handle errors gracefully”.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Per-account `gitUser` controls commit authorship inside the sandbox
// (`git config user.name` / `user.email`). The push credential is a
// separate hardcoded service token — `gitUser` is purely about who
// each commit object is *authored* by.
const gitUser = await resolveGitUser(auth.accountId);
// Per-account `gitUser` controls commit authorship inside the sandbox
// (`git config user.name` / `user.email`). The push credential is a
// separate hardcoded service token — `gitUser` is purely about who
// each commit object is *authored* by.
let gitUser: { name: string; email: string };
try {
gitUser = await resolveGitUser(auth.accountId);
} catch (error) {
console.error("[createSandboxHandler] resolveGitUser failed:", error);
gitUser = {
name: `recoupable-${auth.accountId.slice(0, 8)}`,
email: `${auth.accountId}@users.noreply.recoupable.com`,
};
}
🤖 Prompt for 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.

In `@lib/sandbox/createSandboxHandler.ts` around lines 86 - 91, The call to
resolveGitUser is currently executed outside the provisioning error handling
path, so any exception it throws bypasses the structured error response; wrap
the resolveGitUser call in the same try/catch used for provisioning (or add a
dedicated try/catch immediately around resolveGitUser) inside
createSandboxHandler, catch and convert failures into the same structured error
response/return used by the provisioning flow (same shape and logging), and only
proceed to call provisionSandbox (or the provisioning logic) when resolveGitUser
succeeds.

Comment on lines +33 to +37
const emailRow = emails.find(row => typeof row.email === "string" && row.email.length > 0);

const name = account?.name?.trim() || `recoupable-${accountId.slice(0, 8)}`;
const email = emailRow?.email ?? `${accountId}@users.noreply.recoupable.com`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Trim and validate the selected email before use.

At Line 33/Line 36, whitespace-only values can pass and be forwarded as gitUser.email. Normalize with trim() and fallback when empty.

Proposed fix
-  const emailRow = emails.find(row => typeof row.email === "string" && row.email.length > 0);
+  const emailRow = emails.find(
+    row => typeof row.email === "string" && row.email.trim().length > 0,
+  );
@@
-  const email = emailRow?.email ?? `${accountId}@users.noreply.recoupable.com`;
+  const email = emailRow?.email?.trim() || `${accountId}@users.noreply.recoupable.com`;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const emailRow = emails.find(row => typeof row.email === "string" && row.email.length > 0);
const name = account?.name?.trim() || `recoupable-${accountId.slice(0, 8)}`;
const email = emailRow?.email ?? `${accountId}@users.noreply.recoupable.com`;
const emailRow = emails.find(
row => typeof row.email === "string" && row.email.trim().length > 0,
);
const name = account?.name?.trim() || `recoupable-${accountId.slice(0, 8)}`;
const email = emailRow?.email?.trim() || `${accountId}@users.noreply.recoupable.com`;
🤖 Prompt for 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.

In `@lib/sandbox/resolveGitUser.ts` around lines 33 - 37, The selected email may
be whitespace-only; update the logic that computes email (using emails,
emailRow, accountId, and name) to trim and validate the found email before using
it: derive a trimmedEmail from emailRow?.email, check that trimmedEmail is a
non-empty string, and only then use it; otherwise fall back to the default
`${accountId}@users.noreply.recoupable.com`. Ensure the existing name fallback
using account?.name?.trim() remains unchanged.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

2 issues found across 4 files

Confidence score: 3/5

  • There is a concrete regression risk in lib/sandbox/createSandboxHandler.ts: resolveGitUser may throw before the guarded provisioning block, which can cause uncaught failures on POST /api/sandbox.
  • lib/sandbox/resolveGitUser.ts should trim/normalize email input before validation; whitespace-only values can bypass fallback logic and lead to invalid git config data.
  • Given the high-confidence findings and user-facing failure path, this carries some merge risk until error handling and input normalization are tightened.
  • Pay close attention to lib/sandbox/createSandboxHandler.ts and lib/sandbox/resolveGitUser.ts - uncaught pre-guard exceptions and weak email normalization can break sandbox setup.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="lib/sandbox/createSandboxHandler.ts">

<violation number="1" location="lib/sandbox/createSandboxHandler.ts:90">
P2: `resolveGitUser` can throw before your guarded sandbox-provision block, introducing an uncaught failure path for `POST /api/sandbox`.</violation>
</file>

<file name="lib/sandbox/resolveGitUser.ts">

<violation number="1" location="lib/sandbox/resolveGitUser.ts:33">
P2: Validate and normalize email with `.trim()` before accepting it; whitespace-only emails currently bypass the fallback and can produce invalid git config.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant Client
    participant API as POST /api/sandbox
    participant Auth as Auth Middleware
    participant GitResolver as resolveGitUser()
    participant Accounts as selectAccounts()
    participant Emails as selectAccountEmails()
    participant Sandbox as connectSandbox()
    participant Runtime as Vercel Sandbox Runtime

    Note over Client,Runtime: Per-account gitUser plumbed into sandbox creation flow

    Client->>API: POST /api/sandbox (with session cookie)
    API->>Auth: Validate session & extract accountId
    Auth-->>API: auth.accountId

    Note over API,Runtime: NEW: Resolve per-account git identity
    API->>GitResolver: resolveGitUser(auth.accountId)
    
    GitResolver->>Accounts: selectAccounts(accountId)
    GitResolver->>Emails: selectAccountEmails({ accountIds: accountId })
    
    Accounts-->>GitResolver: [{ id, name }] or []
    Emails-->>GitResolver: [{ email }] or []
    
    alt Account has name AND email exists
        GitResolver->>GitResolver: name = accounts[0].name, email = emailRow.email
    else Name missing, email present
        GitResolver->>GitResolver: name = "recoupable-{accountId[:8]}", email = emailRow.email
    else Name present, no email row
        GitResolver->>GitResolver: name = accounts[0].name, email = "{accountId}@users.noreply.recoupable.com"
    else Both missing
        GitResolver->>GitResolver: name = "recoupable-{accountId[:8]}", email = "{accountId}@users.noreply.recoupable.com"
    end
    
    GitResolver-->>API: { name, email }

    Note over API,Runtime: Forward gitUser to sandbox creation
    API->>Sandbox: connectSandbox({ options: { gitUser, githubToken, ... } })
    
    Sandbox->>Runtime: Configure workspace with gitUser
    Runtime->>Runtime: git config user.name = gitUser.name
    Runtime->>Runtime: git config user.email = gitUser.email
    
    Sandbox-->>API: sandbox instance
    API-->>Client: 200 + sandbox details
Loading

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

// (`git config user.name` / `user.email`). The push credential is a
// separate hardcoded service token — `gitUser` is purely about who
// each commit object is *authored* by.
const gitUser = await resolveGitUser(auth.accountId);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: resolveGitUser can throw before your guarded sandbox-provision block, introducing an uncaught failure path for POST /api/sandbox.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/sandbox/createSandboxHandler.ts, line 90:

<comment>`resolveGitUser` can throw before your guarded sandbox-provision block, introducing an uncaught failure path for `POST /api/sandbox`.</comment>

<file context>
@@ -82,6 +83,12 @@ export async function createSandboxHandler(request: NextRequest): Promise<NextRe
+  // (`git config user.name` / `user.email`). The push credential is a
+  // separate hardcoded service token — `gitUser` is purely about who
+  // each commit object is *authored* by.
+  const gitUser = await resolveGitUser(auth.accountId);
+
   let sandbox;
</file context>
Suggested change
const gitUser = await resolveGitUser(auth.accountId);
let gitUser;
try {
gitUser = await resolveGitUser(auth.accountId);
} catch (error) {
console.error("[createSandboxHandler] resolveGitUser failed:", error);
gitUser = {
name: `recoupable-${auth.accountId.slice(0, 8)}`,
email: `${auth.accountId}@users.noreply.recoupable.com`,
};
}

]);

const account = accounts[0] ?? null;
const emailRow = emails.find(row => typeof row.email === "string" && row.email.length > 0);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Validate and normalize email with .trim() before accepting it; whitespace-only emails currently bypass the fallback and can produce invalid git config.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/sandbox/resolveGitUser.ts, line 33:

<comment>Validate and normalize email with `.trim()` before accepting it; whitespace-only emails currently bypass the fallback and can produce invalid git config.</comment>

<file context>
@@ -0,0 +1,39 @@
+  ]);
+
+  const account = accounts[0] ?? null;
+  const emailRow = emails.find(row => typeof row.email === "string" && row.email.length > 0);
+
+  const name = account?.name?.trim() || `recoupable-${accountId.slice(0, 8)}`;
</file context>

@sweetmantech
Copy link
Copy Markdown
Contributor Author

Smoke test on preview `bd4e9463` (`https://api-3260wobve-recoup.vercel.app\`):

  1. `POST /api/sessions` → session `2afc8087-4ef3-42f9-947a-b185f4883a9f`
  2. `POST /api/sandbox` with `sessionId` + `https://github.com/vercel/next.js\` → ready in 99s, `mode=vercel`, `status: active`
  3. Connected to the running sandbox via `@vercel/sandbox` SDK and ran `git config` inside it:

```
git user.name : Sweetman.eth
git user.email: sweetmantech@gmail.com
```

Both values match the api-key's owning account (`accounts.name` + `account_emails.email`) — the resolver, the handler, and the sandbox runtime all forward correctly. Commit authorship is per-account as designed.

@sweetmantech sweetmantech merged commit 0c51c14 into test May 8, 2026
6 checks passed
@sweetmantech sweetmantech deleted the feat/sandbox-git-user-resolver branch May 8, 2026 01:31
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.

1 participant