@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.
- package/CHANGELOG.md +39 -0
- package/README.md +93 -55
- package/dist/src/approval-policy.d.ts +3 -1
- package/dist/src/approval-policy.js +18 -4
- package/dist/src/approval-policy.js.map +1 -1
- package/dist/src/claude-hook-installer.d.ts +65 -0
- package/dist/src/claude-hook-installer.js +410 -0
- package/dist/src/claude-hook-installer.js.map +1 -0
- package/dist/src/claude-hook-protocol.d.ts +89 -0
- package/dist/src/claude-hook-protocol.js +243 -0
- package/dist/src/claude-hook-protocol.js.map +1 -0
- package/dist/src/claude-hook-store.d.ts +247 -0
- package/dist/src/claude-hook-store.js +1075 -0
- package/dist/src/claude-hook-store.js.map +1 -0
- package/dist/src/claude-terminal-agent-adapter.d.ts +47 -0
- package/dist/src/claude-terminal-agent-adapter.js +935 -0
- package/dist/src/claude-terminal-agent-adapter.js.map +1 -0
- package/dist/src/cli.js +2065 -316
- package/dist/src/cli.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 +98 -275
- 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 +37 -0
- package/dist/src/terminal-agent-adapter.js.map +1 -1
- package/dist/src/terminal-agent-bridge.d.ts +51 -4
- package/dist/src/terminal-agent-bridge.js +326 -43
- package/dist/src/terminal-agent-bridge.js.map +1 -1
- package/dist/src/terminal-agent-registry.js +3 -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/openclaw.plugin.json +5 -5
- package/package.json +10 -5
- 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[]
|
|
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
|
|
74
|
-
|
|
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.
|
|
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
|
|
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 = {}) {
|
|
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
|
|
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.
|
|
110
|
-
|
|
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
|
-
|
|
113
|
-
|
|
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 (
|
|
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.
|
|
125
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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:
|
|
186
|
-
?
|
|
425
|
+
key: recapturedInspection.approval.action.keys.length === 1
|
|
426
|
+
? recapturedInspection.approval.action.keys[0]
|
|
187
427
|
: undefined,
|
|
188
|
-
keys:
|
|
189
|
-
label:
|
|
190
|
-
promptKind:
|
|
191
|
-
command:
|
|
192
|
-
|
|
193
|
-
|
|
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,
|
|
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 &&
|
|
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
|
|
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:
|
|
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
|
-
|
|
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) {
|