pi-codex-auto-review 0.1.4 → 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.
Files changed (127) hide show
  1. package/NOTICE +4 -2
  2. package/README.ko.md +85 -0
  3. package/README.md +51 -52
  4. package/dist/approval-commands.d.ts +3 -3
  5. package/dist/approval-commands.js +112 -58
  6. package/dist/approval-commands.js.map +1 -1
  7. package/dist/approval-settings.d.ts +1 -1
  8. package/dist/approval-settings.js +17 -10
  9. package/dist/approval-settings.js.map +1 -1
  10. package/dist/approvals.d.ts +9 -9
  11. package/dist/approvals.js +195 -75
  12. package/dist/approvals.js.map +1 -1
  13. package/dist/audit.d.ts +1 -1
  14. package/dist/audit.js +37 -13
  15. package/dist/audit.js.map +1 -1
  16. package/dist/cli.js +60 -31
  17. package/dist/cli.js.map +1 -1
  18. package/dist/context-files.d.ts +2 -2
  19. package/dist/context-files.js +40 -19
  20. package/dist/context-files.js.map +1 -1
  21. package/dist/contracts.d.ts +39 -39
  22. package/dist/contracts.js +117 -51
  23. package/dist/contracts.js.map +1 -1
  24. package/dist/index.d.ts +9 -9
  25. package/dist/index.js +145 -48
  26. package/dist/index.js.map +1 -1
  27. package/dist/pi-host.js +3 -3
  28. package/dist/pi-host.js.map +1 -1
  29. package/dist/policy/domains.js +17 -7
  30. package/dist/policy/domains.js.map +1 -1
  31. package/dist/policy/index.d.ts +7 -7
  32. package/dist/policy/index.js +379 -141
  33. package/dist/policy/index.js.map +1 -1
  34. package/dist/policy/paths.js +31 -18
  35. package/dist/policy/paths.js.map +1 -1
  36. package/dist/policy/rule-engine.d.ts +2 -0
  37. package/dist/policy/rule-engine.js +283 -0
  38. package/dist/policy/rule-engine.js.map +1 -0
  39. package/dist/policy/rule-types.d.ts +30 -0
  40. package/dist/policy/rule-types.js +2 -0
  41. package/dist/policy/rule-types.js.map +1 -0
  42. package/dist/policy/rules-sync.d.ts +1 -0
  43. package/dist/policy/rules-sync.js +14 -0
  44. package/dist/policy/rules-sync.js.map +1 -0
  45. package/dist/policy/rules-worker.d.ts +1 -0
  46. package/dist/policy/rules-worker.js +14 -0
  47. package/dist/policy/rules-worker.js.map +1 -0
  48. package/dist/policy/rules.d.ts +4 -31
  49. package/dist/policy/rules.js +126 -38
  50. package/dist/policy/rules.js.map +1 -1
  51. package/dist/policy/shell.js +70 -41
  52. package/dist/policy/shell.js.map +1 -1
  53. package/dist/policy/starlark/builtins.d.ts +6 -0
  54. package/dist/policy/starlark/builtins.js +764 -0
  55. package/dist/policy/starlark/builtins.js.map +1 -0
  56. package/dist/policy/starlark/evaluator.d.ts +18 -0
  57. package/dist/policy/starlark/evaluator.js +518 -0
  58. package/dist/policy/starlark/evaluator.js.map +1 -0
  59. package/dist/policy/starlark/lexer.d.ts +14 -0
  60. package/dist/policy/starlark/lexer.js +189 -0
  61. package/dist/policy/starlark/lexer.js.map +1 -0
  62. package/dist/policy/starlark/parser.d.ts +140 -0
  63. package/dist/policy/starlark/parser.js +508 -0
  64. package/dist/policy/starlark/parser.js.map +1 -0
  65. package/dist/policy/starlark/validation.d.ts +4 -0
  66. package/dist/policy/starlark/validation.js +183 -0
  67. package/dist/policy/starlark/validation.js.map +1 -0
  68. package/dist/policy/starlark/values.d.ts +46 -0
  69. package/dist/policy/starlark/values.js +363 -0
  70. package/dist/policy/starlark/values.js.map +1 -0
  71. package/dist/reports.d.ts +1 -1
  72. package/dist/reports.js +66 -30
  73. package/dist/reports.js.map +1 -1
  74. package/dist/review/context.d.ts +3 -3
  75. package/dist/review/context.js +207 -71
  76. package/dist/review/context.js.map +1 -1
  77. package/dist/review/investigation.d.ts +2 -2
  78. package/dist/review/investigation.js +57 -16
  79. package/dist/review/investigation.js.map +1 -1
  80. package/dist/review/lifecycle.d.ts +1 -1
  81. package/dist/review/lifecycle.js +70 -27
  82. package/dist/review/lifecycle.js.map +1 -1
  83. package/dist/review/policy.js +10 -5
  84. package/dist/review/policy.js.map +1 -1
  85. package/dist/review/upstream-policy.js +1 -1
  86. package/dist/review/upstream-policy.js.map +1 -1
  87. package/dist/review/user-input.d.ts +3 -3
  88. package/dist/review/user-input.js +39 -16
  89. package/dist/review/user-input.js.map +1 -1
  90. package/dist/reviewer.d.ts +8 -8
  91. package/dist/reviewer.js +253 -75
  92. package/dist/reviewer.js.map +1 -1
  93. package/dist/signals.js +24 -9
  94. package/dist/signals.js.map +1 -1
  95. package/dist/startup.d.ts +4 -4
  96. package/dist/startup.js +139 -41
  97. package/dist/startup.js.map +1 -1
  98. package/dist/tools/controller.d.ts +16 -12
  99. package/dist/tools/controller.js +432 -121
  100. package/dist/tools/controller.js.map +1 -1
  101. package/dist/tools/environment.js +3 -1
  102. package/dist/tools/environment.js.map +1 -1
  103. package/dist/tools/executor.d.ts +1 -1
  104. package/dist/tools/executor.js +45 -28
  105. package/dist/tools/executor.js.map +1 -1
  106. package/dist/tools/external.d.ts +8 -8
  107. package/dist/tools/external.js +22 -11
  108. package/dist/tools/external.js.map +1 -1
  109. package/dist/tools/mcp.d.ts +6 -6
  110. package/dist/tools/mcp.js +208 -80
  111. package/dist/tools/mcp.js.map +1 -1
  112. package/dist/tools/mutation-queue.js.map +1 -1
  113. package/docs/publishing.ko.md +114 -0
  114. package/docs/publishing.md +49 -47
  115. package/docs/security/dependencies.ko.md +54 -0
  116. package/docs/security/dependencies.md +27 -26
  117. package/docs/testing/auto-review-protection.ko.md +158 -0
  118. package/docs/testing/auto-review-protection.md +104 -99
  119. package/docs/usage.ko.md +242 -0
  120. package/docs/usage.md +125 -121
  121. package/package.json +106 -95
  122. package/dist/native/darwin-arm64/artifact.json +0 -5
  123. package/dist/native/darwin-arm64/pi-guard-execpolicy +0 -0
  124. package/dist/native/linux-x64/artifact.json +0 -5
  125. package/dist/native/linux-x64/pi-guard-execpolicy +0 -0
  126. package/native/execpolicy/LICENSE +0 -201
  127. package/native/execpolicy/NOTICE +0 -6
