@scotthuang/agent-knock-knock 0.11.5 → 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 +25 -0
- package/README.md +3 -3
- package/dist/src/claude-lifecycle-compatibility.d.ts +16 -0
- package/dist/src/claude-lifecycle-compatibility.js +77 -0
- package/dist/src/claude-lifecycle-compatibility.js.map +1 -0
- package/dist/src/claude-local-transcript-provider.js +37 -17
- package/dist/src/claude-local-transcript-provider.js.map +1 -1
- package/dist/src/claude-terminal-agent-adapter.d.ts +4 -4
- package/dist/src/claude-terminal-agent-adapter.js +42 -35
- package/dist/src/claude-terminal-agent-adapter.js.map +1 -1
- package/dist/src/cli.js +604 -405
- package/dist/src/cli.js.map +1 -1
- package/dist/src/codex-lifecycle-compatibility.js +2 -1
- package/dist/src/codex-lifecycle-compatibility.js.map +1 -1
- package/dist/src/codex-terminal-agent-adapter.d.ts +1 -0
- package/dist/src/codex-terminal-agent-adapter.js +3 -1
- package/dist/src/codex-terminal-agent-adapter.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/openclaw-plugin.js +2 -2
- package/dist/src/openclaw-plugin.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 +4 -14
- package/dist/src/terminal-agent-adapter.js.map +1 -1
- package/dist/src/terminal-agent-bridge.d.ts +8 -2
- package/dist/src/terminal-agent-bridge.js +250 -95
- 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/docs/quickstart-tmux.md +1 -1
- package/package.json +1 -1
- package/templates/openclaw-skills/agent-knock-knock/SKILL.md +1 -1
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { formatTerminalConversationId, parseTerminalConversationId, terminalControlCapabilitiesForAdapter } from "./terminal-agent-adapter.js";
|
|
3
|
-
import { enrichActiveProcessesWithTerminalControl, TerminalControlInputNotSentError
|
|
4
|
-
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
import { enrichActiveProcessesWithTerminalControl, TerminalControlInputNotSentError } from "./terminal-control-provider.js";
|
|
4
|
+
import { hasCanonicalTerminalEndpoint, sameTerminalControlIncarnation, terminalEndpointFromControlRef, terminalEndpointIdentityKey } from "./terminal-control-ref.js";
|
|
5
|
+
// Verified Codex profiles through 0.147.0 keep Enter in paste/newline mode for
|
|
6
|
+
// 120ms after burst input. Cross that boundary rather than landing on it, and
|
|
7
|
+
// also require observable composer stability instead of treating this delay
|
|
8
|
+
// alone as acceptance.
|
|
7
9
|
const CODEX_PASTE_ENTER_SETTLE_MS = 121;
|
|
8
10
|
const CODEX_MULTILINE_SETTLE_POLL_MS = 30;
|
|
9
11
|
const CODEX_MULTILINE_SETTLE_TIMEOUT_MS = 2_000;
|
|
@@ -11,12 +13,20 @@ const CODEX_MULTILINE_STABLE_CAPTURES = 2;
|
|
|
11
13
|
const CODEX_MULTILINE_SETTLE_SCROLLBACK_LINES = 240;
|
|
12
14
|
const CODEX_COMPOSER_MARKER = /^[›»](?:\s|$)/u;
|
|
13
15
|
const CODEX_COMPOSER_FOOTER = /^(?:gpt-[\w.-]+(?:\s|$)|[-\w.]+ default ·)/u;
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
16
|
+
// Keep every exact slash-completion shape closed per behavior profile so a
|
|
17
|
+
// version adding another matching command cannot silently become an authorized
|
|
18
|
+
// native command surface.
|
|
17
19
|
const CODEX_NATIVE_STATUS_POPUP_BY_PROFILE = {
|
|
18
|
-
"codex-tui-0.146.0":
|
|
19
|
-
|
|
20
|
+
"codex-tui-0.146.0": [
|
|
21
|
+
" /status show current session configuration and token usage"
|
|
22
|
+
],
|
|
23
|
+
"codex-tui-0.146.1": [
|
|
24
|
+
" /status show current session configuration and token usage"
|
|
25
|
+
],
|
|
26
|
+
"codex-tui-0.147.0": [
|
|
27
|
+
" /status show current session configuration and token usage",
|
|
28
|
+
" /statusline configure which items appear in the status line"
|
|
29
|
+
]
|
|
20
30
|
};
|
|
21
31
|
const CLAUDE_NATIVE_STATUS_POPUP_BY_PROFILE = {
|
|
22
32
|
"claude-code-2.1.218-native-status": [
|
|
@@ -24,6 +34,12 @@ const CLAUDE_NATIVE_STATUS_POPUP_BY_PROFILE = {
|
|
|
24
34
|
"/statusline Set up Claude Code's status line UI",
|
|
25
35
|
"/ide Manage IDE integrations and show status",
|
|
26
36
|
"/usage Show session cost, plan usage, and activity stats"
|
|
37
|
+
],
|
|
38
|
+
"claude-code-2.1.226-native-status": [
|
|
39
|
+
"/status Show Claude Code status including version, model, account, API connectivity, and tool statuses",
|
|
40
|
+
"/statusline Set up Claude Code's status line UI",
|
|
41
|
+
"/ide Manage IDE integrations and show status",
|
|
42
|
+
"/usage Show session cost, plan usage, and activity stats"
|
|
27
43
|
]
|
|
28
44
|
};
|
|
29
45
|
const CODEX_LARGE_PASTE_CHAR_THRESHOLD = 1_000;
|
|
@@ -109,34 +125,57 @@ export class TerminalAgentBridge {
|
|
|
109
125
|
throw new Error(`process ${process.pid} is not terminal-controlled`);
|
|
110
126
|
}
|
|
111
127
|
this.registry.require(process.agent);
|
|
128
|
+
const terminal = this.terminalProvider.endpoint(process.terminalControl);
|
|
112
129
|
return formatTerminalConversationId({
|
|
113
130
|
agent: process.agent,
|
|
114
|
-
target:
|
|
131
|
+
target: terminal.route.label,
|
|
115
132
|
pid: process.pid
|
|
116
133
|
});
|
|
117
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
|
+
}
|
|
118
156
|
async resolveConversationId(conversationId) {
|
|
119
157
|
const parsed = parseTerminalConversationId(conversationId);
|
|
120
158
|
if (!parsed) {
|
|
121
159
|
return undefined;
|
|
122
160
|
}
|
|
123
161
|
const adapter = this.registry.require(parsed.agent);
|
|
124
|
-
const
|
|
125
|
-
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);
|
|
126
165
|
const verified = this.verifyIdentity
|
|
127
|
-
? (await Promise.all(candidates.map(async (
|
|
128
|
-
const terminalControl =
|
|
166
|
+
? (await Promise.all(candidates.map(async (terminal) => {
|
|
167
|
+
const terminalControl = this.terminalProvider.toControlRef(terminal, terminalControlCapabilitiesForAdapter(adapter));
|
|
129
168
|
try {
|
|
130
169
|
const verifiedTerminalControl = await this.verifyTerminalIdentity(adapter.agent, terminalControl, { pid: parsed.pid });
|
|
131
|
-
return {
|
|
170
|
+
return { terminal, terminalControl: verifiedTerminalControl };
|
|
132
171
|
}
|
|
133
172
|
catch {
|
|
134
173
|
return undefined;
|
|
135
174
|
}
|
|
136
175
|
}))).filter((candidate) => candidate !== undefined)
|
|
137
|
-
: candidates.slice(0, 1).map((
|
|
138
|
-
|
|
139
|
-
terminalControl:
|
|
176
|
+
: candidates.slice(0, 1).map((terminal) => ({
|
|
177
|
+
terminal,
|
|
178
|
+
terminalControl: this.terminalProvider.toControlRef(terminal, terminalControlCapabilitiesForAdapter(adapter))
|
|
140
179
|
}));
|
|
141
180
|
if (verified.length === 0) {
|
|
142
181
|
throw new Error(`terminal-controlled session ${parsed.conversationId} is no longer available`);
|
|
@@ -188,14 +227,34 @@ export class TerminalAgentBridge {
|
|
|
188
227
|
}
|
|
189
228
|
async send(agent, terminalControl, text, options = {}) {
|
|
190
229
|
const adapter = this.registry.require(agent);
|
|
191
|
-
|
|
192
|
-
|
|
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 });
|
|
193
253
|
}
|
|
194
254
|
const normalized = text.trimEnd();
|
|
195
255
|
if (!normalized) {
|
|
196
256
|
throw new TerminalInputNotStartedError("terminal message is empty");
|
|
197
257
|
}
|
|
198
|
-
const multiline = /[\r\n]/u.test(normalized);
|
|
199
258
|
let verifiedForText;
|
|
200
259
|
try {
|
|
201
260
|
verifiedForText = await this.verifyTerminalIdentity(adapter.agent, terminalControl, options.runtime);
|
|
@@ -204,9 +263,10 @@ export class TerminalAgentBridge {
|
|
|
204
263
|
throw new TerminalInputNotStartedError(error instanceof Error ? error.message : String(error), { cause: error });
|
|
205
264
|
}
|
|
206
265
|
try {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
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);
|
|
210
270
|
}
|
|
211
271
|
catch (error) {
|
|
212
272
|
if (error instanceof TerminalControlInputNotSentError) {
|
|
@@ -223,8 +283,9 @@ export class TerminalAgentBridge {
|
|
|
223
283
|
multiline
|
|
224
284
|
});
|
|
225
285
|
// Text and Enter are separate tmux operations. Revalidate between them and
|
|
226
|
-
// never submit after identity or composer drift.
|
|
227
|
-
//
|
|
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.
|
|
228
289
|
let verifiedForEnter;
|
|
229
290
|
if (adapter.agent === "codex" && multiline) {
|
|
230
291
|
verifiedForEnter = await this.settleCodexMultilineComposer(adapter, terminalControl, normalized, options.runtime);
|
|
@@ -234,20 +295,10 @@ export class TerminalAgentBridge {
|
|
|
234
295
|
verifiedForEnter = await this.verifyTerminalIdentity(adapter.agent, terminalControl, options.runtime);
|
|
235
296
|
}
|
|
236
297
|
catch (error) {
|
|
237
|
-
try {
|
|
238
|
-
await this.terminalProvider.sendKeys(verifiedForText.target, ["C-u"], {
|
|
239
|
-
socketPath: verifiedForText.socketPath
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
catch {
|
|
243
|
-
// Best effort only: preserving the identity failure is more important than cleanup.
|
|
244
|
-
}
|
|
245
298
|
throw error;
|
|
246
299
|
}
|
|
247
300
|
}
|
|
248
|
-
await this.terminalProvider.sendKeys(verifiedForEnter
|
|
249
|
-
socketPath: verifiedForEnter.socketPath
|
|
250
|
-
});
|
|
301
|
+
await this.terminalProvider.sendKeys(this.terminalProvider.endpoint(verifiedForEnter), ["C-m"]);
|
|
251
302
|
await options.onTransportStage?.({
|
|
252
303
|
stage: "enter_dispatched",
|
|
253
304
|
agent: adapter.agent,
|
|
@@ -272,6 +323,17 @@ export class TerminalAgentBridge {
|
|
|
272
323
|
const adapter = this.registry.require(agent);
|
|
273
324
|
try {
|
|
274
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
|
+
});
|
|
275
337
|
}
|
|
276
338
|
catch (error) {
|
|
277
339
|
throw nativeInspectionSubmissionError("not_started", error);
|
|
@@ -284,7 +346,10 @@ export class TerminalAgentBridge {
|
|
|
284
346
|
throw nativeInspectionSubmissionError("not_started", error);
|
|
285
347
|
}
|
|
286
348
|
try {
|
|
287
|
-
|
|
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);
|
|
288
353
|
}
|
|
289
354
|
catch (error) {
|
|
290
355
|
if (error instanceof TerminalControlInputNotSentError) {
|
|
@@ -312,7 +377,7 @@ export class TerminalAgentBridge {
|
|
|
312
377
|
try {
|
|
313
378
|
// Exactly one Enter attempt. Any error is submission-uncertain and must
|
|
314
379
|
// never trigger a fallback executable or a blind second Enter.
|
|
315
|
-
await this.terminalProvider.sendKeys(settled.terminalControl
|
|
380
|
+
await this.terminalProvider.sendKeys(this.terminalProvider.endpoint(settled.terminalControl), ["C-m"]);
|
|
316
381
|
}
|
|
317
382
|
catch (error) {
|
|
318
383
|
throw nativeInspectionSubmissionError("enter_uncertain", error);
|
|
@@ -415,7 +480,7 @@ export class TerminalAgentBridge {
|
|
|
415
480
|
});
|
|
416
481
|
verified = await observeExactPanel(verified);
|
|
417
482
|
try {
|
|
418
|
-
await this.terminalProvider.sendKeys(verified
|
|
483
|
+
await this.terminalProvider.sendKeys(this.terminalProvider.endpoint(verified), plan.expectedResult.dismissal.keys);
|
|
419
484
|
}
|
|
420
485
|
catch (error) {
|
|
421
486
|
throw new NativeInspectionDismissalError("native status modal dismissal outcome is uncertain; do not retry automatically", { cause: error });
|
|
@@ -437,11 +502,12 @@ export class TerminalAgentBridge {
|
|
|
437
502
|
}
|
|
438
503
|
async settleNativeInspectionComposer(adapter, terminalControl, plan, runtime) {
|
|
439
504
|
const startedAt = performance.now();
|
|
505
|
+
const settleTimeoutMs = plan.composer.maximumSettleMs;
|
|
440
506
|
let stableDigest;
|
|
441
507
|
let stableKind;
|
|
442
508
|
let stableSince;
|
|
443
509
|
let stableCaptures = 0;
|
|
444
|
-
while (performance.now() - startedAt <=
|
|
510
|
+
while (performance.now() - startedAt <= settleTimeoutMs) {
|
|
445
511
|
const captured = await this.captureInspection(adapter, terminalControl, {
|
|
446
512
|
runtime,
|
|
447
513
|
scrollbackLines: CODEX_MULTILINE_SETTLE_SCROLLBACK_LINES
|
|
@@ -479,7 +545,7 @@ export class TerminalAgentBridge {
|
|
|
479
545
|
stableCaptures = 0;
|
|
480
546
|
}
|
|
481
547
|
const elapsed = performance.now() - startedAt;
|
|
482
|
-
const remaining =
|
|
548
|
+
const remaining = settleTimeoutMs - elapsed;
|
|
483
549
|
if (remaining <= 0) {
|
|
484
550
|
break;
|
|
485
551
|
}
|
|
@@ -626,13 +692,14 @@ export class TerminalAgentBridge {
|
|
|
626
692
|
*/
|
|
627
693
|
async clearInputLine(agent, terminalControl, options = {}) {
|
|
628
694
|
const adapter = this.registry.require(agent);
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
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"]
|
|
635
700
|
});
|
|
701
|
+
const verified = await this.verifyTerminalIdentity(adapter.agent, terminalControl, options.runtime);
|
|
702
|
+
await this.terminalProvider.sendKeys(this.terminalProvider.endpoint(verified), ["C-u"]);
|
|
636
703
|
}
|
|
637
704
|
async cancel(agent, terminalControl, options = {}) {
|
|
638
705
|
const adapter = this.registry.require(agent);
|
|
@@ -657,10 +724,14 @@ export class TerminalAgentBridge {
|
|
|
657
724
|
reason: `${adapter.displayName} terminal cancellation is not supported`
|
|
658
725
|
};
|
|
659
726
|
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
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"]
|
|
663
732
|
});
|
|
733
|
+
const verifiedForCancel = await this.verifyTerminalIdentity(adapter.agent, terminalControl, options.runtime);
|
|
734
|
+
await this.terminalProvider.sendKeys(this.terminalProvider.endpoint(verifiedForCancel), adapter.cancelKeys);
|
|
664
735
|
return {
|
|
665
736
|
cancelRequested: true,
|
|
666
737
|
key: adapter.cancelKeys.length === 1 ? adapter.cancelKeys[0] : undefined,
|
|
@@ -677,6 +748,16 @@ export class TerminalAgentBridge {
|
|
|
677
748
|
reason: `${adapter.displayName} terminal approval is not supported`
|
|
678
749
|
};
|
|
679
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
|
+
});
|
|
680
761
|
const captured = await this.captureInspection(adapter, terminalControl, options);
|
|
681
762
|
const { inspection } = captured;
|
|
682
763
|
const activeTerminalControl = captured.terminalControl;
|
|
@@ -719,10 +800,24 @@ export class TerminalAgentBridge {
|
|
|
719
800
|
screenExcerpt: inspection.screenExcerpt
|
|
720
801
|
};
|
|
721
802
|
}
|
|
722
|
-
const
|
|
803
|
+
const canonicalFingerprint = terminalApprovalFingerprint(adapter.agent, activeTerminalControl, inspection, {
|
|
723
804
|
screen: captured.screen,
|
|
724
805
|
runtime: options.runtime
|
|
725
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;
|
|
726
821
|
if (adapter.agent === "claude" &&
|
|
727
822
|
decisionMode === "keys" &&
|
|
728
823
|
!options.expectedFingerprint) {
|
|
@@ -809,7 +904,7 @@ export class TerminalAgentBridge {
|
|
|
809
904
|
};
|
|
810
905
|
}
|
|
811
906
|
const recapturedDecisionMode = recapturedInspection.approval.action.mode ?? "keys";
|
|
812
|
-
const recapturedFingerprint = terminalApprovalFingerprint(adapter.agent,
|
|
907
|
+
const recapturedFingerprint = terminalApprovalFingerprint(adapter.agent, fingerprintControl, recapturedInspection, {
|
|
813
908
|
screen: recaptured.screen,
|
|
814
909
|
runtime: options.runtime
|
|
815
910
|
});
|
|
@@ -892,7 +987,7 @@ export class TerminalAgentBridge {
|
|
|
892
987
|
};
|
|
893
988
|
}
|
|
894
989
|
const afterReservationMode = afterReservationInspection.approval.action.mode ?? "keys";
|
|
895
|
-
const afterReservationFingerprint = terminalApprovalFingerprint(adapter.agent,
|
|
990
|
+
const afterReservationFingerprint = terminalApprovalFingerprint(adapter.agent, fingerprintControl, afterReservationInspection, {
|
|
896
991
|
screen: afterReservation.screen,
|
|
897
992
|
runtime: options.runtime
|
|
898
993
|
});
|
|
@@ -934,7 +1029,7 @@ export class TerminalAgentBridge {
|
|
|
934
1029
|
if (!sameTerminalControlIdentity(dispatchTerminalControl, verifiedImmediatelyBeforeSend)) {
|
|
935
1030
|
throw new Error("terminal control identity changed after the final approval capture");
|
|
936
1031
|
}
|
|
937
|
-
await this.terminalProvider.sendKeys(verifiedImmediatelyBeforeSend
|
|
1032
|
+
await this.terminalProvider.sendKeys(this.terminalProvider.endpoint(verifiedImmediatelyBeforeSend), dispatchApproval.action.keys);
|
|
938
1033
|
return {
|
|
939
1034
|
approved: true,
|
|
940
1035
|
blocked: false,
|
|
@@ -1007,10 +1102,7 @@ export class TerminalAgentBridge {
|
|
|
1007
1102
|
}
|
|
1008
1103
|
async captureInspection(adapter, terminalControl, options = {}) {
|
|
1009
1104
|
const verifiedTerminalControl = await this.verifyTerminalIdentity(adapter.agent, terminalControl, options.runtime);
|
|
1010
|
-
const screen = await this.terminalProvider.capture(verifiedTerminalControl
|
|
1011
|
-
scrollbackLines: options.scrollbackLines ?? 120,
|
|
1012
|
-
socketPath: verifiedTerminalControl.socketPath
|
|
1013
|
-
});
|
|
1105
|
+
const screen = await this.terminalProvider.capture(this.terminalProvider.endpoint(verifiedTerminalControl), { scrollbackLines: options.scrollbackLines ?? 120 });
|
|
1014
1106
|
return {
|
|
1015
1107
|
terminalControl: verifiedTerminalControl,
|
|
1016
1108
|
screen,
|
|
@@ -1025,21 +1117,46 @@ export class TerminalAgentBridge {
|
|
|
1025
1117
|
};
|
|
1026
1118
|
}
|
|
1027
1119
|
async verifyTerminalIdentity(agent, terminalControl, runtime) {
|
|
1028
|
-
|
|
1029
|
-
|
|
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
|
+
}
|
|
1030
1135
|
}
|
|
1031
|
-
|
|
1032
|
-
|
|
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");
|
|
1033
1141
|
}
|
|
1034
|
-
|
|
1035
|
-
agent,
|
|
1036
|
-
pid: Number(runtime?.pid),
|
|
1037
|
-
terminalControl,
|
|
1038
|
-
runtime
|
|
1039
|
-
});
|
|
1040
|
-
return result?.terminalControl ?? terminalControl;
|
|
1142
|
+
return this.terminalProvider.toControlRef(resolvedEndpoint, verifiedControl.capabilities);
|
|
1041
1143
|
}
|
|
1042
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;
|
|
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(", "));
|
|
1159
|
+
}
|
|
1043
1160
|
function nativeInspectionSubmissionError(stage, error) {
|
|
1044
1161
|
if (error instanceof NativeInspectionSubmissionError) {
|
|
1045
1162
|
const stageRank = {
|
|
@@ -1063,19 +1180,26 @@ function assertClosedStatusInspectionPlan(adapter, terminalControl, plan) {
|
|
|
1063
1180
|
if (!terminalControl.capabilities.includes("screen_status")) {
|
|
1064
1181
|
throw new Error(`${adapter.displayName} terminal screen inspection is not supported`);
|
|
1065
1182
|
}
|
|
1066
|
-
const codexProfile = adapter.agent === "codex" &&
|
|
1067
|
-
|
|
1068
|
-
"codex-tui-0.146.1"
|
|
1069
|
-
].includes(plan.behaviorProfile);
|
|
1183
|
+
const codexProfile = adapter.agent === "codex" &&
|
|
1184
|
+
CODEX_NATIVE_STATUS_POPUP_BY_PROFILE[plan.behaviorProfile] !== undefined;
|
|
1070
1185
|
const claudeProfile = adapter.agent === "claude" &&
|
|
1071
|
-
plan.behaviorProfile
|
|
1186
|
+
CLAUDE_NATIVE_STATUS_POPUP_BY_PROFILE[plan.behaviorProfile] !== undefined;
|
|
1187
|
+
const expectedSettle = codexProfile
|
|
1188
|
+
? { minimumStableMs: CODEX_PASTE_ENTER_SETTLE_MS, maximumSettleMs: 2_000 }
|
|
1189
|
+
: plan.behaviorProfile === "claude-code-2.1.218-native-status"
|
|
1190
|
+
? { minimumStableMs: 80, maximumSettleMs: 2_000 }
|
|
1191
|
+
: plan.behaviorProfile === "claude-code-2.1.226-native-status"
|
|
1192
|
+
? { minimumStableMs: 80, maximumSettleMs: 5_000 }
|
|
1193
|
+
: undefined;
|
|
1072
1194
|
const exactPresentation = codexProfile
|
|
1073
1195
|
? plan.expectedResult.presentation === "inline" &&
|
|
1074
1196
|
plan.expectedResult.dismissal === undefined &&
|
|
1075
|
-
plan.composer.minimumStableMs
|
|
1197
|
+
plan.composer.minimumStableMs === expectedSettle?.minimumStableMs &&
|
|
1198
|
+
plan.composer.maximumSettleMs === expectedSettle.maximumSettleMs
|
|
1076
1199
|
: claudeProfile
|
|
1077
1200
|
? plan.expectedResult.presentation === "modal" &&
|
|
1078
|
-
plan.composer.minimumStableMs
|
|
1201
|
+
plan.composer.minimumStableMs === expectedSettle?.minimumStableMs &&
|
|
1202
|
+
plan.composer.maximumSettleMs === expectedSettle.maximumSettleMs &&
|
|
1079
1203
|
plan.expectedResult.dismissal?.expected === "idle_empty_composer" &&
|
|
1080
1204
|
JSON.stringify(plan.expectedResult.dismissal.keys) ===
|
|
1081
1205
|
JSON.stringify(["Escape"])
|
|
@@ -1087,6 +1211,8 @@ function assertClosedStatusInspectionPlan(adapter, terminalControl, plan) {
|
|
|
1087
1211
|
plan.composer.kind !== "exact" ||
|
|
1088
1212
|
!Number.isFinite(plan.composer.minimumStableMs) ||
|
|
1089
1213
|
plan.composer.minimumStableMs < 0 ||
|
|
1214
|
+
!Number.isFinite(plan.composer.maximumSettleMs) ||
|
|
1215
|
+
plan.composer.maximumSettleMs < plan.composer.minimumStableMs ||
|
|
1090
1216
|
plan.expectedResult.kind !== "native_status" ||
|
|
1091
1217
|
!exactPresentation) {
|
|
1092
1218
|
throw new Error("refusing a non-closed or unverified native inspection plan");
|
|
@@ -1146,9 +1272,8 @@ function exactCodexNativeInspectionComposerCapture(screen, plan) {
|
|
|
1146
1272
|
if (popupRows.length === 0) {
|
|
1147
1273
|
kind = "exact_slash_composer";
|
|
1148
1274
|
}
|
|
1149
|
-
else if (popupRows.
|
|
1150
|
-
|
|
1151
|
-
CODEX_NATIVE_STATUS_POPUP_BY_PROFILE[plan.behaviorProfile]) {
|
|
1275
|
+
else if (JSON.stringify(popupRows.map((line) => line.trimEnd())) ===
|
|
1276
|
+
JSON.stringify(CODEX_NATIVE_STATUS_POPUP_BY_PROFILE[plan.behaviorProfile])) {
|
|
1152
1277
|
kind = "exact_slash_popup";
|
|
1153
1278
|
}
|
|
1154
1279
|
else {
|
|
@@ -1189,7 +1314,7 @@ function exactClaudeNativeInspectionComposerCapture(screen, plan) {
|
|
|
1189
1314
|
return undefined;
|
|
1190
1315
|
}
|
|
1191
1316
|
}
|
|
1192
|
-
if (
|
|
1317
|
+
if (!closedClaudeNativeStatusSuggestionsMatch(suggestions, CLAUDE_NATIVE_STATUS_POPUP_BY_PROFILE[plan.behaviorProfile])) {
|
|
1193
1318
|
return undefined;
|
|
1194
1319
|
}
|
|
1195
1320
|
kind = "exact_slash_popup";
|
|
@@ -1202,7 +1327,33 @@ function exactClaudeNativeInspectionComposerCapture(screen, plan) {
|
|
|
1202
1327
|
};
|
|
1203
1328
|
}
|
|
1204
1329
|
/**
|
|
1205
|
-
*
|
|
1330
|
+
* Claude truncates a suggestion row with a Unicode ellipsis at narrow pane
|
|
1331
|
+
* widths. Keep authorization closed by accepting only an exact ordered row or
|
|
1332
|
+
* an explicit ellipsis whose preceding text is an exact, non-trivial prefix of
|
|
1333
|
+
* that same profiled row. A caller still cannot introduce, omit, or reorder a
|
|
1334
|
+
* slash command.
|
|
1335
|
+
*/
|
|
1336
|
+
function closedClaudeNativeStatusSuggestionsMatch(observed, expected) {
|
|
1337
|
+
if (!expected || observed.length !== expected.length) {
|
|
1338
|
+
return false;
|
|
1339
|
+
}
|
|
1340
|
+
return observed.every((row, index) => {
|
|
1341
|
+
const exact = expected[index];
|
|
1342
|
+
if (row === exact) {
|
|
1343
|
+
return true;
|
|
1344
|
+
}
|
|
1345
|
+
if (!row.endsWith("…")) {
|
|
1346
|
+
return false;
|
|
1347
|
+
}
|
|
1348
|
+
const prefix = row.slice(0, -1);
|
|
1349
|
+
const commandEnd = exact.indexOf(" ");
|
|
1350
|
+
return (commandEnd > 0 &&
|
|
1351
|
+
prefix.length >= commandEnd + 12 &&
|
|
1352
|
+
exact.startsWith(prefix));
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
/**
|
|
1356
|
+
* Prove the exact idle input frame from a verified Claude Code native-status
|
|
1206
1357
|
* profile. A loose or historical `❯` prompt is not enough to authorize even
|
|
1207
1358
|
* the initial literal `/status` injection.
|
|
1208
1359
|
*/
|
|
@@ -1383,13 +1534,7 @@ async function terminalSettleDelay(milliseconds) {
|
|
|
1383
1534
|
await new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
1384
1535
|
}
|
|
1385
1536
|
function sameTerminalControlIdentity(left, right) {
|
|
1386
|
-
return left
|
|
1387
|
-
left.target === right.target &&
|
|
1388
|
-
left.socketPath === right.socketPath &&
|
|
1389
|
-
left.session === right.session &&
|
|
1390
|
-
left.window === right.window &&
|
|
1391
|
-
left.pane === right.pane &&
|
|
1392
|
-
left.panePid === right.panePid;
|
|
1537
|
+
return sameTerminalControlIncarnation(left, right);
|
|
1393
1538
|
}
|
|
1394
1539
|
export function terminalApprovalFingerprint(agent, terminalControl, inspection, options = {}) {
|
|
1395
1540
|
if (!inspection.approval.approvable) {
|
|
@@ -1399,18 +1544,28 @@ export function terminalApprovalFingerprint(agent, terminalControl, inspection,
|
|
|
1399
1544
|
const rawScreenDigest = decisionMode === "keys" && options.screen !== undefined
|
|
1400
1545
|
? createHash("sha256").update(options.screen).digest("hex")
|
|
1401
1546
|
: undefined;
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
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
|
+
: {
|
|
1407
1557
|
target: terminalControl.target,
|
|
1408
1558
|
socket_path: terminalControl.socketPath,
|
|
1409
1559
|
session: terminalControl.session,
|
|
1410
1560
|
window: terminalControl.window,
|
|
1411
1561
|
pane: terminalControl.pane,
|
|
1412
1562
|
pane_pid: terminalControl.panePid
|
|
1413
|
-
}
|
|
1563
|
+
};
|
|
1564
|
+
return createHash("sha256")
|
|
1565
|
+
.update(JSON.stringify({
|
|
1566
|
+
agent,
|
|
1567
|
+
provider: terminal.identity.providerKind,
|
|
1568
|
+
terminal: terminalFingerprint,
|
|
1414
1569
|
runtime: {
|
|
1415
1570
|
pid: options.runtime?.pid,
|
|
1416
1571
|
session_id: options.runtime?.sessionId,
|