@scotthuang/agent-knock-knock 0.12.29 → 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 (64) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/src/claude-terminal-agent-adapter.js +20 -1
  3. package/dist/src/claude-terminal-agent-adapter.js.map +1 -1
  4. package/dist/src/cli-core.js +50 -35
  5. package/dist/src/cli-core.js.map +1 -1
  6. package/dist/src/codex-terminal-agent-adapter.d.ts +2 -1
  7. package/dist/src/codex-terminal-agent-adapter.js +40 -4
  8. package/dist/src/codex-terminal-agent-adapter.js.map +1 -1
  9. package/dist/src/host-adapter.d.ts +1 -1
  10. package/dist/src/host-adapter.js +1 -1
  11. package/dist/src/host-bridge-tools.js +2 -2
  12. package/dist/src/openclaw-plugin-command-adapter.js +155 -24
  13. package/dist/src/openclaw-plugin-command-adapter.js.map +1 -1
  14. package/dist/src/openclaw-plugin-helpers.d.ts +2 -0
  15. package/dist/src/openclaw-plugin-helpers.js +28 -7
  16. package/dist/src/openclaw-plugin-helpers.js.map +1 -1
  17. package/dist/src/openclaw-plugin-schemas.d.ts +104 -0
  18. package/dist/src/openclaw-plugin-schemas.js +108 -0
  19. package/dist/src/openclaw-plugin-schemas.js.map +1 -1
  20. package/dist/src/openclaw-private-authority-offers.d.ts +2 -0
  21. package/dist/src/openclaw-private-authority-offers.js +12 -0
  22. package/dist/src/openclaw-private-authority-offers.js.map +1 -1
  23. package/dist/src/terminal-agent-adapter.d.ts +34 -0
  24. package/dist/src/terminal-agent-adapter.js +54 -0
  25. package/dist/src/terminal-agent-adapter.js.map +1 -1
  26. package/dist/src/terminal-agent-bridge.d.ts +75 -1
  27. package/dist/src/terminal-agent-bridge.js +759 -149
  28. package/dist/src/terminal-agent-bridge.js.map +1 -1
  29. package/dist/src/terminal-command-cli-adapter.d.ts +2 -1
  30. package/dist/src/terminal-command-cli-adapter.js +46 -8
  31. package/dist/src/terminal-command-cli-adapter.js.map +1 -1
  32. package/dist/src/terminal-dispatch-execution.js +15 -0
  33. package/dist/src/terminal-dispatch-execution.js.map +1 -1
  34. package/dist/src/terminal-identity-authority-cli-adapter.d.ts +1 -0
  35. package/dist/src/terminal-identity-authority-cli-adapter.js +10 -1
  36. package/dist/src/terminal-identity-authority-cli-adapter.js.map +1 -1
  37. package/dist/src/terminal-interaction-authority.d.ts +33 -0
  38. package/dist/src/terminal-interaction-authority.js +3 -0
  39. package/dist/src/terminal-interaction-authority.js.map +1 -0
  40. package/dist/src/terminal-interaction-cli-adapter.d.ts +66 -0
  41. package/dist/src/terminal-interaction-cli-adapter.js +354 -0
  42. package/dist/src/terminal-interaction-cli-adapter.js.map +1 -0
  43. package/dist/src/terminal-interaction-protocol.d.ts +111 -0
  44. package/dist/src/terminal-interaction-protocol.js +432 -0
  45. package/dist/src/terminal-interaction-protocol.js.map +1 -0
  46. package/dist/src/terminal-list-renderer.js +31 -2
  47. package/dist/src/terminal-list-renderer.js.map +1 -1
  48. package/dist/src/terminal-monitor-cli-adapter.js +12 -3
  49. package/dist/src/terminal-monitor-cli-adapter.js.map +1 -1
  50. package/dist/src/terminal-monitor-decision-policy.d.ts +1 -1
  51. package/dist/src/terminal-monitor-state-cli-adapter.js +15 -1
  52. package/dist/src/terminal-monitor-state-cli-adapter.js.map +1 -1
  53. package/dist/src/terminal-mutation-cli-runtime.d.ts +44 -0
  54. package/dist/src/terminal-mutation-cli-runtime.js +41 -0
  55. package/dist/src/terminal-mutation-cli-runtime.js.map +1 -0
  56. package/dist/src/terminal-questionnaire-adapter.d.ts +84 -0
  57. package/dist/src/terminal-questionnaire-adapter.js +677 -0
  58. package/dist/src/terminal-questionnaire-adapter.js.map +1 -0
  59. package/docs/host-bridge-profiles.md +2 -2
  60. package/docs/quickstart-herdr.md +1 -1
  61. package/docs/quickstart-tmux.md +3 -1
  62. package/openclaw.plugin.json +4 -0
  63. package/package.json +1 -1
  64. package/templates/openclaw-skills/agent-knock-knock/SKILL.md +11 -8
