fix: block credential-bearing harness arguments

This commit is contained in:
Brad Groux 2026-07-23 11:52:54 -05:00
parent 3c44c001d3
commit 19f57475fd
10 changed files with 267 additions and 12 deletions

View file

@ -172,6 +172,8 @@ Do not run `npm install`, `yarn`, or `bun install`. If lockfile conflicts arise,
- **Path traversal.** `validatePathSegment()` + `ensureWithinBase()` on every user-supplied path.
- **Env passthrough.** Agents receive only the keys in the configured safe allowlist; see
`server/src/utils/codex-env.ts` and `server/src/utils/hermes-env.ts`.
- **Launch arguments.** Never put credential values in provider commands or arguments; use an
allowlisted environment key or run-scoped brokered credential reference.
- **Log redaction.** Trace logs and telemetry run through `TRACE_SECRET_PATTERNS` before storage.
- **No credentials in PR descriptions, test fixtures, or log snippets.**

View file

@ -22,7 +22,10 @@ The profile records stable profile and adapter IDs, transport, executable and
non-mutating authentication probes, version/build invalidation policy,
platforms, launch/worktree behavior, environment and credential allowlists,
conformance fixture identity, documentation, and remediation. The contract
contains credential key names only, never credential values.
contains credential key names only, never credential values. Credential-like
launch arguments are replaced with `[REDACTED]` before the profile is exposed
or hashed, so rotating a secret cannot turn the profile digest into a secret
oracle.
The live status projection uses five tiers:
@ -44,7 +47,10 @@ Task start rechecks the normalized profile before attempt state is created. An
explicit provider must match the profile's executable adapter. A display-only
Claude Code or Copilot profile, an unsupported provider, or an unknown
provider-less profile fails with an actionable `409` and can never fall through
to OpenClaw.
to OpenClaw. Recognized credential material in the configured command or launch
arguments degrades the profile and blocks dispatch before probing or attempt
creation. Put credentials in an allowlisted environment key or a run-scoped
brokered credential reference instead.
For backward compatibility, normalization migrates only known provider-less
Codex and Hermes records when both the built-in type and command identity match

View file

@ -36,4 +36,88 @@ describe('HarnessSupportProfileSchema', () => {
})
).toThrow();
});
it('redacts credential-bearing launch arguments without encoding values in the digest', () => {
const base: AgentConfig = {
type: 'custom-secure-runner',
name: 'Secure Runner',
command: 'runner',
args: [
'--api-key',
'first-sensitive-value',
'--token=inline-sensitive-value',
'--credentials=credential-sensitive-value',
],
enabled: true,
provider: 'codex-cli',
};
const first = normalizeHarnessSupportProfile(base);
const rotated = normalizeHarnessSupportProfile({
...base,
args: [
'--api-key',
'rotated-sensitive-value',
'--token=inline-sensitive-value',
'--credentials=rotated-credential-sensitive-value',
],
});
expect(first.launch.args).toEqual([
'--api-key',
'[REDACTED]',
'--token=[REDACTED]',
'--credentials=[REDACTED]',
]);
expect(JSON.stringify(first)).not.toContain('first-sensitive-value');
expect(JSON.stringify(first)).not.toContain('inline-sensitive-value');
expect(JSON.stringify(first)).not.toContain('credential-sensitive-value');
expect(first.compatibility.configurationDigest).toBe(rotated.compatibility.configurationDigest);
expect(first).toMatchObject({
supportTier: 'degraded',
supportReason: expect.stringMatching(/credential material/i),
});
expect(() => HarnessSupportProfileSchema.parse(first)).not.toThrow();
});
it('redacts and degrades a command containing a credential argument', () => {
const profile = normalizeHarnessSupportProfile({
type: 'custom-secure-runner',
name: 'Secure Runner',
command:
'runner token=inline-command-sensitive-value --authorization command-sensitive-value',
args: [],
enabled: true,
provider: 'codex-cli',
});
expect(profile).toMatchObject({
supportTier: 'degraded',
executable: {
command: 'runner token=[REDACTED] --authorization [REDACTED]',
},
});
expect(JSON.stringify(profile)).not.toContain('inline-command-sensitive-value');
expect(JSON.stringify(profile)).not.toContain('command-sensitive-value');
});
it('rejects unredacted credential material anywhere in public profile evidence', () => {
const profile = normalizeHarnessSupportProfile({
type: 'codex',
name: 'OpenAI Codex',
command: 'codex',
args: [],
enabled: true,
provider: 'codex-cli',
});
expect(() =>
HarnessSupportProfileSchema.parse({
...profile,
conformance: {
...profile.conformance,
providerBuild: 'build token=unredacted-sensitive-value',
},
})
).toThrow(/credentials|secrets/i);
});
});

