feat(cockpit): approval preview — command/diff above Approve/Reject (W1)
Remote one-tap approval was blind (you'd tap Approve without seeing Claude wants to run `rm -rf` or rewrite a config). The pending tool's actual command / diff now renders above the approval bar, on every attached device, riding the same broadcast + late-joiner rails as the existing `gate` field. - src/http/approval-preview.ts (new, pure, never-throws): deriveApprovalPreview — Bash → command; Edit/Write/MultiEdit/NotebookEdit → a synthetic DiffFile; else null. Every line sanitized via sanitizeField (strips control/ANSI); caps 40 lines / 200 chars/line / 4KB (security limits, UTF-8-safe byte clamp). - src/types.ts: additive optional `preview?: ApprovalPreview` on the status ServerMessage + handleHookEvent (older clients ignore it). - src/server.ts /hook/permission: derive preview from tool_input, store on the PendingApproval entry, re-send to late joiners exactly like `gate`. - manager.ts threads it; public/tabs.ts renderApprovalPreview (command → <pre> textContent; diff → reused innerHTML-free renderDiffFile). Unknown tools / plan gates fall back to today's name-only bar. Attacker-influenced tool input → rendered via textContent/diff-renderer only, never innerHTML. Verified independently: typecheck + build:web clean, 1692 pass.
This commit is contained in:
20
src/types.ts
20
src/types.ts
@@ -100,6 +100,21 @@ export type ClaudeStatus = 'working' | 'waiting' | 'idle' | 'unknown' | 'stuck';
|
||||
* gate (three-way approve/auto/keep-planning); 'tool' = an ordinary tool gate. */
|
||||
export type PermissionGate = 'tool' | 'plan';
|
||||
|
||||
/** W1: a compact, BOUNDED preview of what a held tool approval would run, so a
|
||||
* remote one-tap approval is no longer blind. Derived server-side from the hook
|
||||
* `tool_input` (attacker-influenced) — sanitized (control/ANSI chars stripped),
|
||||
* line-capped and byte-capped in src/http/approval-preview.ts. Discriminated on
|
||||
* `kind`:
|
||||
* - 'command' → a shell command string (Bash). Newlines are PRESERVED; every
|
||||
* other control/ANSI char is stripped. The FE renders it in a <pre> via
|
||||
* textContent only (never innerHTML).
|
||||
* - 'diff' → ONE synthetic DiffFile (Edit/Write/MultiEdit/NotebookEdit),
|
||||
* rendered by public/diff.ts renderDiffFile (textContent-only, SEC-H4).
|
||||
* `truncated` = the source exceeded the line/byte cap and was clipped. */
|
||||
export type ApprovalPreview =
|
||||
| { kind: 'command'; text: string; truncated?: boolean }
|
||||
| { kind: 'diff'; file: DiffFile; truncated?: boolean };
|
||||
|
||||
/** server → client. exit.code = shell code; -1 when spawn never succeeded (M4).
|
||||
* exit.reason optional normally, REQUIRED on spawn failure / abnormal exit.
|
||||
* status (H2/H3) = Claude Code activity; `pending` true when a tool approval
|
||||
@@ -116,6 +131,10 @@ export type ServerMessage =
|
||||
detail?: string;
|
||||
pending?: boolean;
|
||||
gate?: PermissionGate;
|
||||
/** W1: bounded preview of the held tool's command/diff; present only on a
|
||||
* held (pending) waiting status for a Bash/Edit-family tool. Older clients
|
||||
* ignore it (additive + optional). */
|
||||
preview?: ApprovalPreview;
|
||||
}
|
||||
| { type: 'telemetry'; telemetry: StatusTelemetry };
|
||||
|
||||
@@ -336,6 +355,7 @@ export interface SessionManager {
|
||||
gate?: PermissionGate,
|
||||
eventClass?: string,
|
||||
toolName?: string,
|
||||
preview?: ApprovalPreview,
|
||||
): void;
|
||||
/** B2: store the latest statusLine telemetry for a session and broadcast a
|
||||
* `telemetry` message to all attached clients. */
|
||||
|
||||
Reference in New Issue
Block a user