Skip to content

Fix cloud-agent attach 403 by using account workspace id in box URLs#19

Merged
khaliqgant merged 2 commits into
mainfrom
fix/cloud-agent-box-account-workspace
May 22, 2026
Merged

Fix cloud-agent attach 403 by using account workspace id in box URLs#19
khaliqgant merged 2 commits into
mainfrom
fix/cloud-agent-box-account-workspace

Conversation

@khaliqgant

@khaliqgant khaliqgant commented May 22, 2026

Copy link
Copy Markdown
Member

Summary

Fixes Failed to warm cloud agent box: Forbidden when creating/attaching a cloud agent from Pear.

  • Root cause: the CLI bearer token is bound to a single workspaceId (the user's account workspace at login). Pear's RelayWorkspaceManager creates a separate relay workspace and persists that id. cloudAgentManager.warmBox POSTed to /api/v1/workspaces/{relayWorkspaceId}/cloud-agents/.../box, and the cloud route's requireWorkspaceSandboxAuthhasWorkspaceAccess rejected the call because auth.workspaceId !== relayWorkspaceId → 403.
  • Why Pear-side: the cloud box-manager already treats the URL workspaceId as advisory and partitions data (provider creds, sticky sandboxes, path token mint) by auth.workspaceId. Sending the account workspace id in the URL aligns the gate check with what the manager actually uses. Changing hasWorkspaceAccess globally would have broadened authorization for routes (e.g. /sandboxes) that genuinely use the URL workspace as a data partition.

Changes

  • src/main/auth.ts — new getAccountWorkspaceId(): resolves auth, hits /api/v1/auth/whoami, reads currentWorkspace.id (with workspaceId / workspace.id fallbacks), caches in auth-meta.json keyed by access-token hash. Cache clears on logout via existing clearTokens() and re-fetches after token rotation.
  • src/main/cloud-agent.tswarmBox, deleteBox, updateMountPaths now use the new helper. startMount continues to use the relayfile workspace id for the mount session (intentional — that path needs the relay workspace).
  • src/main/schemas.tsAuthMetaSchema extended with the optional cache field.

Test plan

  • npx vitest run — 3 files, 10 tests pass.
    • New cloud-agent.test.ts asserts warmBox POSTs to the account-workspace URL, mount session still uses the relay workspace id, whoami called once per attach.
    • New auth.test.ts covers getAccountWorkspaceId: missing auth, each whoami field shape (currentWorkspace.id, top-level workspaceId), cache hit (no whoami), cache invalidation on token change, bad-payload and non-OK whoami responses.
  • npx tsc --noEmit -p tsconfig.node.json — no new errors from the changed files (13 pre-existing Property 'cloudAgent' does not exist errors in cloud-agent.ts are unrelated to this change).
  • Manual smoke test: launch Pear, sign in, create a cloud agent, verify attach succeeds end-to-end. Requires the cloud-side companion PR (below) to also land.

Cloud-side validation (codex-1)

Workspace-id partitioning in cloud/.../box-manager.ts is correct post-fix:

  • findCredential() filters by auth.userId + auth.workspaceId.
  • Sticky sandbox lookup, advisory lock, Daytona labels, sandboxes.workspaceId insert/update, runtime env (RELAY_DEFAULT_WORKSPACE, RELAY_WORKSPACE_ID, RELAYFILE_WORKSPACE(_ID)), and relayfile path-token workspaceId all use input.auth.workspaceId.
  • urlWorkspaceId is not used for partitioning; with the new Pear shape it's inert.
  • Existing box-manager.test.ts ("warms a box using auth.workspaceId, mints a path-scoped token, and returns pear's required fields") already proves mismatched urlWorkspaceId is ignored.

Companion cloud PR

This Pear PR alone unblocks the 403, but a second failure mode the 403 was masking is fixed in:

AgentWorkforce/cloud#936 — "Fix box RelayAuth path-token minting"

The box route was forwarding Pear's cld_at_* cloud bearer to RelayAuth /v1/tokens/path, which expects a relay_ws_* workspace token. Without the companion fix, the warm call would return 503 box_request_failed after this PR lands. The cloud PR has the route stop forwarding the request bearer, mints a RelayAuth workspace token server-side for auth.workspaceId via a new mintRelayAuthWorkspaceToken helper in lib/relay-workspaces.ts, and uses that for the path-scoped token mint. Land both PRs together for end-to-end cloud-agent attach to work.

Collaboration notes

Diagnosis + patch by claude-1 (root-cause trace, auth.test.ts) and codex-1 (auth.ts / cloud-agent.ts / schemas.ts / cloud-agent.test.ts implementation, cloud-side validation, companion cloud PR).

🤖 Generated with Claude Code

The CLI bearer token is bound to the user's account workspace
(`apiTokenSessions.workspaceId`), but Pear's `RelayWorkspaceManager`
creates a separate relay workspace and persists its id. The cloud-agent
box route gates on `requireWorkspaceSandboxAuth` → `hasWorkspaceAccess`,
which for token-source auth requires `auth.workspaceId === workspaceId`,
so POSTing to `/api/v1/workspaces/{relayWorkspaceId}/cloud-agents/.../box`
returned 403 Forbidden.

The cloud `box-manager` already treats the URL workspaceId as advisory
and partitions data by `auth.workspaceId`. Sending the account workspace
id in the URL aligns with what the manager actually uses, so this fix is
Pear-side and minimal.

- `auth.ts`: add `getAccountWorkspaceId()` — resolves auth, hits
  `/api/v1/auth/whoami`, reads `currentWorkspace.id` (with
  `workspaceId`/`workspace.id` fallbacks), and caches the result in
  `auth-meta.json` keyed by access-token hash. Cache is cleared on
  logout via existing `clearTokens()` and re-fetched after token
  rotation.
- `cloud-agent.ts`: switch `warmBox`, `deleteBox`, and
  `updateMountPaths` to the new helper. `startMount` continues to use
  the relayfile workspace id for the mount session.
- `schemas.ts`: extend `AuthMetaSchema` with the optional cache field.
- Tests:
  - `cloud-agent.test.ts`: assert `warmBox` POSTs to the account
    workspace URL, mount session still uses the relay workspace id, and
    whoami is called once per attach.
  - `auth.test.ts`: cover `getAccountWorkspaceId` — missing auth, each
    whoami field shape, cache hit, cache invalidation on token change,
    and both bad-payload and non-OK whoami responses.

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

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 5e0027d9-f428-498d-a015-9e6a718b36ae

📥 Commits

Reviewing files that changed from the base of the PR and between 43f58d0 and e30a3e3.

📒 Files selected for processing (2)
  • src/main/auth.test.ts
  • src/main/auth.ts

📝 Walkthrough

Walkthrough

This PR adds persistent workspace ID caching keyed to access tokens, refactors whoami fetching and parsing, exports getAccountWorkspaceId(), and updates cloud-agent box operations to use account-derived workspaces.

Changes

Account Workspace ID Caching

Layer / File(s) Summary
Schema and Type Definitions
src/main/schemas.ts, src/main/auth.ts
AuthMetaSchema adds optional accountWorkspace with tokenHash and workspaceId; new internal types represent the cached workspace structure in auth metadata.
Auth Metadata Persistence and Token Hashing
src/main/auth.ts
Token hashing helpers introduced; saveAuthMeta and loadAuthMeta extended to compute and store workspace cache keyed by access-token hash; ensureAuthenticated updated to persist access tokens in auth meta.
Whoami Payload Fetching and Workspace Extraction
src/main/auth.ts
fetchWhoamiPayload helper performs timed/abortable whoami requests; accountWorkspaceIdFromWhoami extracts workspace IDs from responses; saveAccountWorkspaceCache persists workspace metadata tied to tokens; fetchWhoami refactored to use these helpers.
Public getAccountWorkspaceId Export
src/main/auth.ts
New exported function resolves cloud auth, validates/returns cached workspace ID using access-token hash, fetches whoami on cache miss, and persists results to auth meta.
Cloud Agent Box Operations Integration
src/main/cloud-agent.ts
Imports getAccountWorkspaceId; adds requireAccountTokenWorkspaceId() helper; updates warmBox(), deleteBox(), and updateMountPaths() to use account-derived workspace IDs for cloud-agent box URLs.
getAccountWorkspaceId Behavior Tests
src/main/auth.test.ts
Vitest suite verifies missing auth errors, successful whoami-based resolution, fallback to top-level workspaceId, cache-hit with token hash matching, cache-miss/refetch on hash change, and error handling for unusable or non-OK whoami responses.
CloudAgentManager Workspace Selection Test
src/main/cloud-agent.test.ts
Integration test confirms that box POST URLs target account-derived workspace (not relay workspace), whoami is called exactly once, and mount inputs use relay workspace ID.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A workspace ID cache, so clever and bright,
Tied to tokens that dance in the Electron light,
The whoami now whispers what clouds need to know,
Box operations now use the account path's flow,
Tests guard every path—no workspace cache woe! 🎉


Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Pro by visiting https://app.coderabbit.ai/login.

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

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

View 4 additional findings in Devin Review.

Open in Devin Review

Comment thread src/main/auth.ts
Comment on lines +268 to +272
return (
firstString(value, ['workspaceId', 'workspace_id']) ||
firstString(firstObject(value, ['currentWorkspace']), ['id', 'workspaceId', 'workspace_id']) ||
firstString(firstObject(value, ['workspace']), ['id', 'workspaceId', 'workspace_id'])
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Workspace ID extraction priority is inverted: top-level workspaceId checked before currentWorkspace

accountWorkspaceIdFromWhoami checks the top-level workspaceId/workspace_id before currentWorkspace.id, but the test at src/main/auth.test.ts:137 is titled "falls back to top-level workspaceId and workspace.id when currentWorkspace is absent" — the word "falls back" unambiguously indicates currentWorkspace is the primary source and workspaceId is the fallback. If the whoami API ever returns both fields with different values (e.g. { workspaceId: 'legacy', currentWorkspace: { id: 'active' } }), the function returns the legacy value instead of the active one. This workspace ID is used to construct API URLs in cloud-agent.ts for warm/delete/patch box operations (src/main/cloud-agent.ts:653, src/main/cloud-agent.ts:713), so the wrong ID would cause requests to target the wrong workspace.

Suggested change
return (
firstString(value, ['workspaceId', 'workspace_id']) ||
firstString(firstObject(value, ['currentWorkspace']), ['id', 'workspaceId', 'workspace_id']) ||
firstString(firstObject(value, ['workspace']), ['id', 'workspaceId', 'workspace_id'])
)
return (
firstString(firstObject(value, ['currentWorkspace']), ['id', 'workspaceId', 'workspace_id']) ||
firstString(value, ['workspaceId', 'workspace_id']) ||
firstString(firstObject(value, ['workspace']), ['id', 'workspaceId', 'workspace_id'])
)
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Addresses Devin review feedback on PR #19: the workspace id selector
in `accountWorkspaceIdFromWhoami` checked the top-level `workspaceId`
before `currentWorkspace.id`. Cloud's whoami doesn't return a top-level
`workspaceId` today, so this is inert — but if a legacy alias is ever
added, it would win over the active workspace. Reorder so
`currentWorkspace.id` is primary and the top-level / nested `workspace`
keys are fallbacks.

Adds a unit test exercising the precedence when both fields are present
to lock in the priority.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@khaliqgant khaliqgant merged commit 8f3132d into main May 22, 2026
2 checks passed
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