From 83ccaa4a8bfb3e70d96077afaa70c339a3e0f0cd Mon Sep 17 00:00:00 2001 From: Neil Date: Fri, 13 Nov 2026 02:10:24 -0800 Subject: [PATCH] fix(cloud): RS-5 a Claude credential is always labelled "Claude subscription": never this computer's email or the Orca name; old labels read the same --- cloud/apps/sandbox-control/src/agent-credentials.ts | 12 ++++++++++-- .../sandbox-control/test/agent-accounts.test.ts | 3 ++- .../orca-cloud-agents/claude-setup-code.test.ts | 1 - src/main/orca-cloud-agents/orca-cloud-agents-ipc.ts | 13 ------------- .../orca-cloud-agents-service.test.ts | 7 +++---- .../orca-cloud-agents/orca-cloud-agents-service.ts | 5 ++--- .../agent-accounts/OrcaCloudAccounts.fixture.tsx | 3 ++- 7 files changed, 19 insertions(+), 25 deletions(-) diff --git a/cloud/apps/sandbox-control/src/agent-credentials.ts b/cloud/apps/sandbox-control/src/agent-credentials.ts index 182369da3bf..6f148b6fdbd 100644 --- a/cloud/apps/sandbox-control/src/agent-credentials.ts +++ b/cloud/apps/sandbox-control/src/agent-credentials.ts @@ -141,11 +141,16 @@ export async function agentCredentialsView(db: D1Database, env: Env, userId: str const view = (agent: CredentialAgent) => { const row = results.find((r) => r.agent === agent) ?? null const state = credentialState(row, now) - return { state, ...(row?.label ? { label: row.label } : {}), ...(row?.expires_at ? { expiresAt: row.expires_at } : {}) } + // RS-5: credentials saved before the fix carry an email or the Orca name; Claude's always reads true. + const label = agent === 'claude' && row ? CLAUDE_CREDENTIAL_LABEL : row?.label + return { state, ...(label ? { label } : {}), ...(row?.expires_at ? { expiresAt: row.expires_at } : {}) } } return { claude: view('claude'), codex: view('codex'), providerEnabled: providerEnabled(env), providerState: providerStates(env) } } +/** RS-5: what a stored Claude credential is called (the approved account itself can't be known). */ +export const CLAUDE_CREDENTIAL_LABEL = 'Claude subscription' + // A Claude setup-token: `sk-ant-oat01-` then URL-safe characters. Shape-checked, never logged. const CLAUDE_TOKEN = /^sk-ant-oat01-[A-Za-z0-9_-]{20,500}$/ export const PutClaudeSchema = z @@ -173,7 +178,10 @@ export async function putClaudeCredential(db: D1Database, keyring: Keyring, env: accountId: caller.accountId, agent: 'claude', value: token, - label: body.label?.trim() || caller.name || null, + // RS-5 (design-squad): a true label only. Neither this computer's Claude Code email nor the Orca + // name says which account approved the setup-token (it prints no email, and its inference-only + // scope can't read the profile), so a Claude credential is always "Claude subscription". + label: CLAUDE_CREDENTIAL_LABEL, expiresAt: Math.min(body.expiresAt ?? now + CLAUDE_SETUP_TOKEN_TTL_MS, now + CLAUDE_SETUP_TOKEN_TTL_MS) }, now diff --git a/cloud/apps/sandbox-control/test/agent-accounts.test.ts b/cloud/apps/sandbox-control/test/agent-accounts.test.ts index 516bf5330b8..a4edac473d2 100644 --- a/cloud/apps/sandbox-control/test/agent-accounts.test.ts +++ b/cloud/apps/sandbox-control/test/agent-accounts.test.ts @@ -108,7 +108,8 @@ describe('Orca-account credentials (D11)', () => { expect((await h.call('PUT', '/v1/agent-credentials/claude', { bearer: mia, body: { token: 'sk-ant-api03-not-a-setup-token' } })).body.reason).toBe('invalid_token') const put = await h.call('PUT', '/v1/agent-credentials/claude', { bearer: mia, body: { token: SETUP_TOKEN, label: 'mia@example.com' } }) expect(put.status).toBe(200) - expect(put.body.claude).toMatchObject({ state: 'active', label: 'mia@example.com' }) + // RS-5: never the email a client sends (it may be another account's): always "Claude subscription". + expect(put.body.claude).toMatchObject({ state: 'active', label: 'Claude subscription' }) expect(put.body.providerEnabled).toEqual({ claude: true, codex: false }) // Codex's flag was never turned on: `off`, which clients word differently from the kill switch. expect(put.body.providerState).toEqual({ claude: 'on', codex: 'off' }) diff --git a/src/main/orca-cloud-agents/claude-setup-code.test.ts b/src/main/orca-cloud-agents/claude-setup-code.test.ts index 9f43fc3acf2..3307784eeef 100644 --- a/src/main/orca-cloud-agents/claude-setup-code.test.ts +++ b/src/main/orca-cloud-agents/claude-setup-code.test.ts @@ -155,7 +155,6 @@ describe('service: the code from the strip, Settings or the chat UI', () => { children.push(child) return child }, - readClaudeEmail: () => null, openExternal: vi.fn(async () => {}), broadcast: (setups) => broadcasts.push(setups), identity: () => 'p1:u1' diff --git a/src/main/orca-cloud-agents/orca-cloud-agents-ipc.ts b/src/main/orca-cloud-agents/orca-cloud-agents-ipc.ts index a7c0b0e58c1..6c58b52b3b6 100644 --- a/src/main/orca-cloud-agents/orca-cloud-agents-ipc.ts +++ b/src/main/orca-cloud-agents/orca-cloud-agents-ipc.ts @@ -9,10 +9,6 @@ import { import { withCloudBuildDefaults } from '../cloud-build-defaults' import { getHostedSandboxConfig } from '../hosted-sandbox/hosted-sandbox-config' import { orcaCloudSandboxIdFor } from '../hosted-sandbox/orca-cloud-sandbox-id' -import { - claudeStateFile, - readClaudeFolderLogin -} from '../claude-accounts/sign-in/claude-account-folder' import { ensureActiveOrcaProfile } from '../orca-profiles/profile-index-store' import { getOrcaProfileIndexGeneration } from '../orca-profiles/profile-index-generation' import { createAgentCredentialsClient } from './agent-credentials-client' @@ -61,14 +57,6 @@ function broadcast(setups: OrcaCloudAgentSetups): void { } } -function readClaudeEmail(): string | null { - try { - return readClaudeFolderLogin(claudeStateFile(process.env.CLAUDE_CONFIG_DIR))?.email ?? null - } catch { - return null - } -} - /** The signed-in Orca user, re-read only when the profile index changes (sign-in, sign-out, switch). */ function orcaIdentityReader(userDataPath: string): () => string | null { let cached: { generation: number; identity: string | null } | null = null @@ -110,7 +98,6 @@ function serviceFor(userDataPath: string): OrcaCloudAgentsService { }) : null, spawnSetupToken: spawnClaudeSetupTokenPty, - readClaudeEmail, openExternal: (url) => shell.openExternal(url), broadcast, identity: orcaIdentityReader(userDataPath) diff --git a/src/main/orca-cloud-agents/orca-cloud-agents-service.test.ts b/src/main/orca-cloud-agents/orca-cloud-agents-service.test.ts index 12f57b6a794..ba4e50bc571 100644 --- a/src/main/orca-cloud-agents/orca-cloud-agents-service.test.ts +++ b/src/main/orca-cloud-agents/orca-cloud-agents-service.test.ts @@ -86,7 +86,6 @@ describe('orca cloud agents service', () => { const service = createOrcaCloudAgentsService({ client, spawnSetupToken: async () => child, - readClaudeEmail: () => 'a@acme.dev', openExternal, broadcast: (setups) => broadcasts.push(setups), identity @@ -102,7 +101,7 @@ describe('orca cloud agents service', () => { } } - it('captures the setup-token in main and stores it with the local account as label', async () => { + it('captures the setup-token in main and stores it with no label (RS-5: the server names it truly)', async () => { const { service, child, broadcasts, client, openExternal } = setup() const reply = service.start('claude') await tick() @@ -117,7 +116,8 @@ describe('orca cloud agents service', () => { child.emit(`Your OAuth token (valid for 1 year):\n\n${TOKEN}\n\nStore this token`) child.exit() await tick() - expect(client.putClaude).toHaveBeenCalledWith(TOKEN, 'a@acme.dev') + // RS-5: never this computer's Claude Code email; it may belong to another account. + expect(client.putClaude).toHaveBeenCalledWith(TOKEN, undefined) expect(service.setups().claude).toEqual({ phase: 'saved' }) expectNoToken(reply, broadcasts, service.setups(), await service.status()) }) @@ -211,7 +211,6 @@ describe('orca cloud agents service', () => { const service = createOrcaCloudAgentsService({ client: null, spawnSetupToken: async () => fakeChild(), - readClaudeEmail: () => null, openExternal: vi.fn(), broadcast: vi.fn() }) diff --git a/src/main/orca-cloud-agents/orca-cloud-agents-service.ts b/src/main/orca-cloud-agents/orca-cloud-agents-service.ts index 8cfeae10a72..368c01ee56c 100644 --- a/src/main/orca-cloud-agents/orca-cloud-agents-service.ts +++ b/src/main/orca-cloud-agents/orca-cloud-agents-service.ts @@ -29,8 +29,6 @@ import { export type OrcaCloudAgentsServiceDeps = { client: AgentCredentialsClient | null spawnSetupToken: SpawnSetupToken - /** The Claude account signed in on this computer, used as the stored credential's label. */ - readClaudeEmail: () => string | null openExternal: (url: string) => Promise broadcast: (setups: OrcaCloudAgentSetups) => void /** @@ -161,7 +159,8 @@ export function createOrcaCloudAgentsService(deps: OrcaCloudAgentsServiceDeps) { isSignInUrl: (url) => isProviderSignInUrl('claude', url), onSetup: (setup) => update('claude', setup), onToken: async (token) => { - await storeClaude(token, deps.readClaudeEmail(), owner) + // RS-5: no label: this computer's Claude Code email may be another account's; the server names it. + await storeClaude(token, null, owner) }, onDone: () => { if (claudeRun === run) { diff --git a/src/renderer/src/components/settings/agent-accounts/OrcaCloudAccounts.fixture.tsx b/src/renderer/src/components/settings/agent-accounts/OrcaCloudAccounts.fixture.tsx index 681a2688dea..55d5d2fd7de 100644 --- a/src/renderer/src/components/settings/agent-accounts/OrcaCloudAccounts.fixture.tsx +++ b/src/renderer/src/components/settings/agent-accounts/OrcaCloudAccounts.fixture.tsx @@ -60,9 +60,10 @@ function fixture( }) } +// RS-5: a Claude credential is always labelled "Claude subscription" (the approved account can't be known). const CLAUDE_ACTIVE: OrcaCloudAccountRow = { kind: 'active', - label: 'a@acme.dev', + label: 'Claude subscription', expiresAt: NOW + YEAR, expiring: false, source: 'sign-in',