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.
Files changed (128) 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 +161 -34
  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 +10 -9
  11. package/dist/approvals.js +196 -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 +63 -34
  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 +147 -43
  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 +382 -139
  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 +16 -3
  75. package/dist/review/context.js +254 -48
  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 +72 -29
  82. package/dist/review/lifecycle.js.map +1 -1
  83. package/dist/review/policy.js +27 -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 +5 -0
  88. package/dist/review/user-input.js +54 -0
  89. package/dist/review/user-input.js.map +1 -0
  90. package/dist/reviewer.d.ts +16 -6
  91. package/dist/reviewer.js +258 -70
  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 +19 -13
  99. package/dist/tools/controller.js +460 -118
  100. package/dist/tools/controller.js.map +1 -1
  101. package/dist/tools/environment.d.ts +1 -0
  102. package/dist/tools/environment.js +5 -0
  103. package/dist/tools/environment.js.map +1 -1
  104. package/dist/tools/executor.d.ts +1 -1
  105. package/dist/tools/executor.js +45 -28
  106. package/dist/tools/executor.js.map +1 -1
  107. package/dist/tools/external.d.ts +9 -8
  108. package/dist/tools/external.js +22 -11
  109. package/dist/tools/external.js.map +1 -1
  110. package/dist/tools/mcp.d.ts +6 -6
  111. package/dist/tools/mcp.js +208 -79
  112. package/dist/tools/mcp.js.map +1 -1
  113. package/dist/tools/mutation-queue.js.map +1 -1
  114. package/docs/publishing.ko.md +114 -0
  115. package/docs/publishing.md +49 -47
  116. package/docs/security/dependencies.ko.md +54 -0
  117. package/docs/security/dependencies.md +27 -26
  118. package/docs/testing/auto-review-protection.ko.md +158 -0
  119. package/docs/testing/auto-review-protection.md +105 -97
  120. package/docs/usage.ko.md +242 -0
  121. package/docs/usage.md +127 -111
  122. package/package.json +106 -95
  123. package/dist/native/darwin-arm64/artifact.json +0 -5
  124. package/dist/native/darwin-arm64/pi-guard-execpolicy +0 -0
  125. package/dist/native/linux-x64/artifact.json +0 -5
  126. package/dist/native/linux-x64/pi-guard-execpolicy +0 -0
  127. package/native/execpolicy/LICENSE +0 -201
  128. package/native/execpolicy/NOTICE +0 -6
@@ -1,33 +1,37 @@
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 } 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 { 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";
14
15
  export class GuardController {
15
16
  options;
16
17
  currentPolicy;
17
- get policy() { return this.currentPolicy; }
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 = 'not-started';
25
+ sessionId = "not-started";
23
26
  isReady = false;
24
- reviewContext = new ReviewContextStore();
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('SETTINGS_CHANGED', 'Approval settings changed'));
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, approvalPolicy: settings.approvalPolicy,
57
- reviewTimeoutMs: settings.reviewTimeoutMs, approvalTimeoutMs: settings.approvalTimeoutMs,
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() { if (!this.isReady || this.stopped.signal.aborted)
65
- throw new GuardError('GUARD_NOT_READY', 'Guard is not ready for execution'); }
66
- 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
+ }
67
77
  reset(sessionId, manager) {
68
- this.stopped.abort(new GuardError('SESSION_CHANGED', 'Session changed'));
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() { this.reviewContext.startTurn(); this.approvals.lifecycle.startTurn(); this.activeReviews.clear(); }
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, { 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
+ });
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 ?? '/bin/bash');
88
- 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();
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 ? 'nested' : 'model');
93
- this.reviewContext.toolCall({ tool: event.toolName, args: JSON.parse(canonicalJson(event.input)) }, event.toolCallId);
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 || (context.mode !== 'tui' && context.mode !== 'rpc'))
151
+ if (!context.hasUI ||
152
+ (context.mode !== "tui" && context.mode !== "rpc"))
107
153
  return undefined;
