Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions dist/index.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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",
Expand Down
57 changes: 57 additions & 0 deletions docs/swe-compliance/2026-08-19-provider-preflight-credentials.md
Original file line number Diff line number Diff line change
@@ -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.
27 changes: 27 additions & 0 deletions src/__tests__/unit/agent-host-preflight.test.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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);
});
});
22 changes: 17 additions & 5 deletions src/agent-host/preflight.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,21 @@ 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' });

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 {
Expand Down Expand Up @@ -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 {
Expand Down