mirror of
https://github.com/stablyai/orca.git
synced 2026-11-15 18:37:24 +00:00
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
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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' })
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
@@ -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<void>
|
||||
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) {
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user