@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.
- package/CHANGELOG.md +46 -0
- package/README.md +114 -147
- package/dist/src/approval-policy.d.ts +2 -1
- package/dist/src/approval-policy.js +14 -6
- package/dist/src/approval-policy.js.map +1 -1
- package/dist/src/claude-hook-protocol.js.map +1 -1
- package/dist/src/claude-hook-store.js +17 -6
- package/dist/src/claude-hook-store.js.map +1 -1
- package/dist/src/claude-local-transcript-provider.d.ts +40 -0
- package/dist/src/claude-local-transcript-provider.js +804 -0
- package/dist/src/claude-local-transcript-provider.js.map +1 -0
- package/dist/src/claude-terminal-agent-adapter.d.ts +3 -0
- package/dist/src/claude-terminal-agent-adapter.js +146 -47
- package/dist/src/claude-terminal-agent-adapter.js.map +1 -1
- package/dist/src/cli.js +2473 -834
- package/dist/src/cli.js.map +1 -1
- package/dist/src/codex-session-provider.js.map +1 -1
- package/dist/src/codex-store-adapter.js.map +1 -1
- package/dist/src/codex-terminal-agent-adapter.js.map +1 -1
- package/dist/src/doctor-capabilities.d.ts +22 -0
- package/dist/src/doctor-capabilities.js +29 -0
- package/dist/src/doctor-capabilities.js.map +1 -0
- package/dist/src/executors.js.map +1 -1
- package/dist/src/openclaw-plugin-helpers.d.ts +49 -0
- package/dist/src/openclaw-plugin-helpers.js +246 -0
- package/dist/src/openclaw-plugin-helpers.js.map +1 -0
- package/dist/src/openclaw-plugin.js +94 -271
- package/dist/src/openclaw-plugin.js.map +1 -1
- package/dist/src/runtime-log.js +54 -2
- package/dist/src/runtime-log.js.map +1 -1
- package/dist/src/store.js +432 -12
- package/dist/src/store.js.map +1 -1
- package/dist/src/terminal-agent-adapter.d.ts +3 -0
- package/dist/src/terminal-agent-adapter.js.map +1 -1
- package/dist/src/terminal-agent-bridge.d.ts +51 -3
- package/dist/src/terminal-agent-bridge.js +276 -39
- package/dist/src/terminal-agent-bridge.js.map +1 -1
- package/dist/src/terminal-agent-registry.js.map +1 -1
- package/dist/src/terminal-control-provider.d.ts +3 -1
- package/dist/src/terminal-control-provider.js +41 -37
- package/dist/src/terminal-control-provider.js.map +1 -1
- package/dist/src/terminal-process-source.d.ts +12 -3
- package/dist/src/terminal-process-source.js +37 -6
- package/dist/src/terminal-process-source.js.map +1 -1
- package/dist/src/transcript.js.map +1 -1
- package/openclaw.plugin.json +4 -4
- package/package.json +11 -6
- 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[]
|
|
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
|
|
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:
|
|
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.
|
|
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
|
|
59
|
-
|
|
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:
|
|
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
|
|
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.
|
|
111
|
-
|
|
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
|
-
|
|
114
|
-
|
|
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
|
|
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.
|
|
164
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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.
|
|
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:
|
|
289
|
-
?
|
|
480
|
+
key: recapturedInspection.approval.action.keys.length === 1
|
|
481
|
+
? recapturedInspection.approval.action.keys[0]
|
|
290
482
|
: undefined,
|
|
291
|
-
keys:
|
|
292
|
-
label:
|
|
293
|
-
promptKind:
|
|
294
|
-
command:
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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,
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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",
|