@@ -1,26 +1,28 @@
1
- import { randomUUID } from 'node:crypto';
2
- import { createBashToolDefinition } from '@earendil-works/pi-coding-agent';
3
- import { pathToFileURL } from 'node:url';
4
- import { createAction, canonicalJson, digest, GuardError, TOOL_NAMES } from '../contracts.js';
5
- import { PolicyEngine, canonicalPath, resolveToolPath } from '../policy/index.js';
6
- import { PiReviewProvider } from '../reviewer.js';
7
- import { redact } from '../audit.js';
8
- import { reviewEnvironment, redactedEnvironmentNames } from './environment.js';
9
- import { withApprovalMutationQueue } from './mutation-queue.js';
10
- import { ReviewContextStore, safeEvidence } from '../review/context.js';
11
- import { LocalInvestigation } from '../review/investigation.js';
12
- import { reviewFeedback } from '../review/lifecycle.js';
13
- import { observeToolUserInput } from '../review/user-input.js';
14
- import { externalInvocations, externalPolicy } from './external.js';
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";
15
15
  export class GuardController {
16
16
  options;
17
17
  currentPolicy;
18
- get policy() { return this.currentPolicy; }
18
+ get policy() {
19
+ return this.currentPolicy;
20
+ }
19
21
  approvals;
20
22
  sources = new Map();
21
23
  ownedOperations = new WeakSet();
22
24
  stopped = new AbortController();
23
- sessionId = 'not-started';
25
+ sessionId = "not-started";
24
26
  isReady = false;
25
27
  reviewContext;
26
28
  activeReviews = new Set();
@@ -48,26 +50,32 @@ export class GuardController {
48
50
  await policy.initialize(cwd);
49
51
  this.assertReady();
50
52
  await persist(settings);
51
- this.stopped.abort(new GuardError('SETTINGS_CHANGED', 'Approval settings changed'));
53
+ this.stopped.abort(new GuardError("SETTINGS_CHANGED", "Approval settings changed"));
52
54
  this.stopped = new AbortController();
53
55
  this.approvals.invalidate();
54
56
  this.activeReviews.clear();
55
57
  this.retryArguments.clear();
56
58
  this.options.settings = settings;
57
59
  Object.assign(this.approvals.options, {
58
- approvalsReviewer: settings.approvalsReviewer, approvalPolicy: settings.approvalPolicy,
59
- reviewTimeoutMs: settings.reviewTimeoutMs, approvalTimeoutMs: settings.approvalTimeoutMs,
60
+ approvalsReviewer: settings.approvalsReviewer,
61
+ approvalPolicy: settings.approvalPolicy,
62
+ reviewTimeoutMs: settings.reviewTimeoutMs,
63
+ approvalTimeoutMs: settings.approvalTimeoutMs,
60
64
  });
61
65
  this.currentPolicy = policy;
62
66
  });
