diff --git a/dist/index.cjs b/dist/index.cjs index d71a695..8ce2a4a 100755 --- a/dist/index.cjs +++ b/dist/index.cjs @@ -32462,10 +32462,10 @@ var MCP_AGENT_IDS = Object.freeze({ claude: "claude-code" }); function commandArgs(configuredCommand) { return Array.isArray(configuredCommand) ? configuredCommand.slice(1) : []; } -function runCheck(binaryPath, args, spawnSyncImpl, timeout = 5e3) { +function runCheck(binaryPath, args, spawnSyncImpl, providerEnvironment, timeout = 5e3) { const result = spawnSyncImpl(binaryPath, args, { encoding: "utf8", - env: buildAgentExecutableEnvironment(binaryPath), + env: buildAgentExecutableEnvironment(binaryPath, providerEnvironment), timeout }); return { @@ -32509,11 +32509,20 @@ async function inspectAgentHost(provider, dependencies = {}) { version: null }; } - const version = runCheck(binaryPath, commandArgs(config.binary.checkCommand), spawnSyncImpl); + const providerEnvironment = buildProviderEnvironment(config, { + baseEnvironment: dependencies.baseEnvironment, + rudiHome: dependencies.rudiHome + }); + const version = runCheck( + binaryPath, + commandArgs(config.binary.checkCommand), + spawnSyncImpl, + providerEnvironment + ); const authArgs = commandArgs(config.binary.authCheck); const versionArgs = commandArgs(config.binary.checkCommand); const authIsObservable = JSON.stringify(authArgs) !== JSON.stringify(versionArgs); - const auth = authIsObservable ? runCheck(binaryPath, authArgs, spawnSyncImpl) : { ok: null }; + const auth = authIsObservable ? runCheck(binaryPath, authArgs, spawnSyncImpl, providerEnvironment) : { ok: null }; return { authenticated: auth.ok, authentication: auth.ok == null ? "unknown" : auth.ok ? "authenticated" : "unauthenticated", diff --git a/docs/swe-compliance/2026-08-19-provider-preflight-credentials.md b/docs/swe-compliance/2026-08-19-provider-preflight-credentials.md new file mode 100644 index 0000000..48d4133 --- /dev/null +++ b/docs/swe-compliance/2026-08-19-provider-preflight-credentials.md @@ -0,0 +1,57 @@ +# Provider Credential Preflight + +## Phase 0: Baseline + +- Agent Host launch plans build the provider-declared environment, including + allowed credentials from RUDI secret storage. +- Agent Host version and authentication probes currently build only executable + `PATH`, so a valid managed credential can be invisible to readiness checks. +- Invariant: preflight and launch use the same provider environment contract, + and unrelated stored secrets are never forwarded. + +## Phase 1: Scope Lock + +- Touch one preflight implementation, one focused test, this checklist, and the + generated CLI bundle. +- Reuse the existing provider environment builder; do not alter provider + contracts, secret storage, or the dirty primary checkout. +- Stack on the green explicit Registry cache PR so full-suite verification is + not exposed to the already-proven cache race. + +## Phase 2: Red + +- Create an isolated temporary RUDI home with one declared Claude credential and + one unrelated secret. +- Run `node scripts/run-tests.js src/__tests__/unit/agent-host-preflight.test.js`. +- Expected failure: the authentication probe does not receive the declared + credential. + +## Phase 3: Implementation + +- Build the provider environment once per inspection from injected dependencies. +- Pass that environment through executable environment construction for both + version and authentication probes. + +## Phase 4: Green And Refactor + +- Status: complete. +- The unchanged focused test passes: 2 passed, 0 failed. +- Provider, environment, private automation, model, and preflight suites pass: + 40 passed, 0 failed. +- No refactor was needed. + +## Phase 5: Full Verification + +- Status: complete. +- `pnpm test`: 635 passed, 0 failed. +- `pnpm build`: passed; a second build produced identical hashes for all three + distribution artifacts. +- Focused architecture debt scan: 0 findings. +- `npm pack --dry-run --json`: 6 expected files. +- `git diff --check`: passed. +- No live provider or secret-storage calls were made; the regression uses only a + temporary fake secret file and injected process stubs. + +## Phase 6: Closure + +- Publish a ready stacked PR linked to CLI issue #23. diff --git a/src/__tests__/unit/agent-host-preflight.test.js b/src/__tests__/unit/agent-host-preflight.test.js index 5c98014..74bfd9a 100644 --- a/src/__tests__/unit/agent-host-preflight.test.js +++ b/src/__tests__/unit/agent-host-preflight.test.js @@ -1,5 +1,7 @@ import { describe, test } from 'node:test'; import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; import path from 'node:path'; import { inspectAgentHost } from '../../agent-host/preflight.js'; @@ -22,4 +24,29 @@ describe('Agent Host preflight', () => { assert.equal(pathEntries.includes(path.dirname(binaryPath)), true); assert.equal(pathEntries.includes(path.dirname(process.execPath)), true); }); + + test('injects only provider-declared RUDI credentials into the authentication probe', async (t) => { + const rudiHome = fs.mkdtempSync(path.join(os.tmpdir(), 'rudi-agent-auth-')); + t.after(() => fs.rmSync(rudiHome, { force: true, recursive: true })); + fs.writeFileSync(path.join(rudiHome, 'secrets.json'), JSON.stringify({ + CLAUDE_CODE_OAUTH_TOKEN: 'oauth-test-token', + UNRELATED_SECRET: 'must-not-be-forwarded', + }), { mode: 0o600 }); + + const calls = []; + const inspected = await inspectAgentHost('claude', { + baseEnvironment: { PATH: '/usr/bin:/bin' }, + binaryPath: '/Users/example/.local/bin/claude', + rudiHome, + spawnSyncImpl(command, args, options) { + calls.push({ args, command, options }); + return { status: 0, stdout: args.includes('--version') ? 'claude 1.0.0' : 'Logged in' }; + }, + }); + + const authCall = calls.find(call => call.args.join(' ') === 'auth status'); + assert.equal(inspected.authenticated, true); + assert.equal(authCall.options.env.CLAUDE_CODE_OAUTH_TOKEN, 'oauth-test-token'); + assert.equal(authCall.options.env.UNRELATED_SECRET, undefined); + }); }); diff --git a/src/agent-host/preflight.js b/src/agent-host/preflight.js index 3af05cd..b651131 100644 --- a/src/agent-host/preflight.js +++ b/src/agent-host/preflight.js @@ -12,7 +12,10 @@ import { resolveAgentProviderBinary, resolveAgentProviderId, } from './providers/index.js'; -import { buildAgentExecutableEnvironment } from './providers/common.js'; +import { + buildAgentExecutableEnvironment, + buildProviderEnvironment, +} from './providers/common.js'; const MCP_AGENT_IDS = Object.freeze({ claude: 'claude-code' }); @@ -20,10 +23,10 @@ function commandArgs(configuredCommand) { return Array.isArray(configuredCommand) ? configuredCommand.slice(1) : []; } -function runCheck(binaryPath, args, spawnSyncImpl, timeout = 5000) { +function runCheck(binaryPath, args, spawnSyncImpl, providerEnvironment, timeout = 5000) { const result = spawnSyncImpl(binaryPath, args, { encoding: 'utf8', - env: buildAgentExecutableEnvironment(binaryPath), + env: buildAgentExecutableEnvironment(binaryPath, providerEnvironment), timeout, }); return { @@ -74,12 +77,21 @@ export async function inspectAgentHost(provider, dependencies = {}) { }; } - const version = runCheck(binaryPath, commandArgs(config.binary.checkCommand), spawnSyncImpl); + const providerEnvironment = buildProviderEnvironment(config, { + baseEnvironment: dependencies.baseEnvironment, + rudiHome: dependencies.rudiHome, + }); + const version = runCheck( + binaryPath, + commandArgs(config.binary.checkCommand), + spawnSyncImpl, + providerEnvironment, + ); const authArgs = commandArgs(config.binary.authCheck); const versionArgs = commandArgs(config.binary.checkCommand); const authIsObservable = JSON.stringify(authArgs) !== JSON.stringify(versionArgs); const auth = authIsObservable - ? runCheck(binaryPath, authArgs, spawnSyncImpl) + ? runCheck(binaryPath, authArgs, spawnSyncImpl, providerEnvironment) : { ok: null }; return {