@scotthuang/agent-knock-knock 0.12.28 → 0.12.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/src/claude-injected-paste-proof.d.ts +59 -0
  3. package/dist/src/claude-injected-paste-proof.js +153 -0
  4. package/dist/src/claude-injected-paste-proof.js.map +1 -0
  5. package/dist/src/claude-terminal-agent-adapter.js +20 -1
  6. package/dist/src/claude-terminal-agent-adapter.js.map +1 -1
  7. package/dist/src/cli-core.js +50 -35
  8. package/dist/src/cli-core.js.map +1 -1
  9. package/dist/src/codex-terminal-agent-adapter.d.ts +2 -1
  10. package/dist/src/codex-terminal-agent-adapter.js +40 -4
  11. package/dist/src/codex-terminal-agent-adapter.js.map +1 -1
  12. package/dist/src/host-adapter.d.ts +1 -1
  13. package/dist/src/host-adapter.js +1 -1
  14. package/dist/src/host-bridge-tools.js +2 -2
  15. package/dist/src/openclaw-plugin-command-adapter.js +155 -24
  16. package/dist/src/openclaw-plugin-command-adapter.js.map +1 -1
  17. package/dist/src/openclaw-plugin-helpers.d.ts +2 -0
  18. package/dist/src/openclaw-plugin-helpers.js +28 -7
  19. package/dist/src/openclaw-plugin-helpers.js.map +1 -1
  20. package/dist/src/openclaw-plugin-schemas.d.ts +104 -0
  21. package/dist/src/openclaw-plugin-schemas.js +108 -0
  22. package/dist/src/openclaw-plugin-schemas.js.map +1 -1
  23. package/dist/src/openclaw-private-authority-offers.d.ts +2 -0
  24. package/dist/src/openclaw-private-authority-offers.js +12 -0
  25. package/dist/src/openclaw-private-authority-offers.js.map +1 -1
  26. package/dist/src/terminal-agent-adapter.d.ts +34 -0
  27. package/dist/src/terminal-agent-adapter.js +54 -0
  28. package/dist/src/terminal-agent-adapter.js.map +1 -1
  29. package/dist/src/terminal-agent-bridge.d.ts +76 -1
  30. package/dist/src/terminal-agent-bridge.js +873 -153
  31. package/dist/src/terminal-agent-bridge.js.map +1 -1
  32. package/dist/src/terminal-command-cli-adapter.d.ts +2 -1
  33. package/dist/src/terminal-command-cli-adapter.js +46 -8
  34. package/dist/src/terminal-command-cli-adapter.js.map +1 -1
  35. package/dist/src/terminal-dispatch-execution.js +15 -0
  36. package/dist/src/terminal-dispatch-execution.js.map +1 -1
  37. package/dist/src/terminal-identity-authority-cli-adapter.d.ts +1 -0
  38. package/dist/src/terminal-identity-authority-cli-adapter.js +10 -1
  39. package/dist/src/terminal-identity-authority-cli-adapter.js.map +1 -1
  40. package/dist/src/terminal-interaction-authority.d.ts +33 -0
  41. package/dist/src/terminal-interaction-authority.js +3 -0
  42. package/dist/src/terminal-interaction-authority.js.map +1 -0
  43. package/dist/src/terminal-interaction-cli-adapter.d.ts +66 -0
  44. package/dist/src/terminal-interaction-cli-adapter.js +354 -0
  45. package/dist/src/terminal-interaction-cli-adapter.js.map +1 -0
  46. package/dist/src/terminal-interaction-protocol.d.ts +111 -0
  47. package/dist/src/terminal-interaction-protocol.js +432 -0
  48. package/dist/src/terminal-interaction-protocol.js.map +1 -0
  49. package/dist/src/terminal-list-renderer.js +31 -2
  50. package/dist/src/terminal-list-renderer.js.map +1 -1
  51. package/dist/src/terminal-monitor-cli-adapter.js +12 -3
  52. package/dist/src/terminal-monitor-cli-adapter.js.map +1 -1
  53. package/dist/src/terminal-monitor-decision-policy.d.ts +1 -1
  54. package/dist/src/terminal-monitor-state-cli-adapter.js +15 -1
  55. package/dist/src/terminal-monitor-state-cli-adapter.js.map +1 -1
  56. package/dist/src/terminal-mutation-cli-runtime.d.ts +44 -0
  57. package/dist/src/terminal-mutation-cli-runtime.js +41 -0
  58. package/dist/src/terminal-mutation-cli-runtime.js.map +1 -0
  59. package/dist/src/terminal-questionnaire-adapter.d.ts +84 -0
  60. package/dist/src/terminal-questionnaire-adapter.js +677 -0
  61. package/dist/src/terminal-questionnaire-adapter.js.map +1 -0
  62. package/docs/host-bridge-profiles.md +2 -2
  63. package/docs/quickstart-herdr.md +1 -1
  64. package/docs/quickstart-tmux.md +3 -1
  65. package/openclaw.plugin.json +4 -0
  66. package/package.json +1 -1
  67. package/templates/openclaw-skills/agent-knock-knock/SKILL.md +11 -8
@@ -1,7 +1,10 @@
1
1
  import { createHash } from "node:crypto";
2
- import { formatTerminalConversationId, parseTerminalConversationId, terminalControlCapabilitiesForAdapter } from "./terminal-agent-adapter.js";
2
+ import { formatTerminalConversationId, parseTerminalConversationId, terminalControlCapabilitiesForAdapter, terminalApprovalActionForDecision, terminalApprovalChoices } from "./terminal-agent-adapter.js";
3
3
  import { enrichActiveProcessesWithTerminalControl, TerminalControlInputNotSentError } from "./terminal-control-provider.js";
4
4
  import { hasCanonicalTerminalEndpoint, sameTerminalControlIncarnation, terminalEndpointFromControlRef, terminalEndpointIdentityKey } from "./terminal-control-ref.js";
5
+ import { CLAUDE_INJECTED_PASTE_FRAME_PROFILE, claudeInjectedPasteProofEligible, revalidateClaudeComposerProof, settleClaudeInjectedPasteComposer } from "./claude-injected-paste-proof.js";
6
+ import { TERMINAL_INTERACTION_SCHEMA, TERMINAL_INTERACTION_VERSION, validateTerminalInteractionProjection, validateTerminalInteractionResponse } from "./terminal-interaction-protocol.js";
7
+ import { inspectNativeQuestionnaire } from "./terminal-questionnaire-adapter.js";
5
8
  // Verified Codex profiles through 0.153.0 keep Enter in paste/newline mode for
6
9
  // 120ms after burst input. Cross that boundary rather than landing on it, and
7
10
  // also require observable composer stability instead of treating this delay
@@ -139,6 +142,9 @@ const CLAUDE_NATIVE_STATUS_SETTLE_BY_PROFILE = {
139
142
  }
140
143
  };
141
144
  const CODEX_LARGE_PASTE_CHAR_THRESHOLD = 1_000;
145
+ const TERMINAL_INTERACTION_TTL_MS = 10 * 60 * 1_000;
146
+ const TERMINAL_INTERACTION_IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
147
+ const TERMINAL_INTERACTION_FINGERPRINT_PATTERN = /^[0-9a-f]{64}$/u;
142
148
  /** A terminal send failed at a boundary that proves input never started. */
143
149
  export class TerminalInputNotStartedError extends Error {
144
150
  code = "AKK_TERMINAL_INPUT_NOT_STARTED";
@@ -213,6 +219,20 @@ export class NativeInspectionDismissalError extends Error {
213
219
  this.name = "NativeInspectionDismissalError";
214
220
  }
215
221
  }