View file

@ -312,4 +312,34 @@ describe('evaluateHarnessSupportStatus', () => {
expect(status.diagnosticCommands).toContain('codex login status --token=[REDACTED]');
expect(JSON.stringify(status)).not.toContain('diagnostic-secret');
});
it('degrades unsafe launch configuration before evaluating runtime readiness', () => {
const candidate = agent({
type: 'custom-secure-runner',
name: 'Secure Runner',
command: 'codex',
args: ['--api-key', 'status-sensitive-value'],
provider: 'codex-cli',
});
const status = evaluateHarnessSupportStatus(candidate, {
type: candidate.type,
name: candidate.name,
enabled: true,
configured: true,
command: candidate.command,
executableFound: true,
authenticated: true,
healthy: true,
checkedAt: '2026-07-23T16:00:00.000Z',
});
expect(status).toMatchObject({
profileId: 'openai-codex-cli',
supportTier: 'degraded',
failureClass: 'unsafe-configuration',
reason: expect.stringMatching(/credential material/i),
});
expect(JSON.stringify(status)).not.toContain('status-sensitive-value');
});
});

View file

@ -1,4 +1,4 @@
import { afterEach, describe, expect, it } from 'vitest';
import { afterEach, describe, expect, it, vi } from 'vitest';
import type { AgentConfig } from '@veritas-kanban/shared';
import { ClawdbotAgentService } from '../services/clawdbot-agent-service.js';
import type { AgentHealthChecker } from '../services/agent-health-service.js';
@ -127,6 +127,29 @@ describe('ClawdbotAgentService provider runtime adapters', () => {
});
});
it('fails closed when launch arguments contain credential material', async () => {
const checkAgent = vi.fn(health.checkAgent);
await expect(
new ClawdbotAgentService({ checkAgent }).probeProviderRuntime({
type: 'custom-secure-runner',
name: 'Secure Runner',
command: 'codex',
args: ['--api-key', 'sensitive-launch-value'],
enabled: true,
provider: 'codex-cli',
})
).rejects.toMatchObject({
statusCode: 409,
code: 'CONFLICT',
details: expect.objectContaining({
profileId: 'openai-codex-cli',
reason: 'Credential material is not allowed in harness launch commands or arguments',
}),
});
expect(checkAgent).not.toHaveBeenCalled();
});
it('rejects a new custom provider-less profile even when its command is codex', async () => {
await expect(
new ClawdbotAgentService(health).probeProviderRuntime({

View file

@ -124,8 +124,17 @@ export const HarnessSupportProfileSchema = z
});
}
const publicText = [
profile.displayName,
profile.supportReason,
profile.executable.command,
...profile.executable.versionArgs,
...(profile.authentication.commandArgs ?? []),
profile.compatibility.policy,
...profile.compatibility.testedVersions,
...profile.launch.args,
...(profile.conformance.providerVersion ? [profile.conformance.providerVersion] : []),
...(profile.conformance.providerBuild ? [profile.conformance.providerBuild] : []),
profile.documentationUrl,
...profile.remediation,
];
if (publicText.some(containsUnredactedProviderRuntimeSecret)) {

View file

@ -414,8 +414,8 @@ export class ClawdbotAgentService {
model: profileLaunch.model ?? agentConfig.model,
}
: agentConfig;
const agentHealth = await this.assertAgentAvailable(agent, profileAgentConfig);
const provider = this.resolveAgentProvider(profileAgentConfig, agent);
const agentHealth = await this.assertAgentAvailable(agent, profileAgentConfig);
const adapter = this.resolveProviderAdapter(provider);
const budgetService = getAgentBudgetService();
const budgetPolicy = budgetService.resolve({
@ -1194,8 +1194,8 @@ export class ClawdbotAgentService {
agent: AgentType = agentConfig.type,
surface: ProviderRuntimeSurface = 'task'
): Promise<ProviderRuntimeManifest> {
const health = await this.assertAgentAvailable(agent, agentConfig);
const provider = this.resolveAgentProvider(agentConfig, agent);
const health = await this.assertAgentAvailable(agent, agentConfig);
return this.resolveProviderAdapter(provider, surface).probe({ agentConfig, health });
}
@ -1281,6 +1281,19 @@ export class ClawdbotAgentService {
// dispatch boundary. A caller-provided supportProfile may carry future
// certification evidence, but it cannot authorize a different adapter.
const profile = agentConfig ? normalizeHarnessSupportProfile(agentConfig) : undefined;
if (profile?.supportTier === 'degraded') {
throw new ConflictError(
`Harness support profile "${profile.id}" has an unsafe launch configuration`,
{
agent,
profileId: profile.id,
adapterId: profile.adapterId,
provider,
reason: 'Credential material is not allowed in harness launch commands or arguments',
remediation: profile.remediation,
}
);
}
if (profile && profile.adapterId !== provider) {
throw new ConflictError(
`Harness support profile "${profile.id}" cannot dispatch through "${provider}"`,

View file

@ -5,6 +5,10 @@ import {
type HarnessSupportProfile,
type HarnessTransport,
} from '@veritas-kanban/shared';
import {
containsUnredactedProviderRuntimeSecret,
sanitizeProviderRuntimeDiagnostic,
} from '../utils/provider-runtime-manifest-sanitize.js';
const ALL_PLATFORMS: HarnessSupportProfile['platforms'] = ['darwin', 'linux', 'win32'];
const INVALIDATION_KEYS: HarnessSupportProfile['compatibility']['invalidateOn'] = [
@ -50,6 +54,16 @@ interface ProfileDefinition {
remediation: string[];
}
interface RedactedLaunchArgs {
args: string[];
containsCredentialMaterial: boolean;
}
interface RedactedCommand {
command: string;
containsCredentialMaterial: boolean;
}
const DEFINITIONS: Record<string, ProfileDefinition> = {
'claude-code': unsupported(
'claude-code',
@ -157,8 +171,12 @@ export function normalizeHarnessSupportProfile(agent: AgentConfig): HarnessSuppo
);
const executableProfile = Boolean(definition.adapterId);
const redactedCommand = redactCommand(agent.command);
const redactedLaunchArgs = redactLaunchArgs(agent.args);
const unsafeLaunchConfiguration =
redactedCommand.containsCredentialMaterial || redactedLaunchArgs.containsCredentialMaterial;
const executable = {
command: agent.command,
command: redactedCommand.command,
versionArgs: ['--version'],
};
const authentication = {
@ -166,7 +184,7 @@ export function normalizeHarnessSupportProfile(agent: AgentConfig): HarnessSuppo
nonMutating: true as const,
};
const launch = {
args: [...agent.args],
args: redactedLaunchArgs.args,
workingDirectory: 'task-worktree' as const,
worktree: 'required' as const,
environmentAllowlist: [
@ -188,6 +206,10 @@ export function normalizeHarnessSupportProfile(agent: AgentConfig): HarnessSuppo
platforms: ALL_PLATFORMS,
launch,
});
const unsafeConfigurationReason =
'Credential material is not allowed in harness launch commands or arguments.';
const unsafeConfigurationRemediation =
'Remove credential values from launch arguments and use an allowlisted environment key or run-scoped credential reference.';
return {
schemaVersion: HARNESS_SUPPORT_PROFILE_SCHEMA_VERSION,
@ -195,10 +217,16 @@ export function normalizeHarnessSupportProfile(agent: AgentConfig): HarnessSuppo
displayName: definition.displayName,
...(definition.adapterId ? { adapterId: definition.adapterId } : {}),
transport: definition.transport,
supportTier: executableProfile ? 'configured' : 'unsupported',
supportReason: executableProfile
? 'An explicit executable adapter is registered; live readiness requires a runtime probe.'
: (definition.remediation[0] ?? 'No executable adapter is registered.'),
supportTier: !executableProfile
? 'unsupported'
: unsafeLaunchConfiguration
? 'degraded'
: 'configured',
supportReason: !executableProfile
? (definition.remediation[0] ?? 'No executable adapter is registered.')
: unsafeLaunchConfiguration
? unsafeConfigurationReason
: 'An explicit executable adapter is registered; live readiness requires a runtime probe.',
executable,
authentication,
compatibility: {
@ -215,10 +243,65 @@ export function normalizeHarnessSupportProfile(agent: AgentConfig): HarnessSuppo
status: 'not-run',
},
documentationUrl: definition.documentationUrl,
remediation: [...definition.remediation],
remediation: [
...(unsafeLaunchConfiguration ? [unsafeConfigurationRemediation] : []),
...definition.remediation,
],
};
}
function redactCommand(command: string): RedactedCommand {
const containsDiagnosticSecret = containsUnredactedProviderRuntimeSecret(command);
const sanitized = containsDiagnosticSecret ? sanitizeProviderRuntimeDiagnostic(command) : command;
const redacted = redactLaunchArgs(sanitized.trim().split(/\s+/));
return containsDiagnosticSecret || redacted.containsCredentialMaterial
? {
command: redacted.args.join(' '),
containsCredentialMaterial: true,
}
: {
command,
containsCredentialMaterial: false,
};
}
function redactLaunchArgs(args: string[]): RedactedLaunchArgs {
const redacted: string[] = [];
let redactNext = false;
let containsCredentialMaterial = false;
for (const arg of args) {
if (redactNext) {
redacted.push('[REDACTED]');
redactNext = false;
containsCredentialMaterial = true;
continue;
}
const normalized = arg.trim();
const credentialArgument = normalized.match(
/^(--?(?:api[-_]?key|access[-_]?token|auth[-_]?token|token|secret|password|authorization|credentials?))(?:=(.*))?$/i
);
if (credentialArgument) {
const [, flag, inlineValue] = credentialArgument;
redacted.push(inlineValue === undefined ? flag : `${flag}=[REDACTED]`);
redactNext = inlineValue === undefined;
containsCredentialMaterial = true;
continue;
}
if (containsUnredactedProviderRuntimeSecret(arg)) {
redacted.push(sanitizeProviderRuntimeDiagnostic(arg));
containsCredentialMaterial = true;
continue;
}
redacted.push(arg);
}
return { args: redacted, containsCredentialMaterial };
}
function digestConfiguration(value: unknown): string {
return `sha256:${createHash('sha256').update(JSON.stringify(value)).digest('hex')}`;
}

View file

@ -110,6 +110,10 @@ export function evaluateHarnessSupportStatus(
);
}
if (profile.supportTier === 'degraded') {
return status(base, 'degraded', 'unsafe-configuration', profile.supportReason);
}
if (!health.executableFound) {
return status(
base,

View file

@ -80,6 +80,7 @@ export type HarnessSupportFailureClass =
| 'unauthenticated'
| 'incompatible-build'
| 'adapter-unavailable'
| 'unsafe-configuration'
| 'probe-failed'
| 'launch-failed'
| 'run-failed'