@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.
- package/CHANGELOG.md +17 -0
- package/dist/src/claude-injected-paste-proof.d.ts +59 -0
- package/dist/src/claude-injected-paste-proof.js +153 -0
- package/dist/src/claude-injected-paste-proof.js.map +1 -0
- package/dist/src/claude-terminal-agent-adapter.js +20 -1
- package/dist/src/claude-terminal-agent-adapter.js.map +1 -1
- package/dist/src/cli-core.js +50 -35
- package/dist/src/cli-core.js.map +1 -1
- package/dist/src/codex-terminal-agent-adapter.d.ts +2 -1
- package/dist/src/codex-terminal-agent-adapter.js +40 -4
- package/dist/src/codex-terminal-agent-adapter.js.map +1 -1
- package/dist/src/host-adapter.d.ts +1 -1
- package/dist/src/host-adapter.js +1 -1
- package/dist/src/host-bridge-tools.js +2 -2
- package/dist/src/openclaw-plugin-command-adapter.js +155 -24
- package/dist/src/openclaw-plugin-command-adapter.js.map +1 -1
- package/dist/src/openclaw-plugin-helpers.d.ts +2 -0
- package/dist/src/openclaw-plugin-helpers.js +28 -7
- package/dist/src/openclaw-plugin-helpers.js.map +1 -1
- package/dist/src/openclaw-plugin-schemas.d.ts +104 -0
- package/dist/src/openclaw-plugin-schemas.js +108 -0
- package/dist/src/openclaw-plugin-schemas.js.map +1 -1
- package/dist/src/openclaw-private-authority-offers.d.ts +2 -0
- package/dist/src/openclaw-private-authority-offers.js +12 -0
- package/dist/src/openclaw-private-authority-offers.js.map +1 -1
- package/dist/src/terminal-agent-adapter.d.ts +34 -0
- package/dist/src/terminal-agent-adapter.js +54 -0
- package/dist/src/terminal-agent-adapter.js.map +1 -1
- package/dist/src/terminal-agent-bridge.d.ts +76 -1
- package/dist/src/terminal-agent-bridge.js +873 -153
- package/dist/src/terminal-agent-bridge.js.map +1 -1
- package/dist/src/terminal-command-cli-adapter.d.ts +2 -1
- package/dist/src/terminal-command-cli-adapter.js +46 -8
- package/dist/src/terminal-command-cli-adapter.js.map +1 -1
- package/dist/src/terminal-dispatch-execution.js +15 -0
- package/dist/src/terminal-dispatch-execution.js.map +1 -1
- package/dist/src/terminal-identity-authority-cli-adapter.d.ts +1 -0
- package/dist/src/terminal-identity-authority-cli-adapter.js +10 -1
- package/dist/src/terminal-identity-authority-cli-adapter.js.map +1 -1
- package/dist/src/terminal-interaction-authority.d.ts +33 -0
- package/dist/src/terminal-interaction-authority.js +3 -0
- package/dist/src/terminal-interaction-authority.js.map +1 -0
- package/dist/src/terminal-interaction-cli-adapter.d.ts +66 -0
- package/dist/src/terminal-interaction-cli-adapter.js +354 -0
- package/dist/src/terminal-interaction-cli-adapter.js.map +1 -0
- package/dist/src/terminal-interaction-protocol.d.ts +111 -0
- package/dist/src/terminal-interaction-protocol.js +432 -0
- package/dist/src/terminal-interaction-protocol.js.map +1 -0
- package/dist/src/terminal-list-renderer.js +31 -2
- package/dist/src/terminal-list-renderer.js.map +1 -1
- package/dist/src/terminal-monitor-cli-adapter.js +12 -3
- package/dist/src/terminal-monitor-cli-adapter.js.map +1 -1
- package/dist/src/terminal-monitor-decision-policy.d.ts +1 -1
- package/dist/src/terminal-monitor-state-cli-adapter.js +15 -1
- package/dist/src/terminal-monitor-state-cli-adapter.js.map +1 -1
- package/dist/src/terminal-mutation-cli-runtime.d.ts +44 -0
- package/dist/src/terminal-mutation-cli-runtime.js +41 -0
- package/dist/src/terminal-mutation-cli-runtime.js.map +1 -0
- package/dist/src/terminal-questionnaire-adapter.d.ts +84 -0
- package/dist/src/terminal-questionnaire-adapter.js +677 -0
- package/dist/src/terminal-questionnaire-adapter.js.map +1 -0
- package/docs/host-bridge-profiles.md +2 -2
- package/docs/quickstart-herdr.md +1 -1
- package/docs/quickstart-tmux.md +3 -1
- package/openclaw.plugin.json +4 -0
- package/package.json +1 -1
- 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
|
-
|
|
597
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
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
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
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
|
-
|
|
1898
|
-
|
|
2006
|
+
decision,
|
|
2007
|
+
key: action.keys.length === 1
|
|
2008
|
+
? action.keys[0]
|
|
1899
2009
|
: undefined,
|
|
1900
|
-
keys:
|
|
1901
|
-
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:
|
|
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
|
|
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
|
-
|
|
1939
|
-
|
|
2060
|
+
decision,
|
|
2061
|
+
key: recapturedAction.keys.length === 1
|
|
2062
|
+
? recapturedAction.keys[0]
|
|
1940
2063
|
: undefined,
|
|
1941
|
-
keys:
|
|
1942
|
-
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:
|
|
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
|
-
|
|
1960
|
-
|
|
2082
|
+
decision,
|
|
2083
|
+
key: recapturedAction.keys.length === 1
|
|
2084
|
+
? recapturedAction.keys[0]
|
|
1961
2085
|
: undefined,
|
|
1962
|
-
keys:
|
|
1963
|
-
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:
|
|
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
|
-
|
|
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
|
|
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:
|
|
2023
|
-
?
|
|
2161
|
+
key: afterReservationAction.keys.length === 1
|
|
2162
|
+
? afterReservationAction.keys[0]
|
|
2024
2163
|
: undefined,
|
|
2025
|
-
keys:
|
|
2026
|
-
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:
|
|
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),
|
|
2194
|
+
await this.terminalProvider.sendKeys(this.terminalProvider.endpoint(verifiedImmediatelyBeforeSend), dispatchAction.keys);
|
|
2055
2195
|
return {
|
|
2056
|
-
approved:
|
|
2196
|
+
approved: decision === "approve_once",
|
|
2197
|
+
decisionDispatched: true,
|
|
2198
|
+
decision,
|
|
2057
2199
|
blocked: false,
|
|
2058
|
-
key:
|
|
2059
|
-
?
|
|
2200
|
+
key: dispatchAction.keys.length === 1
|
|
2201
|
+
? dispatchAction.keys[0]
|
|
2060
2202
|
: undefined,
|
|
2061
|
-
keys:
|
|
2062
|
-
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:
|
|
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
|
|
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
|
-
|
|
2862
|
-
|
|
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:
|
|
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,
|