63
67
  this.settingsUpdates = task.catch(() => { });
64
68
  await task;
65
69
  }
66
- assertReady() { if (!this.isReady || this.stopped.signal.aborted)
67
- throw new GuardError('GUARD_NOT_READY', 'Guard is not ready for execution'); }
68
- isBoundToSession(sessionId) { return this.sessionId === sessionId && !this.stopped.signal.aborted; }
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
+ }
69
77
  reset(sessionId, manager) {
70
- this.stopped.abort(new GuardError('SESSION_CHANGED', 'Session changed'));
78
+ this.stopped.abort(new GuardError("SESSION_CHANGED", "Session changed"));
71
79
  this.stopped = new AbortController();
72
80
  this.sessionId = sessionId;
73
81
  this.reviewContext.reset(sessionId, manager);
@@ -86,25 +94,50 @@ export class GuardController {
86
94
  }
87
95
  async authorizeRetry(id, context) {
88
96
  this.assertReady();
89
- const denial = this.approvals.lifecycle.authorizeRetry(id, { sessionId: context.sessionManager.getSessionId(), contextId: this.reviewContext.identity, cwd: await canonicalPath(context.cwd, context.cwd), policyRevision: this.policy.revision, permissionDigest: digest(this.policy.profile) });
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
+ });
90
104
  const args = this.retryArguments.get(id) ?? denial.action.args;
91
105
  this.retryArguments.delete(id);
92
106
  return { denial, args };
93
107
  }
94
108
  async retryUserBash(action, context) {
95
109
  const next = createAction({ ...action, toolCallId: randomUUID() }, this.policy.profile);
96
- const command = String(next.args.command), shellPath = String(next.args.shellPath ?? '/bin/bash');
97
- await this.admitAndExecute(next, { kind: 'shell', command, shellPath, cwd: next.cwd }, context, { timeoutSeconds: typeof next.args.timeout === 'number' ? next.args.timeout : undefined, env: next.args.environment });
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();
98
121
  }