108
- const scopes = { '한 번 허용': 'once', '이 세션에서 허용': 'session', '규칙으로 저장해 허용': 'persistent', '거부': 'deny' };
109
- return { select: async (action, delta, options) => {
110
- const text = `도구: ${action.tool}\n작업 디렉터리: ${action.cwd}\n입력: ${canonicalJson(action.args)}\n추가 권한: ${canonicalJson(delta)}`;
111
- const selected = await context.ui.select(`실행 승인\n${text}`, isOnceOnly ? ['한 번 허용', '거부'] : Object.keys(scopes), { signal: options.signal, timeout: options.timeoutMs });
112
- if (selected)
113
- this.reviewContext.confirm({ actionDigest: action.digest, choice: selected });
114
- return selected ? scopes[selected] : undefined;
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
- let reviewContext = policyDecision.kind === 'ask' ? this.reviewContext.snapshot(this.options.settings.reviewContextChars, canonicalJson(action).length + canonicalJson(policyDecision.delta).length + 1000) : undefined;
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 data = { ...reviewContext, items: [...reviewContext.items, { id: 'direct-user-bash', source: 'user', trust: 'authorization', content: safeEvidence(trustedAuthorization) }] };
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 ?? 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,
126
- onReviewStart: () => { this.activeReviews.add(action.digest); if (context.hasUI)
127
- context.ui.setStatus('auto-review', `자동 검토 중 (${this.activeReviews.size})`); },
128
- 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) => {
129
233
  this.activeReviews.delete(action.digest);
130
- if (turnIdentity !== this.reviewContext.turnIdentity || !context.hasUI)
234
+ if (turnIdentity !== this.reviewContext.turnIdentity)
131
235
  return;
132
- const labels = { 'approved': '승인', 'denied': '거부', 'timed-out': '시간 초과', 'aborted': '취소', 'failed': '실패' };
133
- context.ui.setStatus('auto-review', this.activeReviews.size ? `자동 검토 중 (${this.activeReviews.size})` : result ? `자동 검토: ${labels[result.status]}` : undefined);
134
- if (result && result.status !== 'approved' && result.status !== 'aborted')
135
- context.ui.notify(`자동 검토 ${labels[result.status]}: ${redact('assessment' in result ? result.assessment.rationale : result.reason).slice(0, 1000)}`, 'warning');
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('PERMISSION_DENIED', admission.reason);
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) || definition.name === 'codemode')
157
- 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");
158
284
  const token = randomUUID(), execute = definition.execute;
159
- this.externalTools.set(definition.name, { token, isEnabled: definition.exposure !== 'hidden' });
160
- const schema = JSON.parse(JSON.stringify({ parameters: definition.parameters, description: definition.description, annotations: definition.annotations ?? {} }));
161
- 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) => {
162
300
  this.assertReady();
163
301
  const lifetime = new AbortController();
164
- 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
+ ]);
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({ toolCallId, tool: definition.name, args: { arguments: args, externalTool: JSON.parse(JSON.stringify(identity)), schema }, 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);
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 !== token || !this.isExternalTool(definition.name) || digest(JSON.parse(JSON.stringify(identify()))) !== identityDigest)
175
- throw new GuardError('STALE_EXTERNAL_TOOL', 'External registration changed; a fresh action must be reviewed');
176
- 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)
177
- 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");
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({ toolCallId, identity, arguments: args, signal, checkCurrent, approveNested: async (nestedIdentity, nestedArgs) => {
190
- 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)
191
368
  return false;
192
- const nested = createAction({ toolCallId: `${toolCallId}:elicitation:${randomUUID()}`, tool: definition.name, args: { arguments: nestedArgs, externalTool: JSON.parse(JSON.stringify(nestedIdentity)), originatingActionDigest: action.digest }, 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);
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
- } }, () => execute.call(definition, toolCallId, args, signal, onUpdate, context));
201
- 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");
202
398
  return result;
203
399
  }
