@scotthuang/agent-knock-knock 0.2.47 → 0.2.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +114 -147
  3. package/dist/src/approval-policy.d.ts +2 -1
  4. package/dist/src/approval-policy.js +14 -6
  5. package/dist/src/approval-policy.js.map +1 -1
  6. package/dist/src/claude-hook-protocol.js.map +1 -1
  7. package/dist/src/claude-hook-store.js +17 -6
  8. package/dist/src/claude-hook-store.js.map +1 -1
  9. package/dist/src/claude-local-transcript-provider.d.ts +40 -0
  10. package/dist/src/claude-local-transcript-provider.js +804 -0
  11. package/dist/src/claude-local-transcript-provider.js.map +1 -0
  12. package/dist/src/claude-terminal-agent-adapter.d.ts +3 -0
  13. package/dist/src/claude-terminal-agent-adapter.js +146 -47
  14. package/dist/src/claude-terminal-agent-adapter.js.map +1 -1
  15. package/dist/src/cli.js +2473 -834
  16. package/dist/src/cli.js.map +1 -1
  17. package/dist/src/codex-session-provider.js.map +1 -1
  18. package/dist/src/codex-store-adapter.js.map +1 -1
  19. package/dist/src/codex-terminal-agent-adapter.js.map +1 -1
  20. package/dist/src/doctor-capabilities.d.ts +22 -0
  21. package/dist/src/doctor-capabilities.js +29 -0
  22. package/dist/src/doctor-capabilities.js.map +1 -0
  23. package/dist/src/executors.js.map +1 -1
  24. package/dist/src/openclaw-plugin-helpers.d.ts +49 -0
  25. package/dist/src/openclaw-plugin-helpers.js +246 -0
  26. package/dist/src/openclaw-plugin-helpers.js.map +1 -0
  27. package/dist/src/openclaw-plugin.js +94 -271
  28. package/dist/src/openclaw-plugin.js.map +1 -1
  29. package/dist/src/runtime-log.js +54 -2
  30. package/dist/src/runtime-log.js.map +1 -1
  31. package/dist/src/store.js +432 -12
  32. package/dist/src/store.js.map +1 -1
  33. package/dist/src/terminal-agent-adapter.d.ts +3 -0
  34. package/dist/src/terminal-agent-adapter.js.map +1 -1
  35. package/dist/src/terminal-agent-bridge.d.ts +51 -3
  36. package/dist/src/terminal-agent-bridge.js +276 -39
  37. package/dist/src/terminal-agent-bridge.js.map +1 -1
  38. package/dist/src/terminal-agent-registry.js.map +1 -1
  39. package/dist/src/terminal-control-provider.d.ts +3 -1
  40. package/dist/src/terminal-control-provider.js +41 -37
  41. package/dist/src/terminal-control-provider.js.map +1 -1
  42. package/dist/src/terminal-process-source.d.ts +12 -3
  43. package/dist/src/terminal-process-source.js +37 -6
  44. package/dist/src/terminal-process-source.js.map +1 -1
  45. package/dist/src/transcript.js.map +1 -1
  46. package/openclaw.plugin.json +4 -4
  47. package/package.json +11 -6
  48. package/templates/openclaw-skills/agent-knock-knock/SKILL.md +10 -24