99
- authorizeUser(text) { this.reviewContext.authorize(text); this.approvals.lifecycle.invalidateRetries(); }
100
122
  noteCall(event) {
101
- this.sources.set(event.toolCallId, event.parentToolCallId ? 'nested' : 'model');
123
+ this.sources.set(event.toolCallId, event.parentToolCallId ? "nested" : "model");
102
124
  const callIdentity = this.reviewContext.callIdentity(event.toolCallId);
103
- this.reviewContext.toolCall({ tool: event.toolName, toolCallId: event.toolCallId, callIdentity, args: JSON.parse(canonicalJson(event.input)),
104
- ...(event.parentToolCallId ? { parentToolCallId: event.parentToolCallId, parentCallIdentity: this.reviewContext.callIdentity(event.parentToolCallId) } : {}),
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
+ : {}),
105
136
  }, callIdentity);
106
137
  }
107
- completeCall(toolCallId) { this.sources.delete(toolCallId); }
138
+ completeCall(toolCallId) {
139
+ this.sources.delete(toolCallId);
140
+ }
108
141
  async close() {
109
142
  this.isReady = false;
110
143
  this.stopped.abort();
@@ -115,40 +148,88 @@ export class GuardController {
115
148
  await this.settingsUpdates;
116
149
  }
117
150
  ui(context, isOnceOnly = false) {
118
- if (!context.hasUI || (context.mode !== 'tui' && context.mode !== 'rpc'))
151
+ if (!context.hasUI ||
152
+ (context.mode !== "tui" && context.mode !== "rpc"))
119
153
  return undefined;
120
- const scopes = { 'Allow once': 'once', 'Allow for this session': 'session', 'Save as an allow rule': 'persistent', 'Deny': 'deny' };
121
- return { select: async (action, delta, options) => {
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) => {
122
162
  const text = `Tool: ${action.tool}\nWorking directory: ${action.cwd}\nInput: ${canonicalJson(action.args)}\nRequested scope: ${canonicalJson(delta)}`;
123
- const selected = await context.ui.select(`Approve this action?\n${text}`, isOnceOnly ? ['Allow once', 'Deny'] : Object.keys(scopes), { signal: options.signal, timeout: options.timeoutMs });
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 });
124
164
  if (selected && selected in scopes)
125
165
  this.reviewContext.confirm({
126
- actionDigest: action.digest, action: JSON.parse(canonicalJson(action)), requestedScope: JSON.parse(canonicalJson(delta)), choice: selected, scope: scopes[selected],
166
+ actionDigest: action.digest,
167
+ action: JSON.parse(canonicalJson(action)),
168
+ requestedScope: JSON.parse(canonicalJson(delta)),
169
+ choice: selected,
170
+ scope: scopes[selected],
127
171
  });
128
- return selected ? scopes[selected] : undefined;
129
- } };
172
+ return selected
173
+ ? scopes[selected]
174
+ : undefined;
175
+ },
176
+ };
130
177
  }