204
400
  catch (error) {
205
- await this.options.audit.record(action, 'execution', 'failed');
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 = '', retryArguments) {
411
+ async admitAndExecute(action, job, context, options, trustedAuthorization = "", retryArguments) {
215
412
  this.assertReady();
216
- 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
+ ]);
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('STALE_AUTHORIZATION', 'User authorization changed during review; request a fresh review');
225
- if (this.sessionId !== action.sessionId || await canonicalPath(context.cwd, context.cwd) !== action.cwd || this.policy.revision !== action.policyRevision)
226
- 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");
227
432
  const finalDecision = await this.policy.evaluate(action, signal);
228
- if (finalDecision.kind === 'deny')
229
- throw new GuardError('PERMISSION_DENIED', finalDecision.reason);
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('STALE_APPROVAL', 'Resolved permissions changed after admission');
232
- if (digest(finalDecision.authority ?? null) !== digest(policyDecision.authority ?? null))
233
- 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");
234
440
  try {
235
441
  const result = await this.options.executor.execute(job, this.policy.profile, admission.delta, {
236
- ...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,
237
447
  });
238
- await this.options.audit.record(action, 'execution', 'settled');
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, 'execution', 'failed');
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('UNKNOWN_LOCAL_TOOL', 'Cannot delegate unknown local tool');
253
- const parameters = tool === 'bash' ? { ...definition.parameters, properties: { ...definition.parameters.properties,
254
- sandbox_permissions: { type: 'string', enum: ['use_default', 'require_escalated'], description: 'Compatibility field: request approval review for this exact command.' },
255
- 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.' },
256
- justification: { type: 'string', description: 'Explain the boundary crossing for review.' },
257
- } } : definition.parameters;
258
- 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) => {
259
503
  this.assertReady();
260
504
  const cwd = await canonicalPath(context.cwd, context.cwd);
261
- if (tool === 'bash') {
505
+ if (tool === "bash") {
262
506
  const input = params;
263
- const requested = JSON.parse(canonicalJson(Object.fromEntries(['sandbox_permissions', 'additional_permissions', 'justification'].filter(key => input[key] !== undefined).map(key => [key, input[key]]))));
264
- 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) => {
265
518
  const env = options.env;
266
- const shellPath = toolOptions?.shellPath ?? '/bin/bash';
267
- const args = { command, shellPath, ...(options.timeout !== undefined ? { timeout: options.timeout } : {}), environment: reviewEnvironment(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
+ };
268
531
  const resolvedCwd = await canonicalPath(finalCwd, finalCwd);
269
- 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);
270
- 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)));
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 === 'string' ? args.path : '.', cwd, tool === 'read');
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({ toolCallId, tool, args, cwd, source: this.sources.get(toolCallId) ?? 'model', sessionId: context.sessionManager.getSessionId(), policyRevision: this.policy.revision }, this.policy.profile);
279
- 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;
280
582
  const delegate = definition;
281
- const execute = () => this.admitAndExecute(action, { kind: 'tool', tool, args: action.args, cwd, toolCallId, ...(options ? { options } : {}) }, context, {
282
- signal, onUpdate: onUpdate ? result => onUpdate(result) : undefined,
283
- delegate: async (admittedSignal) => await delegate.execute(toolCallId, boundParameters, admittedSignal, onUpdate, context),
284
- }, '', JSON.parse(canonicalJson(params)));
285
- 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());
286
600
  return result;
287
- } };
601
+ },
602
+ };
288
603
  }
289
604
  userBashOperations(context, trustedCommand) {
290
- const operations = { exec: async (command, cwd, options) => {
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 ?? '/bin/bash';
294
- const action = createAction({ toolCallId: randomUUID(), tool: 'bash', source: 'user-bash', args: JSON.parse(canonicalJson({ command, shellPath, environment: reviewEnvironment(env), ...(options.timeout !== undefined ? { timeout: options.timeout } : {}) })), cwd: resolvedCwd, sessionId: context.sessionManager.getSessionId(), policyRevision: this.policy.revision }, this.policy.profile);
295
- 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 });
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) { return this.ownedOperations.has(operations); }
640
+ isGuardedOperations(operations) {
641
+ return this.ownedOperations.has(operations);
642
+ }
301
643
  }
302
644
  //# sourceMappingURL=controller.js.map