pi-codex-auto-review 0.1.3 → 0.2.0
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/NOTICE +4 -2
- package/README.ko.md +85 -0
- package/README.md +51 -52
- package/dist/approval-commands.d.ts +3 -3
- package/dist/approval-commands.js +161 -34
- package/dist/approval-commands.js.map +1 -1
- package/dist/approval-settings.d.ts +1 -1
- package/dist/approval-settings.js +17 -10
- package/dist/approval-settings.js.map +1 -1
- package/dist/approvals.d.ts +10 -9
- package/dist/approvals.js +196 -75
- package/dist/approvals.js.map +1 -1
- package/dist/audit.d.ts +1 -1
- package/dist/audit.js +37 -13
- package/dist/audit.js.map +1 -1
- package/dist/cli.js +63 -34
- package/dist/cli.js.map +1 -1
- package/dist/context-files.d.ts +2 -2
- package/dist/context-files.js +40 -19
- package/dist/context-files.js.map +1 -1
- package/dist/contracts.d.ts +39 -39
- package/dist/contracts.js +117 -51
- package/dist/contracts.js.map +1 -1
- package/dist/index.d.ts +9 -9
- package/dist/index.js +147 -43
- package/dist/index.js.map +1 -1
- package/dist/pi-host.js +3 -3
- package/dist/pi-host.js.map +1 -1
- package/dist/policy/domains.js +17 -7
- package/dist/policy/domains.js.map +1 -1
- package/dist/policy/index.d.ts +7 -7
- package/dist/policy/index.js +382 -139
- package/dist/policy/index.js.map +1 -1
- package/dist/policy/paths.js +31 -18
- package/dist/policy/paths.js.map +1 -1
- package/dist/policy/rule-engine.d.ts +2 -0
- package/dist/policy/rule-engine.js +283 -0
- package/dist/policy/rule-engine.js.map +1 -0
- package/dist/policy/rule-types.d.ts +30 -0
- package/dist/policy/rule-types.js +2 -0
- package/dist/policy/rule-types.js.map +1 -0
- package/dist/policy/rules-sync.d.ts +1 -0
- package/dist/policy/rules-sync.js +14 -0
- package/dist/policy/rules-sync.js.map +1 -0
- package/dist/policy/rules-worker.d.ts +1 -0
- package/dist/policy/rules-worker.js +14 -0
- package/dist/policy/rules-worker.js.map +1 -0
- package/dist/policy/rules.d.ts +4 -31
- package/dist/policy/rules.js +126 -38
- package/dist/policy/rules.js.map +1 -1
- package/dist/policy/shell.js +70 -41
- package/dist/policy/shell.js.map +1 -1
- package/dist/policy/starlark/builtins.d.ts +6 -0
- package/dist/policy/starlark/builtins.js +764 -0
- package/dist/policy/starlark/builtins.js.map +1 -0
- package/dist/policy/starlark/evaluator.d.ts +18 -0
- package/dist/policy/starlark/evaluator.js +518 -0
- package/dist/policy/starlark/evaluator.js.map +1 -0
- package/dist/policy/starlark/lexer.d.ts +14 -0
- package/dist/policy/starlark/lexer.js +189 -0
- package/dist/policy/starlark/lexer.js.map +1 -0
- package/dist/policy/starlark/parser.d.ts +140 -0
- package/dist/policy/starlark/parser.js +508 -0
- package/dist/policy/starlark/parser.js.map +1 -0
- package/dist/policy/starlark/validation.d.ts +4 -0
- package/dist/policy/starlark/validation.js +183 -0
- package/dist/policy/starlark/validation.js.map +1 -0
- package/dist/policy/starlark/values.d.ts +46 -0
- package/dist/policy/starlark/values.js +363 -0
- package/dist/policy/starlark/values.js.map +1 -0
- package/dist/reports.d.ts +1 -1
- package/dist/reports.js +66 -30
- package/dist/reports.js.map +1 -1
- package/dist/review/context.d.ts +16 -3
- package/dist/review/context.js +254 -48
- package/dist/review/context.js.map +1 -1
- package/dist/review/investigation.d.ts +2 -2
- package/dist/review/investigation.js +57 -16
- package/dist/review/investigation.js.map +1 -1
- package/dist/review/lifecycle.d.ts +1 -1
- package/dist/review/lifecycle.js +72 -29
- package/dist/review/lifecycle.js.map +1 -1
- package/dist/review/policy.js +27 -5
- package/dist/review/policy.js.map +1 -1
- package/dist/review/upstream-policy.js +1 -1
- package/dist/review/upstream-policy.js.map +1 -1
- package/dist/review/user-input.d.ts +5 -0
- package/dist/review/user-input.js +54 -0
- package/dist/review/user-input.js.map +1 -0
- package/dist/reviewer.d.ts +16 -6
- package/dist/reviewer.js +258 -70
- package/dist/reviewer.js.map +1 -1
- package/dist/signals.js +24 -9
- package/dist/signals.js.map +1 -1
- package/dist/startup.d.ts +4 -4
- package/dist/startup.js +139 -41
- package/dist/startup.js.map +1 -1
- package/dist/tools/controller.d.ts +19 -13
- package/dist/tools/controller.js +460 -118
- package/dist/tools/controller.js.map +1 -1
- package/dist/tools/environment.d.ts +1 -0
- package/dist/tools/environment.js +5 -0
- package/dist/tools/environment.js.map +1 -1
- package/dist/tools/executor.d.ts +1 -1
- package/dist/tools/executor.js +45 -28
- package/dist/tools/executor.js.map +1 -1
- package/dist/tools/external.d.ts +9 -8
- package/dist/tools/external.js +22 -11
- package/dist/tools/external.js.map +1 -1
- package/dist/tools/mcp.d.ts +6 -6
- package/dist/tools/mcp.js +208 -79
- package/dist/tools/mcp.js.map +1 -1
- package/dist/tools/mutation-queue.js.map +1 -1
- package/docs/publishing.ko.md +114 -0
- package/docs/publishing.md +49 -47
- package/docs/security/dependencies.ko.md +54 -0
- package/docs/security/dependencies.md +27 -26
- package/docs/testing/auto-review-protection.ko.md +158 -0
- package/docs/testing/auto-review-protection.md +105 -97
- package/docs/usage.ko.md +242 -0
- package/docs/usage.md +127 -111
- package/package.json +106 -95
- package/dist/native/darwin-arm64/artifact.json +0 -5
- package/dist/native/darwin-arm64/pi-guard-execpolicy +0 -0
- package/dist/native/linux-x64/artifact.json +0 -5
- package/dist/native/linux-x64/pi-guard-execpolicy +0 -0
- package/native/execpolicy/LICENSE +0 -201
- package/native/execpolicy/NOTICE +0 -6
package/dist/tools/controller.js
CHANGED
|
@@ -1,33 +1,37 @@
|
|
|
1
|
-
import { randomUUID } from
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import { externalInvocations, externalPolicy } from
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
import { createBashToolDefinition, } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { redact } from "../audit.js";
|
|
5
|
+
import { canonicalJson, createAction, digest, GuardError, TOOL_NAMES, } from "../contracts.js";
|
|
6
|
+
import { canonicalPath, PolicyEngine, resolveToolPath, } from "../policy/index.js";
|
|
7
|
+
import { ReviewContextStore, safeEvidence } from "../review/context.js";
|
|
8
|
+
import { LocalInvestigation } from "../review/investigation.js";
|
|
9
|
+
import { reviewFeedback } from "../review/lifecycle.js";
|
|
10
|
+
import { observeToolUserInput } from "../review/user-input.js";
|
|
11
|
+
import { PiReviewProvider, } from "../reviewer.js";
|
|
12
|
+
import { redactedEnvironmentNames, reviewEnvironment } from "./environment.js";
|
|
13
|
+
import { externalInvocations, externalPolicy, } from "./external.js";
|
|
14
|
+
import { withApprovalMutationQueue } from "./mutation-queue.js";
|
|
14
15
|
export class GuardController {
|
|
15
16
|
options;
|
|
16
17
|
currentPolicy;
|
|
17
|
-
get policy() {
|
|
18
|
+
get policy() {
|
|
19
|
+
return this.currentPolicy;
|
|
20
|
+
}
|
|
18
21
|
approvals;
|
|
19
22
|
sources = new Map();
|
|
20
23
|
ownedOperations = new WeakSet();
|
|
21
24
|
stopped = new AbortController();
|
|
22
|
-
sessionId =
|
|
25
|
+
sessionId = "not-started";
|
|
23
26
|
isReady = false;
|
|
24
|
-
reviewContext
|
|
27
|
+
reviewContext;
|
|
25
28
|
activeReviews = new Set();
|
|
26
29
|
settingsUpdates = Promise.resolve();
|
|
27
30
|
retryArguments = new Map();
|
|
28
31
|
externalTools = new Map();
|
|
29
32
|
constructor(options) {
|
|
30
33
|
this.options = options;
|
|
34
|
+
this.reviewContext = new ReviewContextStore(options.persistContext);
|
|
31
35
|
this.currentPolicy = new PolicyEngine(options.settings, options.profile);
|
|
32
36
|
this.approvals = options.approvals;
|
|
33
37
|
}
|
|
@@ -46,26 +50,32 @@ export class GuardController {
|
|
|
46
50
|
await policy.initialize(cwd);
|
|
47
51
|
this.assertReady();
|
|
48
52
|
await persist(settings);
|
|
49
|
-
this.stopped.abort(new GuardError(
|
|
53
|
+
this.stopped.abort(new GuardError("SETTINGS_CHANGED", "Approval settings changed"));
|
|
50
54
|
this.stopped = new AbortController();
|
|
51
55
|
this.approvals.invalidate();
|
|
52
56
|
this.activeReviews.clear();
|
|
53
57
|
this.retryArguments.clear();
|
|
54
58
|
this.options.settings = settings;
|
|
55
59
|
Object.assign(this.approvals.options, {
|
|
56
|
-
approvalsReviewer: settings.approvalsReviewer,
|
|
57
|
-
|
|
60
|
+
approvalsReviewer: settings.approvalsReviewer,
|
|
61
|
+
approvalPolicy: settings.approvalPolicy,
|
|
62
|
+
reviewTimeoutMs: settings.reviewTimeoutMs,
|
|
63
|
+
approvalTimeoutMs: settings.approvalTimeoutMs,
|
|
58
64
|
});
|
|
59
65
|
this.currentPolicy = policy;
|
|
60
66
|
});
|
|
61
67
|
this.settingsUpdates = task.catch(() => { });
|
|
62
68
|
await task;
|
|
63
69
|
}
|
|
64
|
-
assertReady() {
|
|
65
|
-
|
|
66
|
-
|
|
70
|
+
assertReady() {
|
|
71
|
+
if (!this.isReady || this.stopped.signal.aborted)
|
|
72
|
+
throw new GuardError("GUARD_NOT_READY", "Guard is not ready for execution");
|
|
73
|
+
}
|
|
74
|
+
isBoundToSession(sessionId) {
|
|
75
|
+
return this.sessionId === sessionId && !this.stopped.signal.aborted;
|
|
76
|
+
}
|
|
67
77
|
reset(sessionId, manager) {
|
|
68
|
-
this.stopped.abort(new GuardError(
|
|
78
|
+
this.stopped.abort(new GuardError("SESSION_CHANGED", "Session changed"));
|
|
69
79
|
this.stopped = new AbortController();
|
|
70
80
|
this.sessionId = sessionId;
|
|
71
81
|
this.reviewContext.reset(sessionId, manager);
|
|
@@ -74,25 +84,60 @@ export class GuardController {
|
|
|
74
84
|
this.activeReviews.clear();
|
|
75
85
|
this.retryArguments.clear();
|
|
76
86
|
}
|
|
77
|
-
startTurn() {
|
|
87
|
+
startTurn(context) {
|
|
88
|
+
this.reviewContext.startTurn();
|
|
89
|
+
this.reviewContext.finalizeInstructions();
|
|
90
|
+
if (context?.getSystemPrompt)
|
|
91
|
+
this.reviewContext.mainPrompt(context.getSystemPrompt());
|
|
92
|
+
this.approvals.lifecycle.startTurn();
|
|
93
|
+
this.activeReviews.clear();
|
|
94
|
+
}
|
|
78
95
|
async authorizeRetry(id, context) {
|
|
79
96
|
this.assertReady();
|
|
80
|
-
const denial = this.approvals.lifecycle.authorizeRetry(id, {
|
|
97
|
+
const denial = this.approvals.lifecycle.authorizeRetry(id, {
|
|
98
|
+
sessionId: context.sessionManager.getSessionId(),
|
|
99
|
+
contextId: this.reviewContext.identity,
|
|
100
|
+
cwd: await canonicalPath(context.cwd, context.cwd),
|
|
101
|
+
policyRevision: this.policy.revision,
|
|
102
|
+
permissionDigest: digest(this.policy.profile),
|
|
103
|
+
});
|
|
81
104
|
const args = this.retryArguments.get(id) ?? denial.action.args;
|
|
82
105
|
this.retryArguments.delete(id);
|
|
83
106
|
return { denial, args };
|
|
84
107
|
}
|
|
85
108
|
async retryUserBash(action, context) {
|
|
86
109
|
const next = createAction({ ...action, toolCallId: randomUUID() }, this.policy.profile);
|
|
87
|
-
const command = String(next.args.command), shellPath = String(next.args.shellPath ??
|
|
88
|
-
await this.admitAndExecute(next, { kind:
|
|
110
|
+
const command = String(next.args.command), shellPath = String(next.args.shellPath ?? "/bin/bash");
|
|
111
|
+
await this.admitAndExecute(next, { kind: "shell", command, shellPath, cwd: next.cwd }, context, {
|
|
112
|
+
timeoutSeconds: typeof next.args.timeout === "number"
|
|
113
|
+
? next.args.timeout
|
|
114
|
+
: undefined,
|
|
115
|
+
env: next.args.environment,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
authorizeUser(text) {
|
|
119
|
+
this.reviewContext.authorize(text);
|
|
120
|
+
this.approvals.lifecycle.invalidateRetries();
|
|
89
121
|
}
|
|
90
|
-
authorizeUser(text) { this.reviewContext.authorize(text); this.approvals.lifecycle.invalidateRetries(); }
|
|
91
122
|
noteCall(event) {
|
|
92
|
-
this.sources.set(event.toolCallId, event.parentToolCallId ?
|
|
93
|
-
this.reviewContext.
|
|
123
|
+
this.sources.set(event.toolCallId, event.parentToolCallId ? "nested" : "model");
|
|
124
|
+
const callIdentity = this.reviewContext.callIdentity(event.toolCallId);
|
|
125
|
+
this.reviewContext.toolCall({
|
|
126
|
+
tool: event.toolName,
|
|
127
|
+
toolCallId: event.toolCallId,
|
|
128
|
+
callIdentity,
|
|
129
|
+
args: JSON.parse(canonicalJson(event.input)),
|
|
130
|
+
...(event.parentToolCallId
|
|
131
|
+
? {
|
|
132
|
+
parentToolCallId: event.parentToolCallId,
|
|
133
|
+
parentCallIdentity: this.reviewContext.callIdentity(event.parentToolCallId),
|
|
134
|
+
}
|
|
135
|
+
: {}),
|
|
136
|
+
}, callIdentity);
|
|
137
|
+
}
|
|
138
|
+
completeCall(toolCallId) {
|
|
139
|
+
this.sources.delete(toolCallId);
|
|
94
140
|
}
|
|
95
|
-
completeCall(toolCallId) { this.sources.delete(toolCallId); }
|
|
96
141
|
async close() {
|
|
97
142
|
this.isReady = false;
|
|
98
143
|
this.stopped.abort();
|
|
@@ -103,45 +148,123 @@ export class GuardController {
|
|
|
103
148
|
await this.settingsUpdates;
|
|
104
149
|
}
|
|
105
150
|
ui(context, isOnceOnly = false) {
|
|
106
|
-
if (!context.hasUI ||
|
|
151
|
+
if (!context.hasUI ||
|
|
152
|
+
(context.mode !== "tui" && context.mode !== "rpc"))
|
|
107
153
|
return undefined;
|
|
108
|
-
const scopes = {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
154
|
+
const scopes = {
|
|
155
|
+
"Allow once": "once",
|
|
156
|
+
"Allow for this session": "session",
|
|
157
|
+
"Save as an allow rule": "persistent",
|
|
158
|
+
Deny: "deny",
|
|
159
|
+
};
|
|
160
|
+
return {
|
|
161
|
+
select: async (action, delta, options) => {
|
|
162
|
+
const text = `Tool: ${action.tool}\nWorking directory: ${action.cwd}\nInput: ${canonicalJson(action.args)}\nRequested scope: ${canonicalJson(delta)}`;
|
|
163
|
+
const selected = await context.ui.select(`Approve this action?\n${text}`, isOnceOnly ? ["Allow once", "Deny"] : Object.keys(scopes), { signal: options.signal, timeout: options.timeoutMs });
|
|
164
|
+
if (selected && selected in scopes)
|
|
165
|
+
this.reviewContext.confirm({
|
|
166
|
+
actionDigest: action.digest,
|
|
167
|
+
action: JSON.parse(canonicalJson(action)),
|
|
168
|
+
requestedScope: JSON.parse(canonicalJson(delta)),
|
|
169
|
+
choice: selected,
|
|
170
|
+
scope: scopes[selected],
|
|
171
|
+
});
|
|
172
|
+
return selected
|
|
173
|
+
? scopes[selected]
|
|
174
|
+
: undefined;
|
|
175
|
+
},
|
|
176
|
+
};
|
|
116
177
|
}
|
|
117
|
-
async requestAdmission(action, policyDecision, context, signal, trustedAuthorization =
|
|
118
|
-
|
|
178
|
+
async requestAdmission(action, policyDecision, context, signal, trustedAuthorization = "") {
|
|
179
|
+
this.reviewContext.refreshInstructions();
|
|
180
|
+
if (context.getSystemPrompt)
|
|
181
|
+
this.reviewContext.mainPrompt(context.getSystemPrompt());
|
|
182
|
+
this.reviewContext.preparedAction(action);
|
|
183
|
+
const executionContext = {
|
|
184
|
+
environmentId: "local",
|
|
185
|
+
platform: process.platform,
|
|
186
|
+
architecture: process.arch,
|
|
187
|
+
osIsolation: false,
|
|
188
|
+
permissionProfile: this.policy.profile,
|
|
189
|
+
approvalPolicy: this.options.settings.approvalPolicy,
|
|
190
|
+
approvalsReviewer: this.options.settings.approvalsReviewer,
|
|
191
|
+
};
|
|
192
|
+
let reviewContext = policyDecision.kind === "ask"
|
|
193
|
+
? this.reviewContext.snapshot(this.options.settings.reviewContextChars, canonicalJson({
|
|
194
|
+
action,
|
|
195
|
+
delta: policyDecision.delta,
|
|
196
|
+
executionContext,
|
|
197
|
+
policyReason: policyDecision.reason,
|
|
198
|
+
}).length + 1000)
|
|
199
|
+
: undefined;
|
|
119
200
|
if (reviewContext && trustedAuthorization) {
|
|
120
|
-
const
|
|
201
|
+
const { digest: previousDigest, ...fields } = reviewContext;
|
|
202
|
+
const data = {
|
|
203
|
+
...fields,
|
|
204
|
+
items: [
|
|
205
|
+
...reviewContext.items,
|
|
206
|
+
{
|
|
207
|
+
id: "direct-user-bash",
|
|
208
|
+
source: "user",
|
|
209
|
+
trust: "authorization",
|
|
210
|
+
content: safeEvidence(trustedAuthorization),
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
};
|
|
121
214
|
reviewContext = { ...data, digest: digest(data) };
|
|
122
215
|
}
|
|
123
216
|
const turnIdentity = this.reviewContext.turnIdentity;
|
|
124
217
|
return this.approvals.admit(action, policyDecision, {
|
|
125
|
-
provider: this.options.provider ??
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
218
|
+
provider: this.options.provider ??
|
|
219
|
+
new PiReviewProvider(context, this.options.settings, new LocalInvestigation(this.options.executor, this.policy.profile, action.cwd)),
|
|
220
|
+
ui: this.ui(context, policyDecision.requiresFreshReview ||
|
|
221
|
+
policyDecision.requiresUserInput),
|
|
222
|
+
trustedAuthorization,
|
|
223
|
+
signal,
|
|
224
|
+
reviewContext,
|
|
225
|
+
settings: this.options.settings,
|
|
226
|
+
executionContext,
|
|
227
|
+
onReviewStart: () => {
|
|
228
|
+
this.activeReviews.add(action.digest);
|
|
229
|
+
if (context.hasUI)
|
|
230
|
+
context.ui.setStatus("auto-review", `Auto-review running (${this.activeReviews.size})`);
|
|
231
|
+
},
|
|
232
|
+
onReviewResult: (result) => {
|
|
129
233
|
this.activeReviews.delete(action.digest);
|
|
130
|
-
if (turnIdentity !== this.reviewContext.turnIdentity
|
|
234
|
+
if (turnIdentity !== this.reviewContext.turnIdentity)
|
|
131
235
|
return;
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (
|
|
135
|
-
|
|
236
|
+
if (result)
|
|
237
|
+
this.reviewContext.assessment(action, result);
|
|
238
|
+
if (!context.hasUI)
|
|
239
|
+
return;
|
|
240
|
+
const labels = {
|
|
241
|
+
approved: "approved",
|
|
242
|
+
denied: "denied",
|
|
243
|
+
"timed-out": "timed out",
|
|
244
|
+
aborted: "cancelled",
|
|
245
|
+
failed: "failed",
|
|
246
|
+
};
|
|
247
|
+
context.ui.setStatus("auto-review", this.activeReviews.size
|
|
248
|
+
? `Auto-review running (${this.activeReviews.size})`
|
|
249
|
+
: result
|
|
250
|
+
? `Auto-review: ${labels[result.status]}`
|
|
251
|
+
: undefined);
|
|
252
|
+
if (result &&
|
|
253
|
+
result.status !== "approved" &&
|
|
254
|
+
result.status !== "aborted")
|
|
255
|
+
context.ui.notify(`Auto-review ${labels[result.status]}: ${redact("assessment" in result ? result.assessment.rationale : result.reason).slice(0, 1000)}`, "warning");
|
|
256
|
+
},
|
|
257
|
+
onInterrupt: () => {
|
|
258
|
+
if (context.hasUI)
|
|
259
|
+
context.ui.notify("Auto-review stopped this turn after repeated denials.", "warning");
|
|
260
|
+
context.abort?.();
|
|
136
261
|
},
|
|
137
|
-
onInterrupt: () => { if (context.hasUI)
|
|
138
|
-
context.ui.notify('자동 검토의 반복 거부 한도에 도달해 현재 작업을 중단합니다.', 'warning'); context.abort?.(); },
|
|
139
262
|
});
|
|
140
263
|
}
|
|
141
264
|
denied(admission, retryArguments) {
|
|
142
265
|
if (admission.denialId && retryArguments)
|
|
143
266
|
this.retryArguments.set(admission.denialId, retryArguments);
|
|
144
|
-
const retained = new Set(this.approvals.lifecycle.recentDenials.map(item => item.id));
|
|
267
|
+
const retained = new Set(this.approvals.lifecycle.recentDenials.map((item) => item.id));
|
|
145
268
|
for (const id of this.retryArguments.keys())
|
|
146
269
|
if (!retained.has(id))
|
|
147
270
|
this.retryArguments.delete(id);
|
|
@@ -149,32 +272,78 @@ export class GuardController {
|
|
|
149
272
|
const feedback = reviewFeedback(admission.review);
|
|
150
273
|
return new GuardError(feedback.code, `[${feedback.code}] ${feedback.message}`);
|
|
151
274
|
}
|
|
152
|
-
return new GuardError(
|
|
275
|
+
return new GuardError("PERMISSION_DENIED", admission.reason);
|
|
276
|
+
}
|
|
277
|
+
isExternalTool(name) {
|
|
278
|
+
return this.externalTools.get(name)?.isEnabled === true;
|
|
153
279
|
}
|
|
154
|
-
isExternalTool(name) { return this.externalTools.get(name)?.isEnabled === true; }
|
|
155
280
|
wrapExternalTool(definition, identify) {
|
|
156
|
-
if (TOOL_NAMES.includes(definition.name) ||
|
|
157
|
-
|
|
281
|
+
if (TOOL_NAMES.includes(definition.name) ||
|
|
282
|
+
definition.name === "codemode")
|
|
283
|
+
throw new GuardError("RESERVED_TOOL", "An external adapter cannot replace guarded local tools");
|
|
158
284
|
const token = randomUUID(), execute = definition.execute;
|
|
159
|
-
this.externalTools.set(definition.name, {
|
|
160
|
-
|
|
161
|
-
|
|
285
|
+
this.externalTools.set(definition.name, {
|
|
286
|
+
token,
|
|
287
|
+
isEnabled: definition.exposure !== "hidden",
|
|
288
|
+
});
|
|
289
|
+
const schema = JSON.parse(JSON.stringify({
|
|
290
|
+
parameters: definition.parameters,
|
|
291
|
+
description: definition.description,
|
|
292
|
+
annotations: definition.annotations ?? {},
|
|
293
|
+
...(definition.namespace
|
|
294
|
+
? { namespace: definition.namespace }
|
|
295
|
+
: {}),
|
|
296
|
+
}));
|
|
297
|
+
return {
|
|
298
|
+
...definition,
|
|
299
|
+
execute: async (toolCallId, params, callerSignal, onUpdate, context) => {
|
|
162
300
|
this.assertReady();
|
|
163
301
|
const lifetime = new AbortController();
|
|
164
|
-
const signal = AbortSignal.any([
|
|
302
|
+
const signal = AbortSignal.any([
|
|
303
|
+
lifetime.signal,
|
|
304
|
+
this.stopped.signal,
|
|
305
|
+
this.approvals.lifecycle.signal,
|
|
306
|
+
...(callerSignal ? [callerSignal] : []),
|
|
307
|
+
]);
|
|
165
308
|
signal.throwIfAborted();
|
|
166
309
|
const args = JSON.parse(canonicalJson(params ?? {}));
|
|
167
310
|
const identity = JSON.parse(JSON.stringify(identify()));
|
|
168
311
|
const identityDigest = digest(identity), cwd = await canonicalPath(context.cwd, context.cwd);
|
|
169
|
-
const action = createAction({
|
|
312
|
+
const action = createAction({
|
|
313
|
+
toolCallId,
|
|
314
|
+
tool: definition.name,
|
|
315
|
+
args: {
|
|
316
|
+
arguments: args,
|
|
317
|
+
externalTool: JSON.parse(JSON.stringify(identity)),
|
|
318
|
+
schema,
|
|
319
|
+
...(identity.connectedAccountEmail
|
|
320
|
+
? {
|
|
321
|
+
connected_account_email: identity.connectedAccountEmail,
|
|
322
|
+
}
|
|
323
|
+
: {}),
|
|
324
|
+
},
|
|
325
|
+
cwd,
|
|
326
|
+
source: this.sources.get(toolCallId) ?? "model",
|
|
327
|
+
sessionId: context.sessionManager.getSessionId(),
|
|
328
|
+
policyRevision: this.policy.revision,
|
|
329
|
+
}, this.policy.profile);
|
|
170
330
|
const authorizationVersion = this.reviewContext.scopeVersion, turn = this.reviewContext.turnIdentity;
|
|
171
331
|
const checkCurrent = async () => {
|
|
172
332
|
this.assertReady();
|
|
173
333
|
signal.throwIfAborted();
|
|
174
|
-
if (this.externalTools.get(definition.name)?.token !==
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
334
|
+
if (this.externalTools.get(definition.name)?.token !==
|
|
335
|
+
token ||
|
|
336
|
+
!this.isExternalTool(definition.name) ||
|
|
337
|
+
digest(JSON.parse(JSON.stringify(identify()))) !==
|
|
338
|
+
identityDigest)
|
|
339
|
+
throw new GuardError("STALE_EXTERNAL_TOOL", "External registration changed; a fresh action must be reviewed");
|
|
340
|
+
if (this.reviewContext.scopeVersion !==
|
|
341
|
+
authorizationVersion ||
|
|
342
|
+
this.reviewContext.turnIdentity !== turn ||
|
|
343
|
+
this.sessionId !== action.sessionId ||
|
|
344
|
+
this.policy.revision !== action.policyRevision ||
|
|
345
|
+
(await canonicalPath(context.cwd, context.cwd)) !== cwd)
|
|
346
|
+
throw new GuardError("STALE_APPROVAL", "External execution context changed after admission");
|
|
178
347
|
signal.throwIfAborted();
|
|
179
348
|
};
|
|
180
349
|
const approve = async (candidate, candidateIdentity) => {
|
|
@@ -186,10 +355,36 @@ export class GuardController {
|
|
|
186
355
|
};
|
|
187
356
|
try {
|
|
188
357
|
await approve(action, identity);
|
|
189
|
-
const result = await externalInvocations.run({
|
|
190
|
-
|
|
358
|
+
const result = await externalInvocations.run({
|
|
359
|
+
toolCallId,
|
|
360
|
+
identity,
|
|
361
|
+
arguments: args,
|
|
362
|
+
signal,
|
|
363
|
+
checkCurrent,
|
|
364
|
+
approveNested: async (nestedIdentity, nestedArgs) => {
|
|
365
|
+
if (nestedIdentity.server !== identity.server ||
|
|
366
|
+
nestedIdentity.registration !==
|
|
367
|
+
identity.registration)
|
|
191
368
|
return false;
|
|
192
|
-
const nested = createAction({
|
|
369
|
+
const nested = createAction({
|
|
370
|
+
toolCallId: `${toolCallId}:elicitation:${randomUUID()}`,
|
|
371
|
+
tool: definition.name,
|
|
372
|
+
args: {
|
|
373
|
+
arguments: nestedArgs,
|
|
374
|
+
externalTool: JSON.parse(JSON.stringify(nestedIdentity)),
|
|
375
|
+
schema,
|
|
376
|
+
originatingActionDigest: action.digest,
|
|
377
|
+
...(nestedIdentity.connectedAccountEmail
|
|
378
|
+
? {
|
|
379
|
+
connected_account_email: nestedIdentity.connectedAccountEmail,
|
|
380
|
+
}
|
|
381
|
+
: {}),
|
|
382
|
+
},
|
|
383
|
+
cwd,
|
|
384
|
+
source: "nested",
|
|
385
|
+
sessionId: action.sessionId,
|
|
386
|
+
policyRevision: action.policyRevision,
|
|
387
|
+
}, this.policy.profile);
|
|
193
388
|
try {
|
|
194
389
|
await approve(nested, nestedIdentity);
|
|
195
390
|
return true;
|
|
@@ -197,23 +392,31 @@ export class GuardController {
|
|
|
197
392
|
catch {
|
|
198
393
|
return false;
|
|
199
394
|
}
|
|
200
|
-
}
|
|
201
|
-
|
|
395
|
+
},
|
|
396
|
+
}, () => execute.call(definition, toolCallId, args, signal, onUpdate, observeToolUserInput(context, action, this.reviewContext, signal)));
|
|
397
|
+
await this.options.audit.record(action, "execution", "settled");
|
|
202
398
|
return result;
|
|
203
399
|
}
|
|
204
400
|
catch (error) {
|
|
205
|
-
await this.options.audit.record(action,
|
|
401
|
+
await this.options.audit.record(action, "execution", "failed");
|
|
206
402
|
throw error;
|
|
207
403
|
}
|
|
208
404
|
finally {
|
|
209
405
|
lifetime.abort();
|
|
210
406
|
this.completeCall(toolCallId);
|
|
211
407
|
}
|
|
212
|
-
}
|
|
408
|
+
},
|
|
409
|
+
};
|
|
213
410
|
}
|
|
214
|
-
async admitAndExecute(action, job, context, options, trustedAuthorization =
|
|
411
|
+
async admitAndExecute(action, job, context, options, trustedAuthorization = "", retryArguments) {
|
|
215
412
|
this.assertReady();
|
|
216
|
-
const signal = AbortSignal.any([
|
|
413
|
+
const signal = AbortSignal.any([
|
|
414
|
+
this.stopped.signal,
|
|
415
|
+
...(action.source === "user-bash"
|
|
416
|
+
? []
|
|
417
|
+
: [this.approvals.lifecycle.signal]),
|
|
418
|
+
...(options.signal ? [options.signal] : []),
|
|
419
|
+
]);
|
|
217
420
|
const authorizationVersion = this.reviewContext.scopeVersion;
|
|
218
421
|
const policyDecision = await this.policy.evaluate(action, signal);
|
|
219
422
|
const admission = await this.requestAdmission(action, policyDecision, context, signal, trustedAuthorization);
|
|
@@ -221,25 +424,32 @@ export class GuardController {
|
|
|
221
424
|
throw this.denied(admission, retryArguments);
|
|
222
425
|
signal.throwIfAborted();
|
|
223
426
|
if (this.reviewContext.scopeVersion !== authorizationVersion)
|
|
224
|
-
throw new GuardError(
|
|
225
|
-
if (this.sessionId !== action.sessionId ||
|
|
226
|
-
|
|
427
|
+
throw new GuardError("STALE_AUTHORIZATION", "User authorization changed during review; request a fresh review");
|
|
428
|
+
if (this.sessionId !== action.sessionId ||
|
|
429
|
+
(await canonicalPath(context.cwd, context.cwd)) !== action.cwd ||
|
|
430
|
+
this.policy.revision !== action.policyRevision)
|
|
431
|
+
throw new GuardError("STALE_APPROVAL", "Execution context changed after admission");
|
|
227
432
|
const finalDecision = await this.policy.evaluate(action, signal);
|
|
228
|
-
if (finalDecision.kind ===
|
|
229
|
-
throw new GuardError(
|
|
433
|
+
if (finalDecision.kind === "deny")
|
|
434
|
+
throw new GuardError("PERMISSION_DENIED", finalDecision.reason);
|
|
230
435
|
if (digest(finalDecision.delta) !== digest(policyDecision.delta))
|
|
231
|
-
throw new GuardError(
|
|
232
|
-
if (digest(finalDecision.authority ?? null) !==
|
|
233
|
-
|
|
436
|
+
throw new GuardError("STALE_APPROVAL", "Resolved permissions changed after admission");
|
|
437
|
+
if (digest(finalDecision.authority ?? null) !==
|
|
438
|
+
digest(policyDecision.authority ?? null))
|
|
439
|
+
throw new GuardError("STALE_APPROVAL", "Execution authority changed after admission");
|
|
234
440
|
try {
|
|
235
441
|
const result = await this.options.executor.execute(job, this.policy.profile, admission.delta, {
|
|
236
|
-
...options,
|
|
442
|
+
...options,
|
|
443
|
+
signal,
|
|
444
|
+
authority: admission.authority,
|
|
445
|
+
timeoutSeconds: options.timeoutSeconds ??
|
|
446
|
+
this.options.settings.executionTimeoutSeconds,
|
|
237
447
|
});
|
|
238
|
-
await this.options.audit.record(action,
|
|
448
|
+
await this.options.audit.record(action, "execution", "settled");
|
|
239
449
|
return result;
|
|
240
450
|
}
|
|
241
451
|
catch (error) {
|
|
242
|
-
await this.options.audit.record(action,
|
|
452
|
+
await this.options.audit.record(action, "execution", "failed");
|
|
243
453
|
throw error;
|
|
244
454
|
}
|
|
245
455
|
finally {
|
|
@@ -249,54 +459,186 @@ export class GuardController {
|
|
|
249
459
|
wrapTool(definition, toolOptions) {
|
|
250
460
|
const tool = definition.name;
|
|
251
461
|
if (!TOOL_NAMES.includes(tool))
|
|
252
|
-
throw new GuardError(
|
|
253
|
-
const parameters = tool ===
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
462
|
+
throw new GuardError("UNKNOWN_LOCAL_TOOL", "Cannot delegate unknown local tool");
|
|
463
|
+
const parameters = tool === "bash"
|
|
464
|
+
? {
|
|
465
|
+
...definition.parameters,
|
|
466
|
+
properties: {
|
|
467
|
+
...definition.parameters.properties,
|
|
468
|
+
sandbox_permissions: {
|
|
469
|
+
type: "string",
|
|
470
|
+
enum: ["use_default", "require_escalated"],
|
|
471
|
+
description: "Compatibility field: request approval review for this exact command.",
|
|
472
|
+
},
|
|
473
|
+
additional_permissions: {
|
|
474
|
+
type: "object",
|
|
475
|
+
properties: {
|
|
476
|
+
readPaths: {
|
|
477
|
+
type: "array",
|
|
478
|
+
items: { type: "string" },
|
|
479
|
+
},
|
|
480
|
+
writePaths: {
|
|
481
|
+
type: "array",
|
|
482
|
+
items: { type: "string" },
|
|
483
|
+
},
|
|
484
|
+
domains: {
|
|
485
|
+
type: "array",
|
|
486
|
+
items: { type: "string" },
|
|
487
|
+
},
|
|
488
|
+
},
|
|
489
|
+
additionalProperties: false,
|
|
490
|
+
description: "Describe the exact additional action scope for approval review.",
|
|
491
|
+
},
|
|
492
|
+
justification: {
|
|
493
|
+
type: "string",
|
|
494
|
+
description: "Explain the boundary crossing for review.",
|
|
495
|
+
},
|
|
496
|
+
},
|
|
497
|
+
}
|
|
498
|
+
: definition.parameters;
|
|
499
|
+
return {
|
|
500
|
+
...definition,
|
|
501
|
+
parameters,
|
|
502
|
+
execute: async (toolCallId, params, signal, onUpdate, context) => {
|
|
259
503
|
this.assertReady();
|
|
260
504
|
const cwd = await canonicalPath(context.cwd, context.cwd);
|
|
261
|
-
if (tool ===
|
|
505
|
+
if (tool === "bash") {
|
|
262
506
|
const input = params;
|
|
263
|
-
const requested = JSON.parse(canonicalJson(Object.fromEntries([
|
|
264
|
-
|
|
507
|
+
const requested = JSON.parse(canonicalJson(Object.fromEntries([
|
|
508
|
+
"sandbox_permissions",
|
|
509
|
+
"additional_permissions",
|
|
510
|
+
"justification",
|
|
511
|
+
]
|
|
512
|
+
.filter((key) => input[key] !== undefined)
|
|
513
|
+
.map((key) => [key, input[key]]))));
|
|
514
|
+
const delegate = createBashToolDefinition(cwd, {
|
|
515
|
+
...toolOptions,
|
|
516
|
+
operations: {
|
|
517
|
+
exec: async (command, finalCwd, options) => {
|
|
265
518
|
const env = options.env;
|
|
266
|
-
const shellPath = toolOptions
|
|
267
|
-
|
|
519
|
+
const shellPath = toolOptions
|
|
520
|
+
?.shellPath ?? "/bin/bash";
|
|
521
|
+
const args = {
|
|
522
|
+
command,
|
|
523
|
+
shellPath,
|
|
524
|
+
...(options.timeout !== undefined
|
|
525
|
+
? { timeout: options.timeout }
|
|
526
|
+
: {}),
|
|
527
|
+
environment: reviewEnvironment(env),
|
|
528
|
+
redactedEnvironmentVariables: redactedEnvironmentNames(env),
|
|
529
|
+
...requested,
|
|
530
|
+
};
|
|
268
531
|
const resolvedCwd = await canonicalPath(finalCwd, finalCwd);
|
|
269
|
-
const action = createAction({
|
|
270
|
-
|
|
271
|
-
|
|
532
|
+
const action = createAction({
|
|
533
|
+
toolCallId,
|
|
534
|
+
tool,
|
|
535
|
+
args: JSON.parse(canonicalJson(args)),
|
|
536
|
+
cwd: resolvedCwd,
|
|
537
|
+
source: this.sources.get(toolCallId) ??
|
|
538
|
+
"model",
|
|
539
|
+
sessionId: context.sessionManager.getSessionId(),
|
|
540
|
+
policyRevision: this.policy.revision,
|
|
541
|
+
}, this.policy.profile);
|
|
542
|
+
return (await this.admitAndExecute(action, {
|
|
543
|
+
kind: "shell",
|
|
544
|
+
command,
|
|
545
|
+
shellPath,
|
|
546
|
+
cwd: resolvedCwd,
|
|
547
|
+
}, { ...context, cwd: finalCwd }, {
|
|
548
|
+
signal: options.signal,
|
|
549
|
+
timeoutSeconds: options.timeout,
|
|
550
|
+
env,
|
|
551
|
+
onData: options.onData,
|
|
552
|
+
}, "", JSON.parse(canonicalJson(params))));
|
|
553
|
+
},
|
|
554
|
+
},
|
|
555
|
+
});
|
|
272
556
|
return delegate.execute(toolCallId, params, signal, onUpdate, context);
|
|
273
557
|
}
|
|
274
558
|
const boundParameters = JSON.parse(canonicalJson(params));
|
|
275
559
|
const args = { ...boundParameters };
|
|
276
|
-
const target = await resolveToolPath(typeof args.path ===
|
|
560
|
+
const target = await resolveToolPath(typeof args.path === "string" ? args.path : ".", cwd, tool === "read");
|
|
277
561
|
args.path = pathToFileURL(target).href;
|
|
278
|
-
const action = createAction({
|
|
279
|
-
|
|
562
|
+
const action = createAction({
|
|
563
|
+
toolCallId,
|
|
564
|
+
tool,
|
|
565
|
+
args,
|
|
566
|
+
cwd,
|
|
567
|
+
source: this.sources.get(toolCallId) ?? "model",
|
|
568
|
+
sessionId: context.sessionManager.getSessionId(),
|
|
569
|
+
policyRevision: this.policy.revision,
|
|
570
|
+
}, this.policy.profile);
|
|
571
|
+
const options = tool === "read"
|
|
572
|
+
? {
|
|
573
|
+
...toolOptions,
|
|
574
|
+
...(context.model?.inputLimits?.images?.resize
|
|
575
|
+
? {
|
|
576
|
+
resizeOptions: context.model.inputLimits.images
|
|
577
|
+
.resize,
|
|
578
|
+
}
|
|
579
|
+
: {}),
|
|
580
|
+
}
|
|
581
|
+
: undefined;
|
|
280
582
|
const delegate = definition;
|
|
281
|
-
const execute = () => this.admitAndExecute(action, {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
583
|
+
const execute = () => this.admitAndExecute(action, {
|
|
584
|
+
kind: "tool",
|
|
585
|
+
tool,
|
|
586
|
+
args: action.args,
|
|
587
|
+
cwd,
|
|
588
|
+
toolCallId,
|
|
589
|
+
...(options ? { options } : {}),
|
|
590
|
+
}, context, {
|
|
591
|
+
signal,
|
|
592
|
+
onUpdate: onUpdate
|
|
593
|
+
? (result) => onUpdate(result)
|
|
594
|
+
: undefined,
|
|
595
|
+
delegate: async (admittedSignal) => (await delegate.execute(toolCallId, boundParameters, admittedSignal, onUpdate, context)),
|
|
596
|
+
}, "", JSON.parse(canonicalJson(params)));
|
|
597
|
+
const result = await (tool === "write" || tool === "edit"
|
|
598
|
+
? withApprovalMutationQueue(await canonicalPath(target, cwd), execute)
|
|
599
|
+
: execute());
|
|
286
600
|
return result;
|
|
287
|
-
}
|
|
601
|
+
},
|
|
602
|
+
};
|
|
288
603
|
}
|
|
289
604
|
userBashOperations(context, trustedCommand) {
|
|
290
|
-
const operations = {
|
|
605
|
+
const operations = {
|
|
606
|
+
exec: async (command, cwd, options) => {
|
|
291
607
|
const resolvedCwd = await canonicalPath(cwd, cwd);
|
|
292
608
|
const env = options.env;
|
|
293
|
-
const shellPath = this.options.shellPath ??
|
|
294
|
-
const action = createAction({
|
|
295
|
-
|
|
296
|
-
|
|
609
|
+
const shellPath = this.options.shellPath ?? "/bin/bash";
|
|
610
|
+
const action = createAction({
|
|
611
|
+
toolCallId: randomUUID(),
|
|
612
|
+
tool: "bash",
|
|
613
|
+
source: "user-bash",
|
|
614
|
+
args: JSON.parse(canonicalJson({
|
|
615
|
+
command,
|
|
616
|
+
shellPath,
|
|
617
|
+
environment: reviewEnvironment(env),
|
|
618
|
+
redactedEnvironmentVariables: redactedEnvironmentNames(env),
|
|
619
|
+
...(options.timeout !== undefined
|
|
620
|
+
? { timeout: options.timeout }
|
|
621
|
+
: {}),
|
|
622
|
+
})),
|
|
623
|
+
cwd: resolvedCwd,
|
|
624
|
+
sessionId: context.sessionManager.getSessionId(),
|
|
625
|
+
policyRevision: this.policy.revision,
|
|
626
|
+
}, this.policy.profile);
|
|
627
|
+
return (await this.admitAndExecute(action, { kind: "shell", command, shellPath, cwd: resolvedCwd }, context, {
|
|
628
|
+
signal: options.signal,
|
|
629
|
+
timeoutSeconds: options.timeout,
|
|
630
|
+
env: options.env,
|
|
631
|
+
onData: options.onData,
|
|
632
|
+
}, trustedCommand === undefined
|
|
633
|
+
? ""
|
|
634
|
+
: `The user directly requested this shell command: ${trustedCommand}`, { command: trustedCommand ?? command }));
|
|
635
|
+
},
|
|
636
|
+
};
|
|
297
637
|
this.ownedOperations.add(operations);
|
|
298
638
|
return operations;
|
|
299
639
|
}
|
|
300
|
-
isGuardedOperations(operations) {
|
|
640
|
+
isGuardedOperations(operations) {
|
|
641
|
+
return this.ownedOperations.has(operations);
|
|
642
|
+
}
|
|
301
643
|
}
|
|
302
644
|
//# sourceMappingURL=controller.js.map
|