131
- async requestAdmission(action, policyDecision, context, signal, trustedAuthorization = '') {
178
+ async requestAdmission(action, policyDecision, context, signal, trustedAuthorization = "") {
132
179
  this.reviewContext.refreshInstructions();
133
180
  if (context.getSystemPrompt)
134
181
  this.reviewContext.mainPrompt(context.getSystemPrompt());
135
182
  this.reviewContext.preparedAction(action);
136
183
  const executionContext = {
137
- environmentId: 'local', platform: process.platform, architecture: process.arch, osIsolation: false,
138
- permissionProfile: this.policy.profile, approvalPolicy: this.options.settings.approvalPolicy, approvalsReviewer: this.options.settings.approvalsReviewer,
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,
139
191
  };
140
- let reviewContext = policyDecision.kind === 'ask' ? this.reviewContext.snapshot(this.options.settings.reviewContextChars, canonicalJson({ action, delta: policyDecision.delta, executionContext, policyReason: policyDecision.reason }).length + 1000) : undefined;
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;
141
200
  if (reviewContext && trustedAuthorization) {
142
201
  const { digest: previousDigest, ...fields } = reviewContext;
143
- const data = { ...fields, items: [...reviewContext.items, { id: 'direct-user-bash', source: 'user', trust: 'authorization', content: safeEvidence(trustedAuthorization) }] };
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
+ };
144
214
  reviewContext = { ...data, digest: digest(data) };
145
215
  }
146
216
  const turnIdentity = this.reviewContext.turnIdentity;
147
217
  return this.approvals.admit(action, policyDecision, {
148
- provider: this.options.provider ?? new PiReviewProvider(context, this.options.settings, new LocalInvestigation(this.options.executor, this.policy.profile, action.cwd)), ui: this.ui(context, policyDecision.requiresFreshReview || policyDecision.requiresUserInput), trustedAuthorization, signal, reviewContext, settings: this.options.settings, executionContext,
149
- onReviewStart: () => { this.activeReviews.add(action.digest); if (context.hasUI)
150
- context.ui.setStatus('auto-review', `Auto-review running (${this.activeReviews.size})`); },
151
- onReviewResult: result => {
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) => {
152
233
  this.activeReviews.delete(action.digest);
153
234
  if (turnIdentity !== this.reviewContext.turnIdentity)
154
235
  return;
@@ -156,19 +237,34 @@ export class GuardController {
156
237
  this.reviewContext.assessment(action, result);
157
238
  if (!context.hasUI)
158
239
  return;
159
- const labels = { 'approved': 'approved', 'denied': 'denied', 'timed-out': 'timed out', 'aborted': 'cancelled', 'failed': 'failed' };
160
- context.ui.setStatus('auto-review', this.activeReviews.size ? `Auto-review running (${this.activeReviews.size})` : result ? `Auto-review: ${labels[result.status]}` : undefined);
161
- if (result && result.status !== 'approved' && result.status !== 'aborted')
162
- context.ui.notify(`Auto-review ${labels[result.status]}: ${redact('assessment' in result ? result.assessment.rationale : result.reason).slice(0, 1000)}`, 'warning');
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?.();
163
261
  },
164
- onInterrupt: () => { if (context.hasUI)
165
- context.ui.notify('Auto-review stopped this turn after repeated denials.', 'warning'); context.abort?.(); },
166
262
  });
167
263
  }
168
264
  denied(admission, retryArguments) {
169
265
  if (admission.denialId && retryArguments)
170
266
  this.retryArguments.set(admission.denialId, retryArguments);
171
- 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));
172
268
  for (const id of this.retryArguments.keys())
173
269
  if (!retained.has(id))
174
270
  this.retryArguments.delete(id);
@@ -176,34 +272,78 @@ export class GuardController {
176
272
  const feedback = reviewFeedback(admission.review);
177
273
  return new GuardError(feedback.code, `[${feedback.code}] ${feedback.message}`);
178
274
  }
179
- return new GuardError('PERMISSION_DENIED', admission.reason);
275
+ return new GuardError("PERMISSION_DENIED", admission.reason);
276
+ }
277
+ isExternalTool(name) {
278
+ return this.externalTools.get(name)?.isEnabled === true;
180
279
  }
