Fix cloud-agent attach 403 by using account workspace id in box URLs#19
Conversation
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>
ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Free Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThis 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. ChangesAccount Workspace ID Caching
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
Note 🎁 Summarized by CodeRabbit FreeYour 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 |
| return ( | ||
| firstString(value, ['workspaceId', 'workspace_id']) || | ||
| firstString(firstObject(value, ['currentWorkspace']), ['id', 'workspaceId', 'workspace_id']) || | ||
| firstString(firstObject(value, ['workspace']), ['id', 'workspaceId', 'workspace_id']) | ||
| ) |
There was a problem hiding this comment.
🟡 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.
| 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']) | |
| ) |
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>
Summary
Fixes
Failed to warm cloud agent box: Forbiddenwhen creating/attaching a cloud agent from Pear.workspaceId(the user's account workspace at login). Pear'sRelayWorkspaceManagercreates a separate relay workspace and persists that id.cloudAgentManager.warmBoxPOSTed to/api/v1/workspaces/{relayWorkspaceId}/cloud-agents/.../box, and the cloud route'srequireWorkspaceSandboxAuth→hasWorkspaceAccessrejected the call becauseauth.workspaceId !== relayWorkspaceId→ 403.box-manageralready treats the URL workspaceId as advisory and partitions data (provider creds, sticky sandboxes, path token mint) byauth.workspaceId. Sending the account workspace id in the URL aligns the gate check with what the manager actually uses. ChanginghasWorkspaceAccessglobally would have broadened authorization for routes (e.g./sandboxes) that genuinely use the URL workspace as a data partition.Changes
src/main/auth.ts— newgetAccountWorkspaceId(): resolves auth, hits/api/v1/auth/whoami, readscurrentWorkspace.id(withworkspaceId/workspace.idfallbacks), caches inauth-meta.jsonkeyed by access-token hash. Cache clears on logout via existingclearTokens()and re-fetches after token rotation.src/main/cloud-agent.ts—warmBox,deleteBox,updateMountPathsnow use the new helper.startMountcontinues to use the relayfile workspace id for the mount session (intentional — that path needs the relay workspace).src/main/schemas.ts—AuthMetaSchemaextended with the optional cache field.Test plan
npx vitest run— 3 files, 10 tests pass.cloud-agent.test.tsassertswarmBoxPOSTs to the account-workspace URL, mount session still uses the relay workspace id, whoami called once per attach.auth.test.tscoversgetAccountWorkspaceId: missing auth, each whoami field shape (currentWorkspace.id, top-levelworkspaceId), 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-existingProperty 'cloudAgent' does not existerrors incloud-agent.tsare unrelated to this change).Cloud-side validation (codex-1)
Workspace-id partitioning in
cloud/.../box-manager.tsis correct post-fix:findCredential()filters byauth.userId+auth.workspaceId.sandboxes.workspaceIdinsert/update, runtime env (RELAY_DEFAULT_WORKSPACE,RELAY_WORKSPACE_ID,RELAYFILE_WORKSPACE(_ID)), and relayfile path-tokenworkspaceIdall useinput.auth.workspaceId.urlWorkspaceIdis not used for partitioning; with the new Pear shape it's inert.box-manager.test.ts("warms a box using auth.workspaceId, mints a path-scoped token, and returns pear's required fields") already proves mismatchedurlWorkspaceIdis 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 arelay_ws_*workspace token. Without the companion fix, the warm call would return503 box_request_failedafter this PR lands. The cloud PR has the route stop forwarding the request bearer, mints a RelayAuth workspace token server-side forauth.workspaceIdvia a newmintRelayAuthWorkspaceTokenhelper inlib/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) andcodex-1(auth.ts/cloud-agent.ts/schemas.ts/cloud-agent.test.tsimplementation, cloud-side validation, companion cloud PR).🤖 Generated with Claude Code