222
+ /**
223
+ * A response reservation or terminal input may already have happened. The
224
+ * same interaction must never be dispatched automatically after this error.
225
+ */
226
+ export class TerminalInteractionDispatchReservedError extends Error {
227
+ stage;
228
+ code = "AKK_TERMINAL_INTERACTION_DISPATCH_RESERVED";
229
+ doNotRetry = true;
230
+ constructor(stage, message, options = {}) {
231
+ super(message, options);
232
+ this.stage = stage;
233
+ this.name = "TerminalInteractionDispatchReservedError";
234
+ }
235
+ }
216
236
  function terminalSendComposerRequirements(agent, multiline, options) {
217
237
  const exactEmptyRetryAuthority = options.requireExactEmptyComposerAfterBeforeText;
218
238
  const requireExactEmptyComposerBeforeText = options.requireExactEmptyComposerBeforeText === true;
@@ -271,11 +291,152 @@ function throwPostTextHookFailureAfterUserEnter(enabled, error) {
271
291
  return;
272
292
  throw new TerminalEnterDispatchReservedError("user-explicit Codex Enter was dispatched but post-text bookkeeping failed; do not retry", { cause: error });
273
293
  }
294
+ function terminalApprovalPreflight(input) {
295
+ const { adapter, inspection, decision } = input;
296
+ if (!inspection.approval.approvable) {
297
+ return { ready: false, result: {
298
+ approved: false,
299
+ blocked: inspection.approval.blocked,
300
+ reason: inspection.approval.reason,
301
+ promptKind: inspection.approval.promptKind,
302
+ command: inspection.approval.command,
303
+ screenExcerpt: inspection.screenExcerpt
304
+ } };
305
+ }
306
+ if (decision === "approve_once" &&
307
+ (inspection.approval.action.mode ?? "keys") === "keys" &&
308
+ inspection.approval.action.keys.length === 0) {
309
+ return { ready: false, result: {
310
+ approved: false,
311
+ blocked: true,
312
+ decision,
313
+ reason: `${adapter.displayName} approval action has no keys`,
314
+ label: inspection.approval.action.label,
315
+ promptKind: inspection.approval.promptKind,
316
+ command: inspection.approval.command,
317
+ screenExcerpt: inspection.screenExcerpt
318
+ } };
319
+ }
320
+ const action = terminalApprovalActionForDecision(inspection.approval, decision);
321
+ if (!action) {
322
+ return { ready: false, result: {
323
+ approved: false,
324
+ blocked: true,
325
+ decision,
326
+ reason: `${adapter.displayName} does not expose the ${decision} decision for this exact approval prompt`,
327
+ promptKind: inspection.approval.promptKind,
328
+ command: inspection.approval.command,
329
+ screenExcerpt: inspection.screenExcerpt
330
+ } };
331
+ }
332
+ const decisionMode = action.mode ?? "keys";
333
+ const canonicalFingerprint = terminalApprovalFingerprint(adapter.agent, input.activeTerminalControl, inspection, { screen: input.screen, runtime: input.runtime, decision });
334
+ if (input.requiredDecisionMode &&
335
+ decisionMode !== input.requiredDecisionMode) {
336
+ return { ready: false, result: {
337
+ approved: false,
338
+ blocked: true,
339
+ reason: `${adapter.displayName} approval mode ${decisionMode} is not eligible for this decision`,
340
+ decision,
341
+ label: action.label,
342
+ promptKind: inspection.approval.promptKind,
343
+ command: inspection.approval.command,
344
+ fingerprint: canonicalFingerprint,
345
+ screenExcerpt: inspection.screenExcerpt,
346
+ decisionMode,
347
+ requestId: action.requestId
348
+ } };
349
+ }
350
+ if (decisionMode === "keys" && action.keys.length === 0) {
351
+ return { ready: false, result: {
352
+ approved: false,
353
+ blocked: true,
354
+ reason: `${adapter.displayName} approval action has no keys`,
355
+ decision,
356
+ label: action.label,
357
+ promptKind: inspection.approval.promptKind,
358
+ command: inspection.approval.command,
359
+ screenExcerpt: inspection.screenExcerpt
360
+ } };
361
+ }
362
+ if (!isTerminalApprovalPromptEvidence(inspection.approval.promptEvidence)) {
363
+ return { ready: false, result: {
364
+ approved: false,
365
+ blocked: true,
366
+ reason: `${adapter.displayName} approval prompt has no adapter-verified prompt evidence`,
367
+ decision,
368
+ label: action.label,
369
+ promptKind: inspection.approval.promptKind,
370
+ command: inspection.approval.command,
371
+ screenExcerpt: inspection.screenExcerpt,
372
+ decisionMode,
373
+ requestId: action.requestId
374
+ } };
375
+ }
376
+ const legacyFingerprint = hasCanonicalTerminalEndpoint(input.terminalControl)
377
+ ? undefined
378
+ : terminalApprovalFingerprint(adapter.agent, input.terminalControl, inspection, { screen: input.screen, runtime: input.runtime, decision });
379
+ const useLegacyFingerprint = Boolean(input.expectedFingerprint &&
380
+ input.expectedFingerprint === legacyFingerprint);
381
+ const fingerprintControl = useLegacyFingerprint
382
+ ? input.terminalControl
383
+ : input.activeTerminalControl;
384
+ const fingerprint = useLegacyFingerprint
385
+ ? legacyFingerprint
386
+ : canonicalFingerprint;
387
+ if (adapter.agent === "claude" &&
388
+ !input.expectedFingerprint) {
389
+ return { ready: false, result: {
390
+ approved: false,
391
+ blocked: true,
392
+ reason: "screen approval requires the latest expected fingerprint",
393
+ decision,
394
+ key: action.keys.length === 1 ? action.keys[0] : undefined,
395
+ keys: action.keys,
396
+ label: action.label,
397
+ promptKind: inspection.approval.promptKind,
398
+ command: inspection.approval.command,
399
+ cwd: inspection.approval.cwd,
400
+ toolName: inspection.approval.toolName,
401
+ requestDetail: inspection.approval.requestDetail,
402
+ fingerprint,
403
+ screenExcerpt: inspection.screenExcerpt,
404
+ decisionMode,
405
+ requestId: action.requestId
406
+ } };
407
+ }
408
+ if (input.expectedFingerprint &&
409
+ input.expectedFingerprint !== fingerprint) {
410
+ return { ready: false, result: {
411
+ approved: false,
412
+ blocked: true,
413
+ reason: "approval fingerprint changed before execution",
414
+ decision,
415
+ key: action.keys.length === 1 ? action.keys[0] : undefined,
416
+ keys: action.keys,
417
+ label: action.label,
418
+ promptKind: inspection.approval.promptKind,
419
+ command: inspection.approval.command,
420
+ fingerprint,
421
+ screenExcerpt: inspection.screenExcerpt,
422
+ decisionMode,
423
+ requestId: action.requestId
424
+ } };
425
+ }
426
+ return {
427
+ ready: true,
428
+ action,
429
+ decisionMode,
430
+ fingerprintControl,
431
+ fingerprint
432
+ };
433
+ }
274
434
  export class TerminalAgentBridge {
275
435
  registry;
276
436
  terminalProvider;
277
437
  verifyIdentity;
278
438
  nowMs;
439
+ now;
279
440
  sleep;
280
441
  diagnosticLog;
281
442
  constructor(options) {
@@ -283,6 +444,7 @@ export class TerminalAgentBridge {
283
444
  this.terminalProvider = options.terminalProvider;
284
445
  this.verifyIdentity = options.verifyIdentity;
285
446
  this.nowMs = options.nowMs ?? (() => performance.now());
447
+ this.now = options.now ?? (() => new Date());
286
448
  this.sleep = options.sleep ?? terminalSettleDelay;
287
449
  this.diagnosticLog = options.diagnosticLog;
288
450
  }
@@ -404,7 +566,8 @@ export class TerminalAgentBridge {
404
566
  const captured = await this.captureInspection(adapter, terminalControl, options);
405
567
  return statusFromInspection(adapter, captured.terminalControl, captured.inspection, {
406
568
  screen: captured.screen,
407
- runtime: options.runtime
569
+ runtime: options.runtime,
570
+ now: this.now()
408
571
  });
409
572
  }
410
573
  catch (error) {
@@ -552,8 +715,15 @@ export class TerminalAgentBridge {
552
715
  }
553
716
  const textInjectedAt = this.nowMs();
554
717
  const preserveUserInput = options.userExplicitEnterAfterTextWithoutComposerVeto === true;
718
+ const allowClaudeInjectedPastePlaceholder = claudeInjectedPasteProofEligible({
719
+ agent: adapter.agent,
720
+ multiline,
721
+ composerVerifiedImmediatelyBeforeText,
722
+ terminalControl: verifiedForText
723
+ });
555
724
  let postTextHookError;
556
725
  let verifiedForEnter;
726
+ let claudeComposerProof;
557
727
  try {
558
728
  postTextHookError = await recordTerminalSendStage(options, {
559
729
  stage: "text_injected",
@@ -571,6 +741,16 @@ export class TerminalAgentBridge {
571
741
  allowWorkingComposer: options.allowWorkingComposerForUserExplicit === true
572
742
  });
573
743
  }
744
+ else if (allowClaudeInjectedPastePlaceholder) {
745
+ claudeComposerProof = await settleClaudeInjectedPasteComposer({
746
+ terminalControl: verifiedForText,
747
+ expectedText: normalized,
748
+ runtime: options.runtime,
749
+ allowWorkingComposer: options.allowWorkingComposerForUserExplicit === true,
750
+ ports: this.claudeInjectedPasteProofPorts(adapter)
751
+ });
752
+ verifiedForEnter = claudeComposerProof.terminalControl;
753
+ }
574
754
  else if (requireExactComposer) {
575
755
  verifiedForEnter = await this.verifyExactComposerBeforeEnter(adapter, terminalControl, normalized, options.runtime, options.allowWorkingComposerForUserExplicit === true);
576
756
  }
@@ -593,12 +773,24 @@ export class TerminalAgentBridge {
593
773
  // can still edit the composer or change the native thread while that
594
774
  // callback is pending, so recapture before attempting Enter.
595
775
  if (requireExactComposer) {
596
- verifiedForEnter = adapter.agent === "codex"
597
- ? await this.settleCodexMultilineComposer(adapter, terminalControl, normalized, options.runtime, {
776
+ if (adapter.agent === "codex") {
777
+ verifiedForEnter = await this.settleCodexMultilineComposer(adapter, terminalControl, normalized, options.runtime, {
598
778
  allowOpaqueLargePastePlaceholder: exactEmptyRetryAuthority === undefined,
599
779
  allowWorkingComposer: options.allowWorkingComposerForUserExplicit === true
600
- })
601
- : await this.verifyExactComposerBeforeEnter(adapter, terminalControl, normalized, options.runtime, options.allowWorkingComposerForUserExplicit === true);
780
+ });
781
+ }
782
+ else if (allowClaudeInjectedPastePlaceholder && claudeComposerProof) {
783
+ verifiedForEnter = await revalidateClaudeComposerProof({
784
+ proof: claudeComposerProof,
785
+ expectedText: normalized,
786
+ runtime: options.runtime,
787
+ allowWorkingComposer: options.allowWorkingComposerForUserExplicit === true,
788
+ ports: this.claudeInjectedPasteProofPorts(adapter)
789
+ });
790
+ }
791
+ else {
792
+ verifiedForEnter = await this.verifyExactComposerBeforeEnter(adapter, terminalControl, normalized, options.runtime, options.allowWorkingComposerForUserExplicit === true);
793
+ }
602
794
  }
603
795
  else if (options.beforeEnter) {
604
796
  verifiedForEnter = await this.verifyTerminalIdentity(adapter.agent, terminalControl, options.runtime);
@@ -938,6 +1130,19 @@ export class TerminalAgentBridge {
938
1130
  }
939
1131
  return verifiedImmediatelyBeforeEnter;
940
1132
  }
1133
+ claudeInjectedPasteProofPorts(adapter) {
1134
+ return {
1135
+ nowMs: this.nowMs,
1136
+ sleep: this.sleep,
1137
+ capture: (terminalControl, runtime) => this.captureInspection(adapter, terminalControl, {
1138
+ runtime,
1139
+ scrollbackLines: CODEX_MULTILINE_SETTLE_SCROLLBACK_LINES
1140
+ }),
1141
+ verifyIdentity: (terminalControl, runtime) => this.verifyTerminalIdentity(adapter.agent, terminalControl, runtime),
1142
+ exactDraft: exactClaudeComposerCapture,
1143
+ exactInjectedPastePlaceholder: exactClaudeInjectedPastePlaceholderCapture
1144
+ };
1145
+ }
941
1146
  async verifyExactEmptyComposerBeforeText(adapter, terminalControl, expectedText, runtime, allowWorkingComposer = false) {
942
1147
  if (adapter.agent === "codex") {
943
1148
  const snapshot = await this.captureCodexComposerSnapshot(adapter, terminalControl, expectedText, runtime);
@@ -1748,6 +1953,7 @@ export class TerminalAgentBridge {
1748
1953
  }
1749
1954
  async approve(agent, terminalControl, options = {}) {
1750
1955
  const adapter = this.registry.require(agent);
1956
+ const decision = options.decision ?? "approve_once";
1751
1957
  if (!adapter.capabilities.terminalApproval ||
1752
1958
  !terminalControl.capabilities.includes("terminal_approval")) {
1753
1959
  return {
@@ -1769,124 +1975,27 @@ export class TerminalAgentBridge {
1769
1975
  const captured = await this.captureInspection(adapter, terminalControl, options);
1770
1976
  const { inspection } = captured;
1771
1977
  const activeTerminalControl = captured.terminalControl;
1772
- if (!inspection.approval.approvable) {
1773
- return {
1774
- approved: false,
1775
- blocked: inspection.approval.blocked,
1776
- reason: inspection.approval.reason,
1777
- promptKind: inspection.approval.promptKind,
1778
- command: inspection.approval.command,
1779
- screenExcerpt: inspection.screenExcerpt
1780
- };
1781
- }
1782
- const decisionMode = inspection.approval.action.mode ?? "keys";
1783
- if (options.requiredDecisionMode && decisionMode !== options.requiredDecisionMode) {
1784
- return {
1785
- approved: false,
1786
- blocked: true,
1787
- reason: `${adapter.displayName} approval mode ${decisionMode} is not eligible for this decision`,
1788
- label: inspection.approval.action.label,
1789
- promptKind: inspection.approval.promptKind,
1790
- command: inspection.approval.command,
1791
- fingerprint: terminalApprovalFingerprint(adapter.agent, activeTerminalControl, inspection, {
1792
- screen: captured.screen,
1793
- runtime: options.runtime
1794
- }),
1795
- screenExcerpt: inspection.screenExcerpt,
1796
- decisionMode,
1797
- requestId: inspection.approval.action.requestId
1798
- };
1799
- }
1800
- if (decisionMode === "keys" && inspection.approval.action.keys.length === 0) {
1801
- return {
1802
- approved: false,
1803
- blocked: true,
1804
- reason: `${adapter.displayName} approval action has no keys`,
1805
- label: inspection.approval.action.label,
1806
- promptKind: inspection.approval.promptKind,
1807
- command: inspection.approval.command,
1808
- screenExcerpt: inspection.screenExcerpt
1809
- };
1810
- }
1811
- if (decisionMode === "keys" &&
1812
- !isTerminalApprovalPromptEvidence(inspection.approval.promptEvidence)) {
1813
- return {
1814
- approved: false,
1815
- blocked: true,
1816
- reason: `${adapter.displayName} approval prompt has no adapter-verified prompt evidence`,
1817
- label: inspection.approval.action.label,
1818
- promptKind: inspection.approval.promptKind,
1819
- command: inspection.approval.command,
1820
- screenExcerpt: inspection.screenExcerpt,
1821
- decisionMode,
1822
- requestId: inspection.approval.action.requestId
1823
- };
1824
- }
1825
- const canonicalFingerprint = terminalApprovalFingerprint(adapter.agent, activeTerminalControl, inspection, {
1978
+ const preflight = terminalApprovalPreflight({
1979
+ adapter,
1980
+ terminalControl,
1981
+ activeTerminalControl,
1982
+ inspection,
1826
1983
  screen: captured.screen,
1984
+ decision,
1985
+ expectedFingerprint: options.expectedFingerprint,
1986
+ requiredDecisionMode: options.requiredDecisionMode,
1827
1987
  runtime: options.runtime
1828
1988
  });
1829
- const legacyFingerprint = hasCanonicalTerminalEndpoint(terminalControl)
1830
- ? undefined
1831
- : terminalApprovalFingerprint(adapter.agent, terminalControl, inspection, {
1832
- screen: captured.screen,
1833
- runtime: options.runtime
1834
- });
1835
- const useLegacyFingerprint = Boolean(options.expectedFingerprint &&
1836
- options.expectedFingerprint === legacyFingerprint);
1837
- const fingerprintControl = useLegacyFingerprint
1838
- ? terminalControl
1839
- : activeTerminalControl;
1840
- const fingerprint = useLegacyFingerprint
1841
- ? legacyFingerprint
1842
- : canonicalFingerprint;
1843
- if (adapter.agent === "claude" &&
1844
- decisionMode === "keys" &&
1845
- !options.expectedFingerprint) {
1846
- return {
1847
- approved: false,
1848
- blocked: true,
1849
- reason: "screen approval requires the latest expected fingerprint",
1850
- key: inspection.approval.action.keys.length === 1
1851
- ? inspection.approval.action.keys[0]
1852
- : undefined,
1853
- keys: inspection.approval.action.keys,
1854
- label: inspection.approval.action.label,
1855
- promptKind: inspection.approval.promptKind,
1856
- command: inspection.approval.command,
1857
- cwd: inspection.approval.cwd,
1858
- toolName: inspection.approval.toolName,
1859
- requestDetail: inspection.approval.requestDetail,
1860
- fingerprint,
1861
- screenExcerpt: inspection.screenExcerpt,
1862
- decisionMode,
1863
- requestId: inspection.approval.action.requestId
1864
- };
1865
- }
1866
- if (options.expectedFingerprint && options.expectedFingerprint !== fingerprint) {
1867
- return {
1868
- approved: false,
1869
- blocked: true,
1870
- reason: "approval fingerprint changed before execution",
1871
- key: inspection.approval.action.keys.length === 1
1872
- ? inspection.approval.action.keys[0]
1873
- : undefined,
1874
- keys: inspection.approval.action.keys,
1875
- label: inspection.approval.action.label,
1876
- promptKind: inspection.approval.promptKind,
1877
- command: inspection.approval.command,
1878
- fingerprint,
1879
- screenExcerpt: inspection.screenExcerpt,
1880
- decisionMode,
1881
- requestId: inspection.approval.action.requestId
1882
- };
1883
- }
1989
+ if (!preflight.ready)
1990
+ return preflight.result;
1991
+ const { action, decisionMode, fingerprintControl, fingerprint } = preflight;
1884
1992
  if (options.authorize) {
1885
1993
  const authorization = await options.authorize({
1886
1994
  agent: adapter.agent,
1887
1995
  terminalControl: activeTerminalControl,
1888
1996
  inspection,
1889
1997
  fingerprint,
1998
+ decision,
1890
1999
  runtime: options.runtime
1891
2000
  });
1892
2001
  if (!authorization.approved) {
@@ -1894,11 +2003,12 @@ export class TerminalAgentBridge {
1894
2003
  approved: false,
1895
2004
  blocked: true,
1896
2005
  reason: authorization.reason ?? "approval was not authorized",
1897
- key: inspection.approval.action.keys.length === 1
1898
- ? inspection.approval.action.keys[0]
2006
+ decision,
2007
+ key: action.keys.length === 1
2008
+ ? action.keys[0]
1899
2009
  : undefined,
1900
- keys: inspection.approval.action.keys,
1901
- label: inspection.approval.action.label,
2010
+ keys: action.keys,
2011
+ label: action.label,
1902
2012
  promptKind: inspection.approval.promptKind,
1903
2013
  command: inspection.approval.command,
1904
2014
  toolName: inspection.approval.toolName,
@@ -1906,7 +2016,7 @@ export class TerminalAgentBridge {
1906
2016
  fingerprint,
1907
2017
  screenExcerpt: inspection.screenExcerpt,
1908
2018
  decisionMode,
1909
- requestId: inspection.approval.action.requestId
2019
+ requestId: action.requestId
1910
2020
  };
1911
2021
  }
1912
2022
  }
@@ -1925,21 +2035,34 @@ export class TerminalAgentBridge {
1925
2035
  screenExcerpt: recapturedInspection.screenExcerpt
1926
2036
  };
1927
2037
  }
1928
- const recapturedDecisionMode = recapturedInspection.approval.action.mode ?? "keys";
2038
+ const recapturedAction = terminalApprovalActionForDecision(recapturedInspection.approval, decision);
2039
+ if (!recapturedAction) {
2040
+ return {
2041
+ approved: false,
2042
+ blocked: true,
2043
+ decision,
2044
+ reason: `the ${decision} decision is no longer available after authorization`,
2045
+ promptKind: recapturedInspection.approval.promptKind,
2046
+ screenExcerpt: recapturedInspection.screenExcerpt
2047
+ };
2048
+ }
2049
+ const recapturedDecisionMode = recapturedAction.mode ?? "keys";
1929
2050
  const recapturedFingerprint = terminalApprovalFingerprint(adapter.agent, fingerprintControl, recapturedInspection, {
1930
2051
  screen: recaptured.screen,
1931
- runtime: options.runtime
2052
+ runtime: options.runtime,
2053
+ decision
1932
2054
  });
1933
2055
  if (recapturedDecisionMode !== decisionMode) {
1934
2056
  return {
1935
2057
  approved: false,
1936
2058
  blocked: true,
1937
2059
  reason: "approval decision mode changed after authorization",
1938
- key: recapturedInspection.approval.action.keys.length === 1
1939
- ? recapturedInspection.approval.action.keys[0]
2060
+ decision,
2061
+ key: recapturedAction.keys.length === 1
2062
+ ? recapturedAction.keys[0]
1940
2063
  : undefined,
1941
- keys: recapturedInspection.approval.action.keys,
1942
- label: recapturedInspection.approval.action.label,
2064
+ keys: recapturedAction.keys,
2065
+ label: recapturedAction.label,
1943
2066
  promptKind: recapturedInspection.approval.promptKind,
1944
2067
  command: recapturedInspection.approval.command,
1945
2068
  cwd: recapturedInspection.approval.cwd,
@@ -1948,7 +2071,7 @@ export class TerminalAgentBridge {
1948
2071
  fingerprint: recapturedFingerprint,
1949
2072
  screenExcerpt: recapturedInspection.screenExcerpt,
1950
2073
  decisionMode: recapturedDecisionMode,
1951
- requestId: recapturedInspection.approval.action.requestId
2074
+ requestId: recapturedAction.requestId
1952
2075
  };
1953
2076
  }
1954
2077
  if (recapturedFingerprint !== fingerprint) {
@@ -1956,11 +2079,12 @@ export class TerminalAgentBridge {
1956
2079
  approved: false,
1957
2080
  blocked: true,
1958
2081
  reason: "approval fingerprint changed after authorization",
1959
- key: recapturedInspection.approval.action.keys.length === 1
1960
- ? recapturedInspection.approval.action.keys[0]
2082
+ decision,
2083
+ key: recapturedAction.keys.length === 1
2084
+ ? recapturedAction.keys[0]
1961
2085
  : undefined,
1962
- keys: recapturedInspection.approval.action.keys,
1963
- label: recapturedInspection.approval.action.label,
2086
+ keys: recapturedAction.keys,
2087
+ label: recapturedAction.label,
1964
2088
  promptKind: recapturedInspection.approval.promptKind,
1965
2089
  command: recapturedInspection.approval.command,
1966
2090
  cwd: recapturedInspection.approval.cwd,
@@ -1969,7 +2093,7 @@ export class TerminalAgentBridge {
1969
2093
  fingerprint: recapturedFingerprint,
1970
2094
  screenExcerpt: recapturedInspection.screenExcerpt,
1971
2095
  decisionMode: recapturedDecisionMode,
1972
- requestId: recapturedInspection.approval.action.requestId
2096
+ requestId: recapturedAction.requestId
1973
2097
  };
1974
2098
  }
1975
2099
  const verifiedForApproval = await this.verifyTerminalIdentity(adapter.agent, recaptured.terminalControl, options.runtime);
@@ -1987,11 +2111,13 @@ export class TerminalAgentBridge {
1987
2111
  terminalControl: verifiedForApproval,
1988
2112
  inspection: recapturedInspection,
1989
2113
  fingerprint: recapturedFingerprint,
1990
- keys: recapturedInspection.approval.action.keys,
2114
+ decision,
2115
+ keys: recapturedAction.keys,
1991
2116
  runtime: options.runtime
1992
2117
  });
1993
2118
  let dispatchTerminalControl = verifiedForApproval;
1994
2119
  let dispatchInspection = recapturedInspection;
2120
+ let dispatchAction = recapturedAction;
1995
2121
  if (options.beforeKeyDispatch) {
1996
2122
  const afterReservation = await this.captureInspection(adapter, verifiedForApproval, options);
1997
2123
  const afterReservationInspection = afterReservation.inspection;
@@ -2008,22 +2134,35 @@ export class TerminalAgentBridge {
2008
2134
  screenExcerpt: afterReservationInspection.screenExcerpt
2009
2135
  };
2010
2136
  }
2011
- const afterReservationMode = afterReservationInspection.approval.action.mode ?? "keys";
2137
+ const afterReservationAction = terminalApprovalActionForDecision(afterReservationInspection.approval, decision);
2138
+ if (!afterReservationAction) {
2139
+ return {
2140
+ approved: false,
2141
+ blocked: true,
2142
+ decision,
2143
+ reason: `the ${decision} decision is no longer available after dispatch reservation`,
2144
+ promptKind: afterReservationInspection.approval.promptKind,
2145
+ screenExcerpt: afterReservationInspection.screenExcerpt
2146
+ };
2147
+ }
2148
+ const afterReservationMode = afterReservationAction.mode ?? "keys";
2012
2149
  const afterReservationFingerprint = terminalApprovalFingerprint(adapter.agent, fingerprintControl, afterReservationInspection, {
2013
2150
  screen: afterReservation.screen,
2014
- runtime: options.runtime
2151
+ runtime: options.runtime,
2152
+ decision
2015
2153
  });
2016
2154
  if (afterReservationMode !== "keys" ||
2017
2155
  afterReservationFingerprint !== recapturedFingerprint) {
2018
2156
  return {
2019
2157
  approved: false,
2020
2158
  blocked: true,
2159
+ decision,
2021
2160
  reason: "approval fingerprint changed after dispatch reservation",
2022
- key: afterReservationInspection.approval.action.keys.length === 1
2023
- ? afterReservationInspection.approval.action.keys[0]
2161
+ key: afterReservationAction.keys.length === 1
2162
+ ? afterReservationAction.keys[0]
2024
2163
  : undefined,
2025
- keys: afterReservationInspection.approval.action.keys,
2026
- label: afterReservationInspection.approval.action.label,
2164
+ keys: afterReservationAction.keys,
2165
+ label: afterReservationAction.label,
2027
2166
  promptKind: afterReservationInspection.approval.promptKind,
2028
2167
  command: afterReservationInspection.approval.command,
2029
2168
  cwd: afterReservationInspection.approval.cwd,
@@ -2032,11 +2171,12 @@ export class TerminalAgentBridge {
2032
2171
  fingerprint: afterReservationFingerprint,
2033
2172
  screenExcerpt: afterReservationInspection.screenExcerpt,
2034
2173
  decisionMode: afterReservationMode,
2035
- requestId: afterReservationInspection.approval.action.requestId
2174
+ requestId: afterReservationAction.requestId
2036
2175
  };
2037
2176
  }
2038
2177
  dispatchTerminalControl = afterReservation.terminalControl;
2039
2178
  dispatchInspection = afterReservationInspection;
2179
+ dispatchAction = afterReservationAction;
2040
2180
  }
2041
2181
  if (!dispatchInspection.approval.approvable) {
2042
2182
  return {
@@ -2051,22 +2191,108 @@ export class TerminalAgentBridge {
2051
2191
  if (!sameTerminalControlIdentity(dispatchTerminalControl, verifiedImmediatelyBeforeSend)) {
2052
2192
  throw new Error("terminal control identity changed after the final approval capture");
2053
2193
  }
2054
- await this.terminalProvider.sendKeys(this.terminalProvider.endpoint(verifiedImmediatelyBeforeSend), dispatchApproval.action.keys);
2194
+ await this.terminalProvider.sendKeys(this.terminalProvider.endpoint(verifiedImmediatelyBeforeSend), dispatchAction.keys);
2055
2195
  return {
2056
- approved: true,
2196
+ approved: decision === "approve_once",
2197
+ decisionDispatched: true,
2198
+ decision,
2057
2199
  blocked: false,
2058
- key: dispatchApproval.action.keys.length === 1
2059
- ? dispatchApproval.action.keys[0]
2200
+ key: dispatchAction.keys.length === 1
2201
+ ? dispatchAction.keys[0]
2060
2202
  : undefined,
2061
- keys: dispatchApproval.action.keys,
2062
- label: dispatchApproval.action.label,
2203
+ keys: dispatchAction.keys,
2204
+ label: dispatchAction.label,
2063
2205
  promptKind: dispatchApproval.promptKind,
2064
2206
  command: dispatchApproval.command,
2065
2207
  cwd: dispatchApproval.cwd,
2066
2208
  fingerprint: recapturedFingerprint,
2067
2209
  screenExcerpt: dispatchInspection.screenExcerpt,
2068
2210
  decisionMode: recapturedDecisionMode,
2069
- requestId: dispatchApproval.action.requestId
2211
+ requestId: dispatchAction.requestId
2212
+ };
2213
+ }
2214
+ async respondInteraction(agent, terminalControl, response, options) {
2215
+ const runtime = {
2216
+ ...options.runtime,
2217
+ agentVersion: options.agentVersion
2218
+ };
2219
+ const optionReason = terminalInteractionOptionPreflightReason(options, runtime);
2220
+ if (optionReason) {
2221
+ return blockedTerminalInteractionResponse(response, optionReason);
2222
+ }
2223
+ const first = await this.captureTerminalInteractionOffer(agent, terminalControl, runtime, options.scrollbackLines).catch((error) => error);
2224
+ if (first instanceof Error) {
2225
+ return blockedTerminalInteractionResponse(response, first.message);
2226
+ }
2227
+ const firstReason = terminalInteractionOfferPreflightReason(first.offer, options);
2228
+ if (firstReason || !first.offer) {
2229
+ return blockedTerminalInteractionResponse(response, firstReason ?? "native questionnaire has no safe interaction offer", first.offer);
2230
+ }
2231
+ const validatedResponse = validateTerminalInteractionResponse(response, first.offer.projection, { now: this.now() });
2232
+ const planReason = terminalInteractionPlanPreflightReason(validatedResponse, first.offer.actionPlan);
2233
+ if (planReason) {
2234
+ return blockedTerminalInteractionResponse(validatedResponse, planReason, first.offer);
2235
+ }
2236
+ const capabilityReason = terminalInteractionCapabilityReason(this.terminalProvider, first.terminalControl, first.offer.actionPlan);
2237
+ if (capabilityReason) {
2238
+ return blockedTerminalInteractionResponse(validatedResponse, capabilityReason, first.offer);
2239
+ }
2240
+ const authorization = await authorizeTerminalInteraction(options.authorize, interactionHookContext(agent, first.terminalControl, first.offer, validatedResponse, runtime));
2241
+ if (!authorization.approved) {
2242
+ return blockedTerminalInteractionResponse(validatedResponse, authorization.reason ?? "terminal interaction response was not authorized", first.offer);
2243
+ }
2244
+ const afterAuthorization = await this.captureTerminalInteractionOffer(agent, first.terminalControl, runtime, options.scrollbackLines).catch((error) => error);
2245
+ if (afterAuthorization instanceof Error) {
2246
+ return blockedTerminalInteractionResponse(validatedResponse, afterAuthorization.message, first.offer);
2247
+ }
2248
+ if (!sameTerminalInteractionOffer(first, afterAuthorization)) {
2249
+ return blockedTerminalInteractionResponse(validatedResponse, "native questionnaire changed after authorization", first.offer);
2250
+ }
2251
+ await reserveTerminalInteractionDispatch(options.beforeDispatch, interactionHookContext(agent, afterAuthorization.terminalControl, afterAuthorization.offer, validatedResponse, runtime));
2252
+ const finalCapture = await this.captureTerminalInteractionOffer(agent, afterAuthorization.terminalControl, runtime, options.scrollbackLines).catch((error) => {
2253
+ if (options.beforeDispatch) {
2254
+ throw terminalInteractionReservedError("reservation_uncertain", error, "cannot recapture native questionnaire after dispatch reservation");
2255
+ }
2256
+ return error;
2257
+ });
2258
+ if (finalCapture instanceof Error) {
2259
+ return blockedTerminalInteractionResponse(validatedResponse, finalCapture.message, first.offer);
2260
+ }
2261
+ if (!sameTerminalInteractionOffer(afterAuthorization, finalCapture)) {
2262
+ if (options.beforeDispatch) {
2263
+ throw new TerminalInteractionDispatchReservedError("reservation_uncertain", "native questionnaire changed after dispatch reservation");
2264
+ }
2265
+ return blockedTerminalInteractionResponse(validatedResponse, "native questionnaire changed before dispatch", first.offer);
2266
+ }
2267
+ const verified = await this.verifyTerminalIdentity(agent, finalCapture.terminalControl, runtime).catch((error) => {
2268
+ throw terminalInteractionReservedError(options.beforeDispatch ? "reservation_uncertain" : "key_uncertain", error, "terminal identity could not be verified immediately before interaction dispatch");
2269
+ });
2270
+ if (!sameTerminalControlIdentity(finalCapture.terminalControl, verified)) {
2271
+ throw new TerminalInteractionDispatchReservedError(options.beforeDispatch ? "reservation_uncertain" : "key_uncertain", "terminal identity changed after the final questionnaire capture");
2272
+ }
2273
+ const answer = validatedResponse.answers[0];
2274
+ const outcome = await dispatchTerminalInteractionAnswer({
2275
+ agent,
2276
+ provider: this.terminalProvider,
2277
+ terminalControl: verified,
2278
+ actionPlan: finalCapture.offer.actionPlan,
2279
+ answer,
2280
+ sleep: this.sleep,
2281
+ reverifyTerminalIdentity: async (currentControl) => {
2282
+ const reverified = await this.verifyTerminalIdentity(agent, currentControl, runtime);
2283
+ if (!sameTerminalControlIdentity(currentControl, reverified)) {
2284
+ throw new Error("terminal identity changed between interaction text and Enter");
2285
+ }
2286
+ return reverified;
2287
+ }
2288
+ });
2289
+ return {
2290
+ responded: true,
2291
+ blocked: false,
2292
+ interactionId: validatedResponse.interaction_id,
2293
+ questionId: answer.question_id,
2294
+ responseKind: answer.response_kind,
2295
+ outcome
2070
2296
  };
2071
2297
  }
2072
2298
  async monitorPoll(options) {
@@ -2083,7 +2309,8 @@ export class TerminalAgentBridge {
2083
2309
  inspection = captured.inspection;
2084
2310
  status = statusFromInspection(adapter, captured.terminalControl, inspection, {
2085
2311
  screen: captured.screen,
2086
- runtime: options.screenOptions?.runtime
2312
+ runtime: options.screenOptions?.runtime,
2313
+ now: this.now()
2087
2314
  });
2088
2315
  }
2089
2316
  catch (error) {
@@ -2138,6 +2365,25 @@ export class TerminalAgentBridge {
2138
2365
  })
2139
2366
  };
2140
2367
  }
2368
+ async captureTerminalInteractionOffer(agent, terminalControl, runtime, scrollbackLines) {
2369
+ const adapter = this.registry.require(agent);
2370
+ const captured = await this.captureInspection(adapter, terminalControl, {
2371
+ scrollbackLines,
2372
+ runtime
2373
+ });
2374
+ return {
2375
+ terminalControl: captured.terminalControl,
2376
+ inspection: captured.inspection,
2377
+ offer: terminalInteractionBridgeOffer({
2378
+ agent,
2379
+ terminalControl: captured.terminalControl,
2380
+ inspection: captured.inspection,
2381
+ screen: captured.screen,
2382
+ runtime,
2383
+ now: this.now()
2384
+ })
2385
+ };
2386
+ }
2141
2387
  async verifyTerminalIdentity(agent, terminalControl, runtime) {
2142
2388
  let verifiedControl = terminalControl;
2143
2389
  if (this.verifyIdentity) {
@@ -2791,6 +3037,73 @@ function exactClaudeComposerCapture(screen, expectedText) {
2791
3037
  .digest("hex")
2792
3038
  };
2793
3039
  }
3040
+ function exactClaudeInjectedPastePlaceholderCapture(screen, expectedText) {
3041
+ const expectedNewlineCount = (composerComparableText(expectedText).match(/\n/gu) ?? []).length;
3042
+ if (expectedNewlineCount === 0) {
3043
+ return undefined;
3044
+ }
3045
+ const frame = exactClaudeComposerFrame(screen);
3046
+ if (!frame ||
3047
+ frame.composerRows.length !== 1 ||
3048
+ !claudeInjectedPastePlaceholderTrailingMatches(frame.trailing)) {
3049
+ return undefined;
3050
+ }
3051
+ const placeholder = /^\s*❯\s*\[Pasted text #([1-9]\d*) \+([1-9]\d*) lines\]\s*$/u
3052
+ .exec(frame.composerRows[0]);
3053
+ if (!placeholder) {
3054
+ return undefined;
3055
+ }
3056
+ const pasteId = Number(placeholder[1]);
3057
+ const newlineCount = Number(placeholder[2]);
3058
+ if (!Number.isSafeInteger(pasteId) ||
3059
+ !Number.isSafeInteger(newlineCount) ||
3060
+ newlineCount !== expectedNewlineCount) {
3061
+ return undefined;
3062
+ }
3063
+ return {
3064
+ state: "exact_injected_paste_placeholder",
3065
+ digest: createHash("sha256")
3066
+ .update(frame.lines
3067
+ .slice(frame.openIndex, frame.closeIndex + 1)
3068
+ .concat(frame.trailing)
3069
+ .join("\n"))
3070
+ .digest("hex"),
3071
+ pasteId,
3072
+ newlineCount,
3073
+ frameProfile: CLAUDE_INJECTED_PASTE_FRAME_PROFILE
3074
+ };
3075
+ }
3076
+ function claudeInjectedPastePlaceholderTrailingMatches(lines) {
3077
+ if (lines.length === 0) {
3078
+ return false;
3079
+ }
3080
+ const hint = "paste again to expand";
3081
+ const first = lines[0].trimStart();
3082
+ if (!first.startsWith(hint)) {
3083
+ return false;
3084
+ }
3085
+ const sameRowStatus = first.slice(hint.length);
3086
+ if (sameRowStatus.length > 0 && !/^\s{2,}\S/u.test(sameRowStatus)) {
3087
+ return false;
3088
+ }
3089
+ const footerRows = [
3090
+ ...(sameRowStatus.trim().length > 0 ? [sameRowStatus.trim()] : []),
3091
+ ...lines.slice(1).map((line) => line.trim())
3092
+ ];
3093
+ return footerRows.length === 0 ||
3094
+ claudeNativeInspectionTrailingIsFooter(footerRows) ||
3095
+ claudeInjectedPasteAuxiliaryFooterMatches(footerRows);
3096
+ }
3097
+ function claudeInjectedPasteAuxiliaryFooterMatches(lines) {
3098
+ if (lines.length === 0 || lines.length > 2) {
3099
+ return false;
3100
+ }
3101
+ const autoUpdate = lines.filter((line) => line === "✘ Auto-update failed · Run claude doctor").length;
3102
+ const effort = lines.filter((line) => /^[●○◐◉] (?:low|medium|high|xhigh|max|ultracode) · \/effort$/u
3103
+ .test(line)).length;
3104
+ return autoUpdate <= 1 && effort <= 1 &&
3105
+ autoUpdate + effort === lines.length;
3106
+ }
2794
3107
  function composerComparableText(value) {
2795
3108
  return value.replace(/\r\n?/gu, "\n");
2796
3109
  }
@@ -2804,7 +3117,13 @@ export function terminalApprovalFingerprint(agent, terminalControl, inspection,
2804
3117
  if (!inspection.approval.approvable) {
2805
3118
  return undefined;
2806
3119
  }
2807
- const decisionMode = inspection.approval.action.mode ?? "keys";
3120
+ const decision = options.decision ?? "approve_once";
3121
+ const choices = terminalApprovalChoices(inspection.approval);
3122
+ const action = terminalApprovalActionForDecision(inspection.approval, decision);
3123
+ if (!action) {
3124
+ return undefined;
3125
+ }
3126
+ const decisionMode = action.mode ?? "keys";
2808
3127
  const promptEvidence = inspection.approval.promptEvidence;
2809
3128
  if (decisionMode === "keys" &&
2810
3129
  !isTerminalApprovalPromptEvidence(promptEvidence)) {
@@ -2858,8 +3177,16 @@ export function terminalApprovalFingerprint(agent, terminalControl, inspection,
2858
3177
  message_id: options.runtime?.messageId,
2859
3178
  terminal_target: options.runtime?.terminalTarget
2860
3179
  },
2861
- keys: inspection.approval.action.keys,
2862
- label: inspection.approval.action.label,
3180
+ decision,
3181
+ keys: action.keys,
3182
+ label: action.label,
3183
+ available_choices: choices.map((choice) => ({
3184
+ decision: choice.decision,
3185
+ keys: choice.keys,
3186
+ label: choice.label,
3187
+ mode: choice.mode ?? "keys",
3188
+ request_id: choice.requestId
3189
+ })),
2863
3190
  prompt_kind: inspection.approval.promptKind,
2864
3191
  command: inspection.approval.command,
2865
3192
  cwd: inspection.approval.cwd,
@@ -2882,7 +3209,7 @@ export function terminalApprovalFingerprint(agent, terminalControl, inspection,
2882
3209
  }
2883
3210
  : undefined,
2884
3211
  decision_mode: decisionMode,
2885
- request_id: inspection.approval.action.requestId
3212
+ request_id: action.requestId
2886
3213
  }))
2887
3214
  .digest("hex");
2888
3215
  }
@@ -2896,12 +3223,396 @@ function isTerminalApprovalPromptEvidence(value) {
2896
3223
  typeof evidence.sha256 === "string" &&
2897
3224
  /^[0-9a-f]{64}$/u.test(evidence.sha256);
2898
3225
  }
3226
+ function terminalInteractionTurnId(runtime) {
3227
+ // Questionnaire mutation is managed-Turn-only. A raw terminal's
3228
+ // conversationId is a discovery label, not durable response authority.
3229
+ const candidate = runtime?.turnId;
3230
+ return typeof candidate === "string" &&
3231
+ TERMINAL_INTERACTION_IDENTIFIER_PATTERN.test(candidate)
3232
+ ? candidate
3233
+ : undefined;
3234
+ }
3235
+ function terminalInteractionExpiry(now) {
3236
+ const nowMs = now.getTime();
3237
+ if (!Number.isFinite(nowMs)) {
3238
+ return undefined;
3239
+ }
3240
+ const windowStart = Math.floor(nowMs / TERMINAL_INTERACTION_TTL_MS) *
3241
+ TERMINAL_INTERACTION_TTL_MS;
3242
+ return new Date(windowStart + TERMINAL_INTERACTION_TTL_MS).toISOString();
3243
+ }
3244
+ function terminalInteractionQuestion(question) {
3245
+ const common = {
3246
+ question_id: question.question_id,
3247
+ prompt: question.prompt,
3248
+ required: question.required
3249
+ };
3250
+ if (question.response_kind === "single_select" ||
3251
+ question.response_kind === "multi_select") {
3252
+ return {
3253
+ ...common,
3254
+ response_kind: question.response_kind,
3255
+ options: question.options ?? []
3256
+ };
3257
+ }
3258
+ return {
3259
+ ...common,
3260
+ response_kind: question.response_kind
3261
+ };
3262
+ }
3263
+ function terminalInteractionCanonicalIdentity(terminalControl, runtime) {
3264
+ if (!hasCanonicalTerminalEndpoint(terminalControl)) {
3265
+ return undefined;
3266
+ }
3267
+ const terminal = terminalEndpointFromControlRef(terminalControl);
3268
+ if (!Number.isInteger(terminal.processAnchorPid) ||
3269
+ Number(terminal.processAnchorPid) <= 0) {
3270
+ return undefined;
3271
+ }
3272
+ return {
3273
+ terminal_identity: terminalEndpointIdentityKey(terminal),
3274
+ terminal_process_anchor_pid: terminal.processAnchorPid,
3275
+ agent_pid: runtime.pid,
3276
+ native_session_id: runtime.nativeSessionId,
3277
+ native_process_uuid: runtime.nativeProcessUuid,
3278
+ native_process_birth: runtime.nativeProcessBirth,
3279
+ native_process_started_at: runtime.nativeProcessStartedAt,
3280
+ native_rollout: runtime.nativeRollout
3281
+ };
3282
+ }
3283
+ function terminalInteractionBridgeOffer(input) {
3284
+ const turnId = terminalInteractionTurnId(input.runtime);
3285
+ const agentVersion = input.runtime?.agentVersion;
3286
+ const expiry = terminalInteractionExpiry(input.now);
3287
+ const canonicalIdentity = input.runtime
3288
+ ? terminalInteractionCanonicalIdentity(input.terminalControl, input.runtime)
3289
+ : undefined;
3290
+ if (!turnId ||
3291
+ typeof agentVersion !== "string" ||
3292
+ agentVersion.length === 0 ||
3293
+ !expiry ||
3294
+ !canonicalIdentity ||
3295
+ input.inspection.approval.blocked) {
3296
+ return undefined;
3297
+ }
3298
+ const nativeInspection = inspectNativeQuestionnaire({
3299
+ agent: input.agent,
3300
+ version: agentVersion,
3301
+ screen: input.screen
3302
+ });
3303
+ if (nativeInspection.status === "none") {
3304
+ return undefined;
3305
+ }
3306
+ const authorityMaterial = JSON.stringify({
3307
+ version: 1,
3308
+ agent: input.agent,
3309
+ turn_id: turnId,
3310
+ terminal: canonicalIdentity,
3311
+ profile: nativeInspection.profile,
3312
+ prompt_sha256: nativeInspection.prompt_evidence.sha256
3313
+ });
3314
+ const promptFingerprint = createHash("sha256")
3315
+ .update(`terminal-interaction-authority\0${authorityMaterial}`, "utf8")
3316
+ .digest("hex");
3317
+ const interactionId = `ti_${createHash("sha256")
3318
+ .update(`terminal-interaction-public\0${authorityMaterial}`, "utf8")
3319
+ .digest("hex")
3320
+ .slice(0, 40)}`;
3321
+ const question = terminalInteractionQuestion(nativeInspection.question);
3322
+ const executable = nativeInspection.status === "actionable" &&
3323
+ nativeInspection.action_plan.kind !== "manual_only";
3324
+ const responseUncertain = input.runtime?.interactionDispatchState === "reserved" ||
3325
+ input.runtime?.interactionDispatchState === "uncertain";
3326
+ const projection = validateTerminalInteractionProjection({
3327
+ schema: TERMINAL_INTERACTION_SCHEMA,
3328
+ version: TERMINAL_INTERACTION_VERSION,
3329
+ interaction_id: interactionId,
3330
+ turn_id: turnId,
3331
+ agent: input.agent,
3332
+ kind: "questionnaire",
3333
+ state: responseUncertain
3334
+ ? "response_uncertain"
3335
+ : executable
3336
+ ? "pending"
3337
+ : "manual_required",
3338
+ step: {
3339
+ index: nativeInspection.current_step,
3340
+ total: nativeInspection.total_steps
3341
+ },
3342
+ questions: [question],
3343
+ expires_at: expiry,
3344
+ capabilities: {
3345
+ respond: executable && !responseUncertain,
3346
+ batch_response: false,
3347
+ free_text: question.response_kind === "free_text",
3348
+ multi_select: question.response_kind === "multi_select"
3349
+ }
3350
+ });
3351
+ return {
3352
+ projection,
3353
+ promptFingerprint,
3354
+ actionPlan: nativeInspection.action_plan,
3355
+ nativeInspection
3356
+ };
3357
+ }
3358
+ function terminalInteractionOptionPreflightReason(options, runtime) {
3359
+ if (typeof options.agentVersion !== "string" ||
3360
+ !/^\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?$/u.test(options.agentVersion)) {
3361
+ return "an exact semantic agent version is required";
3362
+ }
3363
+ if (options.runtime?.agentVersion !== undefined &&
3364
+ options.runtime.agentVersion !== options.agentVersion) {
3365
+ return "runtime agent version does not match the requested interaction profile";
3366
+ }
3367
+ if (!terminalInteractionTurnId(runtime)) {
3368
+ return "a protocol-safe exact Turn id is required";
3369
+ }
3370
+ if (!TERMINAL_INTERACTION_FINGERPRINT_PATTERN.test(options.expectedFingerprint)) {
3371
+ return "a valid interaction prompt fingerprint is required";
3372
+ }
3373
+ if (!Number.isFinite(Date.parse(options.expectedExpiresAt))) {
3374
+ return "a valid interaction expiry is required";
3375
+ }
3376
+ return undefined;
3377
+ }
3378
+ function terminalInteractionOfferPreflightReason(offer, options) {
3379
+ if (!offer) {
3380
+ return "native questionnaire has no identity-fenced interaction offer";
3381
+ }
3382
+ if (offer.projection.state !== "pending" ||
3383
+ !offer.projection.capabilities.respond ||
3384
+ offer.actionPlan.kind === "manual_only") {
3385
+ return `native questionnaire requires manual response: ${offer.nativeInspection.status === "manual_required"
3386
+ ? offer.nativeInspection.reason
3387
+ : "unsupported action plan"}`;
3388
+ }
3389
+ if (offer.promptFingerprint !== options.expectedFingerprint) {
3390
+ return "native questionnaire fingerprint changed before execution";
3391
+ }
3392
+ if (offer.projection.expires_at !== options.expectedExpiresAt) {
3393
+ return "native questionnaire expiry changed before execution";
3394
+ }
3395
+ return undefined;
3396
+ }
3397
+ function terminalInteractionCapabilityReason(provider, terminalControl, plan) {
3398
+ const requiresText = plan.kind === "free_text";
3399
+ const missing = [
3400
+ !terminalControl.capabilities.includes("screen_status")
3401
+ ? "terminal:screen_status"
3402
+ : undefined,
3403
+ !terminalControl.capabilities.includes("send_keys")
3404
+ ? "terminal:send_keys"
3405
+ : undefined,
3406
+ !provider.providerCapabilities.includes("stable_resource_resolution")
3407
+ ? "provider:stable_resource_resolution"
3408
+ : undefined,
3409
+ !provider.providerCapabilities.includes("screen_capture")
3410
+ ? "provider:screen_capture"
3411
+ : undefined,
3412
+ !provider.providerCapabilities.includes("key_delivery")
3413
+ ? "provider:key_delivery"
3414
+ : undefined,
3415
+ requiresText && !provider.providerCapabilities.includes("text_delivery")
3416
+ ? "provider:text_delivery"
3417
+ : undefined
3418
+ ].filter((value) => value !== undefined);
3419
+ return missing.length > 0
3420
+ ? `terminal interaction capability preflight failed: ${missing.join(", ")}`
3421
+ : undefined;
3422
+ }
3423
+ function terminalInteractionPlanPreflightReason(response, plan) {
3424
+ const answer = response.answers[0];
3425
+ if (plan.kind !== "free_text" || answer?.response_kind !== "free_text") {
3426
+ return undefined;
3427
+ }
3428
+ const textStage = plan.stages[0];
3429
+ if (textStage?.kind !== "answer_text" ||
3430
+ !Number.isSafeInteger(textStage.max_characters) ||
3431
+ textStage.max_characters < 1) {
3432
+ return "native free-text interaction has no verified input limit";
3433
+ }
3434
+ return answer.text.length <= textStage.max_characters
3435
+ ? undefined
3436
+ : `free-text answer exceeds the verified native limit of ${textStage.max_characters} characters`;
3437
+ }
3438
+ function blockedTerminalInteractionResponse(response, reason, offer) {
3439
+ const answer = Array.isArray(response.answers) ? response.answers[0] : undefined;
3440
+ return {
3441
+ responded: false,
3442
+ blocked: true,
3443
+ reason,
3444
+ interactionId: response.interaction_id,
3445
+ questionId: answer?.question_id ?? offer?.projection.questions[0]?.question_id,
3446
+ responseKind: answer?.response_kind
3447
+ };
3448
+ }
3449
+ function interactionHookContext(agent, terminalControl, offer, response, runtime) {
3450
+ return {
3451
+ agent,
3452
+ terminalControl,
3453
+ fingerprint: offer.promptFingerprint,
3454
+ projection: offer.projection,
3455
+ response,
3456
+ runtime
3457
+ };
3458
+ }
3459
+ async function authorizeTerminalInteraction(authorize, context) {
3460
+ return authorize ? authorize(context) : { approved: true };
3461
+ }
3462
+ async function reserveTerminalInteractionDispatch(beforeDispatch, context) {
3463
+ if (!beforeDispatch) {
3464
+ return;
3465
+ }
3466
+ try {
3467
+ await beforeDispatch(context);
3468
+ }
3469
+ catch (error) {
3470
+ throw terminalInteractionReservedError("reservation_uncertain", error, "terminal interaction dispatch reservation failed");
3471
+ }
3472
+ }
3473
+ function sameTerminalInteractionOffer(left, right) {
3474
+ return Boolean(left.offer &&
3475
+ right.offer &&
3476
+ sameTerminalControlIdentity(left.terminalControl, right.terminalControl) &&
3477
+ left.offer.promptFingerprint === right.offer.promptFingerprint &&
3478
+ JSON.stringify(left.offer.projection) === JSON.stringify(right.offer.projection) &&
3479
+ JSON.stringify(left.offer.actionPlan) === JSON.stringify(right.offer.actionPlan));
3480
+ }
3481
+ function terminalInteractionReservedError(stage, error, message) {
3482
+ if (error instanceof TerminalInteractionDispatchReservedError) {
3483
+ return error;
3484
+ }
3485
+ const detail = error instanceof Error ? error.message : String(error);
3486
+ return new TerminalInteractionDispatchReservedError(stage, `${message}: ${detail}`, { cause: error });
3487
+ }
3488
+ function terminalInteractionChoiceAction(plan, answer) {
3489
+ const choice = plan.choices.find((candidate) => candidate.option_id === answer.selected_option_ids[0]);
3490
+ const stage = choice?.stages[0];
3491
+ if (!choice ||
3492
+ choice.stages.length !== 1 ||
3493
+ stage?.kind !== "key" ||
3494
+ !/^[1-5]$/u.test(stage.key)) {
3495
+ throw new Error("single-select interaction has no closed adapter action");
3496
+ }
3497
+ return {
3498
+ key: stage.key,
3499
+ outcome: choice.outcome === "open_custom_text"
3500
+ ? "custom_text_opened"
3501
+ : "submitted_or_advanced"
3502
+ };
3503
+ }
3504
+ function terminalInteractionConfirmKey(plan, answer) {
3505
+ const stages = answer.confirm ? plan.confirm_stages : plan.cancel_stages;
3506
+ const stage = stages[0];
3507
+ if (stages.length !== 1 ||
3508
+ stage?.kind !== "key" ||
3509
+ !["C-m", "Escape", "1", "2"].includes(stage.key) ||
3510
+ JSON.stringify(plan.confirm_stages) === JSON.stringify(plan.cancel_stages)) {
3511
+ throw new Error("confirmation interaction has no closed adapter action");
3512
+ }
3513
+ return stage.key;
3514
+ }
3515
+ async function sendTerminalInteractionKeys(provider, terminalControl, keys) {
3516
+ try {
3517
+ await provider.sendKeys(provider.endpoint(terminalControl), keys);
3518
+ }
3519
+ catch (error) {
3520
+ throw terminalInteractionReservedError("key_uncertain", error, "terminal interaction key dispatch is uncertain");
3521
+ }
3522
+ }
3523
+ async function dispatchTerminalInteractionFreeText(input) {
3524
+ const [textStage, enterStage] = input.plan.stages;
3525
+ if (input.plan.stages.length !== 2 ||
3526
+ textStage?.kind !== "answer_text" ||
3527
+ textStage.single_line !== true ||
3528
+ textStage.max_characters < input.answer.text.length ||
3529
+ enterStage?.kind !== "key" ||
3530
+ enterStage.key !== "C-m") {
3531
+ throw new TerminalInteractionDispatchReservedError("reservation_uncertain", "free-text interaction has no closed adapter action");
3532
+ }
3533
+ try {
3534
+ await input.provider.sendText(input.provider.endpoint(input.terminalControl), input.answer.text);
3535
+ }
3536
+ catch (error) {
3537
+ throw terminalInteractionReservedError("text_uncertain", error, "terminal interaction text dispatch is uncertain");
3538
+ }
3539
+ if (input.agent === "codex") {
3540
+ try {
3541
+ await input.sleep(CODEX_PASTE_ENTER_SETTLE_MS);
3542
+ }
3543
+ catch (error) {
3544
+ throw terminalInteractionReservedError("text_uncertain", error, "terminal interaction paste-settle outcome is uncertain");
3545
+ }
3546
+ }
3547
+ let enterControl;
3548
+ try {
3549
+ enterControl = await input.reverifyTerminalIdentity(input.terminalControl);
3550
+ if (!sameTerminalControlIdentity(input.terminalControl, enterControl)) {
3551
+ throw new Error("terminal identity changed between interaction text and Enter");
3552
+ }
3553
+ }
3554
+ catch (error) {
3555
+ throw terminalInteractionReservedError("text_uncertain", error, "terminal identity after interaction text dispatch is uncertain");
3556
+ }
3557
+ await sendTerminalInteractionKeys(input.provider, enterControl, [enterStage.key]);
3558
+ return "submitted_or_advanced";
3559
+ }
3560
+ async function dispatchTerminalInteractionAnswer(input) {
3561
+ if (input.actionPlan.kind === "single_select" &&
3562
+ input.answer.response_kind === "single_select") {
3563
+ const action = terminalInteractionChoiceAction(input.actionPlan, input.answer);
3564
+ await sendTerminalInteractionKeys(input.provider, input.terminalControl, [action.key]);
3565
+ return action.outcome;
3566
+ }
3567
+ if (input.actionPlan.kind === "confirm" &&
3568
+ input.answer.response_kind === "confirm") {
3569
+ const key = terminalInteractionConfirmKey(input.actionPlan, input.answer);
3570
+ await sendTerminalInteractionKeys(input.provider, input.terminalControl, [key]);
3571
+ return input.answer.confirm ? "confirmed" : "cancelled";
3572
+ }
3573
+ if (input.actionPlan.kind === "free_text" &&
3574
+ input.answer.response_kind === "free_text") {
3575
+ return dispatchTerminalInteractionFreeText({
3576
+ agent: input.agent,
3577
+ provider: input.provider,
3578
+ terminalControl: input.terminalControl,
3579
+ plan: input.actionPlan,
3580
+ answer: input.answer,
3581
+ reverifyTerminalIdentity: input.reverifyTerminalIdentity,
3582
+ sleep: input.sleep
3583
+ });
3584
+ }
3585
+ throw new TerminalInteractionDispatchReservedError("reservation_uncertain", "terminal interaction answer does not have a closed adapter action");
3586
+ }
2899
3587
  function statusFromInspection(adapter, terminalControl, inspection, options = {}) {
2900
3588
  const approval = dispatchableApprovalInspection(adapter, inspection.approval);
2901
3589
  const fingerprint = terminalApprovalFingerprint(adapter.agent, terminalControl, {
2902
3590
  ...inspection,
2903
3591
  approval
2904
3592
  }, options);
3593
+ const choices = approval.approvable
3594
+ ? terminalApprovalChoices(approval).flatMap((choice) => {
3595
+ if (!choice.decision) {
3596
+ return [];
3597
+ }
3598
+ const choiceFingerprint = terminalApprovalFingerprint(adapter.agent, terminalControl, { ...inspection, approval }, { ...options, decision: choice.decision });
3599
+ return choiceFingerprint ? [{
3600
+ decision: choice.decision,
3601
+ label: choice.label,
3602
+ fingerprint: choiceFingerprint
3603
+ }] : [];
3604
+ })
3605
+ : [];
3606
+ const interaction = options.screen === undefined
3607
+ ? undefined
3608
+ : terminalInteractionBridgeOffer({
3609
+ agent: adapter.agent,
3610
+ terminalControl,
3611
+ inspection: { ...inspection, approval },
3612
+ screen: options.screen,
3613
+ runtime: options.runtime,
3614
+ now: options.now ?? new Date()
3615
+ });
2905
3616
  return {
2906
3617
  provider: terminalControl.kind,
2907
3618
  target: terminalControl.target,
@@ -2926,6 +3637,7 @@ function statusFromInspection(adapter, terminalControl, inspection, options = {}
2926
3637
  request_detail: approval.requestDetail,
2927
3638
  reason: approval.approvable ? undefined : approval.reason,
2928
3639
  fingerprint,
3640
+ choices: choices.length > 0 ? choices : undefined,
2929
3641
  decision_mode: approval.approvable ? approval.action.mode ?? "keys" : undefined,
2930
3642
  request_id: approval.approvable ? approval.action.requestId : undefined,
2931
3643
  policy_evidence: approval.approvable && approval.policyEvidence
@@ -2944,7 +3656,11 @@ function statusFromInspection(adapter, terminalControl, inspection, options = {}
2944
3656
  ? undefined
2945
3657
  : createHash("sha256").update(options.screen).digest("hex"),
2946
3658
  approval: approvalOutput(approval)
2947
- }
3659
+ },
3660
+ ...(interaction === undefined ? {} : {
3661
+ interaction_state: interaction.projection,
3662
+ interaction_prompt_fingerprint: interaction.promptFingerprint
3663
+ })
2948
3664
  };
2949
3665
  }
2950
3666
  function dispatchableApprovalInspection(adapter, approval) {
@@ -3007,6 +3723,10 @@ function approvalOutput(approval) {
3007
3723
  cwd: approval.cwd,
3008
3724
  toolName: approval.toolName,
3009
3725
  requestDetail: approval.requestDetail,
3726
+ choices: terminalApprovalChoices(approval).map((choice) => ({
3727
+ decision: choice.decision,
3728
+ label: choice.label
3729
+ })),
3010
3730
  policyEvidence: approval.policyEvidence
3011
3731
  ? {
3012
3732
  source: approval.policyEvidence.source,