181
- isExternalTool(name) { return this.externalTools.get(name)?.isEnabled === true; }
182
280
  wrapExternalTool(definition, identify) {
183
- if (TOOL_NAMES.includes(definition.name) || definition.name === 'codemode')
184
- throw new GuardError('RESERVED_TOOL', 'An external adapter cannot replace guarded local tools');
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");
185
284
  const token = randomUUID(), execute = definition.execute;
186
- this.externalTools.set(definition.name, { token, isEnabled: definition.exposure !== 'hidden' });
187
- const schema = JSON.parse(JSON.stringify({ parameters: definition.parameters, description: definition.description, annotations: definition.annotations ?? {}, ...(definition.namespace ? { namespace: definition.namespace } : {}) }));
188
- return { ...definition, execute: async (toolCallId, params, callerSignal, onUpdate, context) => {
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) => {
189
300
  this.assertReady();
190
301
  const lifetime = new AbortController();
191
- const signal = AbortSignal.any([lifetime.signal, this.stopped.signal, this.approvals.lifecycle.signal, ...(callerSignal ? [callerSignal] : [])]);
302
+ const signal = AbortSignal.any([
303
+ lifetime.signal,
304
+ this.stopped.signal,
305
+ this.approvals.lifecycle.signal,
306
+ ...(callerSignal ? [callerSignal] : []),
307
+ ]);
192
308
  signal.throwIfAborted();
193
309
  const args = JSON.parse(canonicalJson(params ?? {}));
194
310
  const identity = JSON.parse(JSON.stringify(identify()));
195
311
  const identityDigest = digest(identity), cwd = await canonicalPath(context.cwd, context.cwd);
196
- const action = createAction({ toolCallId, tool: definition.name, args: { arguments: args, externalTool: JSON.parse(JSON.stringify(identity)), schema,
197
- ...(identity.connectedAccountEmail ? { connected_account_email: identity.connectedAccountEmail } : {}),
198
- }, cwd, source: this.sources.get(toolCallId) ?? 'model', sessionId: context.sessionManager.getSessionId(), policyRevision: this.policy.revision }, this.policy.profile);
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);
199
330
  const authorizationVersion = this.reviewContext.scopeVersion, turn = this.reviewContext.turnIdentity;
200
331
  const checkCurrent = async () => {
201
332
  this.assertReady();
202
333
  signal.throwIfAborted();
203
- if (this.externalTools.get(definition.name)?.token !== token || !this.isExternalTool(definition.name) || digest(JSON.parse(JSON.stringify(identify()))) !== identityDigest)
204
- throw new GuardError('STALE_EXTERNAL_TOOL', 'External registration changed; a fresh action must be reviewed');
205
- if (this.reviewContext.scopeVersion !== authorizationVersion || this.reviewContext.turnIdentity !== turn || this.sessionId !== action.sessionId || this.policy.revision !== action.policyRevision || await canonicalPath(context.cwd, context.cwd) !== cwd)
206
- throw new GuardError('STALE_APPROVAL', 'External execution context changed after admission');
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");
207
347
  signal.throwIfAborted();
208
348
  };
209
349
  const approve = async (candidate, candidateIdentity) => {
@@ -215,12 +355,36 @@ export class GuardController {
215
355
  };
216
356
  try {
217
357
  await approve(action, identity);
218
- const result = await externalInvocations.run({ toolCallId, identity, arguments: args, signal, checkCurrent, approveNested: async (nestedIdentity, nestedArgs) => {
219
- if (nestedIdentity.server !== identity.server || nestedIdentity.registration !== identity.registration)
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)
220
368
  return false;
221
- const nested = createAction({ toolCallId: `${toolCallId}:elicitation:${randomUUID()}`, tool: definition.name, args: { arguments: nestedArgs, externalTool: JSON.parse(JSON.stringify(nestedIdentity)), schema, originatingActionDigest: action.digest,
222
- ...(nestedIdentity.connectedAccountEmail ? { connected_account_email: nestedIdentity.connectedAccountEmail } : {}),
223
- }, cwd, source: 'nested', sessionId: action.sessionId, policyRevision: action.policyRevision }, this.policy.profile);
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);
224
388
  try {
225
389
  await approve(nested, nestedIdentity);
226
390
  return true;
@@ -228,23 +392,31 @@ export class GuardController {
228
392
  catch {
229
393
  return false;
230
394
  }
231
- } }, () => execute.call(definition, toolCallId, args, signal, onUpdate, observeToolUserInput(context, action, this.reviewContext, signal)));
232
- await this.options.audit.record(action, 'execution', 'settled');
395
+ },
396
+ }, () => execute.call(definition, toolCallId, args, signal, onUpdate, observeToolUserInput(context, action, this.reviewContext, signal)));
397
+ await this.options.audit.record(action, "execution", "settled");
233
398
  return result;
234
399
  }
235
400
  catch (error) {
236
- await this.options.audit.record(action, 'execution', 'failed');
401
+ await this.options.audit.record(action, "execution", "failed");
237
402
  throw error;
238
403
  }
239
404
  finally {
240
405
  lifetime.abort();
241
406
  this.completeCall(toolCallId);
242
407
  }
243
- } };
408
+ },
409
+ };
244
410
  }
245
- async admitAndExecute(action, job, context, options, trustedAuthorization = '', retryArguments) {
411
+ async admitAndExecute(action, job, context, options, trustedAuthorization = "", retryArguments) {
246
412
  this.assertReady();
247
- const signal = AbortSignal.any([this.stopped.signal, ...(action.source === 'user-bash' ? [] : [this.approvals.lifecycle.signal]), ...(options.signal ? [options.signal] : [])]);
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
+ ]);
248
420
  const authorizationVersion = this.reviewContext.scopeVersion;
249
421
  const policyDecision = await this.policy.evaluate(action, signal);
