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
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { formatRedactedJson, formatToolIntent } from '@maka/ui';
import { formatToolInvocationLine, projectToolArgsPreview } from '@maka/core/tool-quiet-preview';

describe('tool args redaction', () => {
it('redacts JSON-shaped args before they are rendered', () => {
Expand All @@ -44,4 +45,24 @@ describe('tool args redaction', () => {
assert.ok(rendered.length <= 241);

});

it('keeps secrets out of the collapsed-row invocation line and its wire preview', () => {
// Built at runtime so no literal secret ever sits in the repo.
const bearerToken = ['sk', 'live', 'test', '9f8e7d6c5b4a'].join('-');
const passwordValue = ['maka', 'pw', '1a2b3c4d'].join('-');
const args = {
command: `curl -H "Authorization: Bearer ${bearerToken}" https://example.test`,
password: passwordValue,
};
const line = formatToolInvocationLine({ toolName: 'Bash', args }, 'en');
assert.ok(line !== undefined);
assert.doesNotMatch(line, new RegExp(bearerToken));
assert.match(line, /redacted/i);

const preview = projectToolArgsPreview('Bash', args);
const serialized = JSON.stringify(preview ?? null);
assert.doesNotMatch(serialized, new RegExp(bearerToken));
assert.doesNotMatch(serialized, new RegExp(passwordValue));
assert.doesNotMatch(serialized, /password/);
});
});
118 changes: 118 additions & 0 deletions packages/cli/src/__tests__/pi-transcript.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4249,6 +4249,124 @@ describe('Maka Pi TUI transcript', () => {
}
});

test('names a live quiet Bash row from the wire args preview', () => {
const state = createMakaPiTranscriptState();
// Runtime Host live tool_start omits full args; the bounded preview is all
// the compact row has until the turn-end reconcile.
applyMakaSessionEventToTranscript(
state,
event({
type: 'tool_start',
toolUseId: 'bash-preview',
toolName: 'Bash',
args: undefined,
argsPreview: { command: 'git status --porcelain' },
}),
);
applyMakaSessionEventToTranscript(
state,
event({
type: 'tool_result',
toolUseId: 'bash-preview',
isError: false,
content: {
kind: 'terminal',
cwd: '/repo',
cmd: 'git status --porcelain',
status: 'completed',
exitCode: 0,
output: { mode: 'pipes', stdout: '', stderr: '' },
},
}),
);

const rendered = renderMakaPiTranscript(state, meta(), 80).map(stripAnsi).join('\n');
assert.match(rendered, /\$ git status --porcelain/);
// Once the row names the call, the quiet-success disclaimer is noise.
assert.doesNotMatch(rendered, /\(no output\)/);
});

test('never renders a secret Bash command from the durable shell_run result', () => {
const state = createMakaPiTranscriptState();
const secret = 'super-secret-token-value';
const command = `# preserve the multiline result-side path\ncurl -H \"Authorization: Bearer ${secret}\" https://example.com`;
applyMakaSessionEventToTranscript(
state,
event({
type: 'tool_start',
toolUseId: 'bash-durable-redaction',
toolName: 'Bash',
args: { command },
}),
);
applyMakaSessionEventToTranscript(
state,
event({
type: 'tool_result',
toolUseId: 'bash-durable-redaction',
isError: false,
content: shellRun({
cmd: command,
status: 'completed',
completedAt: 2_000,
exitCode: 0,
}),
}),
);
assert.equal(toggleAllToolExpansion(state), true);

const rendered = renderMakaPiTranscript(state, meta(), 100).map(stripAnsi).join('\n');
assert.doesNotMatch(rendered, new RegExp(secret));
assert.match(rendered, /redacted/i);
});

test('keeps the no-output placeholder when the row cannot name the call', () => {
const state = createMakaPiTranscriptState();
applyMakaSessionEventToTranscript(
state,
event({ type: 'tool_start', toolUseId: 'bash-blind', toolName: 'Bash', args: undefined }),
);
applyMakaSessionEventToTranscript(
state,
event({
type: 'tool_result',
toolUseId: 'bash-blind',
isError: false,
content: {
kind: 'terminal',
cwd: '/repo',
cmd: 'true',
status: 'completed',
exitCode: 0,
output: { mode: 'pipes', stdout: '', stderr: '' },
},
}),
);

const rendered = renderMakaPiTranscript(state, meta(), 80).map(stripAnsi).join('\n');
assert.match(rendered, /\(no output\)/);
});

test('names a task_create row by its first subject, not a JSON dump', () => {
const state = createMakaPiTranscriptState();
applyMakaSessionEventToTranscript(
state,
event({
type: 'tool_start',
toolUseId: 'task-1',
toolName: 'task_create',
displayName: 'Task Create',
args: undefined,
argsPreview: { tasks: [{ subject: '修复登录 bug' }], tasksTotal: 2 },
}),
);

const rendered = renderMakaPiTranscript(state, meta(), 80).map(stripAnsi).join('\n');
assert.match(rendered, /修复登录 bug/);
assert.doesNotMatch(rendered, /tasks:/);
assert.doesNotMatch(rendered, /\(no output\)/);
});

test('orders and de-dupes tool_output_delta by seq and marks redacted chunks', () => {
const state = createMakaPiTranscriptState();
applyMakaSessionEventToTranscript(
Expand Down
29 changes: 23 additions & 6 deletions packages/cli/src/pi-transcript-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import type { ToolOutputStream, ToolResultContent } from '@maka/core/events';
import { formatQuietJsonValue, formatToolInvocationLine } from '@maka/core/tool-quiet-preview';
import { redactSecrets } from '@maka/core/redaction';
import {
isActiveShellRunStatus,
type PtyShellOutput,
Expand Down Expand Up @@ -93,14 +94,19 @@ function toolDurationText(entry: MakaPiToolEntry): string {
* expand, so the row needs neither
* a separator glyph nor an expand marker. Short annotations are reserved
* whole during truncation: a long command can never hide an `exit 1`.
*
* The `no output` placeholder appears only when the row cannot name the call
* (no input summary): once the target says what ran, `● Bash $ git add -A`
* reads complete on its own and the disclaimer is noise.
*/
function renderCompactToolBlock(entry: MakaPiToolEntry, width: number): string[] {
const inputSummary = collapseToSingleLine(toolInputSummary(entry));
const head = `${toolDisc(entry)} ${entry.title ?? entry.toolName}`;
const annotation = compactAnnotation(entry);
const annotationText = annotation.placeholderOnly && inputSummary ? '' : annotation.text;
return [
fitLine(
assembleCompactToolRow(head, inputSummary, annotation.text, width, annotation.protect),
assembleCompactToolRow(head, inputSummary, annotationText, width, annotation.protect),
width,
),
];
Expand All @@ -114,19 +120,26 @@ function renderCompactToolBlock(entry: MakaPiToolEntry, width: number): string[]
* `protect` reports whether every part is a fixed shape (durations always
* are): only protected annotations are reserved whole during truncation.
*/
function compactAnnotation(entry: MakaPiToolEntry): { text: string; protect: boolean } {
function compactAnnotation(entry: MakaPiToolEntry): {
text: string;
protect: boolean;
/** True when the annotation is solely the dim `no output` placeholder. */
placeholderOnly: boolean;
} {
const parts: string[] = [];
const duration = toolDurationText(entry);
if (duration) parts.push(duration);
let protect = true;
let placeholderOnly = false;
if (makaPiToolPresentationStatus(entry) !== 'running') {
const summary = compactToolSummary(entry);
if (summary && !(summary.placeholder && parts.length > 0)) {
parts.push(collapseToSingleLine(summary.text));
protect = summary.protect === true;
placeholderOnly = summary.placeholder === true && parts.length === 1;
}
}
return { text: parts.length > 0 ? `(${parts.join(' · ')})` : '', protect };
return { text: parts.length > 0 ? `(${parts.join(' · ')})` : '', protect, placeholderOnly };
}

/**
Expand Down Expand Up @@ -679,7 +692,7 @@ function renderShellRunResult(
const inputShowsFullCommand =
typeof command === 'string' && command.trim() !== '' && !command.includes('\n');
if (!inputShowsFullCommand) {
lines.push(...renderIndented(ansi.dim(`$ ${content.cmd}`), width, 2));
lines.push(...renderIndented(ansi.dim(`$ ${redactSecrets(content.cmd)}`), width, 2));
}
lines.push(...renderIndented(ansi.dim(`cwd: ${content.cwd}`), width, 2));
const settled = !isActiveShellRunStatus(content.status) && content.status !== 'completed';
Expand Down Expand Up @@ -756,7 +769,7 @@ function toolInputSummary(entry: MakaPiToolEntry): string {
.split('\n')
.map((line) => line.trim())
.find((line) => line !== '' && !line.startsWith('#'));
return `$ ${firstRealLine ?? command.split('\n')[0]!.trim()}`;
return `$ ${redactSecrets(firstRealLine ?? command.split('\n')[0]!.trim())}`;
}
break;
}
Expand Down Expand Up @@ -823,7 +836,11 @@ function toolInputSummary(entry: MakaPiToolEntry): string {
const line = formatToolInvocationLine({ toolName: entry.toolName, args: input }, 'en');
if (line) return limitText(line, 600);
// Absolute last resort — still single-line for the compact header contract.
return `input: ${limitText(formatUnknownInline(input), 600)}`;
// An empty args object carries no information; leave the row bare instead of
// printing `input: {}` noise (and let a quiet result keep its placeholder).
const inline = formatUnknownInline(input);
if (inline === '{}') return '';
return `input: ${limitText(inline, 600)}`;
}

/**
Expand Down
5 changes: 4 additions & 1 deletion packages/cli/src/pi-transcript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -821,7 +821,10 @@ export function applyMakaSessionEventToTranscript(
toolUseId: event.toolUseId,
toolName: event.toolName,
...(event.displayName ? { title: event.displayName } : {}),
input: projectToolActivityArgs(event.toolName, event.args),
// Live Runtime Host frames omit full args; the bounded wire preview
// still lets the compact row name the call. The turn-end reconcile
// replaces it with the durable full args.
input: projectToolActivityArgs(event.toolName, event.args ?? event.argsPreview),
resultVersion: 0,
progress: createProgressBuffer(),
outputDeltas: createOutputBuffer(),
Expand Down
Loading