@scotthuang/agent-knock-knock 0.11.6 → 0.11.7
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 +13 -0
- package/dist/src/cli.js +585 -408
- package/dist/src/cli.js.map +1 -1
- package/dist/src/managed-session.d.ts +15 -0
- package/dist/src/managed-session.js +105 -4
- package/dist/src/managed-session.js.map +1 -1
- package/dist/src/native-thread-resume-snapshot.d.ts +5 -2
- package/dist/src/native-thread-resume-snapshot.js +65 -15
- package/dist/src/native-thread-resume-snapshot.js.map +1 -1
- package/dist/src/session-store.js +5 -0
- package/dist/src/session-store.js.map +1 -1
- package/dist/src/terminal-agent-adapter.d.ts +2 -14
- package/dist/src/terminal-agent-adapter.js.map +1 -1
- package/dist/src/terminal-agent-bridge.d.ts +7 -1
- package/dist/src/terminal-agent-bridge.js +178 -73
- package/dist/src/terminal-agent-bridge.js.map +1 -1
- package/dist/src/terminal-control-provider.d.ts +49 -21
- package/dist/src/terminal-control-provider.js +328 -30
- package/dist/src/terminal-control-provider.js.map +1 -1
- package/dist/src/terminal-control-ref.d.ts +119 -0
- package/dist/src/terminal-control-ref.js +410 -0
- package/dist/src/terminal-control-ref.js.map +1 -0
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { formatTerminalConversationId, parseTerminalConversationId, terminalControlCapabilitiesForAdapter } from "./terminal-agent-adapter.js";
|
|
3
|
-
import { enrichActiveProcessesWithTerminalControl, TerminalControlInputNotSentError
|
|
3
|
+
import { enrichActiveProcessesWithTerminalControl, TerminalControlInputNotSentError } from "./terminal-control-provider.js";
|
|
4
|
+
import { hasCanonicalTerminalEndpoint, sameTerminalControlIncarnation, terminalEndpointFromControlRef, terminalEndpointIdentityKey } from "./terminal-control-ref.js";
|
|
4
5
|
// Verified Codex profiles through 0.147.0 keep Enter in paste/newline mode for
|
|
5
6
|
// 120ms after burst input. Cross that boundary rather than landing on it, and
|
|
6
7
|
// also require observable composer stability instead of treating this delay
|
|
@@ -124,34 +125,57 @@ export class TerminalAgentBridge {
|
|
|
124
125
|
throw new Error(`process ${process.pid} is not terminal-controlled`);
|
|
125
126
|
}
|
|
126
127
|
this.registry.require(process.agent);
|
|
128
|
+
const terminal = this.terminalProvider.endpoint(process.terminalControl);
|
|
127
129
|
return formatTerminalConversationId({
|
|
128
130
|
agent: process.agent,
|
|
129
|
-
target:
|
|
131
|
+
target: terminal.route.label,
|
|
130
132
|
pid: process.pid
|
|
131
133
|
});
|
|
132
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* Resolve a persisted control reference by stable endpoint identity. This is
|
|
137
|
+
* the managed-Session path: its route-shaped v2 terminal id is only a
|
|
138
|
+
* compatibility alias and may be stale after an explicit provider refresh.
|
|
139
|
+
*/
|
|
140
|
+
async resolveStoredTerminal(agent, pid, terminalControl, runtime = { pid }) {
|
|
141
|
+
const adapter = this.registry.require(agent);
|
|
142
|
+
const freshControl = await this.verifyTerminalIdentity(agent, terminalControl, { ...runtime, pid });
|
|
143
|
+
return {
|
|
144
|
+
conversationId: this.terminalConversationId({
|
|
145
|
+
agent,
|
|
146
|
+
pid,
|
|
147
|
+
terminalControl: freshControl
|
|
148
|
+
}),
|
|
149
|
+
agent,
|
|
150
|
+
pid,
|
|
151
|
+
legacy: false,
|
|
152
|
+
adapter,
|
|
153
|
+
terminalControl: freshControl
|
|
154
|
+
};
|
|
155
|
+
}
|
|
133
156
|
async resolveConversationId(conversationId) {
|
|
134
157
|
const parsed = parseTerminalConversationId(conversationId);
|
|
135
158
|
if (!parsed) {
|
|
136
159
|
return undefined;
|
|
137
160
|
}
|
|
138
161
|
const adapter = this.registry.require(parsed.agent);
|
|
139
|
-
const
|
|
140
|
-
const candidates =
|
|
162
|
+
const terminals = await this.terminalProvider.listTerminals();
|
|
163
|
+
const candidates = terminals.filter((candidate) => candidate.identity.providerKind === parsed.kind &&
|
|
164
|
+
candidate.route.label === parsed.target);
|
|
141
165
|
const verified = this.verifyIdentity
|
|
142
|
-
? (await Promise.all(candidates.map(async (
|
|
143
|
-
const terminalControl =
|
|
166
|
+
? (await Promise.all(candidates.map(async (terminal) => {
|
|
167
|
+
const terminalControl = this.terminalProvider.toControlRef(terminal, terminalControlCapabilitiesForAdapter(adapter));
|
|
144
168
|
try {
|
|
145
169
|
const verifiedTerminalControl = await this.verifyTerminalIdentity(adapter.agent, terminalControl, { pid: parsed.pid });
|
|
146
|
-
return {
|
|
170
|
+
return { terminal, terminalControl: verifiedTerminalControl };
|
|
147
171
|
}
|
|
148
172
|
catch {
|
|
149
173
|
return undefined;
|
|
150
174
|
}
|
|
151
175
|
}))).filter((candidate) => candidate !== undefined)
|
|
152
|
-
: candidates.slice(0, 1).map((
|
|
153
|
-
|
|
154
|
-
terminalControl:
|
|
176
|
+
: candidates.slice(0, 1).map((terminal) => ({
|
|
177
|
+
terminal,
|
|
178
|
+
terminalControl: this.terminalProvider.toControlRef(terminal, terminalControlCapabilitiesForAdapter(adapter))
|
|
155
179
|
}));
|
|
156
180
|
if (verified.length === 0) {
|
|
157
181
|
throw new Error(`terminal-controlled session ${parsed.conversationId} is no longer available`);
|
|
@@ -203,14 +227,34 @@ export class TerminalAgentBridge {
|
|
|
203
227
|
}
|
|
204
228
|
async send(agent, terminalControl, text, options = {}) {
|
|
205
229
|
const adapter = this.registry.require(agent);
|
|
206
|
-
|
|
207
|
-
|
|
230
|
+
const multiline = /[\r\n]/u.test(text.trimEnd());
|
|
231
|
+
try {
|
|
232
|
+
assertTerminalMutationCapabilities({
|
|
233
|
+
provider: this.terminalProvider,
|
|
234
|
+
terminal: this.terminalProvider.endpoint(terminalControl),
|
|
235
|
+
semantic: [
|
|
236
|
+
"send_keys",
|
|
237
|
+
...(adapter.agent === "codex" && multiline
|
|
238
|
+
? ["screen_status"]
|
|
239
|
+
: [])
|
|
240
|
+
],
|
|
241
|
+
transport: [
|
|
242
|
+
"stable_resource_resolution",
|
|
243
|
+
"text_delivery",
|
|
244
|
+
"key_delivery",
|
|
245
|
+
...(adapter.agent === "codex" && multiline
|
|
246
|
+
? ["screen_capture"]
|
|
247
|
+
: [])
|
|
248
|
+
]
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
throw new TerminalInputNotStartedError(error instanceof Error ? error.message : String(error), { cause: error });
|
|
208
253
|
}
|
|
209
254
|
const normalized = text.trimEnd();
|
|
210
255
|
if (!normalized) {
|
|
211
256
|
throw new TerminalInputNotStartedError("terminal message is empty");
|
|
212
257
|
}
|
|
213
|
-
const multiline = /[\r\n]/u.test(normalized);
|
|
214
258
|
let verifiedForText;
|
|
215
259
|
try {
|
|
216
260
|
verifiedForText = await this.verifyTerminalIdentity(adapter.agent, terminalControl, options.runtime);
|
|
@@ -219,9 +263,10 @@ export class TerminalAgentBridge {
|
|
|
219
263
|
throw new TerminalInputNotStartedError(error instanceof Error ? error.message : String(error), { cause: error });
|
|
220
264
|
}
|
|
221
265
|
try {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
266
|
+
if (!verifiedForText.capabilities.includes("send_keys")) {
|
|
267
|
+
throw new TerminalControlInputNotSentError(`${adapter.displayName} terminal input capability changed before injection`);
|
|
268
|
+
}
|
|
269
|
+
await this.terminalProvider.sendText(this.terminalProvider.endpoint(verifiedForText), normalized);
|
|
225
270
|
}
|
|
226
271
|
catch (error) {
|
|
227
272
|
if (error instanceof TerminalControlInputNotSentError) {
|
|
@@ -238,8 +283,9 @@ export class TerminalAgentBridge {
|
|
|
238
283
|
multiline
|
|
239
284
|
});
|
|
240
285
|
// Text and Enter are separate tmux operations. Revalidate between them and
|
|
241
|
-
// never submit after identity or composer drift.
|
|
242
|
-
//
|
|
286
|
+
// never submit after identity or composer drift. A failed revalidation
|
|
287
|
+
// leaves the injected draft untouched because cleanup is also terminal
|
|
288
|
+
// input and cannot safely use a stale route.
|
|
243
289
|
let verifiedForEnter;
|
|
244
290
|
if (adapter.agent === "codex" && multiline) {
|
|
245
291
|
verifiedForEnter = await this.settleCodexMultilineComposer(adapter, terminalControl, normalized, options.runtime);
|
|
@@ -249,20 +295,10 @@ export class TerminalAgentBridge {
|
|
|
249
295
|
verifiedForEnter = await this.verifyTerminalIdentity(adapter.agent, terminalControl, options.runtime);
|
|
250
296
|
}
|
|
251
297
|
catch (error) {
|
|
252
|
-
try {
|
|
253
|
-
await this.terminalProvider.sendKeys(verifiedForText.target, ["C-u"], {
|
|
254
|
-
socketPath: verifiedForText.socketPath
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
catch {
|
|
258
|
-
// Best effort only: preserving the identity failure is more important than cleanup.
|
|
259
|
-
}
|
|
260
298
|
throw error;
|
|
261
299
|
}
|
|
262
300
|
}
|
|
263
|
-
await this.terminalProvider.sendKeys(verifiedForEnter
|
|
264
|
-
socketPath: verifiedForEnter.socketPath
|
|
265
|
-
});
|
|
301
|
+
await this.terminalProvider.sendKeys(this.terminalProvider.endpoint(verifiedForEnter), ["C-m"]);
|
|
266
302
|
await options.onTransportStage?.({
|
|
267
303
|
stage: "enter_dispatched",
|
|
268
304
|
agent: adapter.agent,
|
|
@@ -287,6 +323,17 @@ export class TerminalAgentBridge {
|
|
|
287
323
|
const adapter = this.registry.require(agent);
|
|
288
324
|
try {
|
|
289
325
|
assertClosedStatusInspectionPlan(adapter, terminalControl, plan);
|
|
326
|
+
assertTerminalMutationCapabilities({
|
|
327
|
+
provider: this.terminalProvider,
|
|
328
|
+
terminal: this.terminalProvider.endpoint(terminalControl),
|
|
329
|
+
semantic: ["send_keys", "screen_status"],
|
|
330
|
+
transport: [
|
|
331
|
+
"stable_resource_resolution",
|
|
332
|
+
"screen_capture",
|
|
333
|
+
"text_delivery",
|
|
334
|
+
"key_delivery"
|
|
335
|
+
]
|
|
336
|
+
});
|
|
290
337
|
}
|
|
291
338
|
catch (error) {
|
|
292
339
|
throw nativeInspectionSubmissionError("not_started", error);
|
|
@@ -299,7 +346,10 @@ export class TerminalAgentBridge {
|
|
|
299
346
|
throw nativeInspectionSubmissionError("not_started", error);
|
|
300
347
|
}
|
|
301
348
|
try {
|
|
302
|
-
|
|
349
|
+
if (!verifiedForText.capabilities.includes("send_keys")) {
|
|
350
|
+
throw new TerminalControlInputNotSentError(`${adapter.displayName} native inspection input capability changed`);
|
|
351
|
+
}
|
|
352
|
+
await this.terminalProvider.sendText(this.terminalProvider.endpoint(verifiedForText), plan.command);
|
|
303
353
|
}
|
|
304
354
|
catch (error) {
|
|
305
355
|
if (error instanceof TerminalControlInputNotSentError) {
|
|
@@ -327,7 +377,7 @@ export class TerminalAgentBridge {
|
|
|
327
377
|
try {
|
|
328
378
|
// Exactly one Enter attempt. Any error is submission-uncertain and must
|
|
329
379
|
// never trigger a fallback executable or a blind second Enter.
|
|
330
|
-
await this.terminalProvider.sendKeys(settled.terminalControl
|
|
380
|
+
await this.terminalProvider.sendKeys(this.terminalProvider.endpoint(settled.terminalControl), ["C-m"]);
|
|
331
381
|
}
|
|
332
382
|
catch (error) {
|
|
333
383
|
throw nativeInspectionSubmissionError("enter_uncertain", error);
|
|
@@ -430,7 +480,7 @@ export class TerminalAgentBridge {
|
|
|
430
480
|
});
|
|
431
481
|
verified = await observeExactPanel(verified);
|
|
432
482
|
try {
|
|
433
|
-
await this.terminalProvider.sendKeys(verified
|
|
483
|
+
await this.terminalProvider.sendKeys(this.terminalProvider.endpoint(verified), plan.expectedResult.dismissal.keys);
|
|
434
484
|
}
|
|
435
485
|
catch (error) {
|
|
436
486
|
throw new NativeInspectionDismissalError("native status modal dismissal outcome is uncertain; do not retry automatically", { cause: error });
|
|
@@ -642,13 +692,14 @@ export class TerminalAgentBridge {
|
|
|
642
692
|
*/
|
|
643
693
|
async clearInputLine(agent, terminalControl, options = {}) {
|
|
644
694
|
const adapter = this.registry.require(agent);
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
socketPath: verified.socketPath
|
|
695
|
+
assertTerminalMutationCapabilities({
|
|
696
|
+
provider: this.terminalProvider,
|
|
697
|
+
terminal: this.terminalProvider.endpoint(terminalControl),
|
|
698
|
+
semantic: ["send_keys"],
|
|
699
|
+
transport: ["stable_resource_resolution", "key_delivery"]
|
|
651
700
|
});
|
|
701
|
+
const verified = await this.verifyTerminalIdentity(adapter.agent, terminalControl, options.runtime);
|
|
702
|
+
await this.terminalProvider.sendKeys(this.terminalProvider.endpoint(verified), ["C-u"]);
|
|
652
703
|
}
|
|
653
704
|
async cancel(agent, terminalControl, options = {}) {
|
|
654
705
|
const adapter = this.registry.require(agent);
|
|
@@ -673,10 +724,14 @@ export class TerminalAgentBridge {
|
|
|
673
724
|
reason: `${adapter.displayName} terminal cancellation is not supported`
|
|
674
725
|
};
|
|
675
726
|
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
727
|
+
assertTerminalMutationCapabilities({
|
|
728
|
+
provider: this.terminalProvider,
|
|
729
|
+
terminal: this.terminalProvider.endpoint(terminalControl),
|
|
730
|
+
semantic: ["terminal_cancel", "send_keys"],
|
|
731
|
+
transport: ["stable_resource_resolution", "key_delivery"]
|
|
679
732
|
});
|
|
733
|
+
const verifiedForCancel = await this.verifyTerminalIdentity(adapter.agent, terminalControl, options.runtime);
|
|
734
|
+
await this.terminalProvider.sendKeys(this.terminalProvider.endpoint(verifiedForCancel), adapter.cancelKeys);
|
|
680
735
|
return {
|
|
681
736
|
cancelRequested: true,
|
|
682
737
|
key: adapter.cancelKeys.length === 1 ? adapter.cancelKeys[0] : undefined,
|
|
@@ -693,6 +748,16 @@ export class TerminalAgentBridge {
|
|
|
693
748
|
reason: `${adapter.displayName} terminal approval is not supported`
|
|
694
749
|
};
|
|
695
750
|
}
|
|
751
|
+
assertTerminalMutationCapabilities({
|
|
752
|
+
provider: this.terminalProvider,
|
|
753
|
+
terminal: this.terminalProvider.endpoint(terminalControl),
|
|
754
|
+
semantic: ["screen_status", "terminal_approval", "send_keys"],
|
|
755
|
+
transport: [
|
|
756
|
+
"stable_resource_resolution",
|
|
757
|
+
"screen_capture",
|
|
758
|
+
"key_delivery"
|
|
759
|
+
]
|
|
760
|
+
});
|
|
696
761
|
const captured = await this.captureInspection(adapter, terminalControl, options);
|
|
697
762
|
const { inspection } = captured;
|
|
698
763
|
const activeTerminalControl = captured.terminalControl;
|
|
@@ -735,10 +800,24 @@ export class TerminalAgentBridge {
|
|
|
735
800
|
screenExcerpt: inspection.screenExcerpt
|
|
736
801
|
};
|
|
737
802
|
}
|
|
738
|
-
const
|
|
803
|
+
const canonicalFingerprint = terminalApprovalFingerprint(adapter.agent, activeTerminalControl, inspection, {
|
|
739
804
|
screen: captured.screen,
|
|
740
805
|
runtime: options.runtime
|
|
741
806
|
});
|
|
807
|
+
const legacyFingerprint = hasCanonicalTerminalEndpoint(terminalControl)
|
|
808
|
+
? undefined
|
|
809
|
+
: terminalApprovalFingerprint(adapter.agent, terminalControl, inspection, {
|
|
810
|
+
screen: captured.screen,
|
|
811
|
+
runtime: options.runtime
|
|
812
|
+
});
|
|
813
|
+
const useLegacyFingerprint = Boolean(options.expectedFingerprint &&
|
|
814
|
+
options.expectedFingerprint === legacyFingerprint);
|
|
815
|
+
const fingerprintControl = useLegacyFingerprint
|
|
816
|
+
? terminalControl
|
|
817
|
+
: activeTerminalControl;
|
|
818
|
+
const fingerprint = useLegacyFingerprint
|
|
819
|
+
? legacyFingerprint
|
|
820
|
+
: canonicalFingerprint;
|
|
742
821
|
if (adapter.agent === "claude" &&
|
|
743
822
|
decisionMode === "keys" &&
|
|
744
823
|
!options.expectedFingerprint) {
|
|
@@ -825,7 +904,7 @@ export class TerminalAgentBridge {
|
|
|
825
904
|
};
|
|
826
905
|
}
|
|
827
906
|
const recapturedDecisionMode = recapturedInspection.approval.action.mode ?? "keys";
|
|
828
|
-
const recapturedFingerprint = terminalApprovalFingerprint(adapter.agent,
|
|
907
|
+
const recapturedFingerprint = terminalApprovalFingerprint(adapter.agent, fingerprintControl, recapturedInspection, {
|
|
829
908
|
screen: recaptured.screen,
|
|
830
909
|
runtime: options.runtime
|
|
831
910
|
});
|
|
@@ -908,7 +987,7 @@ export class TerminalAgentBridge {
|
|
|
908
987
|
};
|
|
909
988
|
}
|
|
910
989
|
const afterReservationMode = afterReservationInspection.approval.action.mode ?? "keys";
|
|
911
|
-
const afterReservationFingerprint = terminalApprovalFingerprint(adapter.agent,
|
|
990
|
+
const afterReservationFingerprint = terminalApprovalFingerprint(adapter.agent, fingerprintControl, afterReservationInspection, {
|
|
912
991
|
screen: afterReservation.screen,
|
|
913
992
|
runtime: options.runtime
|
|
914
993
|
});
|
|
@@ -950,7 +1029,7 @@ export class TerminalAgentBridge {
|
|
|
950
1029
|
if (!sameTerminalControlIdentity(dispatchTerminalControl, verifiedImmediatelyBeforeSend)) {
|
|
951
1030
|
throw new Error("terminal control identity changed after the final approval capture");
|
|
952
1031
|
}
|
|
953
|
-
await this.terminalProvider.sendKeys(verifiedImmediatelyBeforeSend
|
|
1032
|
+
await this.terminalProvider.sendKeys(this.terminalProvider.endpoint(verifiedImmediatelyBeforeSend), dispatchApproval.action.keys);
|
|
954
1033
|
return {
|
|
955
1034
|
approved: true,
|
|
956
1035
|
blocked: false,
|
|
@@ -1023,10 +1102,7 @@ export class TerminalAgentBridge {
|
|
|
1023
1102
|
}
|
|
1024
1103
|
async captureInspection(adapter, terminalControl, options = {}) {
|
|
1025
1104
|
const verifiedTerminalControl = await this.verifyTerminalIdentity(adapter.agent, terminalControl, options.runtime);
|
|
1026
|
-
const screen = await this.terminalProvider.capture(verifiedTerminalControl
|
|
1027
|
-
scrollbackLines: options.scrollbackLines ?? 120,
|
|
1028
|
-
socketPath: verifiedTerminalControl.socketPath
|
|
1029
|
-
});
|
|
1105
|
+
const screen = await this.terminalProvider.capture(this.terminalProvider.endpoint(verifiedTerminalControl), { scrollbackLines: options.scrollbackLines ?? 120 });
|
|
1030
1106
|
return {
|
|
1031
1107
|
terminalControl: verifiedTerminalControl,
|
|
1032
1108
|
screen,
|
|
@@ -1041,20 +1117,45 @@ export class TerminalAgentBridge {
|
|
|
1041
1117
|
};
|
|
1042
1118
|
}
|
|
1043
1119
|
async verifyTerminalIdentity(agent, terminalControl, runtime) {
|
|
1044
|
-
|
|
1045
|
-
|
|
1120
|
+
let verifiedControl = terminalControl;
|
|
1121
|
+
if (this.verifyIdentity) {
|
|
1122
|
+
if (!Number.isInteger(runtime?.pid) || Number(runtime?.pid) <= 0) {
|
|
1123
|
+
throw new Error(`refusing terminal access for ${agent}:${terminalControl.target} without an exact agent pid; reattach this legacy tmux session before controlling it`);
|
|
1124
|
+
}
|
|
1125
|
+
const result = await this.verifyIdentity({
|
|
1126
|
+
agent,
|
|
1127
|
+
pid: Number(runtime?.pid),
|
|
1128
|
+
terminalControl,
|
|
1129
|
+
runtime
|
|
1130
|
+
});
|
|
1131
|
+
verifiedControl = result?.terminalControl ?? terminalControl;
|
|
1132
|
+
if (!sameTerminalControlIncarnation(terminalControl, verifiedControl)) {
|
|
1133
|
+
throw new Error("terminal control identity changed during identity verification");
|
|
1134
|
+
}
|
|
1046
1135
|
}
|
|
1047
|
-
|
|
1048
|
-
|
|
1136
|
+
const verifiedEndpoint = this.terminalProvider.endpoint(verifiedControl);
|
|
1137
|
+
const resolvedEndpoint = await this.terminalProvider.resolve(verifiedEndpoint);
|
|
1138
|
+
if (hasCanonicalTerminalEndpoint(verifiedControl) &&
|
|
1139
|
+
!sameTerminalControlIncarnation(verifiedEndpoint, resolvedEndpoint)) {
|
|
1140
|
+
throw new Error("terminal stable resource or process anchor changed during fresh resolution");
|
|
1049
1141
|
}
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1142
|
+
return this.terminalProvider.toControlRef(resolvedEndpoint, verifiedControl.capabilities);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
function assertTerminalMutationCapabilities({ provider, terminal, semantic, transport }) {
|
|
1146
|
+
const missingSemantic = semantic.filter((capability) => !provider.supportedCapabilities.includes(capability) ||
|
|
1147
|
+
!terminal.capabilities.includes(capability));
|
|
1148
|
+
const missingTransport = transport.filter((capability) => !provider.providerCapabilities.includes(capability));
|
|
1149
|
+
if (missingSemantic.length === 0 && missingTransport.length === 0) {
|
|
1150
|
+
return;
|
|
1057
1151
|
}
|
|
1152
|
+
const missing = [
|
|
1153
|
+
...missingSemantic.map((value) => `terminal:${value}`),
|
|
1154
|
+
...missingTransport.map((value) => `provider:${value}`)
|
|
1155
|
+
];
|
|
1156
|
+
throw new TerminalControlInputNotSentError(`terminal action capability preflight failed for ` +
|
|
1157
|
+
`${terminal.identity.providerKind}:${terminal.route.label}: ` +
|
|
1158
|
+
missing.join(", "));
|
|
1058
1159
|
}
|
|
1059
1160
|
function nativeInspectionSubmissionError(stage, error) {
|
|
1060
1161
|
if (error instanceof NativeInspectionSubmissionError) {
|
|
@@ -1433,13 +1534,7 @@ async function terminalSettleDelay(milliseconds) {
|
|
|
1433
1534
|
await new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
1434
1535
|
}
|
|
1435
1536
|
function sameTerminalControlIdentity(left, right) {
|
|
1436
|
-
return left
|
|
1437
|
-
left.target === right.target &&
|
|
1438
|
-
left.socketPath === right.socketPath &&
|
|
1439
|
-
left.session === right.session &&
|
|
1440
|
-
left.window === right.window &&
|
|
1441
|
-
left.pane === right.pane &&
|
|
1442
|
-
left.panePid === right.panePid;
|
|
1537
|
+
return sameTerminalControlIncarnation(left, right);
|
|
1443
1538
|
}
|
|
1444
1539
|
export function terminalApprovalFingerprint(agent, terminalControl, inspection, options = {}) {
|
|
1445
1540
|
if (!inspection.approval.approvable) {
|
|
@@ -1449,18 +1544,28 @@ export function terminalApprovalFingerprint(agent, terminalControl, inspection,
|
|
|
1449
1544
|
const rawScreenDigest = decisionMode === "keys" && options.screen !== undefined
|
|
1450
1545
|
? createHash("sha256").update(options.screen).digest("hex")
|
|
1451
1546
|
: undefined;
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1547
|
+
const terminal = terminalEndpointFromControlRef(terminalControl);
|
|
1548
|
+
// A pending v0.11.x approval may outlive an upgrade. Preserve its exact
|
|
1549
|
+
// fingerprint until the persisted control record has canonical endpoint
|
|
1550
|
+
// evidence; new records bind the fingerprint to stable endpoint identity.
|
|
1551
|
+
const terminalFingerprint = hasCanonicalTerminalEndpoint(terminalControl)
|
|
1552
|
+
? {
|
|
1553
|
+
identity: terminalEndpointIdentityKey(terminal),
|
|
1554
|
+
process_anchor_pid: terminal.processAnchorPid
|
|
1555
|
+
}
|
|
1556
|
+
: {
|
|
1457
1557
|
target: terminalControl.target,
|
|
1458
1558
|
socket_path: terminalControl.socketPath,
|
|
1459
1559
|
session: terminalControl.session,
|
|
1460
1560
|
window: terminalControl.window,
|
|
1461
1561
|
pane: terminalControl.pane,
|
|
1462
1562
|
pane_pid: terminalControl.panePid
|
|
1463
|
-
}
|
|
1563
|
+
};
|
|
1564
|
+
return createHash("sha256")
|
|
1565
|
+
.update(JSON.stringify({
|
|
1566
|
+
agent,
|
|
1567
|
+
provider: terminal.identity.providerKind,
|
|
1568
|
+
terminal: terminalFingerprint,
|
|
1464
1569
|
runtime: {
|
|
1465
1570
|
pid: options.runtime?.pid,
|
|
1466
1571
|
session_id: options.runtime?.sessionId,
|