250
422
  const admission = await this.requestAdmission(action, policyDecision, context, signal, trustedAuthorization);
@@ -252,25 +424,32 @@ export class GuardController {
252
424
  throw this.denied(admission, retryArguments);
253
425
  signal.throwIfAborted();
254
426
  if (this.reviewContext.scopeVersion !== authorizationVersion)
255
- throw new GuardError('STALE_AUTHORIZATION', 'User authorization changed during review; request a fresh review');
256
- if (this.sessionId !== action.sessionId || await canonicalPath(context.cwd, context.cwd) !== action.cwd || this.policy.revision !== action.policyRevision)
257
- throw new GuardError('STALE_APPROVAL', 'Execution context changed after admission');
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");
258
432
  const finalDecision = await this.policy.evaluate(action, signal);
259
- if (finalDecision.kind === 'deny')
260
- throw new GuardError('PERMISSION_DENIED', finalDecision.reason);
433
+ if (finalDecision.kind === "deny")
434
+ throw new GuardError("PERMISSION_DENIED", finalDecision.reason);
261
435
  if (digest(finalDecision.delta) !== digest(policyDecision.delta))
262
- throw new GuardError('STALE_APPROVAL', 'Resolved permissions changed after admission');
263
- if (digest(finalDecision.authority ?? null) !== digest(policyDecision.authority ?? null))
264
- throw new GuardError('STALE_APPROVAL', 'Execution authority changed after admission');
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");
265
440
  try {
266
441
  const result = await this.options.executor.execute(job, this.policy.profile, admission.delta, {
267
- ...options, signal, authority: admission.authority, timeoutSeconds: options.timeoutSeconds ?? this.options.settings.executionTimeoutSeconds,
442
+ ...options,
443
+ signal,
444
+ authority: admission.authority,
445
+ timeoutSeconds: options.timeoutSeconds ??
446
+ this.options.settings.executionTimeoutSeconds,
268
447
  });
269
- await this.options.audit.record(action, 'execution', 'settled');
448
+ await this.options.audit.record(action, "execution", "settled");
270
449
  return result;
271
450
  }
272
451
  catch (error) {
273
- await this.options.audit.record(action, 'execution', 'failed');
452
+ await this.options.audit.record(action, "execution", "failed");
274
453
  throw error;
275
454
  }
276
455
  finally {
@@ -280,54 +459,186 @@ export class GuardController {
280
459
  wrapTool(definition, toolOptions) {
281
460
  const tool = definition.name;
282
461
  if (!TOOL_NAMES.includes(tool))
283
- throw new GuardError('UNKNOWN_LOCAL_TOOL', 'Cannot delegate unknown local tool');
284
- const parameters = tool === 'bash' ? { ...definition.parameters, properties: { ...definition.parameters.properties,
285
- sandbox_permissions: { type: 'string', enum: ['use_default', 'require_escalated'], description: 'Compatibility field: request approval review for this exact command.' },
286
- additional_permissions: { type: 'object', properties: { readPaths: { type: 'array', items: { type: 'string' } }, writePaths: { type: 'array', items: { type: 'string' } }, domains: { type: 'array', items: { type: 'string' } } }, additionalProperties: false, description: 'Describe the exact additional action scope for approval review.' },
287
- justification: { type: 'string', description: 'Explain the boundary crossing for review.' },
288
- } } : definition.parameters;
289
- return { ...definition, parameters, execute: async (toolCallId, params, signal, onUpdate, context) => {
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) => {
290
503
  this.assertReady();
291
504
  const cwd = await canonicalPath(context.cwd, context.cwd);
292
- if (tool === 'bash') {
505
+ if (tool === "bash") {
293
506
  const input = params;
294
- const requested = JSON.parse(canonicalJson(Object.fromEntries(['sandbox_permissions', 'additional_permissions', 'justification'].filter(key => input[key] !== undefined).map(key => [key, input[key]]))));
295
- const delegate = createBashToolDefinition(cwd, { ...toolOptions, operations: { exec: async (command, finalCwd, options) => {
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) => {
296
518
  const env = options.env;
297
- const shellPath = toolOptions?.shellPath ?? '/bin/bash';
298
- const args = { command, shellPath, ...(options.timeout !== undefined ? { timeout: options.timeout } : {}), environment: reviewEnvironment(env), redactedEnvironmentVariables: redactedEnvironmentNames(env), ...requested };
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
+ };
299
531
  const resolvedCwd = await canonicalPath(finalCwd, finalCwd);
300
- const action = createAction({ toolCallId, tool, args: JSON.parse(canonicalJson(args)), cwd: resolvedCwd, source: this.sources.get(toolCallId) ?? 'model', sessionId: context.sessionManager.getSessionId(), policyRevision: this.policy.revision }, this.policy.profile);
301
- return await this.admitAndExecute(action, { kind: 'shell', command, shellPath, cwd: resolvedCwd }, { ...context, cwd: finalCwd }, { signal: options.signal, timeoutSeconds: options.timeout, env, onData: options.onData }, '', JSON.parse(canonicalJson(params)));
302
- } } });
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
+ });
303
556
  return delegate.execute(toolCallId, params, signal, onUpdate, context);
304
557
  }
305
558
  const boundParameters = JSON.parse(canonicalJson(params));
306
559
  const args = { ...boundParameters };
307
- const target = await resolveToolPath(typeof args.path === 'string' ? args.path : '.', cwd, tool === 'read');
560
+ const target = await resolveToolPath(typeof args.path === "string" ? args.path : ".", cwd, tool === "read");
308
561
  args.path = pathToFileURL(target).href;
309
- const action = createAction({ toolCallId, tool, args, cwd, source: this.sources.get(toolCallId) ?? 'model', sessionId: context.sessionManager.getSessionId(), policyRevision: this.policy.revision }, this.policy.profile);
310
- const options = tool === 'read' ? { ...toolOptions, ...(context.model?.inputLimits?.images?.resize ? { resizeOptions: context.model.inputLimits.images.resize } : {}) } : undefined;
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;
311
582
  const delegate = definition;
312
- const execute = () => this.admitAndExecute(action, { kind: 'tool', tool, args: action.args, cwd, toolCallId, ...(options ? { options } : {}) }, context, {
313
- signal, onUpdate: onUpdate ? result => onUpdate(result) : undefined,
314
- delegate: async (admittedSignal) => await delegate.execute(toolCallId, boundParameters, admittedSignal, onUpdate, context),
315
- }, '', JSON.parse(canonicalJson(params)));
316
- const result = await (tool === 'write' || tool === 'edit' ? withApprovalMutationQueue(await canonicalPath(target, cwd), execute) : execute());
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());
317
600
  return result;
318
- } };
601
+ },
602
+ };
319
603
  }
