@scotthuang/agent-knock-knock 0.2.46 → 0.2.48

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 (44) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +93 -55
  3. package/dist/src/approval-policy.d.ts +3 -1
  4. package/dist/src/approval-policy.js +18 -4
  5. package/dist/src/approval-policy.js.map +1 -1
  6. package/dist/src/claude-hook-installer.d.ts +65 -0
  7. package/dist/src/claude-hook-installer.js +410 -0
  8. package/dist/src/claude-hook-installer.js.map +1 -0
  9. package/dist/src/claude-hook-protocol.d.ts +89 -0
  10. package/dist/src/claude-hook-protocol.js +243 -0
  11. package/dist/src/claude-hook-protocol.js.map +1 -0
  12. package/dist/src/claude-hook-store.d.ts +247 -0
  13. package/dist/src/claude-hook-store.js +1075 -0
  14. package/dist/src/claude-hook-store.js.map +1 -0
  15. package/dist/src/claude-terminal-agent-adapter.d.ts +47 -0
  16. package/dist/src/claude-terminal-agent-adapter.js +935 -0
  17. package/dist/src/claude-terminal-agent-adapter.js.map +1 -0
  18. package/dist/src/cli.js +2065 -316
  19. package/dist/src/cli.js.map +1 -1
  20. package/dist/src/openclaw-plugin-helpers.d.ts +49 -0
  21. package/dist/src/openclaw-plugin-helpers.js +246 -0
  22. package/dist/src/openclaw-plugin-helpers.js.map +1 -0
  23. package/dist/src/openclaw-plugin.js +98 -275
  24. package/dist/src/openclaw-plugin.js.map +1 -1
  25. package/dist/src/runtime-log.js +54 -2
  26. package/dist/src/runtime-log.js.map +1 -1
  27. package/dist/src/store.js +432 -12
  28. package/dist/src/store.js.map +1 -1
  29. package/dist/src/terminal-agent-adapter.d.ts +37 -0
  30. package/dist/src/terminal-agent-adapter.js.map +1 -1
  31. package/dist/src/terminal-agent-bridge.d.ts +51 -4
  32. package/dist/src/terminal-agent-bridge.js +326 -43
  33. package/dist/src/terminal-agent-bridge.js.map +1 -1
  34. package/dist/src/terminal-agent-registry.js +3 -1
  35. package/dist/src/terminal-agent-registry.js.map +1 -1
  36. package/dist/src/terminal-control-provider.d.ts +3 -1
  37. package/dist/src/terminal-control-provider.js +41 -37
  38. package/dist/src/terminal-control-provider.js.map +1 -1
  39. package/dist/src/terminal-process-source.d.ts +12 -3
  40. package/dist/src/terminal-process-source.js +37 -6
  41. package/dist/src/terminal-process-source.js.map +1 -1
  42. package/openclaw.plugin.json +5 -5
  43. package/package.json +10 -5
  44. package/templates/openclaw-skills/agent-knock-knock/SKILL.md +19 -30
@@ -1,5 +1,5 @@
1
1
  import type { ExecutorKind } from "./executors.js";
2
- import { type ActiveTerminalProcess, type TerminalAgentAdapter, type TerminalAgentAdapterCapabilities, type TerminalAgentAdapterRegistry, type TerminalCompletionEvidence, type TerminalControlRef, type TerminalDurableCompletionRequest, type TerminalProcessSnapshot, type TerminalScreenInspection } from "./terminal-agent-adapter.js";
2
+ import { type ActiveTerminalProcess, type TerminalAgentAdapter, type TerminalAgentAdapterCapabilities, type TerminalAgentAdapterRegistry, type TerminalCompletionEvidence, type TerminalControlRef, type TerminalDurableCompletionRequest, type TerminalProcessSnapshot, type TerminalRuntimeIdentity, type TerminalScreenInspection } from "./terminal-agent-adapter.js";
3
3
  import { type TerminalControlProvider } from "./terminal-control-provider.js";
