feat(cloud): sign agents out before sandbox delete, scrub env-build images, recipe env warning (A9, P-4)

- Worker: run orca-agent-signout (each agent's logout, then credential files) on a running sandbox
  before the provider delete; bounded (20 s) and best effort, never blocks the delete.
- Environment builds: scrub agent auth (--scrub-only) before the snapshot; fail closed.
- New VM script registered in VM_SCRIPTS + provision loop; vm-scripts regenerated.
- Main IPC ephemeralVm:forwardedEnvSummary returns names only, grouped by credential-looking.
- P-4 warning component (+ fixture) shown under Settings > recipes; delete dialog line for awake
  Orca Cloud sandboxes.
This commit is contained in:
Neil
2026-11-10 04:07:13 -08:00
parent 0f8ab90314
commit 12fe0c867f
21 changed files with 690 additions and 4 deletions
@@ -0,0 +1,32 @@
import { describe, expect, it } from 'vitest'
import { summarizeRecipeForwardedEnv } from './ephemeral-vm-recipe-forwarded-env'
describe('summarizeRecipeForwardedEnv', () => {
it('returns names only, grouping the ones that look like credentials', () => {
const summary = summarizeRecipeForwardedEnv({
PATH: '/usr/bin',
HOME: '/home/me',
GITHUB_TOKEN: 'ghp_secret_value',
AWS_SECRET_ACCESS_KEY: 'aws-secret-value',
DB_PASSWORD: 'hunter2',
ANTHROPIC_BASE_URL: 'https://gateway.example',
openai_api_key: 'sk-lower',
KEYCHAIN_PATH: '/tmp/k',
UNSET: undefined
})
expect(summary).toEqual({
count: 8,
keyLike: [
'ANTHROPIC_BASE_URL',
'AWS_SECRET_ACCESS_KEY',
'DB_PASSWORD',
'GITHUB_TOKEN',
'openai_api_key'
],
other: ['HOME', 'KEYCHAIN_PATH', 'PATH']
})
expect(JSON.stringify(summary)).not.toMatch(
/ghp_secret_value|aws-secret-value|hunter2|sk-lower/
)
})
})
@@ -0,0 +1,46 @@
import { AGENT_CREDENTIAL_NAMES } from './agent-credentials/agent-credential-names'
/** Names only: recipe create scripts get this computer's whole env, so values never leave main. */
export type RecipeForwardedEnvSummary = {
count: number
/** Sorted names that look like credentials (key/token/secret/password, or an agent credential). */
keyLike: string[]
/** Every other forwarded name, sorted. */
other: string[]
}
const AGENT_CREDENTIAL_NAME_SET = new Set(Object.values(AGENT_CREDENTIAL_NAMES).flat())
const CREDENTIAL_SEGMENTS = new Set([
'KEY',
'KEYS',
'APIKEY',
'TOKEN',
'TOKENS',
'SECRET',
'SECRETS',
'CREDENTIAL',
'CREDENTIALS',
'PAT'
])
const CREDENTIAL_SUBSTRINGS = /PASSWORD|PASSWD|SECRET|TOKEN|PRIVATE_KEY|API_?KEY/
export function looksLikeCredentialEnvName(name: string): boolean {
// Windows env names are case-insensitive; match them the way agents would read them.
const upper = name.toUpperCase()
return (
AGENT_CREDENTIAL_NAME_SET.has(upper) ||
CREDENTIAL_SUBSTRINGS.test(upper) ||
upper.split(/[_\-.]/).some((segment) => CREDENTIAL_SEGMENTS.has(segment))
)
}
export function summarizeRecipeForwardedEnv(env: NodeJS.ProcessEnv): RecipeForwardedEnvSummary {
const names = Object.keys(env)
.filter((name) => name.length > 0 && env[name] !== undefined)
.sort((a, b) => a.localeCompare(b))
return {
count: names.length,
keyLike: names.filter(looksLikeCredentialEnvName),
other: names.filter((name) => !looksLikeCredentialEnvName(name))
}
}