@@ -18,6 +18,7 @@ export interface TerminalBridgeStatus {
18
18
  label?: string;
19
19
  prompt_kind?: string;
20
20
  command?: string;
21
+ cwd?: string;
21
22
  tool_name?: string;
22
23
  request_detail?: string;
23
24
  reason?: string;
@@ -27,6 +28,8 @@ export interface TerminalBridgeStatus {
27
28
  };
28
29
  screen: {
29
30
  excerpt?: string;
31
+ /** SHA-256 of the raw capture. The raw terminal contents are never exposed here. */
32
+ digest?: string;
30
33
  approval?: Record<string, unknown>;
31
34
  error?: string;
32
35
  };
@@ -49,6 +52,7 @@ export interface TerminalApprovalExecution {
49
52
  label?: string;
50
53
  promptKind?: string;
51
54
  command?: string;
55
+ cwd?: string;
52
56
  toolName?: string;
53
57
  requestDetail?: string;
54
58
  fingerprint?: string;
@@ -56,6 +60,34 @@ export interface TerminalApprovalExecution {
56
60
  decisionMode?: "keys" | "structured";
57
61
  requestId?: string;
58
62
  }
63
+ export interface TerminalIdentityVerificationRequest {
64
+ agent: ExecutorKind;
65
+ pid: number;
66
+ terminalControl: TerminalControlRef;
67
+ }
68
+ export interface TerminalIdentityVerificationResult {
69
+ terminalControl?: TerminalControlRef;
70
+ }
71
+ export type TerminalIdentityVerifier = (request: TerminalIdentityVerificationRequest) => Promise<TerminalIdentityVerificationResult | void>;
72
+ export interface TerminalApprovalAuthorizationContext {
73
+ agent: ExecutorKind;
74
+ terminalControl: TerminalControlRef;
75
+ inspection: TerminalScreenInspection;
76
+ fingerprint?: string;
77
+ runtime?: TerminalRuntimeIdentity;
78
+ }
79
+ export interface TerminalApprovalAuthorizationDecision {
80
+ approved: boolean;
81
+ reason?: string;
82
+ }
83
+ export interface TerminalApprovalKeyDispatchContext {
84
+ agent: ExecutorKind;
85
+ terminalControl: TerminalControlRef;
86
+ inspection: TerminalScreenInspection;
87
+ fingerprint: string;
88
+ keys: readonly string[];
89
+ runtime?: TerminalRuntimeIdentity;
90
+ }
59
91
  export interface TerminalMonitorPoll {
60
92
  status: TerminalBridgeStatus;
61
93
  inspection?: TerminalScreenInspection;
@@ -65,21 +97,27 @@ export interface TerminalMonitorPoll {
65
97
  export declare class TerminalAgentBridge {
66
98
  readonly registry: TerminalAgentAdapterRegistry;
67
99
  readonly terminalProvider: TerminalControlProvider;
100
+ private readonly verifyIdentity?;
68
101
  constructor(options: {
69
102
  registry: TerminalAgentAdapterRegistry;
70
103
  terminalProvider: TerminalControlProvider;
104
+ verifyIdentity?: TerminalIdentityVerifier;
71
105
  });
72
106
  adapterFor(agent: ExecutorKind | string): TerminalAgentAdapter;
73
107
  listProcesses(snapshots: readonly TerminalProcessSnapshot[], agents?: readonly ExecutorKind[]): Promise<ActiveTerminalProcess[]>;
74
108
  discoverProcesses(snapshots: readonly TerminalProcessSnapshot[], agents?: readonly ExecutorKind[]): Promise<ActiveTerminalProcess[]>;
75
- attachProcesses<T extends ActiveTerminalProcess>(agent: ExecutorKind, processes: T[]): Promise<T[]>;
109
+ attachProcesses<T extends ActiveTerminalProcess>(agent: ExecutorKind, processes: T[], options?: {
110
+ processTree?: readonly TerminalProcessSnapshot[];
111
+ }): Promise<T[]>;
76
112
  terminalConversationId(process: Pick<ActiveTerminalProcess, "agent" | "pid" | "terminalControl">): string;
77
113
  resolveConversationId(conversationId: string | undefined): Promise<ResolvedTerminalConversation | undefined>;
78
114
  status(agent: ExecutorKind, terminalControl: TerminalControlRef, options?: {
79
115
  scrollbackLines?: number;
80
116
  runtime?: TerminalRuntimeIdentity;
81
117
  }): Promise<TerminalBridgeStatus>;
82
- send(agent: ExecutorKind, terminalControl: TerminalControlRef, text: string): Promise<void>;
118
+ send(agent: ExecutorKind, terminalControl: TerminalControlRef, text: string, options?: {
119
+ runtime?: TerminalRuntimeIdentity;
120
+ }): Promise<void>;
83
121
  cancel(agent: ExecutorKind, terminalControl: TerminalControlRef, options?: {
84
122
  runtime?: TerminalRuntimeIdentity;
85
123
  scrollbackLines?: number;
@@ -96,6 +134,12 @@ export declare class TerminalAgentBridge {
96
134
  scrollbackLines?: number;
97
135
  runtime?: TerminalRuntimeIdentity;
98
136
  requiredDecisionMode?: "keys" | "structured";
137
+ authorize?: (context: TerminalApprovalAuthorizationContext) => TerminalApprovalAuthorizationDecision | Promise<TerminalApprovalAuthorizationDecision>;
138
+ /**
139
+ * Persist an at-most-once dispatch reservation after final prompt/identity validation
140
+ * and immediately before tmux receives the approval keys.
141
+ */
142
+ beforeKeyDispatch?: (context: TerminalApprovalKeyDispatchContext) => void | Promise<void>;
99
143
  }): Promise<TerminalApprovalExecution>;
100
144
  monitorPoll(options: {
101
145
  agent: ExecutorKind;
@@ -110,5 +154,9 @@ export declare class TerminalAgentBridge {
110
154
  durableRequest?: TerminalDurableCompletionRequest;
111
155
  }): Promise<TerminalMonitorPoll>;
112
156
  private captureInspection;
157
+ private verifyTerminalIdentity;
113
158
  }
114
- export declare function terminalApprovalFingerprint(agent: ExecutorKind, terminalControl: Pick<TerminalControlRef, "target">, inspection: TerminalScreenInspection): string | undefined;
159
+ export declare function terminalApprovalFingerprint(agent: ExecutorKind, terminalControl: TerminalControlRef, inspection: TerminalScreenInspection, options?: {
160
+ screen?: string;
161
+ runtime?: TerminalRuntimeIdentity;
162
+ }): string | undefined;
@@ -4,9 +4,11 @@ import { enrichActiveProcessesWithTerminalControl, terminalRefFromPane } from ".
4
4
  export class TerminalAgentBridge {
5
5
  registry;
6
6
  terminalProvider;
7
+ verifyIdentity;
7
8
  constructor(options) {
8
9
  this.registry = options.registry;
9
10
  this.terminalProvider = options.terminalProvider;
11
+ this.verifyIdentity = options.verifyIdentity;
10
12
  }
11
13
  adapterFor(agent) {
12
14
  return this.registry.require(agent);
@@ -24,17 +26,21 @@ export class TerminalAgentBridge {
24
26
  .map((snapshot) => adapter.classifyProcess(snapshot))
25
27
  .filter((process) => process !== undefined)
26
28
  .map((process) => ({ ...process, agent: adapter.agent }));
27
- discovered.push(...await this.attachProcesses(adapter.agent, classified));
29
+ discovered.push(...await enrichActiveProcessesWithTerminalControl(classified, this.terminalProvider, {
30
+ capabilities: terminalControlCapabilitiesForAdapter(adapter),
31
+ processTree: snapshots
32
+ }));
28
33
  }
29
34
  return discovered;
30
35
  }
31
36
  async discoverProcesses(snapshots, agents) {
32
37
  return this.listProcesses(snapshots, agents);
33
38
  }
34
- async attachProcesses(agent, processes) {
39
+ async attachProcesses(agent, processes, options = {}) {
35
40
  const adapter = this.registry.require(agent);
36
41
  return enrichActiveProcessesWithTerminalControl(processes, this.terminalProvider, {
37
- capabilities: terminalControlCapabilitiesForAdapter(adapter)
42
+ capabilities: terminalControlCapabilitiesForAdapter(adapter),
43
+ processTree: options.processTree
38
44
  });
39
45
  }
40
46
  terminalConversationId(process) {
@@ -55,17 +61,35 @@ export class TerminalAgentBridge {
55
61
  }
56
62
  const adapter = this.registry.require(parsed.agent);
57
63
  const panes = await this.terminalProvider.listPanes();
58
- const pane = panes.find((candidate) => candidate.kind === parsed.kind && candidate.target === parsed.target);
59
- if (!pane) {
64
+ const candidates = panes.filter((candidate) => candidate.kind === parsed.kind && candidate.target === parsed.target);
65
+ const verified = this.verifyIdentity
66
+ ? (await Promise.all(candidates.map(async (pane) => {
67
+ const terminalControl = terminalRefFromPane(pane, terminalControlCapabilitiesForAdapter(adapter));
68
+ try {
69
+ const verifiedTerminalControl = await this.verifyTerminalIdentity(adapter.agent, terminalControl, { pid: parsed.pid });
70
+ return { pane, terminalControl: verifiedTerminalControl };
71
+ }
72
+ catch {
73
+ return undefined;
74
+ }
75
+ }))).filter((candidate) => candidate !== undefined)
76
+ : candidates.slice(0, 1).map((pane) => ({
77
+ pane,
78
+ terminalControl: terminalRefFromPane(pane, terminalControlCapabilitiesForAdapter(adapter))
79
+ }));
80
+ if (verified.length === 0) {
60
81
  throw new Error(`terminal-controlled session ${parsed.conversationId} is no longer available`);
61
82
  }
83
+ if (verified.length > 1) {
84
+ throw new Error(`terminal-controlled session ${parsed.conversationId} matches multiple active panes`);
85
+ }
62
86
  return {
63
87
  conversationId: parsed.conversationId,
64
88
  agent: parsed.agent,
65
89
  pid: parsed.pid,
66
90
  legacy: parsed.legacy,
67
91
  adapter,
68
- terminalControl: terminalRefFromPane(pane, terminalControlCapabilitiesForAdapter(adapter))
92
+ terminalControl: verified[0].terminalControl
69
93
  };
70
94
  }
71
95
  async status(agent, terminalControl, options = {}) {
@@ -75,8 +99,11 @@ export class TerminalAgentBridge {
75
99
  return unsupportedScreenStatus(adapter, terminalControl);
76
100
  }
77
101
  try {
78
- const { inspection } = await this.captureInspection(adapter, terminalControl, options);
79
- return statusFromInspection(adapter, terminalControl, inspection);
102
+ const captured = await this.captureInspection(adapter, terminalControl, options);
103
+ return statusFromInspection(adapter, captured.terminalControl, captured.inspection, {
104
+ screen: captured.screen,
105
+ runtime: options.runtime
106
+ });
80
107
  }
81
108
  catch (error) {
82
109
  const message = error instanceof Error ? error.message : String(error);
@@ -98,7 +125,7 @@ export class TerminalAgentBridge {
98
125
  };
99
126
  }
100
127
  }
101
- async send(agent, terminalControl, text) {
128
+ async send(agent, terminalControl, text, options = {}) {
102
129
  const adapter = this.registry.require(agent);
103
130
  if (!terminalControl.capabilities.includes("send_keys")) {
104
131
  throw new Error(`${adapter.displayName} terminal input is not supported`);
@@ -107,11 +134,29 @@ export class TerminalAgentBridge {
107
134
  if (!normalized) {
108
135
  throw new Error("terminal message is empty");
109
136
  }
110
- await this.terminalProvider.sendText(terminalControl.target, normalized, {
111
- socketPath: terminalControl.socketPath
137
+ const verifiedForText = await this.verifyTerminalIdentity(adapter.agent, terminalControl, options.runtime);
138
+ await this.terminalProvider.sendText(verifiedForText.target, normalized, {
139
+ socketPath: verifiedForText.socketPath
112
140
  });
113
- await this.terminalProvider.sendKeys(terminalControl.target, ["C-m"], {
114
- socketPath: terminalControl.socketPath
141
+ // Text and Enter are separate tmux operations. Revalidate between them; if the
142
+ // identity changed, clear the line best-effort and never submit it.
143
+ let verifiedForEnter;
144
+ try {
145
+ verifiedForEnter = await this.verifyTerminalIdentity(adapter.agent, terminalControl, options.runtime);
146
+ }
147
+ catch (error) {
148
+ try {
149
+ await this.terminalProvider.sendKeys(verifiedForText.target, ["C-u"], {
150
+ socketPath: verifiedForText.socketPath
151
+ });
152
+ }
153
+ catch {
154
+ // Best effort only: preserving the identity failure is more important than cleanup.
155
+ }
156
+ throw error;
157
+ }
158
+ await this.terminalProvider.sendKeys(verifiedForEnter.target, ["C-m"], {
159
+ socketPath: verifiedForEnter.socketPath
115
160
  });
116
161
  }
117
162
  async cancel(agent, terminalControl, options = {}) {
@@ -121,15 +166,20 @@ export class TerminalAgentBridge {
121
166
  terminalControl.capabilities.includes("terminal_approval") &&
122
167
  adapter.capabilities.screenStatus &&
123
168
  terminalControl.capabilities.includes("screen_status")) {
124
- const { inspection } = await this.captureInspection(adapter, terminalControl, options);
169
+ const captured = await this.captureInspection(adapter, terminalControl, options);
170
+ const { inspection } = captured;
125
171
  if (inspection.approval.approvable && inspection.approval.action.mode === "structured") {
126
- const fingerprint = terminalApprovalFingerprint(adapter.agent, terminalControl, inspection);
172
+ const fingerprint = terminalApprovalFingerprint(adapter.agent, captured.terminalControl, inspection, {
173
+ screen: captured.screen,
174
+ runtime: options.runtime
175
+ });
127
176
  if (!fingerprint) {
128
177
  return {
129
178
  cancelRequested: false,
130
179
  reason: `${adapter.displayName} structured approval has no fingerprint`
131
180
  };
132
181
  }
182
+ await this.verifyTerminalIdentity(adapter.agent, captured.terminalControl, options.runtime);
133
183
  const decision = await adapter.resolveApproval({
134
184
  decision: "deny",
135
185
  expectedFingerprint: fingerprint,
@@ -160,8 +210,9 @@ export class TerminalAgentBridge {
160
210
  reason: `${adapter.displayName} terminal cancellation is not supported`
161
211
  };
162
212
  }
163
- await this.terminalProvider.sendKeys(terminalControl.target, adapter.cancelKeys, {
164
- socketPath: terminalControl.socketPath
213
+ const verifiedForCancel = await this.verifyTerminalIdentity(adapter.agent, terminalControl, options.runtime);
214
+ await this.terminalProvider.sendKeys(verifiedForCancel.target, adapter.cancelKeys, {
215
+ socketPath: verifiedForCancel.socketPath
165
216
  });
166
217
  return {
167
218
  cancelRequested: true,
@@ -179,7 +230,9 @@ export class TerminalAgentBridge {
179
230
  reason: `${adapter.displayName} terminal approval is not supported`
180
231
  };
181
232
  }
182
- const { inspection } = await this.captureInspection(adapter, terminalControl, options);
233
+ const captured = await this.captureInspection(adapter, terminalControl, options);
234
+ const { inspection } = captured;
235
+ const activeTerminalControl = captured.terminalControl;
183
236
  if (!inspection.approval.approvable) {
184
237
  return {
185
238
  approved: false,
@@ -199,7 +252,10 @@ export class TerminalAgentBridge {
199
252
  label: inspection.approval.action.label,
200
253
  promptKind: inspection.approval.promptKind,
201
254
  command: inspection.approval.command,
202
- fingerprint: terminalApprovalFingerprint(adapter.agent, terminalControl, inspection),
255
+ fingerprint: terminalApprovalFingerprint(adapter.agent, activeTerminalControl, inspection, {
256
+ screen: captured.screen,
257
+ runtime: options.runtime
258
+ }),
203
259
  screenExcerpt: inspection.screenExcerpt,
204
260
  decisionMode,
205
261
  requestId: inspection.approval.action.requestId
@@ -216,7 +272,33 @@ export class TerminalAgentBridge {
216
272
  screenExcerpt: inspection.screenExcerpt
217
273
  };
218
274
  }
219
- const fingerprint = terminalApprovalFingerprint(adapter.agent, terminalControl, inspection);
275
+ const fingerprint = terminalApprovalFingerprint(adapter.agent, activeTerminalControl, inspection, {
276
+ screen: captured.screen,
277
+ runtime: options.runtime
278
+ });
279
+ if (adapter.agent === "claude" &&
280
+ decisionMode === "keys" &&
281
+ !options.expectedFingerprint) {
282
+ return {
283
+ approved: false,
284
+ blocked: true,
285
+ reason: "screen approval requires the latest expected fingerprint",
286
+ key: inspection.approval.action.keys.length === 1
287
+ ? inspection.approval.action.keys[0]
288
+ : undefined,
289
+ keys: inspection.approval.action.keys,
290
+ label: inspection.approval.action.label,
291
+ promptKind: inspection.approval.promptKind,
292
+ command: inspection.approval.command,
293
+ cwd: inspection.approval.cwd,
294
+ toolName: inspection.approval.toolName,
295
+ requestDetail: inspection.approval.requestDetail,
296
+ fingerprint,
297
+ screenExcerpt: inspection.screenExcerpt,
298
+ decisionMode,
299
+ requestId: inspection.approval.action.requestId
300
+ };
301
+ }
220
302
  if (options.expectedFingerprint && options.expectedFingerprint !== fingerprint) {
221
303
  return {
222
304
  approved: false,
@@ -235,6 +317,35 @@ export class TerminalAgentBridge {
235
317
  requestId: inspection.approval.action.requestId
236
318
  };
237
319
  }
320
+ if (options.authorize) {
321
+ const authorization = await options.authorize({
322
+ agent: adapter.agent,
323
+ terminalControl: activeTerminalControl,
324
+ inspection,
325
+ fingerprint,
326
+ runtime: options.runtime
327
+ });
328
+ if (!authorization.approved) {
329
+ return {
330
+ approved: false,
331
+ blocked: true,
332
+ reason: authorization.reason ?? "approval was not authorized",
333
+ key: inspection.approval.action.keys.length === 1
334
+ ? inspection.approval.action.keys[0]
335
+ : undefined,
336
+ keys: inspection.approval.action.keys,
337
+ label: inspection.approval.action.label,
338
+ promptKind: inspection.approval.promptKind,
339
+ command: inspection.approval.command,
340
+ toolName: inspection.approval.toolName,
341
+ requestDetail: inspection.approval.requestDetail,
342
+ fingerprint,
343
+ screenExcerpt: inspection.screenExcerpt,
344
+ decisionMode,
345
+ requestId: inspection.approval.action.requestId
346
+ };
347
+ }
348
+ }
238
349
  if (decisionMode === "structured") {
239
350
  if (!options.expectedFingerprint) {
240
351
  return {
@@ -261,6 +372,7 @@ export class TerminalAgentBridge {
261
372
  requestId: inspection.approval.action.requestId
262
373
  };
263
374
  }
375
+ await this.verifyTerminalIdentity(adapter.agent, activeTerminalControl, options.runtime);
264
376
  const resolved = await adapter.resolveApproval({
265
377
  decision: "allow",
266
378
  expectedFingerprint: options.expectedFingerprint,
@@ -281,21 +393,102 @@ export class TerminalAgentBridge {
281
393
  requestId: resolved.requestId ?? inspection.approval.action.requestId
282
394
  };
283
395
  }
284
- await this.terminalProvider.sendKeys(terminalControl.target, inspection.approval.action.keys, { socketPath: terminalControl.socketPath });
396
+ const recaptured = await this.captureInspection(adapter, activeTerminalControl, options);
397
+ const recapturedInspection = recaptured.inspection;
398
+ if (!recapturedInspection.approval.approvable) {
399
+ return {
400
+ approved: false,
401
+ blocked: true,
402
+ reason: "approval prompt is no longer approvable after authorization",
403
+ promptKind: recapturedInspection.approval.promptKind,
404
+ command: recapturedInspection.approval.command,
405
+ cwd: recapturedInspection.approval.cwd,
406
+ toolName: recapturedInspection.approval.toolName,
407
+ requestDetail: recapturedInspection.approval.requestDetail,
408
+ screenExcerpt: recapturedInspection.screenExcerpt
409
+ };
410
+ }
411
+ const recapturedDecisionMode = recapturedInspection.approval.action.mode ?? "keys";
412
+ const recapturedFingerprint = terminalApprovalFingerprint(adapter.agent, recaptured.terminalControl, recapturedInspection, {
413
+ screen: recaptured.screen,
414
+ runtime: options.runtime
415
+ });
416
+ if (recapturedDecisionMode !== decisionMode) {
417
+ return {
418
+ approved: false,
419
+ blocked: true,
420
+ reason: "approval decision mode changed after authorization",
421
+ key: recapturedInspection.approval.action.keys.length === 1
422
+ ? recapturedInspection.approval.action.keys[0]
423
+ : undefined,
424
+ keys: recapturedInspection.approval.action.keys,
425
+ label: recapturedInspection.approval.action.label,
426
+ promptKind: recapturedInspection.approval.promptKind,
427
+ command: recapturedInspection.approval.command,
428
+ cwd: recapturedInspection.approval.cwd,
429
+ toolName: recapturedInspection.approval.toolName,
430
+ requestDetail: recapturedInspection.approval.requestDetail,
431
+ fingerprint: recapturedFingerprint,
432
+ screenExcerpt: recapturedInspection.screenExcerpt,
433
+ decisionMode: recapturedDecisionMode,
434
+ requestId: recapturedInspection.approval.action.requestId
435
+ };
436
+ }
437
+ if (recapturedFingerprint !== fingerprint) {
438
+ return {
439
+ approved: false,
440
+ blocked: true,
441
+ reason: "approval fingerprint changed after authorization",
442
+ key: recapturedInspection.approval.action.keys.length === 1
443
+ ? recapturedInspection.approval.action.keys[0]
444
+ : undefined,
445
+ keys: recapturedInspection.approval.action.keys,
446
+ label: recapturedInspection.approval.action.label,
447
+ promptKind: recapturedInspection.approval.promptKind,
448
+ command: recapturedInspection.approval.command,
449
+ cwd: recapturedInspection.approval.cwd,
450
+ toolName: recapturedInspection.approval.toolName,
451
+ requestDetail: recapturedInspection.approval.requestDetail,
452
+ fingerprint: recapturedFingerprint,
453
+ screenExcerpt: recapturedInspection.screenExcerpt,
454
+ decisionMode: recapturedDecisionMode,
455
+ requestId: recapturedInspection.approval.action.requestId
456
+ };
457
+ }
458
+ const verifiedForApproval = await this.verifyTerminalIdentity(adapter.agent, recaptured.terminalControl, options.runtime);
459
+ if (!recapturedFingerprint) {
460
+ return {
461
+ approved: false,
462
+ blocked: true,
463
+ reason: "approval has no dispatch fingerprint",
464
+ screenExcerpt: recapturedInspection.screenExcerpt,
465
+ decisionMode: recapturedDecisionMode
466
+ };
467
+ }
468
+ await options.beforeKeyDispatch?.({
469
+ agent: adapter.agent,
470
+ terminalControl: verifiedForApproval,
471
+ inspection: recapturedInspection,
472
+ fingerprint: recapturedFingerprint,
473
+ keys: recapturedInspection.approval.action.keys,
474
+ runtime: options.runtime
475
+ });
476
+ await this.terminalProvider.sendKeys(verifiedForApproval.target, recapturedInspection.approval.action.keys, { socketPath: verifiedForApproval.socketPath });
285
477
  return {
286
478
  approved: true,
287
479
  blocked: false,
288
- key: inspection.approval.action.keys.length === 1
289
- ? inspection.approval.action.keys[0]
480
+ key: recapturedInspection.approval.action.keys.length === 1
481
+ ? recapturedInspection.approval.action.keys[0]
290
482
  : undefined,
291
- keys: inspection.approval.action.keys,
292
- label: inspection.approval.action.label,
293
- promptKind: inspection.approval.promptKind,
294
- command: inspection.approval.command,
295
- fingerprint,
296
- screenExcerpt: inspection.screenExcerpt,
297
- decisionMode,
298
- requestId: inspection.approval.action.requestId
483
+ keys: recapturedInspection.approval.action.keys,
484
+ label: recapturedInspection.approval.action.label,
485
+ promptKind: recapturedInspection.approval.promptKind,
486
+ command: recapturedInspection.approval.command,
487
+ cwd: recapturedInspection.approval.cwd,
488
+ fingerprint: recapturedFingerprint,
489
+ screenExcerpt: recapturedInspection.screenExcerpt,
490
+ decisionMode: recapturedDecisionMode,
491
+ requestId: recapturedInspection.approval.action.requestId
299
492
  };
300
493
  }
301
494
  async monitorPoll(options) {
@@ -307,7 +500,10 @@ export class TerminalAgentBridge {
307
500
  try {
308
501
  const captured = await this.captureInspection(adapter, options.terminalControl, options.screenOptions);
309
502
  inspection = captured.inspection;
310
- status = statusFromInspection(adapter, options.terminalControl, inspection);
503
+ status = statusFromInspection(adapter, captured.terminalControl, inspection, {
504
+ screen: captured.screen,
505
+ runtime: options.screenOptions?.runtime
506
+ });
311
507
  }
312
508
  catch (error) {
313
509
  status = failedScreenStatus(adapter, options.terminalControl, error);
@@ -346,11 +542,13 @@ export class TerminalAgentBridge {
346
542
  };
347
543
  }
348
544
  async captureInspection(adapter, terminalControl, options = {}) {
349
- const screen = await this.terminalProvider.capture(terminalControl.target, {
545
+ const verifiedTerminalControl = await this.verifyTerminalIdentity(adapter.agent, terminalControl, options.runtime);
546
+ const screen = await this.terminalProvider.capture(verifiedTerminalControl.target, {
350
547
  scrollbackLines: options.scrollbackLines ?? 120,
351
- socketPath: terminalControl.socketPath
548
+ socketPath: verifiedTerminalControl.socketPath
352
549
  });
353
550
  return {
551
+ terminalControl: verifiedTerminalControl,
354
552
  screen,
355
553
  inspection: adapter.inspectScreen({
356
554
  screen,
@@ -361,32 +559,65 @@ export class TerminalAgentBridge {
361
559
  })
362
560
  };
363
561
  }
562
+ async verifyTerminalIdentity(agent, terminalControl, runtime) {
563
+ if (!this.verifyIdentity) {
564
+ return terminalControl;
565
+ }
566
+ if (!Number.isInteger(runtime?.pid) || Number(runtime?.pid) <= 0) {
567
+ throw new Error(`refusing terminal access for ${agent}:${terminalControl.target} without an exact agent pid; reattach this legacy tmux session before controlling it`);
568
+ }
569
+ const result = await this.verifyIdentity({
570
+ agent,
571
+ pid: Number(runtime?.pid),
572
+ terminalControl
573
+ });
574
+ return result?.terminalControl ?? terminalControl;
575
+ }
364
576
  }
365
- export function terminalApprovalFingerprint(agent, terminalControl, inspection) {
577
+ export function terminalApprovalFingerprint(agent, terminalControl, inspection, options = {}) {
366
578
  if (!inspection.approval.approvable) {
367
579
  return undefined;
368
580
  }
369
581
  const decisionMode = inspection.approval.action.mode ?? "keys";
582
+ const rawScreenDigest = decisionMode === "keys" && options.screen !== undefined
583
+ ? createHash("sha256").update(options.screen).digest("hex")
584
+ : undefined;
370
585
  return createHash("sha256")
371
586
  .update(JSON.stringify({
372
587
  agent,
373
588
  provider: "tmux",
374
- target: terminalControl.target,
589
+ terminal: {
590
+ target: terminalControl.target,
591
+ socket_path: terminalControl.socketPath,
592
+ session: terminalControl.session,
593
+ window: terminalControl.window,
594
+ pane: terminalControl.pane,
595
+ pane_pid: terminalControl.panePid
596
+ },
597
+ runtime: {
598
+ pid: options.runtime?.pid,
599
+ session_id: options.runtime?.sessionId,
600
+ cwd: options.runtime?.cwd,
601
+ conversation_id: options.runtime?.conversationId,
602
+ message_id: options.runtime?.messageId,
603
+ terminal_target: options.runtime?.terminalTarget
604
+ },
375
605
  keys: inspection.approval.action.keys,
376
606
  label: inspection.approval.action.label,
377
607
  prompt_kind: inspection.approval.promptKind,
378
608
  command: inspection.approval.command,
609
+ cwd: inspection.approval.cwd,
379
610
  tool_name: inspection.approval.toolName,
380
611
  request_detail: inspection.approval.requestDetail,
381
- screen_excerpt: decisionMode === "structured" ? undefined : inspection.screenExcerpt,
612
+ raw_screen_sha256: rawScreenDigest,
382
613
  decision_mode: decisionMode,
383
614
  request_id: inspection.approval.action.requestId
384
615
  }))
385
616
  .digest("hex");
386
617
  }
387
- function statusFromInspection(adapter, terminalControl, inspection) {
618
+ function statusFromInspection(adapter, terminalControl, inspection, options = {}) {
388
619
  const approval = inspection.approval;
389
- const fingerprint = terminalApprovalFingerprint(adapter.agent, terminalControl, inspection);
620
+ const fingerprint = terminalApprovalFingerprint(adapter.agent, terminalControl, inspection, options);
390
621
  return {
391
622
  provider: terminalControl.kind,
392
623
  target: terminalControl.target,
@@ -406,6 +637,7 @@ function statusFromInspection(adapter, terminalControl, inspection) {
406
637
  label: approval.approvable ? approval.action.label : undefined,
407
638
  prompt_kind: approval.promptKind,
408
639
  command: approval.command,
640
+ cwd: approval.cwd,
409
641
  tool_name: approval.toolName,
410
642
  request_detail: approval.requestDetail,
411
643
  reason: approval.approvable ? undefined : approval.reason,
@@ -415,6 +647,9 @@ function statusFromInspection(adapter, terminalControl, inspection) {
415
647
  },
416
648
  screen: {
417
649
  excerpt: inspection.screenExcerpt,
650
+ digest: options.screen === undefined
651
+ ? undefined
652
+ : createHash("sha256").update(options.screen).digest("hex"),
418
653
  approval: approvalOutput(approval)
419
654
  }
420
655
  };
@@ -446,6 +681,7 @@ function approvalOutput(approval) {
446
681
  reason: approval.reason,
447
682
  promptKind: approval.promptKind,
448
683
  command: approval.command,
684
+ cwd: approval.cwd,
449
685
  toolName: approval.toolName,
450
686
  requestDetail: approval.requestDetail
451
687
  };
@@ -458,6 +694,7 @@ function approvalOutput(approval) {
458
694
  label: approval.action.label,
459
695
  promptKind: approval.promptKind,
460
696
  command: approval.command,
697
+ cwd: approval.cwd,
461
698
  toolName: approval.toolName,
462
699
  requestDetail: approval.requestDetail,
463
700
  decisionMode: approval.action.mode ?? "keys",