feat: voice command mapping — context-gated approve/reject over voice
While a tool-permission gate is held on the active tab, spoken confirm phrases resolve it via the existing approve/reject WS channel — no server change (byte-shuttle intact). Everything else stays ordinary dictation. Safety: whole-utterance-exact matching + leading-negation guard + reject precedence + confidence gate (approve only) + a cancellable 1.5s confirm window whose commit re-validates gate identity/epoch/connectivity (TOCTOU guard) + stale-gate epoch bound at PTT-start. New pure modules public/voice-commands.ts and public/voice-confirm.ts at 100% coverage; wiring in voice/terminal-session/tabs/main. 1307 tests pass, typecheck clean, build:web OK. Independent code + security reviews flagged a confirm-window TOCTOU and an unwired cancel path — both fixed and covered. Plan: docs/PLAN_VOICE_COMMANDS.md.
This commit is contained in:
@@ -95,6 +95,11 @@ export class TerminalSession {
|
||||
private pendingToolValue: string | undefined = undefined
|
||||
private telemetryValue: StatusTelemetry | null = null
|
||||
private pendingGateValue: PermissionGate | null = null
|
||||
// VC: nonce that increments on every false→true pendingApproval flip — lets a
|
||||
// caller (e.g. a voice command captured at PTT-start) detect a stale gate: a
|
||||
// slow transcript must not resolve a NEWER held permission than the one it
|
||||
// was spoken against (PLAN_VOICE_COMMANDS.md §5, safeguard 5).
|
||||
private pendingEpochValue = 0
|
||||
|
||||
private ws: WebSocket | null = null
|
||||
private sessionId: string | null
|
||||
@@ -189,6 +194,11 @@ export class TerminalSession {
|
||||
return this.pendingGateValue
|
||||
}
|
||||
|
||||
/** Nonce counting false→true pendingApproval flips (VC stale-gate guard, §5). */
|
||||
get pendingEpoch(): number {
|
||||
return this.pendingEpochValue
|
||||
}
|
||||
|
||||
private setStatus(s: SessionStatus): void {
|
||||
this.statusValue = s
|
||||
this.onStatus?.(s)
|
||||
@@ -293,9 +303,11 @@ export class TerminalSession {
|
||||
break
|
||||
}
|
||||
case 'status': {
|
||||
const nextPending = msg.pending === true
|
||||
if (nextPending && !this.pendingApprovalValue) this.pendingEpochValue++
|
||||
this.claudeStatusValue = msg.status
|
||||
this.pendingApprovalValue = msg.pending === true
|
||||
this.pendingToolValue = msg.pending === true ? msg.detail : undefined
|
||||
this.pendingApprovalValue = nextPending
|
||||
this.pendingToolValue = nextPending ? msg.detail : undefined
|
||||
this.pendingGateValue = msg.gate ?? null
|
||||
this.onClaudeStatus?.(msg.status, msg.detail)
|
||||
break
|
||||
|
||||
Reference in New Issue
Block a user