4
4
  export interface TerminalBridgeStatus {
5
5
  provider: "tmux";
@@ -18,8 +18,13 @@ export interface TerminalBridgeStatus {
18
18
  label?: string;
19
19
  prompt_kind?: string;
20
20
  command?: string;
21
+ cwd?: string;
22
+ tool_name?: string;
23
+ request_detail?: string;
21
24
  reason?: string;
22
25
  fingerprint?: string;
26
+ decision_mode?: "keys" | "structured";
27
+ request_id?: string;
23
28
  };
24
29
  screen: {
25
30
  excerpt?: string;
@@ -45,8 +50,33 @@ export interface TerminalApprovalExecution {
45
50
  label?: string;
46
51
  promptKind?: string;
47
52
  command?: string;
53
+ cwd?: string;
54
+ toolName?: string;
55
+ requestDetail?: string;
48
56
  fingerprint?: string;
49
57
  screenExcerpt?: string;
58
+ decisionMode?: "keys" | "structured";
59
+ requestId?: string;
60
+ }
61
+ export interface TerminalIdentityVerificationRequest {
62
+ agent: ExecutorKind;
63
+ pid: number;
64
+ terminalControl: TerminalControlRef;
65
+ }
66
+ export interface TerminalIdentityVerificationResult {
67
+ terminalControl?: TerminalControlRef;
68
+ }
69
+ export type TerminalIdentityVerifier = (request: TerminalIdentityVerificationRequest) => Promise<TerminalIdentityVerificationResult | void>;
70
+ export interface TerminalApprovalAuthorizationContext {
71
+ agent: ExecutorKind;
72
+ terminalControl: TerminalControlRef;
73
+ inspection: TerminalScreenInspection;
74
+ fingerprint?: string;
75
+ runtime?: TerminalRuntimeIdentity;
76
+ }
77
+ export interface TerminalApprovalAuthorizationDecision {
78
+ approved: boolean;
79
+ reason?: string;
50
80
  }
51
81
  export interface TerminalMonitorPoll {
52
82
  status: TerminalBridgeStatus;
@@ -57,29 +87,44 @@ export interface TerminalMonitorPoll {
57
87
  export declare class TerminalAgentBridge {
58
88
  readonly registry: TerminalAgentAdapterRegistry;
59
89
  readonly terminalProvider: TerminalControlProvider;
90
+ private readonly verifyIdentity?;
60
91
  constructor(options: {
61
92
  registry: TerminalAgentAdapterRegistry;
62
93
  terminalProvider: TerminalControlProvider;
94
+ verifyIdentity?: TerminalIdentityVerifier;
63
95
  });
64
96
  adapterFor(agent: ExecutorKind | string): TerminalAgentAdapter;
65
97
  listProcesses(snapshots: readonly TerminalProcessSnapshot[], agents?: readonly ExecutorKind[]): Promise<ActiveTerminalProcess[]>;
66
98
  discoverProcesses(snapshots: readonly TerminalProcessSnapshot[], agents?: readonly ExecutorKind[]): Promise<ActiveTerminalProcess[]>;
67
- attachProcesses<T extends ActiveTerminalProcess>(agent: ExecutorKind, processes: T[]): Promise<T[]>;
99
+ attachProcesses<T extends ActiveTerminalProcess>(agent: ExecutorKind, processes: T[], options?: {
100
+ processTree?: readonly TerminalProcessSnapshot[];
101
+ }): Promise<T[]>;
68
102
  terminalConversationId(process: Pick<ActiveTerminalProcess, "agent" | "pid" | "terminalControl">): string;
69
103
  resolveConversationId(conversationId: string | undefined): Promise<ResolvedTerminalConversation | undefined>;
70
104
  status(agent: ExecutorKind, terminalControl: TerminalControlRef, options?: {
71
105
  scrollbackLines?: number;
106
+ runtime?: TerminalRuntimeIdentity;
72
107
  }): Promise<TerminalBridgeStatus>;
73
- send(agent: ExecutorKind, terminalControl: TerminalControlRef, text: string): Promise<void>;
74
- cancel(agent: ExecutorKind, terminalControl: TerminalControlRef): Promise<{
108
+ send(agent: ExecutorKind, terminalControl: TerminalControlRef, text: string, options?: {
109
+ runtime?: TerminalRuntimeIdentity;
110
+ }): Promise<void>;
111
+ cancel(agent: ExecutorKind, terminalControl: TerminalControlRef, options?: {
112
+ runtime?: TerminalRuntimeIdentity;
113
+ scrollbackLines?: number;
114
+ }): Promise<{
75
115
  cancelRequested: boolean;
76
116
  key?: string;
77
117
  keys?: readonly string[];
78
118
  reason?: string;
119
+ deniedApproval?: boolean;
120
+ requestId?: string;
79
121
  }>;
80
122
  approve(agent: ExecutorKind, terminalControl: TerminalControlRef, options?: {
81
123
  expectedFingerprint?: string;
82
124
  scrollbackLines?: number;
125
+ runtime?: TerminalRuntimeIdentity;
126
+ requiredDecisionMode?: "keys" | "structured";
127
+ authorize?: (context: TerminalApprovalAuthorizationContext) => TerminalApprovalAuthorizationDecision | Promise<TerminalApprovalAuthorizationDecision>;
83
128
  }): Promise<TerminalApprovalExecution>;
84
129
  monitorPoll(options: {
85
130
  agent: ExecutorKind;
@@ -89,9 +134,11 @@ export declare class TerminalAgentBridge {
89
134
  requestText?: string;
90
135
  screenChangedSinceSend?: boolean;
91
136
  maxExcerptLength?: number;
137
+ runtime?: TerminalRuntimeIdentity;
92
138
  };
93
139
  durableRequest?: TerminalDurableCompletionRequest;
94
140
  }): Promise<TerminalMonitorPoll>;
95
141
  private captureInspection;
142
+ private verifyTerminalIdentity;
96
143
  }
97
144
  export declare function terminalApprovalFingerprint(agent: ExecutorKind, terminalControl: Pick<TerminalControlRef, "target">, inspection: TerminalScreenInspection): 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,27 +61,46 @@ 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 = {}) {
72
96
  const adapter = this.registry.require(agent);
73
- if (!adapter.capabilities.screenStatus) {
97
+ if (!adapter.capabilities.screenStatus ||
98
+ !terminalControl.capabilities.includes("screen_status")) {
74
99
  return unsupportedScreenStatus(adapter, terminalControl);
75
100
  }
76
101
  try {
77
- const { inspection } = await this.captureInspection(adapter, terminalControl, options);
78
- return statusFromInspection(adapter, terminalControl, inspection);
102
+ const captured = await this.captureInspection(adapter, terminalControl, options);
103
+ return statusFromInspection(adapter, captured.terminalControl, captured.inspection);
79
104
  }
80
105
  catch (error) {
81
106
  const message = error instanceof Error ? error.message : String(error);
@@ -97,7 +122,7 @@ export class TerminalAgentBridge {
97
122
  };
98
123
  }
99
124
  }
100
- async send(agent, terminalControl, text) {
125
+ async send(agent, terminalControl, text, options = {}) {
101
126
  const adapter = this.registry.require(agent);
102
127
  if (!terminalControl.capabilities.includes("send_keys")) {
103
128
  throw new Error(`${adapter.displayName} terminal input is not supported`);
@@ -106,23 +131,82 @@ export class TerminalAgentBridge {
106
131
  if (!normalized) {
107
132
  throw new Error("terminal message is empty");
108
133
  }
109
- await this.terminalProvider.sendText(terminalControl.target, normalized, {
110
- socketPath: terminalControl.socketPath
134
+ const verifiedForText = await this.verifyTerminalIdentity(adapter.agent, terminalControl, options.runtime);
135
+ await this.terminalProvider.sendText(verifiedForText.target, normalized, {
136
+ socketPath: verifiedForText.socketPath
111
137
  });
112
- await this.terminalProvider.sendKeys(terminalControl.target, ["C-m"], {
113
- socketPath: terminalControl.socketPath
138
+ // Text and Enter are separate tmux operations. Revalidate between them; if the
139
+ // identity changed, clear the line best-effort and never submit it.
140
+ let verifiedForEnter;
141
+ try {
142
+ verifiedForEnter = await this.verifyTerminalIdentity(adapter.agent, terminalControl, options.runtime);
143
+ }
144
+ catch (error) {
145
+ try {
146
+ await this.terminalProvider.sendKeys(verifiedForText.target, ["C-u"], {
147
+ socketPath: verifiedForText.socketPath
148
+ });
149
+ }
150
+ catch {
151
+ // Best effort only: preserving the identity failure is more important than cleanup.
152
+ }
153
+ throw error;
154
+ }
155
+ await this.terminalProvider.sendKeys(verifiedForEnter.target, ["C-m"], {
156
+ socketPath: verifiedForEnter.socketPath
114
157
  });
115
158
  }
116
- async cancel(agent, terminalControl) {
159
+ async cancel(agent, terminalControl, options = {}) {
117
160
  const adapter = this.registry.require(agent);
118
- if (!adapter.capabilities.cancellation || adapter.cancelKeys.length === 0) {
161
+ if (adapter.capabilities.terminalApproval &&
162
+ adapter.resolveApproval &&
163
+ terminalControl.capabilities.includes("terminal_approval") &&
164
+ adapter.capabilities.screenStatus &&
165
+ terminalControl.capabilities.includes("screen_status")) {
166
+ const captured = await this.captureInspection(adapter, terminalControl, options);
167
+ const { inspection } = captured;
168
+ if (inspection.approval.approvable && inspection.approval.action.mode === "structured") {
169
+ const fingerprint = terminalApprovalFingerprint(adapter.agent, captured.terminalControl, inspection);
170
+ if (!fingerprint) {
171
+ return {
172
+ cancelRequested: false,
173
+ reason: `${adapter.displayName} structured approval has no fingerprint`
174
+ };
175
+ }
176
+ await this.verifyTerminalIdentity(adapter.agent, captured.terminalControl, options.runtime);
177
+ const decision = await adapter.resolveApproval({
178
+ decision: "deny",
179
+ expectedFingerprint: fingerprint,
180
+ actualFingerprint: fingerprint,
181
+ inspection,
182
+ runtime: options.runtime,
183
+ interrupt: true
184
+ });
185
+ return {
186
+ cancelRequested: decision.resolved,
187
+ deniedApproval: decision.resolved,
188
+ requestId: decision.requestId,
189
+ reason: decision.reason
190
+ };
191
+ }
192
+ if (inspection.approval.blocked && !inspection.approval.approvable) {
193
+ return {
194
+ cancelRequested: false,
195
+ reason: inspection.approval.reason
196
+ };
197
+ }
198
+ }
199
+ if (!adapter.capabilities.cancellation ||
200
+ adapter.cancelKeys.length === 0 ||
201
+ !terminalControl.capabilities.includes("terminal_cancel")) {
119
202
  return {
120
203
  cancelRequested: false,
121
204
  reason: `${adapter.displayName} terminal cancellation is not supported`
122
205
  };
123
206
  }
124
- await this.terminalProvider.sendKeys(terminalControl.target, adapter.cancelKeys, {
125
- socketPath: terminalControl.socketPath
207
+ const verifiedForCancel = await this.verifyTerminalIdentity(adapter.agent, terminalControl, options.runtime);
208
+ await this.terminalProvider.sendKeys(verifiedForCancel.target, adapter.cancelKeys, {
209
+ socketPath: verifiedForCancel.socketPath
126
210
  });
127
211
  return {
128
212
  cancelRequested: true,
@@ -132,14 +216,17 @@ export class TerminalAgentBridge {
132
216
  }
133
217
  async approve(agent, terminalControl, options = {}) {
134
218
  const adapter = this.registry.require(agent);
135
- if (!adapter.capabilities.terminalApproval) {
219
+ if (!adapter.capabilities.terminalApproval ||
220
+ !terminalControl.capabilities.includes("terminal_approval")) {
136
221
  return {
137
222
  approved: false,
138
223
  blocked: true,
139
224
  reason: `${adapter.displayName} terminal approval is not supported`
140
225
  };
141
226
  }
142
- const { inspection } = await this.captureInspection(adapter, terminalControl, options);
227
+ const captured = await this.captureInspection(adapter, terminalControl, options);
228
+ const { inspection } = captured;
229
+ const activeTerminalControl = captured.terminalControl;
143
230
  if (!inspection.approval.approvable) {
144
231
  return {
145
232
  approved: false,
@@ -150,7 +237,22 @@ export class TerminalAgentBridge {
150
237
  screenExcerpt: inspection.screenExcerpt
151
238
  };
152
239
  }
153
- if (inspection.approval.action.keys.length === 0) {
240
+ const decisionMode = inspection.approval.action.mode ?? "keys";
241
+ if (options.requiredDecisionMode && decisionMode !== options.requiredDecisionMode) {
242
+ return {
243
+ approved: false,
244
+ blocked: true,
245
+ reason: `${adapter.displayName} approval mode ${decisionMode} is not eligible for this decision`,
246
+ label: inspection.approval.action.label,
247
+ promptKind: inspection.approval.promptKind,
248
+ command: inspection.approval.command,
249
+ fingerprint: terminalApprovalFingerprint(adapter.agent, activeTerminalControl, inspection),
250
+ screenExcerpt: inspection.screenExcerpt,
251
+ decisionMode,
252
+ requestId: inspection.approval.action.requestId
253
+ };
254
+ }
255
+ if (decisionMode === "keys" && inspection.approval.action.keys.length === 0) {
154
256
  return {
155
257
  approved: false,
156
258
  blocked: true,
@@ -161,7 +263,7 @@ export class TerminalAgentBridge {
161
263
  screenExcerpt: inspection.screenExcerpt
162
264
  };
163
265
  }
164
- const fingerprint = terminalApprovalFingerprint(adapter.agent, terminalControl, inspection);
266
+ const fingerprint = terminalApprovalFingerprint(adapter.agent, activeTerminalControl, inspection);
165
267
  if (options.expectedFingerprint && options.expectedFingerprint !== fingerprint) {
166
268
  return {
167
269
  approved: false,
@@ -175,33 +277,175 @@ export class TerminalAgentBridge {
175
277
  promptKind: inspection.approval.promptKind,
176
278
  command: inspection.approval.command,
177
279
  fingerprint,
178
- screenExcerpt: inspection.screenExcerpt
280
+ screenExcerpt: inspection.screenExcerpt,
281
+ decisionMode,
282
+ requestId: inspection.approval.action.requestId
179
283
  };
180
284
  }
181
- await this.terminalProvider.sendKeys(terminalControl.target, inspection.approval.action.keys, { socketPath: terminalControl.socketPath });
285
+ if (options.authorize) {
286
+ const authorization = await options.authorize({
287
+ agent: adapter.agent,
288
+ terminalControl: activeTerminalControl,
289
+ inspection,
290
+ fingerprint,
291
+ runtime: options.runtime
292
+ });
293
+ if (!authorization.approved) {
294
+ return {
295
+ approved: false,
296
+ blocked: true,
297
+ reason: authorization.reason ?? "approval was not authorized",
298
+ key: inspection.approval.action.keys.length === 1
299
+ ? inspection.approval.action.keys[0]
300
+ : undefined,
301
+ keys: inspection.approval.action.keys,
302
+ label: inspection.approval.action.label,
303
+ promptKind: inspection.approval.promptKind,
304
+ command: inspection.approval.command,
305
+ toolName: inspection.approval.toolName,
306
+ requestDetail: inspection.approval.requestDetail,
307
+ fingerprint,
308
+ screenExcerpt: inspection.screenExcerpt,
309
+ decisionMode,
310
+ requestId: inspection.approval.action.requestId
311
+ };
312
+ }
313
+ }
314
+ if (decisionMode === "structured") {
315
+ if (!options.expectedFingerprint) {
316
+ return {
317
+ approved: false,
318
+ blocked: true,
319
+ reason: "structured approval requires the latest expected fingerprint",
320
+ label: inspection.approval.action.label,
321
+ promptKind: inspection.approval.promptKind,
322
+ command: inspection.approval.command,
323
+ fingerprint,
324
+ screenExcerpt: inspection.screenExcerpt,
325
+ decisionMode,
326
+ requestId: inspection.approval.action.requestId
327
+ };
328
+ }
329
+ if (!fingerprint || !adapter.resolveApproval) {
330
+ return {
331
+ approved: false,
332
+ blocked: true,
333
+ reason: `${adapter.displayName} structured approval resolver is unavailable`,
334
+ fingerprint,
335
+ screenExcerpt: inspection.screenExcerpt,
336
+ decisionMode,
337
+ requestId: inspection.approval.action.requestId
338
+ };
339
+ }
340
+ await this.verifyTerminalIdentity(adapter.agent, activeTerminalControl, options.runtime);
341
+ const resolved = await adapter.resolveApproval({
342
+ decision: "allow",
343
+ expectedFingerprint: options.expectedFingerprint,
344
+ actualFingerprint: fingerprint,
345
+ inspection,
346
+ runtime: options.runtime
347
+ });
348
+ return {
349
+ approved: resolved.resolved,
350
+ blocked: !resolved.resolved,
351
+ reason: resolved.reason,
352
+ label: inspection.approval.action.label,
353
+ promptKind: inspection.approval.promptKind,
354
+ command: inspection.approval.command,
355
+ fingerprint,
356
+ screenExcerpt: inspection.screenExcerpt,
357
+ decisionMode,
358
+ requestId: resolved.requestId ?? inspection.approval.action.requestId
359
+ };
360
+ }
361
+ const recaptured = await this.captureInspection(adapter, activeTerminalControl, options);
362
+ const recapturedInspection = recaptured.inspection;
363
+ if (!recapturedInspection.approval.approvable) {
364
+ return {
365
+ approved: false,
366
+ blocked: true,
367
+ reason: "approval prompt is no longer approvable after authorization",
368
+ promptKind: recapturedInspection.approval.promptKind,
369
+ command: recapturedInspection.approval.command,
370
+ cwd: recapturedInspection.approval.cwd,
371
+ toolName: recapturedInspection.approval.toolName,
372
+ requestDetail: recapturedInspection.approval.requestDetail,
373
+ screenExcerpt: recapturedInspection.screenExcerpt
374
+ };
375
+ }
376
+ const recapturedDecisionMode = recapturedInspection.approval.action.mode ?? "keys";
377
+ const recapturedFingerprint = terminalApprovalFingerprint(adapter.agent, recaptured.terminalControl, recapturedInspection);
378
+ if (recapturedDecisionMode !== decisionMode) {
379
+ return {
380
+ approved: false,
381
+ blocked: true,
382
+ reason: "approval decision mode changed after authorization",
383
+ key: recapturedInspection.approval.action.keys.length === 1
384
+ ? recapturedInspection.approval.action.keys[0]
385
+ : undefined,
386
+ keys: recapturedInspection.approval.action.keys,
387
+ label: recapturedInspection.approval.action.label,
388
+ promptKind: recapturedInspection.approval.promptKind,
389
+ command: recapturedInspection.approval.command,
390
+ cwd: recapturedInspection.approval.cwd,
391
+ toolName: recapturedInspection.approval.toolName,
392
+ requestDetail: recapturedInspection.approval.requestDetail,
393
+ fingerprint: recapturedFingerprint,
394
+ screenExcerpt: recapturedInspection.screenExcerpt,
395
+ decisionMode: recapturedDecisionMode,
396
+ requestId: recapturedInspection.approval.action.requestId
397
+ };
398
+ }
399
+ if (recapturedFingerprint !== fingerprint) {
400
+ return {
401
+ approved: false,
402
+ blocked: true,
403
+ reason: "approval fingerprint changed after authorization",
404
+ key: recapturedInspection.approval.action.keys.length === 1
405
+ ? recapturedInspection.approval.action.keys[0]
406
+ : undefined,
407
+ keys: recapturedInspection.approval.action.keys,
408
+ label: recapturedInspection.approval.action.label,
409
+ promptKind: recapturedInspection.approval.promptKind,
410
+ command: recapturedInspection.approval.command,
411
+ cwd: recapturedInspection.approval.cwd,
412
+ toolName: recapturedInspection.approval.toolName,
413
+ requestDetail: recapturedInspection.approval.requestDetail,
414
+ fingerprint: recapturedFingerprint,
415
+ screenExcerpt: recapturedInspection.screenExcerpt,
416
+ decisionMode: recapturedDecisionMode,
417
+ requestId: recapturedInspection.approval.action.requestId
418
+ };
419
+ }
420
+ const verifiedForApproval = await this.verifyTerminalIdentity(adapter.agent, recaptured.terminalControl, options.runtime);
421
+ await this.terminalProvider.sendKeys(verifiedForApproval.target, recapturedInspection.approval.action.keys, { socketPath: verifiedForApproval.socketPath });
182
422
  return {
183
423
  approved: true,
184
424
  blocked: false,
185
- key: inspection.approval.action.keys.length === 1
186
- ? inspection.approval.action.keys[0]
425
+ key: recapturedInspection.approval.action.keys.length === 1
426
+ ? recapturedInspection.approval.action.keys[0]
187
427
  : undefined,
188
- keys: inspection.approval.action.keys,
189
- label: inspection.approval.action.label,
190
- promptKind: inspection.approval.promptKind,
191
- command: inspection.approval.command,
192
- fingerprint,
193
- screenExcerpt: inspection.screenExcerpt
428
+ keys: recapturedInspection.approval.action.keys,
429
+ label: recapturedInspection.approval.action.label,
430
+ promptKind: recapturedInspection.approval.promptKind,
431
+ command: recapturedInspection.approval.command,
432
+ cwd: recapturedInspection.approval.cwd,
433
+ fingerprint: recapturedFingerprint,
434
+ screenExcerpt: recapturedInspection.screenExcerpt,
435
+ decisionMode: recapturedDecisionMode,
436
+ requestId: recapturedInspection.approval.action.requestId
194
437
  };
195
438
  }
196
439
  async monitorPoll(options) {
197
440
  const adapter = this.registry.require(options.agent);
198
441
  let inspection;
199
442
  let status = unsupportedScreenStatus(adapter, options.terminalControl);
200
- if (adapter.capabilities.screenStatus) {
443
+ if (adapter.capabilities.screenStatus &&
444
+ options.terminalControl.capabilities.includes("screen_status")) {
201
445
  try {
202
446
  const captured = await this.captureInspection(adapter, options.terminalControl, options.screenOptions);
203
447
  inspection = captured.inspection;
204
- status = statusFromInspection(adapter, options.terminalControl, inspection);
448
+ status = statusFromInspection(adapter, captured.terminalControl, inspection);
205
449
  }
206
450
  catch (error) {
207
451
  status = failedScreenStatus(adapter, options.terminalControl, error);
@@ -210,14 +454,17 @@ export class TerminalAgentBridge {
210
454
  let durableCompletion;
211
455
  let durableError;
212
456
  try {
213
- durableCompletion = adapter.capabilities.durableCompletion && options.durableRequest
457
+ durableCompletion = adapter.capabilities.durableCompletion &&
458
+ options.terminalControl.capabilities.includes("durable_completion") &&
459
+ options.durableRequest
214
460
  ? await adapter.detectDurableCompletion?.(options.durableRequest)
215
461
  : undefined;
216
462
  }
217
463
  catch (error) {
218
464
  durableError = error instanceof Error ? error.message : String(error);
219
465
  }
220
- const screenCompletion = adapter.capabilities.screenCompletion
466
+ const screenCompletion = adapter.capabilities.screenCompletion &&
467
+ options.terminalControl.capabilities.includes("screen_completion")
221
468
  ? inspection?.completion
222
469
  : undefined;
223
470
  const limitations = [
@@ -237,25 +484,43 @@ export class TerminalAgentBridge {
237
484
  };
238
485
  }
239
486
  async captureInspection(adapter, terminalControl, options = {}) {
240
- const screen = await this.terminalProvider.capture(terminalControl.target, {
487
+ const verifiedTerminalControl = await this.verifyTerminalIdentity(adapter.agent, terminalControl, options.runtime);
488
+ const screen = await this.terminalProvider.capture(verifiedTerminalControl.target, {
241
489
  scrollbackLines: options.scrollbackLines ?? 120,
242
- socketPath: terminalControl.socketPath
490
+ socketPath: verifiedTerminalControl.socketPath
243
491
  });
244
492
  return {
493
+ terminalControl: verifiedTerminalControl,
245
494
  screen,
246
495
  inspection: adapter.inspectScreen({
247
496
  screen,
248
497
  requestText: options.requestText,
249
498
  screenChangedSinceSend: options.screenChangedSinceSend,
250
- maxExcerptLength: options.maxExcerptLength
499
+ maxExcerptLength: options.maxExcerptLength,
500
+ runtime: options.runtime
251
501
  })
252
502
  };
253
503
  }
504
+ async verifyTerminalIdentity(agent, terminalControl, runtime) {
505
+ if (!this.verifyIdentity) {
506
+ return terminalControl;
507
+ }
508
+ if (!Number.isInteger(runtime?.pid) || Number(runtime?.pid) <= 0) {
509
+ throw new Error(`refusing terminal access for ${agent}:${terminalControl.target} without an exact agent pid; reattach this legacy tmux session before controlling it`);
510
+ }
511
+ const result = await this.verifyIdentity({
512
+ agent,
513
+ pid: Number(runtime?.pid),
514
+ terminalControl
515
+ });
516
+ return result?.terminalControl ?? terminalControl;
517
+ }
254
518
  }
255
519
  export function terminalApprovalFingerprint(agent, terminalControl, inspection) {
256
520
  if (!inspection.approval.approvable) {
257
521
  return undefined;
258
522
  }
523
+ const decisionMode = inspection.approval.action.mode ?? "keys";
259
524
  return createHash("sha256")
260
525
  .update(JSON.stringify({
261
526
  agent,
@@ -265,7 +530,12 @@ export function terminalApprovalFingerprint(agent, terminalControl, inspection)
265
530
  label: inspection.approval.action.label,
266
531
  prompt_kind: inspection.approval.promptKind,
267
532
  command: inspection.approval.command,
268
- screen_excerpt: inspection.screenExcerpt
533
+ cwd: inspection.approval.cwd,
534
+ tool_name: inspection.approval.toolName,
535
+ request_detail: inspection.approval.requestDetail,
536
+ screen_excerpt: decisionMode === "structured" ? undefined : inspection.screenExcerpt,
537
+ decision_mode: decisionMode,
538
+ request_id: inspection.approval.action.requestId
269
539
  }))
270
540
  .digest("hex");
271
541
  }
@@ -291,8 +561,13 @@ function statusFromInspection(adapter, terminalControl, inspection) {
291
561
  label: approval.approvable ? approval.action.label : undefined,
292
562
  prompt_kind: approval.promptKind,
293
563
  command: approval.command,
564
+ cwd: approval.cwd,
565
+ tool_name: approval.toolName,
566
+ request_detail: approval.requestDetail,
294
567
  reason: approval.approvable ? undefined : approval.reason,
295
- fingerprint
568
+ fingerprint,
569
+ decision_mode: approval.approvable ? approval.action.mode ?? "keys" : undefined,
570
+ request_id: approval.approvable ? approval.action.requestId : undefined
296
571
  },
297
572
  screen: {
298
573
  excerpt: inspection.screenExcerpt,
@@ -326,7 +601,10 @@ function approvalOutput(approval) {
326
601
  approvable: false,
327
602
  reason: approval.reason,
328
603
  promptKind: approval.promptKind,
329
- command: approval.command
604
+ command: approval.command,
605
+ cwd: approval.cwd,
606
+ toolName: approval.toolName,
607
+ requestDetail: approval.requestDetail
330
608
  };
331
609
  }
332
610
  return {
@@ -336,7 +614,12 @@ function approvalOutput(approval) {
336
614
  keys: approval.action.keys,
337
615
  label: approval.action.label,
338
616
  promptKind: approval.promptKind,
339
- command: approval.command
617
+ command: approval.command,
618
+ cwd: approval.cwd,
619
+ toolName: approval.toolName,
620
+ requestDetail: approval.requestDetail,
621
+ decisionMode: approval.action.mode ?? "keys",
622
+ requestId: approval.action.requestId
340
623
  };
341
624
  }
342
625
  function unsupportedScreenStatus(adapter, terminalControl) {