Skip to content
Open
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
18 changes: 13 additions & 5 deletions src/cli/commands/invoke/command.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,20 @@ async function handleInvokeCLI(options: InvokeOptions, preloadedContext?: Invoke
}
}

function redactSensitiveText(value: string): string {
return value
.replace(/(bearer\s+)[a-z0-9\-._~+/]+=*/gi, '$1[REDACTED]')
.replace(/(client[_-]?secret\s*[:=]\s*)([^,\s]+)/gi, '$1[REDACTED]')
.replace(/(token\s*[:=]\s*)([^,\s]+)/gi, '$1[REDACTED]');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regexes don't match JSON-quoted forms — the most common shape this PR is meant to cover.

The client_secret and token patterns require secret / token to be followed (after optional whitespace) by a literal : or =. In JSON output (the PR's stated motivating case — --json), the actual shape is:

"client_secret":"abc123"
"token":"abc123"
"access_token":"jwt.token.here"

There's a " between the key and the :, so \s*[:=]\s* does not match and these values are not redacted. Quick check:

redact('{"client_secret":"abc123"}')
// => '{"client_secret":"abc123"}'   (unchanged)
redact('{"token":"abc123"}')
// => '{"token":"abc123"}'           (unchanged)

This means agent responses or upstream error bodies that echo headers/tokens as JSON (a realistic case for misbehaving harnesses or 4xx response bodies) still leak.

Options:

  1. Allow an optional " (or any non-word char) between the key and the separator, e.g. /(client[_-]?secret["']?\s*[:=]\s*["']?)([^"',\s}]+)/gi and similarly for token.
  2. Add separate JSON-shape patterns alongside the existing key=value / key: value ones.
  3. Redact the known-sensitive value directly when we have it (e.g., when options.bearerToken is set, do a literal replaceAll(options.bearerToken, '[REDACTED]') in addition to the heuristic regexes). This is the most reliable for the bearer case and is independent of the surrounding syntax.

Whichever path you take, please widen the value character class so the closing " / } aren't consumed as part of the redacted value (the current [^,\s]+ would swallow them when no comma/whitespace follows).

}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No tests for redactSensitiveText.

This is a security-sensitive helper that's easy to get subtly wrong (see the JSON-shape comment above — a unit test would have caught it immediately). A small table-driven test in src/cli/commands/invoke/__tests__/ covering at least:

  • Bearer <jwt> in a plain string and inside a JSON-stringified body
  • client_secret=... and "client_secret":"..."
  • token=... and "access_token":"..."
  • A negative case (no sensitive content → unchanged)

would be enough to lock down the behavior and prevent regressions like the one this PR is fixing.


function printInvokeResult(result: InvokeResult, options: InvokeOptions): void {
if (options.json) {
console.log(JSON.stringify(serializeResult(result)));
const serialized = serializeResult(result);
if (typeof serialized.response === 'string') serialized.response = redactSensitiveText(serialized.response);
if (typeof serialized.error === 'string') serialized.error = redactSensitiveText(serialized.error);
console.log(JSON.stringify(serialized));
} else if (options.stream) {
// Streaming already wrote to stdout, just show session and log path
if (result.sessionId) {
console.error(`\nSession: ${result.sessionId}`);
console.error(`To resume: agentcore invoke --session-id ${result.sessionId}`);
Expand All @@ -100,11 +109,10 @@ function printInvokeResult(result: InvokeResult, options: InvokeOptions): void {
console.error(`Log: ${result.logFilePath}`);
}
} else {
// Non-streaming, non-json: print provider info and response or error
if (result.success && result.response) {
console.log(result.response);
console.log(redactSensitiveText(result.response));
} else if (!result.success && result.error) {
console.error(result.error.message);
console.error(redactSensitiveText(result.error.message));
}
if (result.sessionId) {
console.error(`\nSession: ${result.sessionId}`);
Expand Down
Loading