Skip to content

Commit a412849

Browse files
GiniGini
authored andcommitted
feat: distinguish safe CLI evidence in artifact rail
1 parent 4f1c78a commit a412849

3 files changed

Lines changed: 33 additions & 5 deletions

File tree

src/components/ComputerTimeline.test.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, expect, it } from 'vitest'
2-
import { activityPreviewFor, artifactRailItems, causalVisualItemsFor, defaultComputerItem, evidenceItemId, filterItemsByRun, formatDuration, matchesRailQuery, runIdsFor, terminalActivityFor, virtualRailRange, type ComputerItem } from './computer-timeline-activity'
2+
import { activityPreviewFor, artifactRailItems, causalVisualItemsFor, commandFor, defaultComputerItem, evidenceItemId, filterItemsByRun, formatDuration, matchesRailQuery, runIdsFor, terminalActivityFor, virtualRailRange, type ComputerItem } from './computer-timeline-activity'
33
import type { RuntimeEvent } from '../types'
44

55
const event = (id: string, type: string, payload: Record<string, unknown>, content?: string): RuntimeEvent => ({
@@ -71,6 +71,14 @@ describe('Computer timeline terminal inspection', () => {
7171
expect(activityPreviewFor({ input: { operation: 'write', paths: ['src/App.tsx', 'src/index.css'], api_key: 'do-not-show' } })).toBe('write · src/App.tsx, src/index.css')
7272
})
7373

74+
it('projects a CLI command without exposing credentials or host paths', () => {
75+
const command = commandFor({ command: 'API_KEY=not-for-the-rail node /Users/operator/project/build.mjs --token top-secret' })
76+
expect(command).toBe('API_KEY=<redacted> node <host-path> --token=<redacted>')
77+
const activity = terminalActivityFor({ id: 'command', kind: 'terminal', title: 'Bash', createdAt: '2026-07-16T00:00:00.000Z', payload: { input: { command: 'pwd' } } }, [])
78+
expect(activity.command).toBe('pwd')
79+
expect(activity.workspaceLabel).toBe('Sandbox workspace')
80+
})
81+
7482
it('folds a completed tool result into its originating rail card without changing evidence order', () => {
7583
const started: ComputerItem = { id: 'tool-start', kind: 'terminal', eventType: 'tool_call_started', title: 'Read', createdAt: '2026-07-16T00:00:00.000Z', eventHash: 'start-hash', payload: { toolUseId: 'tool-1', input: { path: 'README.md' } } }
7684
const completed: ComputerItem = { id: 'tool-finish', kind: 'terminal', eventType: 'tool_call_completed', title: 'Read complete', createdAt: '2026-07-16T00:00:01.000Z', eventHash: 'finish-hash', payload: { toolUseId: 'tool-1' } }

src/components/ComputerTimeline.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ const withCacheBust = (uri: string, value: number) => `${uri}${uri.includes('?')
1010
const ArtifactRailEntry = ({ item, previousRunId, index, selected, total, events, onMove }: { item: ComputerItem; previousRunId?: string; index: number; selected: boolean; total: number; events: TaskSnapshot['events']; onMove: (index: number) => void }) => {
1111
const activity = item.kind === 'terminal' ? terminalActivityFor(item, events) : undefined
1212
const elapsed = formatDuration(activity?.durationMs)
13-
return <div className="computer-rail-entry">{item.runId && item.runId !== previousRunId && <div className="computer-run-marker">Run {item.runId.slice(-6)}</div>}<button className={selected ? 'selected' : ''} aria-current={selected ? 'true' : undefined} onClick={() => onMove(index)} aria-label={`${item.title}, event ${index + 1} of ${total}`}><span>{iconFor(item)}</span><div><strong>{item.title}</strong>{item.activityPreview && <code title={item.activityPreview}>{item.activityPreview}</code>}{item.relatedEventIds?.length ? <small className={activity?.failed ? 'computer-pair-status failed' : 'computer-pair-status'}>{activity?.failed ? 'failed' : `complete${elapsed ? ` · ${elapsed}` : ''}`}</small> : <small>{new Date(item.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })}</small>}</div>{item.kind === 'screenshot' && item.uri && !item.live && <img className="computer-rail-thumbnail" src={item.uri} alt="" loading="lazy" />}</button></div>
13+
const title = activity?.command ? `CLI command · ${item.title}` : item.title
14+
return <div className="computer-rail-entry">{item.runId && item.runId !== previousRunId && <div className="computer-run-marker">Run {item.runId.slice(-6)}</div>}<button className={selected ? 'selected' : ''} aria-current={selected ? 'true' : undefined} onClick={() => onMove(index)} aria-label={`${title}, event ${index + 1} of ${total}`}><span>{iconFor(item)}</span><div><strong>{title}</strong>{item.activityPreview && <code title={item.activityPreview}>{item.activityPreview}</code>}{item.relatedEventIds?.length ? <small className={activity?.failed ? 'computer-pair-status failed' : 'computer-pair-status'}>{activity?.failed ? 'failed' : `complete${elapsed ? ` · ${elapsed}` : ''}`}</small> : <small>{new Date(item.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })}</small>}</div>{item.kind === 'screenshot' && item.uri && !item.live && <img className="computer-rail-thumbnail" src={item.uri} alt="" loading="lazy" />}</button></div>
1415
}
1516

1617
export const ComputerTimeline = ({ task }: { task: TaskSnapshot }) => {
@@ -145,7 +146,7 @@ export const ComputerTimeline = ({ task }: { task: TaskSnapshot }) => {
145146
{active?.kind === 'slide' && <div className="computer-file"><Presentation size={28} /><strong>{active.detail ?? active.title}</strong><span>Deck evidence is preserved. Open the Files tab to download the PPTX or inspect the rendered viewer.</span></div>}
146147
{active?.kind === 'approval' && <div className="computer-file"><CheckCircle2 size={28} /><strong>{active.title}</strong><span>{active.detail ?? 'Approval evidence is recorded separately from the browser and can be verified in the task history.'}</span></div>}
147148
{(active?.kind === 'file' || active?.kind === 'diff') && <div className="computer-file"><FileCode2 size={28} /><strong>{active.detail ?? active.title}</strong><span>{active.kind === 'diff' ? 'Open the Code tab to inspect the recorded version change.' : 'Open the Files or Code tab to inspect this artifact.'}</span></div>}
148-
{active?.kind === 'terminal' && <div className="computer-terminal"><div className="computer-terminal-meta"><span>{active.title}</span>{terminalActivity?.failed ? <b>tool error</b> : <em>{terminalActivity?.durationMs !== undefined ? `completed in ${formatDuration(terminalActivity.durationMs)}` : 'recorded activity'}</em>}</div>{active.payload?.browserTool === true && <small className="computer-browser-evidence">Governed browser evidence · sandbox only</small>}{terminalActivity?.request !== undefined && <section><label>Request</label><pre><code>{formatInspectable(terminalActivity.request)}</code></pre></section>}{terminalActivity?.output && <section><label>{terminalActivity.failed ? 'Error output' : 'Result'}</label><pre><code>{formatInspectable(terminalActivity.output)}</code></pre></section>}{relatedVisuals.length > 0 && <div className="computer-checkpoints"><span>{relatedVisuals.length} causal visual checkpoint{relatedVisuals.length === 1 ? '' : 's'}</span><div className="computer-checkpoint-gallery">{relatedVisuals.map((visual, index) => <button key={visual.id} onClick={() => inspectVisual(visual.id)} aria-label={`Inspect visual checkpoint ${index + 1} for ${active.title}`}><img src={withCacheBust(visual.uri ?? '', frame)} alt={`Checkpoint ${index + 1}: ${visual.title}`} loading="lazy" /><small>Frame {index + 1} · #{visual.sequence ?? '—'}</small></button>)}</div></div>}{terminalActivity?.toolUseId && <small>Tool call {terminalActivity.toolUseId.slice(-8)} · correlated with its paired result and visual checkpoints in this run.</small>}</div>}
149+
{active?.kind === 'terminal' && <div className="computer-terminal"><div className="computer-terminal-meta"><span>{terminalActivity?.command ? `CLI command · ${active.title}` : active.title}</span>{terminalActivity?.failed ? <b>tool error</b> : <em>{terminalActivity?.durationMs !== undefined ? `completed in ${formatDuration(terminalActivity.durationMs)}` : 'recorded activity'}</em>}</div>{active.payload?.browserTool === true && <small className="computer-browser-evidence">Governed browser evidence · sandbox only</small>}{terminalActivity?.command ? <section><label>Command · {terminalActivity.workspaceLabel}</label><pre><code>$ {terminalActivity.command}</code></pre></section> : terminalActivity?.request !== undefined && <section><label>Request</label><pre><code>{formatInspectable(terminalActivity.request)}</code></pre></section>}{terminalActivity?.output && <section><label>{terminalActivity.failed ? 'Error output' : terminalActivity.command ? 'Command output' : 'Result'}</label><pre><code>{formatInspectable(terminalActivity.output)}</code></pre></section>}{relatedVisuals.length > 0 && <div className="computer-checkpoints"><span>{relatedVisuals.length} causal visual checkpoint{relatedVisuals.length === 1 ? '' : 's'}</span><div className="computer-checkpoint-gallery">{relatedVisuals.map((visual, index) => <button key={visual.id} onClick={() => inspectVisual(visual.id)} aria-label={`Inspect visual checkpoint ${index + 1} for ${active.title}`}><img src={withCacheBust(visual.uri ?? '', frame)} alt={`Checkpoint ${index + 1}: ${visual.title}`} loading="lazy" /><small>Frame {index + 1} · #{visual.sequence ?? '—'}</small></button>)}</div></div>}{terminalActivity?.toolUseId && <small>Tool call {terminalActivity.toolUseId.slice(-8)} · correlated with its paired result and visual checkpoints in this run.</small>}</div>}
149150
</section>
150151
</div>
151152
}

src/components/computer-timeline-activity.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,25 @@ export type ComputerItem = {
2020

2121
const redactedKeys = new Set(['api_key', 'apikey', 'authorization', 'password', 'secret', 'token'])
2222

23+
const redactCommand = (command: string) => command
24+
.replace(/\b(authorization|api[_-]?key|token|password|secret)\s*=\s*([^\s'"`]+)/gi, '$1=<redacted>')
25+
.replace(/(--(?:api[-_]?key|token|password|secret))(?:=|\s+)([^\s'"`]+)/gi, '$1=<redacted>')
26+
.replace(/\bBearer\s+[^\s'"`]+/gi, 'Bearer <redacted>')
27+
.replace(/\/(?:Users|home)\/[^\s'"`]+/g, '<host-path>')
28+
29+
const recordValue = (value: unknown) => value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : undefined
30+
31+
export const commandFor = (value: unknown) => {
32+
const input = recordValue(value)
33+
return typeof input?.command === 'string' ? redactCommand(input.command) : undefined
34+
}
35+
2336
const compactValue = (value: unknown): string | undefined => {
2437
if (typeof value === 'string') return value.length > 104 ? `${value.slice(0, 101)}…` : value
2538
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined
2639
const input = value as Record<string, unknown>
27-
if (typeof input.command === 'string') return `$ ${input.command.length > 100 ? `${input.command.slice(0, 97)}…` : input.command}`
40+
const command = commandFor(input)
41+
if (command) return `$ ${command.length > 100 ? `${command.slice(0, 97)}…` : command}`
2842
const operation = typeof input.operation === 'string' ? input.operation : undefined
2943
const paths = Array.isArray(input.paths) ? input.paths.filter((path): path is string => typeof path === 'string').slice(0, 2) : []
3044
if (operation) return `${operation}${paths.length ? ` · ${paths.join(', ')}` : ''}`
@@ -114,12 +128,17 @@ export const terminalActivityFor = (item: ComputerItem, events: RuntimeEvent[])
114128
const toolUseId = typeof item.payload?.toolUseId === 'string' ? item.payload.toolUseId : undefined
115129
const paired = toolUseId ? events.find((event) => event.id !== item.id && event.type === 'tool_call_completed' && event.payload.toolUseId === toolUseId) : undefined
116130
const request = item.payload?.input ?? paired?.payload.input
131+
const command = commandFor(request)
117132
// A start event often contains a human-readable request summary. Prefer the
118133
// paired terminal result so the unified rail card reads request → outcome.
119134
const output = item.eventType === 'tool_call_started' ? paired?.content : item.detail ?? paired?.content
120135
const failed = item.payload?.isError === true || paired?.payload.isError === true
121136
const elapsed = paired && item.eventType === 'tool_call_started' ? Date.parse(paired.createdAt) - Date.parse(item.createdAt) : undefined
122-
return { request, output, failed, toolUseId, durationMs: elapsed !== undefined && Number.isFinite(elapsed) && elapsed >= 0 ? elapsed : undefined }
137+
return {
138+
request, output, failed, toolUseId, command,
139+
workspaceLabel: command ? 'Sandbox workspace' : undefined,
140+
durationMs: elapsed !== undefined && Number.isFinite(elapsed) && elapsed >= 0 ? elapsed : undefined,
141+
}
123142
}
124143

125144
export const causalVisualItemsFor = (eventId: string, items: ComputerItem[]) => items.filter((item) => item.kind === 'screenshot' && item.payload?.causedByEventId === eventId)

0 commit comments

Comments
 (0)