320
604
  userBashOperations(context, trustedCommand) {
321
- const operations = { exec: async (command, cwd, options) => {
605
+ const operations = {
606
+ exec: async (command, cwd, options) => {
322
607
  const resolvedCwd = await canonicalPath(cwd, cwd);
323
608
  const env = options.env;
324
- const shellPath = this.options.shellPath ?? '/bin/bash';
325
- const action = createAction({ toolCallId: randomUUID(), tool: 'bash', source: 'user-bash', args: JSON.parse(canonicalJson({ command, shellPath, environment: reviewEnvironment(env), redactedEnvironmentVariables: redactedEnvironmentNames(env), ...(options.timeout !== undefined ? { timeout: options.timeout } : {}) })), cwd: resolvedCwd, sessionId: context.sessionManager.getSessionId(), policyRevision: this.policy.revision }, this.policy.profile);
326
- return await this.admitAndExecute(action, { kind: 'shell', command, shellPath, cwd: resolvedCwd }, context, { signal: options.signal, timeoutSeconds: options.timeout, env: options.env, onData: options.onData }, trustedCommand === undefined ? '' : `The user directly requested this shell command: ${trustedCommand}`, { command: trustedCommand ?? command });
327
- } };
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
+ };
328
637
  this.ownedOperations.add(operations);
329
638
  return operations;
330
639
  }
331
- isGuardedOperations(operations) { return this.ownedOperations.has(operations); }
640
+ isGuardedOperations(operations) {
641
+ return this.ownedOperations.has(operations);
642
+ }
332
643
  }
333
644
  //# sourceMappingURL=controller.js.map