@@ -1,8 +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
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";
6
8
  // Verified Codex profiles through 0.153.0 keep Enter in paste/newline mode for
7
9
  // 120ms after burst input. Cross that boundary rather than landing on it, and
8
10
  // also require observable composer stability instead of treating this delay
@@ -140,6 +142,9 @@ const CLAUDE_NATIVE_STATUS_SETTLE_BY_PROFILE = {
140
142
  }
141
143
  };
142
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;
143
148
  /** A terminal send failed at a boundary that proves input never started. */
144
149
  export class TerminalInputNotStartedError extends Error {
145
150
  code = "AKK_TERMINAL_INPUT_NOT_STARTED";
@@ -214,6 +219,20 @@ export class NativeInspectionDismissalError extends Error {
214
219
  this.name = "NativeInspectionDismissalError";
215
220
  }
216
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
+ }
217
236
  function terminalSendComposerRequirements(agent, multiline, options) {
218
237
  const exactEmptyRetryAuthority = options.requireExactEmptyComposerAfterBeforeText;
219
238
  const requireExactEmptyComposerBeforeText = options.requireExactEmptyComposerBeforeText === true;
@@ -272,11 +291,152 @@ function throwPostTextHookFailureAfterUserEnter(enabled, error) {
272
291
  return;
273
292
  throw new TerminalEnterDispatchReservedError("user-explicit Codex Enter was dispatched but post-text bookkeeping failed; do not retry", { cause: error });
274
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
+ }
275
434
  export class TerminalAgentBridge {
276
435
  registry;
277
436
  terminalProvider;
278
437
  verifyIdentity;
279
438
  nowMs;
439
+ now;
280
440
  sleep;
281
441
  diagnosticLog;
282
442
  constructor(options) {
@@ -284,6 +444,7 @@ export class TerminalAgentBridge {
284
444
  this.terminalProvider = options.terminalProvider;
285
445
  this.verifyIdentity = options.verifyIdentity;
286
446
  this.nowMs = options.nowMs ?? (() => performance.now());
447
+ this.now = options.now ?? (() => new Date());
287
448
  this.sleep = options.sleep ?? terminalSettleDelay;
288
449
  this.diagnosticLog = options.diagnosticLog;
289
450
  }
@@ -405,7 +566,8 @@ export class TerminalAgentBridge {
405
566
  const captured = await this.captureInspection(adapter, terminalControl, options);
406
567
  return statusFromInspection(adapter, captured.terminalControl, captured.inspection, {
407
568
  screen: captured.screen,
408
- runtime: options.runtime
569
+ runtime: options.runtime,
570
+ now: this.now()
409
571
  });
410
572
  }
411
573
  catch (error) {
@@ -1791,6 +1953,7 @@ export class TerminalAgentBridge {
1791
1953
  }
1792
1954
  async approve(agent, terminalControl, options = {}) {
1793
1955
  const adapter = this.registry.require(agent);
1956
+ const decision = options.decision ?? "approve_once";
1794
1957
  if (!adapter.capabilities.terminalApproval ||
1795
1958
  !terminalControl.capabilities.includes("terminal_approval")) {
1796
1959
  return {
@@ -1812,124 +1975,27 @@ export class TerminalAgentBridge {
1812
1975
  const captured = await this.captureInspection(adapter, terminalControl, options);
1813
1976
  const { inspection } = captured;
1814
1977
  const activeTerminalControl = captured.terminalControl;
1815
- if (!inspection.approval.approvable) {
1816
- return {
1817
- approved: false,
1818
- blocked: inspection.approval.blocked,
1819
- reason: inspection.approval.reason,
1820
- promptKind: inspection.approval.promptKind,
1821
- command: inspection.approval.command,
1822
- screenExcerpt: inspection.screenExcerpt
1823
- };
1824
- }
1825
- const decisionMode = inspection.approval.action.mode ?? "keys";
1826
- if (options.requiredDecisionMode && decisionMode !== options.requiredDecisionMode) {
1827
- return {
1828
- approved: false,
1829
- blocked: true,
1830
- reason: `${adapter.displayName} approval mode ${decisionMode} is not eligible for this decision`,
1831
- label: inspection.approval.action.label,
1832
- promptKind: inspection.approval.promptKind,
1833
- command: inspection.approval.command,
1834
- fingerprint: terminalApprovalFingerprint(adapter.agent, activeTerminalControl, inspection, {
1835
- screen: captured.screen,
1836
- runtime: options.runtime
1837
- }),
1838
- screenExcerpt: inspection.screenExcerpt,
1839
- decisionMode,
1840
- requestId: inspection.approval.action.requestId
1841
- };
1842
- }
1843
- if (decisionMode === "keys" && inspection.approval.action.keys.length === 0) {
1844
- return {
1845
- approved: false,
1846
- blocked: true,
1847
- reason: `${adapter.displayName} approval action has no keys`,
1848
- label: inspection.approval.action.label,
1849
- promptKind: inspection.approval.promptKind,
1850
- command: inspection.approval.command,
1851
- screenExcerpt: inspection.screenExcerpt
1852
- };
1853
- }
1854
- if (decisionMode === "keys" &&
1855
- !isTerminalApprovalPromptEvidence(inspection.approval.promptEvidence)) {
1856
- return {
1857
- approved: false,
1858
- blocked: true,
1859
- reason: `${adapter.displayName} approval prompt has no adapter-verified prompt evidence`,
1860
- label: inspection.approval.action.label,
1861
- promptKind: inspection.approval.promptKind,
1862
- command: inspection.approval.command,
1863
- screenExcerpt: inspection.screenExcerpt,
1864
- decisionMode,
1865
- requestId: inspection.approval.action.requestId
1866
- };
1867
- }
1868
- const canonicalFingerprint = terminalApprovalFingerprint(adapter.agent, activeTerminalControl, inspection, {
1978
+ const preflight = terminalApprovalPreflight({
1979
+ adapter,
1980
+ terminalControl,
1981
+ activeTerminalControl,
1982
+ inspection,
1869
1983
  screen: captured.screen,
1984
+ decision,
1985
+ expectedFingerprint: options.expectedFingerprint,
1986
+ requiredDecisionMode: options.requiredDecisionMode,
1870
1987
  runtime: options.runtime
1871
1988
  });
1872
- const legacyFingerprint = hasCanonicalTerminalEndpoint(terminalControl)
1873
- ? undefined
1874
- : terminalApprovalFingerprint(adapter.agent, terminalControl, inspection, {
1875
- screen: captured.screen,
1876
- runtime: options.runtime
1877
- });
1878
- const useLegacyFingerprint = Boolean(options.expectedFingerprint &&
1879
- options.expectedFingerprint === legacyFingerprint);
1880
- const fingerprintControl = useLegacyFingerprint
1881
- ? terminalControl
1882
- : activeTerminalControl;
1883
- const fingerprint = useLegacyFingerprint
1884
- ? legacyFingerprint
1885
- : canonicalFingerprint;
1886
- if (adapter.agent === "claude" &&
1887
- decisionMode === "keys" &&
1888
- !options.expectedFingerprint) {
1889
- return {
1890
- approved: false,
1891
- blocked: true,
1892
- reason: "screen approval requires the latest expected fingerprint",
1893
- key: inspection.approval.action.keys.length === 1
1894
- ? inspection.approval.action.keys[0]
1895
- : undefined,
1896
- keys: inspection.approval.action.keys,
1897
- label: inspection.approval.action.label,
1898
- promptKind: inspection.approval.promptKind,
1899
- command: inspection.approval.command,
1900
- cwd: inspection.approval.cwd,
1901
- toolName: inspection.approval.toolName,
1902
- requestDetail: inspection.approval.requestDetail,
1903
- fingerprint,
1904
- screenExcerpt: inspection.screenExcerpt,
1905
- decisionMode,
1906
- requestId: inspection.approval.action.requestId
1907
- };
1908
- }
1909
- if (options.expectedFingerprint && options.expectedFingerprint !== fingerprint) {
1910
- return {
1911
- approved: false,
1912
- blocked: true,
1913
- reason: "approval fingerprint changed before execution",
1914
- key: inspection.approval.action.keys.length === 1
1915
- ? inspection.approval.action.keys[0]
1916
- : undefined,
1917
- keys: inspection.approval.action.keys,
1918
- label: inspection.approval.action.label,
1919
- promptKind: inspection.approval.promptKind,
1920
- command: inspection.approval.command,
1921
- fingerprint,
1922
- screenExcerpt: inspection.screenExcerpt,
1923
- decisionMode,
1924
- requestId: inspection.approval.action.requestId
1925
- };
1926
- }
1989
+ if (!preflight.ready)
1990
+ return preflight.result;
1991
+ const { action, decisionMode, fingerprintControl, fingerprint } = preflight;
1927
1992
  if (options.authorize) {
1928
1993
  const authorization = await options.authorize({
1929
1994
  agent: adapter.agent,
1930
1995
  terminalControl: activeTerminalControl,
1931
1996
  inspection,
1932
1997
  fingerprint,
1998
+ decision,
1933
1999
  runtime: options.runtime
1934
2000
  });
1935
2001
  if (!authorization.approved) {
@@ -1937,11 +2003,12 @@ export class TerminalAgentBridge {
1937
2003
  approved: false,
1938
2004
  blocked: true,
1939
2005
  reason: authorization.reason ?? "approval was not authorized",
1940
- key: inspection.approval.action.keys.length === 1
1941
- ? inspection.approval.action.keys[0]
2006
+ decision,
2007
+ key: action.keys.length === 1
2008
+ ? action.keys[0]
1942
2009
  : undefined,
1943
- keys: inspection.approval.action.keys,
1944
- label: inspection.approval.action.label,
2010
+ keys: action.keys,
2011
+ label: action.label,
1945
2012
  promptKind: inspection.approval.promptKind,
1946
2013
  command: inspection.approval.command,
1947
2014
  toolName: inspection.approval.toolName,
@@ -1949,7 +2016,7 @@ export class TerminalAgentBridge {
1949
2016
  fingerprint,
1950
2017
  screenExcerpt: inspection.screenExcerpt,
1951
2018
  decisionMode,
1952
- requestId: inspection.approval.action.requestId
2019
+ requestId: action.requestId
1953
2020
  };
1954
2021
  }
1955
2022
  }
@@ -1968,21 +2035,34 @@ export class TerminalAgentBridge {
1968
2035
  screenExcerpt: recapturedInspection.screenExcerpt
1969
2036
  };
1970
2037
  }
1971
- 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";
1972
2050
  const recapturedFingerprint = terminalApprovalFingerprint(adapter.agent, fingerprintControl, recapturedInspection, {
1973
2051
  screen: recaptured.screen,
1974
- runtime: options.runtime
2052
+ runtime: options.runtime,
2053
+ decision
1975
2054
  });
1976
2055
  if (recapturedDecisionMode !== decisionMode) {
1977
2056
  return {
1978
2057
  approved: false,
1979
2058
  blocked: true,
1980
2059
  reason: "approval decision mode changed after authorization",
1981
- key: recapturedInspection.approval.action.keys.length === 1
1982
- ? recapturedInspection.approval.action.keys[0]
2060
+ decision,
2061
+ key: recapturedAction.keys.length === 1
2062
+ ? recapturedAction.keys[0]
1983
2063
  : undefined,
1984
- keys: recapturedInspection.approval.action.keys,
1985
- label: recapturedInspection.approval.action.label,
2064
+ keys: recapturedAction.keys,
2065
+ label: recapturedAction.label,
1986
2066
  promptKind: recapturedInspection.approval.promptKind,
1987
2067
  command: recapturedInspection.approval.command,
1988
2068
  cwd: recapturedInspection.approval.cwd,
@@ -1991,7 +2071,7 @@ export class TerminalAgentBridge {
1991
2071
  fingerprint: recapturedFingerprint,
1992
2072
  screenExcerpt: recapturedInspection.screenExcerpt,
1993
2073
  decisionMode: recapturedDecisionMode,
1994
- requestId: recapturedInspection.approval.action.requestId
2074
+ requestId: recapturedAction.requestId
1995
2075
  };
1996
2076
  }
1997
2077
  if (recapturedFingerprint !== fingerprint) {
@@ -1999,11 +2079,12 @@ export class TerminalAgentBridge {
1999
2079
  approved: false,
2000
2080
  blocked: true,
2001
2081
  reason: "approval fingerprint changed after authorization",
2002
- key: recapturedInspection.approval.action.keys.length === 1
2003
- ? recapturedInspection.approval.action.keys[0]
2082
+ decision,
2083
+ key: recapturedAction.keys.length === 1
2084
+ ? recapturedAction.keys[0]
2004
2085
  : undefined,
2005
- keys: recapturedInspection.approval.action.keys,
2006
- label: recapturedInspection.approval.action.label,
2086
+ keys: recapturedAction.keys,
2087
+ label: recapturedAction.label,
2007
2088
  promptKind: recapturedInspection.approval.promptKind,
2008
2089
  command: recapturedInspection.approval.command,
2009
2090
  cwd: recapturedInspection.approval.cwd,
@@ -2012,7 +2093,7 @@ export class TerminalAgentBridge {
2012
2093
  fingerprint: recapturedFingerprint,
2013
2094
  screenExcerpt: recapturedInspection.screenExcerpt,
2014
2095
  decisionMode: recapturedDecisionMode,
2015
- requestId: recapturedInspection.approval.action.requestId
2096
+ requestId: recapturedAction.requestId
2016
2097
  };
2017
2098
  }
2018
2099
  const verifiedForApproval = await this.verifyTerminalIdentity(adapter.agent, recaptured.terminalControl, options.runtime);
@@ -2030,11 +2111,13 @@ export class TerminalAgentBridge {
2030
2111
  terminalControl: verifiedForApproval,
2031
2112
  inspection: recapturedInspection,
2032
2113
  fingerprint: recapturedFingerprint,
2033
- keys: recapturedInspection.approval.action.keys,
2114
+ decision,
2115
+ keys: recapturedAction.keys,
2034
2116
  runtime: options.runtime
2035
2117
  });
2036
2118
  let dispatchTerminalControl = verifiedForApproval;
2037
2119
  let dispatchInspection = recapturedInspection;
2120
+ let dispatchAction = recapturedAction;
2038
2121
  if (options.beforeKeyDispatch) {
2039
2122
  const afterReservation = await this.captureInspection(adapter, verifiedForApproval, options);
2040
2123
  const afterReservationInspection = afterReservation.inspection;
@@ -2051,22 +2134,35 @@ export class TerminalAgentBridge {
2051
2134
  screenExcerpt: afterReservationInspection.screenExcerpt
2052
2135
  };
2053
2136
  }
2054
- 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";
2055
2149
  const afterReservationFingerprint = terminalApprovalFingerprint(adapter.agent, fingerprintControl, afterReservationInspection, {
2056
2150
  screen: afterReservation.screen,
2057
- runtime: options.runtime
2151
+ runtime: options.runtime,
2152
+ decision
2058
2153
  });
2059
2154
  if (afterReservationMode !== "keys" ||
2060
2155
  afterReservationFingerprint !== recapturedFingerprint) {
2061
2156
  return {
2062
2157
  approved: false,
2063
2158
  blocked: true,
2159
+ decision,
2064
2160
  reason: "approval fingerprint changed after dispatch reservation",
2065
- key: afterReservationInspection.approval.action.keys.length === 1
2066
- ? afterReservationInspection.approval.action.keys[0]
2161
+ key: afterReservationAction.keys.length === 1
2162
+ ? afterReservationAction.keys[0]
2067
2163
  : undefined,
2068
- keys: afterReservationInspection.approval.action.keys,
2069
- label: afterReservationInspection.approval.action.label,
2164
+ keys: afterReservationAction.keys,
2165
+ label: afterReservationAction.label,
2070
2166
  promptKind: afterReservationInspection.approval.promptKind,
2071
2167
  command: afterReservationInspection.approval.command,
2072
2168
  cwd: afterReservationInspection.approval.cwd,
@@ -2075,11 +2171,12 @@ export class TerminalAgentBridge {
2075
2171
  fingerprint: afterReservationFingerprint,
2076
2172
  screenExcerpt: afterReservationInspection.screenExcerpt,
2077
2173
  decisionMode: afterReservationMode,
2078
- requestId: afterReservationInspection.approval.action.requestId
2174
+ requestId: afterReservationAction.requestId
2079
2175
  };
2080
2176
  }
2081
2177
  dispatchTerminalControl = afterReservation.terminalControl;
2082
2178
  dispatchInspection = afterReservationInspection;
2179
+ dispatchAction = afterReservationAction;
2083
2180
  }
2084
2181
  if (!dispatchInspection.approval.approvable) {
2085
2182
  return {
@@ -2094,22 +2191,108 @@ export class TerminalAgentBridge {
2094
2191
  if (!sameTerminalControlIdentity(dispatchTerminalControl, verifiedImmediatelyBeforeSend)) {
2095
2192
  throw new Error("terminal control identity changed after the final approval capture");
2096
2193
  }
2097
- await this.terminalProvider.sendKeys(this.terminalProvider.endpoint(verifiedImmediatelyBeforeSend), dispatchApproval.action.keys);
2194
+ await this.terminalProvider.sendKeys(this.terminalProvider.endpoint(verifiedImmediatelyBeforeSend), dispatchAction.keys);
2098
2195
  return {
2099
- approved: true,
2196
+ approved: decision === "approve_once",
2197
+ decisionDispatched: true,
2198
+ decision,
2100
2199
  blocked: false,
2101
- key: dispatchApproval.action.keys.length === 1
2102
- ? dispatchApproval.action.keys[0]
2200
+ key: dispatchAction.keys.length === 1
2201
+ ? dispatchAction.keys[0]
2103
2202
  : undefined,
2104
- keys: dispatchApproval.action.keys,
2105
- label: dispatchApproval.action.label,
2203
+ keys: dispatchAction.keys,
2204
+ label: dispatchAction.label,
2106
2205
  promptKind: dispatchApproval.promptKind,
2107
2206
  command: dispatchApproval.command,
2108
2207
  cwd: dispatchApproval.cwd,
2109
2208
  fingerprint: recapturedFingerprint,
2110
2209
  screenExcerpt: dispatchInspection.screenExcerpt,
2111
2210
  decisionMode: recapturedDecisionMode,
2112
- 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
2113
2296
  };
2114
2297
  }
2115
2298
  async monitorPoll(options) {
@@ -2126,7 +2309,8 @@ export class TerminalAgentBridge {
2126
2309
  inspection = captured.inspection;
2127
2310
  status = statusFromInspection(adapter, captured.terminalControl, inspection, {
2128
2311
  screen: captured.screen,
2129
- runtime: options.screenOptions?.runtime
2312
+ runtime: options.screenOptions?.runtime,
2313
+ now: this.now()
2130
2314
  });
2131
2315
  }
2132
2316
  catch (error) {
@@ -2181,6 +2365,25 @@ export class TerminalAgentBridge {
2181
2365
  })
2182
2366
  };
2183
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
+ }
2184
2387
  async verifyTerminalIdentity(agent, terminalControl, runtime) {
2185
2388
  let verifiedControl = terminalControl;
2186
2389
  if (this.verifyIdentity) {
@@ -2914,7 +3117,13 @@ export function terminalApprovalFingerprint(agent, terminalControl, inspection,
2914
3117
  if (!inspection.approval.approvable) {
2915
3118
  return undefined;
2916
3119
  }
2917
- 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";
2918
3127
  const promptEvidence = inspection.approval.promptEvidence;
2919
3128
  if (decisionMode === "keys" &&
2920
3129
  !isTerminalApprovalPromptEvidence(promptEvidence)) {
@@ -2968,8 +3177,16 @@ export function terminalApprovalFingerprint(agent, terminalControl, inspection,
2968
3177
  message_id: options.runtime?.messageId,
2969
3178
  terminal_target: options.runtime?.terminalTarget
2970
3179
  },
2971
- keys: inspection.approval.action.keys,
2972
- 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
+ })),
2973
3190
  prompt_kind: inspection.approval.promptKind,
2974
3191
  command: inspection.approval.command,
2975
3192
  cwd: inspection.approval.cwd,
@@ -2992,7 +3209,7 @@ export function terminalApprovalFingerprint(agent, terminalControl, inspection,
2992
3209
  }
2993
3210
  : undefined,
2994
3211
  decision_mode: decisionMode,
2995
- request_id: inspection.approval.action.requestId
3212
+ request_id: action.requestId
2996
3213
  }))
2997
3214
  .digest("hex");
2998
3215
  }
@@ -3006,12 +3223,396 @@ function isTerminalApprovalPromptEvidence(value) {
3006
3223
  typeof evidence.sha256 === "string" &&
3007
3224
  /^[0-9a-f]{64}$/u.test(evidence.sha256);
3008
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
+ }
3009
3587
  function statusFromInspection(adapter, terminalControl, inspection, options = {}) {
3010
3588
  const approval = dispatchableApprovalInspection(adapter, inspection.approval);
3011
3589
  const fingerprint = terminalApprovalFingerprint(adapter.agent, terminalControl, {
3012
3590
  ...inspection,
3013
3591
  approval
3014
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
+ });
3015
3616
  return {
3016
3617
  provider: terminalControl.kind,
3017
3618
  target: terminalControl.target,
@@ -3036,6 +3637,7 @@ function statusFromInspection(adapter, terminalControl, inspection, options = {}
3036
3637
  request_detail: approval.requestDetail,
3037
3638
  reason: approval.approvable ? undefined : approval.reason,
3038
3639
  fingerprint,
3640
+ choices: choices.length > 0 ? choices : undefined,
3039
3641
  decision_mode: approval.approvable ? approval.action.mode ?? "keys" : undefined,
3040
3642
  request_id: approval.approvable ? approval.action.requestId : undefined,
3041
3643
  policy_evidence: approval.approvable && approval.policyEvidence
@@ -3054,7 +3656,11 @@ function statusFromInspection(adapter, terminalControl, inspection, options = {}
3054
3656
  ? undefined
3055
3657
  : createHash("sha256").update(options.screen).digest("hex"),
3056
3658
  approval: approvalOutput(approval)
3057
- }
3659
+ },
3660
+ ...(interaction === undefined ? {} : {
3661
+ interaction_state: interaction.projection,
3662
+ interaction_prompt_fingerprint: interaction.promptFingerprint
3663
+ })
3058
3664
  };
3059
3665
  }
3060
3666
  function dispatchableApprovalInspection(adapter, approval) {
@@ -3117,6 +3723,10 @@ function approvalOutput(approval) {
3117
3723
  cwd: approval.cwd,
3118
3724
  toolName: approval.toolName,
3119
3725
  requestDetail: approval.requestDetail,
3726
+ choices: terminalApprovalChoices(approval).map((choice) => ({
3727
+ decision: choice.decision,
3728
+ label: choice.label
3729
+ })),
3120
3730
  policyEvidence: approval.policyEvidence
3121
3731
  ? {
3122
3732
  source: approval.policyEvidence.source,