vgxness 1.20.2 → 1.20.4

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.
@@ -0,0 +1,462 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ const grantableCategories = new Set(['shell', 'test-run', 'git']);
3
+ const grantStatuses = new Set(['active', 'revoked', 'expired', 'exhausted']);
4
+ export class TaskScopedCommandGrantRepository {
5
+ db;
6
+ constructor(db) {
7
+ this.db = db;
8
+ }
9
+ create(input) {
10
+ const validation = validateCreate(input);
11
+ if (!validation.ok)
12
+ return validation;
13
+ const run = this.getRunId(input.runId);
14
+ if (!run.ok)
15
+ return run;
16
+ const result = this.db.transaction(() => {
17
+ const id = randomUUID();
18
+ const createdAt = new Date().toISOString();
19
+ this.db.connection
20
+ .prepare(`
21
+ INSERT INTO task_scoped_command_grants(
22
+ id, run_id, agent_id, task_fingerprint, categories_json, external_roots_json,
23
+ canonical_external_roots_json, cwd_patterns_json, command_policy_json, expires_at,
24
+ max_uses, used_count, status, created_by_json, created_at, audit_json
25
+ ) VALUES (
26
+ @id, @runId, @agentId, @taskFingerprint, @categoriesJson, @externalRootsJson,
27
+ @canonicalExternalRootsJson, @cwdPatternsJson, @commandPolicyJson, @expiresAt,
28
+ @maxUses, 0, 'active', @createdByJson, @createdAt, @auditJson
29
+ )
30
+ `)
31
+ .run({
32
+ id,
33
+ runId: input.runId,
34
+ agentId: input.agentId,
35
+ taskFingerprint: input.taskFingerprint,
36
+ categoriesJson: JSON.stringify(input.categories),
37
+ externalRootsJson: JSON.stringify(input.externalRoots),
38
+ canonicalExternalRootsJson: JSON.stringify(input.canonicalExternalRoots),
39
+ cwdPatternsJson: input.cwdPatterns === undefined ? null : JSON.stringify(input.cwdPatterns),
40
+ commandPolicyJson: JSON.stringify(input.commandPolicy),
41
+ expiresAt: input.expiresAt,
42
+ maxUses: input.maxUses,
43
+ createdByJson: JSON.stringify(input.createdBy),
44
+ createdAt,
45
+ auditJson: JSON.stringify(input.audit),
46
+ });
47
+ this.insertEvent({
48
+ runId: input.runId,
49
+ title: 'Task-scoped command grant created',
50
+ payload: {
51
+ grantId: id,
52
+ agentId: input.agentId,
53
+ taskFingerprint: input.taskFingerprint,
54
+ categories: input.categories,
55
+ commandPolicy: { kind: input.commandPolicy.kind, ids: input.commandPolicy.ids },
56
+ expiresAt: input.expiresAt,
57
+ maxUses: input.maxUses,
58
+ approvedScopeSummary: input.audit.approvedScopeSummary,
59
+ },
60
+ relatedId: id,
61
+ });
62
+ const created = this.get(id);
63
+ if (!created.ok)
64
+ return created;
65
+ return ok(created.value);
66
+ });
67
+ return result.ok ? result.value : fail(result.error.message, result.error.cause, result.error.code);
68
+ }
69
+ get(id) {
70
+ if (!id.trim())
71
+ return validationFailure('Grant id is required');
72
+ try {
73
+ const row = this.db.connection.prepare('SELECT * FROM task_scoped_command_grants WHERE id=?').get(id);
74
+ if (row === undefined)
75
+ return missing(`Task-scoped command grant not found: ${id}`);
76
+ return mapGrantRow(row);
77
+ }
78
+ catch (cause) {
79
+ return fail('Failed to read task-scoped command grant', cause);
80
+ }
81
+ }
82
+ list(filters = {}) {
83
+ if (filters.limit !== undefined && (!Number.isInteger(filters.limit) || filters.limit < 1))
84
+ return validationFailure('Grant list limit must be positive');
85
+ if (filters.status !== undefined && !grantStatuses.has(filters.status))
86
+ return validationFailure(`Unsupported grant status: ${filters.status}`);
87
+ try {
88
+ const where = [];
89
+ const parameters = {};
90
+ if (filters.runId !== undefined) {
91
+ where.push('run_id=@runId');
92
+ parameters.runId = filters.runId;
93
+ }
94
+ if (filters.agentId !== undefined) {
95
+ where.push('agent_id=@agentId');
96
+ parameters.agentId = filters.agentId;
97
+ }
98
+ if (filters.status !== undefined) {
99
+ where.push('status=@status');
100
+ parameters.status = filters.status;
101
+ }
102
+ if (filters.limit !== undefined)
103
+ parameters.limit = filters.limit;
104
+ const rows = this.db.connection
105
+ .prepare(`
106
+ SELECT * FROM task_scoped_command_grants
107
+ ${where.length > 0 ? `WHERE ${where.join(' AND ')}` : ''}
108
+ ORDER BY created_at DESC, id ASC
109
+ ${filters.limit === undefined ? '' : 'LIMIT @limit'}
110
+ `)
111
+ .all(parameters);
112
+ const grants = [];
113
+ for (const row of rows) {
114
+ const mapped = mapGrantRow(row);
115
+ if (!mapped.ok)
116
+ return mapped;
117
+ grants.push(mapped.value);
118
+ }
119
+ return ok(grants);
120
+ }
121
+ catch (cause) {
122
+ return fail('Failed to list task-scoped command grants', cause);
123
+ }
124
+ }
125
+ revoke(input) {
126
+ const validation = validateRevoke(input);
127
+ if (!validation.ok)
128
+ return validation;
129
+ const existing = this.get(input.grantId);
130
+ if (!existing.ok)
131
+ return existing;
132
+ if (existing.value.status !== 'active')
133
+ return validationFailure(`Grant is not active: ${existing.value.status}`);
134
+ const result = this.db.transaction(() => {
135
+ const revokedAt = input.now ?? new Date().toISOString();
136
+ const update = this.db.connection
137
+ .prepare(`
138
+ UPDATE task_scoped_command_grants
139
+ SET status='revoked', revoked_by_json=@revokedByJson, revoked_at=@revokedAt, revoke_reason=@reason
140
+ WHERE id=@id AND status='active'
141
+ `)
142
+ .run({ id: input.grantId, revokedByJson: JSON.stringify(input.actor), revokedAt, reason: input.reason });
143
+ if (update.changes === 0)
144
+ return validationFailure(`Grant is already inactive or changed by another writer: ${input.grantId}`);
145
+ this.insertEvent({
146
+ runId: existing.value.runId,
147
+ title: 'Task-scoped command grant revoked',
148
+ payload: {
149
+ grantId: input.grantId,
150
+ actor: { type: input.actor.type, id: input.actor.id, ...(input.actor.displayName === undefined ? {} : { displayName: input.actor.displayName }) },
151
+ reason: input.reason,
152
+ revokedAt,
153
+ },
154
+ relatedId: input.grantId,
155
+ });
156
+ return this.get(input.grantId);
157
+ });
158
+ return result.ok ? result.value : fail(result.error.message, result.error.cause, result.error.code);
159
+ }
160
+ consumeUse(input) {
161
+ if (!input.grantId.trim())
162
+ return validationFailure('Grant id is required');
163
+ if (!isIsoDate(input.now))
164
+ return validationFailure('Consume timestamp must be a valid ISO date');
165
+ const existing = this.get(input.grantId);
166
+ if (!existing.ok)
167
+ return existing;
168
+ const result = this.db.transaction(() => {
169
+ const update = this.db.connection
170
+ .prepare(`
171
+ UPDATE task_scoped_command_grants
172
+ SET
173
+ used_count = used_count + 1,
174
+ status = CASE
175
+ WHEN used_count + 1 >= max_uses THEN 'exhausted'
176
+ ELSE status
177
+ END
178
+ WHERE id = @id
179
+ AND status = 'active'
180
+ AND used_count < max_uses
181
+ AND expires_at > @now
182
+ `)
183
+ .run({ id: input.grantId, now: input.now });
184
+ if (update.changes === 0) {
185
+ this.markExpiredIfNeeded(input.grantId, input.now);
186
+ return validationFailure(`Grant cannot be consumed because it is inactive, expired, exhausted, or concurrently consumed: ${input.grantId}`);
187
+ }
188
+ const consumed = this.get(input.grantId);
189
+ if (!consumed.ok)
190
+ return consumed;
191
+ this.insertEvent({
192
+ runId: consumed.value.runId,
193
+ kind: 'evidence',
194
+ title: 'Task-scoped command grant used',
195
+ payload: {
196
+ grantId: consumed.value.id,
197
+ usedCountBefore: existing.value.usedCount,
198
+ usedCountAfter: consumed.value.usedCount,
199
+ status: consumed.value.status,
200
+ commandId: input.commandId ?? null,
201
+ canonicalCwd: input.canonicalCwd ?? null,
202
+ matchedExternalRoot: input.matchedExternalRoot ?? null,
203
+ },
204
+ relatedId: consumed.value.id,
205
+ });
206
+ return ok(consumed.value);
207
+ });
208
+ return result.ok ? result.value : fail(result.error.message, result.error.cause, result.error.code);
209
+ }
210
+ getRunId(runId) {
211
+ try {
212
+ const row = this.db.connection.prepare('SELECT id FROM runs WHERE id=?').get(runId);
213
+ return row === undefined ? missing(`Run not found: ${runId}`) : ok(row.id);
214
+ }
215
+ catch (cause) {
216
+ return fail('Failed to read run for task-scoped command grant', cause);
217
+ }
218
+ }
219
+ markExpiredIfNeeded(grantId, now) {
220
+ this.db.connection
221
+ .prepare(`
222
+ UPDATE task_scoped_command_grants
223
+ SET status='expired'
224
+ WHERE id=@id AND status='active' AND expires_at <= @now
225
+ `)
226
+ .run({ id: grantId, now });
227
+ }
228
+ insertEvent(input) {
229
+ const next = this.nextSequence(input.runId);
230
+ const event = {
231
+ id: randomUUID(),
232
+ runId: input.runId,
233
+ sequence: next,
234
+ kind: input.kind ?? 'timeline',
235
+ title: input.title,
236
+ payloadJson: JSON.stringify(input.payload),
237
+ relatedType: 'task-scoped-command-grant',
238
+ relatedId: input.relatedId,
239
+ createdAt: new Date().toISOString(),
240
+ };
241
+ this.db.connection
242
+ .prepare(`
243
+ INSERT INTO run_events(id, run_id, sequence, kind, title, payload_json, related_type, related_id, created_at)
244
+ VALUES (@id, @runId, @sequence, @kind, @title, @payloadJson, @relatedType, @relatedId, @createdAt)
245
+ `)
246
+ .run(event);
247
+ return {
248
+ id: event.id,
249
+ runId: event.runId,
250
+ sequence: event.sequence,
251
+ kind: event.kind,
252
+ title: event.title,
253
+ payload: JSON.parse(event.payloadJson),
254
+ relatedType: event.relatedType,
255
+ relatedId: event.relatedId,
256
+ createdAt: event.createdAt,
257
+ };
258
+ }
259
+ nextSequence(runId) {
260
+ const row = this.db.connection.prepare('SELECT COALESCE(MAX(sequence), 0) + 1 AS sequence FROM run_events WHERE run_id=?').get(runId);
261
+ return row.sequence;
262
+ }
263
+ }
264
+ function validateCreate(input) {
265
+ if (!input.runId.trim())
266
+ return validationFailure('Grant runId is required');
267
+ if (!input.agentId.trim())
268
+ return validationFailure('Grant agentId is required');
269
+ if (!input.taskFingerprint.trim())
270
+ return validationFailure('Grant taskFingerprint is required');
271
+ if (input.createdBy.type !== 'human')
272
+ return validationFailure('Grant creator must be a human actor');
273
+ if (!input.createdBy.id.trim())
274
+ return validationFailure('Grant creator id is required');
275
+ if (!Array.isArray(input.categories) || input.categories.length === 0)
276
+ return validationFailure('Grant categories are required');
277
+ if (new Set(input.categories).size !== input.categories.length)
278
+ return validationFailure('Grant categories must be unique');
279
+ for (const category of input.categories)
280
+ if (!grantableCategories.has(category))
281
+ return validationFailure(`Unsupported grant category: ${category}`);
282
+ if (!Array.isArray(input.externalRoots))
283
+ return validationFailure('Grant externalRoots must be an array');
284
+ if (!Array.isArray(input.canonicalExternalRoots))
285
+ return validationFailure('Grant canonicalExternalRoots must be an array');
286
+ if (input.cwdPatterns !== undefined && !Array.isArray(input.cwdPatterns))
287
+ return validationFailure('Grant cwdPatterns must be an array when provided');
288
+ const commandPolicy = validateCommandPolicy(input.commandPolicy);
289
+ if (!commandPolicy.ok)
290
+ return commandPolicy;
291
+ if (!isIsoDate(input.expiresAt))
292
+ return validationFailure('Grant expiresAt must be a valid ISO date');
293
+ if (!Number.isInteger(input.maxUses) || input.maxUses < 1)
294
+ return validationFailure('Grant maxUses must be a positive integer');
295
+ if (!input.audit.creationReason.trim())
296
+ return validationFailure('Grant audit creationReason is required');
297
+ if (!input.audit.approvedScopeSummary.trim())
298
+ return validationFailure('Grant audit approvedScopeSummary is required');
299
+ return ok(undefined);
300
+ }
301
+ function validateCommandPolicy(policy) {
302
+ if (policy.kind !== 'allowlisted-command-ids')
303
+ return validationFailure('Grant command policy must use allowlisted command ids');
304
+ if (!Array.isArray(policy.ids) || policy.ids.length === 0)
305
+ return validationFailure('Grant command policy ids are required');
306
+ if (new Set(policy.ids).size !== policy.ids.length)
307
+ return validationFailure('Grant command policy ids must be unique');
308
+ for (const id of policy.ids)
309
+ if (!id.trim())
310
+ return validationFailure('Grant command policy ids must be non-empty');
311
+ return ok(undefined);
312
+ }
313
+ function validateRevoke(input) {
314
+ if (!input.grantId.trim())
315
+ return validationFailure('Grant id is required');
316
+ if (input.actor.type !== 'human' && input.actor.type !== 'system')
317
+ return validationFailure('Grant revoke actor must be human or system');
318
+ if (!input.actor.id.trim())
319
+ return validationFailure('Grant revoke actor id is required');
320
+ if (!input.reason.trim())
321
+ return validationFailure('Grant revoke reason is required');
322
+ if (input.now !== undefined && !isIsoDate(input.now))
323
+ return validationFailure('Grant revoke timestamp must be a valid ISO date');
324
+ return ok(undefined);
325
+ }
326
+ function mapGrantRow(row) {
327
+ const status = readString(row.status);
328
+ if (!grantStatuses.has(status))
329
+ return validationFailure(`Persisted grant has unsupported status: ${status}`);
330
+ const categories = parseJsonArray(row.categories_json, isTaskScopedGrantCategory, 'categories_json');
331
+ if (!categories.ok)
332
+ return categories;
333
+ const externalRoots = parseJsonArray(row.external_roots_json, isString, 'external_roots_json');
334
+ if (!externalRoots.ok)
335
+ return externalRoots;
336
+ const canonicalExternalRoots = parseJsonArray(row.canonical_external_roots_json, isString, 'canonical_external_roots_json');
337
+ if (!canonicalExternalRoots.ok)
338
+ return canonicalExternalRoots;
339
+ const cwdPatterns = row.cwd_patterns_json === null ? ok(undefined) : parseJsonArray(row.cwd_patterns_json, isString, 'cwd_patterns_json');
340
+ if (!cwdPatterns.ok)
341
+ return cwdPatterns;
342
+ const commandPolicy = parseJsonObject(row.command_policy_json, isCommandPolicy, 'command_policy_json');
343
+ if (!commandPolicy.ok)
344
+ return commandPolicy;
345
+ const createdBy = parseJsonObject(row.created_by_json, isHumanActor, 'created_by_json');
346
+ if (!createdBy.ok)
347
+ return createdBy;
348
+ const revokedBy = row.revoked_by_json === null ? ok(undefined) : parseJsonObject(row.revoked_by_json, isRevocationActor, 'revoked_by_json');
349
+ if (!revokedBy.ok)
350
+ return revokedBy;
351
+ const audit = parseJsonObject(row.audit_json, isGrantAudit, 'audit_json');
352
+ if (!audit.ok)
353
+ return audit;
354
+ const grant = {
355
+ id: readString(row.id),
356
+ runId: readString(row.run_id),
357
+ agentId: readString(row.agent_id),
358
+ taskFingerprint: readString(row.task_fingerprint),
359
+ categories: categories.value,
360
+ externalRoots: externalRoots.value,
361
+ canonicalExternalRoots: canonicalExternalRoots.value,
362
+ commandPolicy: commandPolicy.value,
363
+ expiresAt: readString(row.expires_at),
364
+ maxUses: readNumber(row.max_uses),
365
+ usedCount: readNumber(row.used_count),
366
+ status: status,
367
+ createdBy: createdBy.value,
368
+ createdAt: readString(row.created_at),
369
+ audit: audit.value,
370
+ };
371
+ if (cwdPatterns.value !== undefined)
372
+ grant.cwdPatterns = cwdPatterns.value;
373
+ if (revokedBy.value !== undefined)
374
+ grant.revokedBy = revokedBy.value;
375
+ const revokedAt = readNullableString(row.revoked_at);
376
+ if (revokedAt !== undefined)
377
+ grant.revokedAt = revokedAt;
378
+ const revokeReason = readNullableString(row.revoke_reason);
379
+ if (revokeReason !== undefined)
380
+ grant.revokeReason = revokeReason;
381
+ return ok(grant);
382
+ }
383
+ function parseJsonArray(value, predicate, column) {
384
+ const parsed = parseJson(value, column);
385
+ if (!parsed.ok)
386
+ return parsed;
387
+ if (!Array.isArray(parsed.value) || !parsed.value.every(predicate))
388
+ return validationFailure(`Persisted grant ${column} is invalid`);
389
+ return ok(parsed.value);
390
+ }
391
+ function parseJsonObject(value, predicate, column) {
392
+ const parsed = parseJson(value, column);
393
+ if (!parsed.ok)
394
+ return parsed;
395
+ if (!predicate(parsed.value))
396
+ return validationFailure(`Persisted grant ${column} is invalid`);
397
+ return ok(parsed.value);
398
+ }
399
+ function parseJson(value, column) {
400
+ if (typeof value !== 'string')
401
+ return validationFailure(`Persisted grant ${column} is not JSON text`);
402
+ try {
403
+ return ok(JSON.parse(value));
404
+ }
405
+ catch (cause) {
406
+ return fail(`Persisted grant ${column} is invalid JSON`, cause);
407
+ }
408
+ }
409
+ function isTaskScopedGrantCategory(value) {
410
+ return typeof value === 'string' && grantableCategories.has(value);
411
+ }
412
+ function isCommandPolicy(value) {
413
+ if (!isRecord(value))
414
+ return false;
415
+ return value.kind === 'allowlisted-command-ids' && Array.isArray(value.ids) && value.ids.every(isString);
416
+ }
417
+ function isHumanActor(value) {
418
+ return isRecord(value) && value.type === 'human' && typeof value.id === 'string' && optionalString(value.displayName);
419
+ }
420
+ function isRevocationActor(value) {
421
+ return isRecord(value) && (value.type === 'human' || value.type === 'system') && typeof value.id === 'string' && optionalString(value.displayName);
422
+ }
423
+ function isGrantAudit(value) {
424
+ return isRecord(value) && typeof value.creationReason === 'string' && typeof value.approvedScopeSummary === 'string';
425
+ }
426
+ function isRecord(value) {
427
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
428
+ }
429
+ function isString(value) {
430
+ return typeof value === 'string';
431
+ }
432
+ function optionalString(value) {
433
+ return value === undefined || typeof value === 'string';
434
+ }
435
+ function readString(value) {
436
+ return typeof value === 'string' ? value : '';
437
+ }
438
+ function readNullableString(value) {
439
+ return typeof value === 'string' ? value : undefined;
440
+ }
441
+ function readNumber(value) {
442
+ return typeof value === 'number' ? value : Number(value);
443
+ }
444
+ function isIsoDate(value) {
445
+ const timestamp = Date.parse(value);
446
+ return Number.isFinite(timestamp) && new Date(timestamp).toISOString() === value;
447
+ }
448
+ function ok(value) {
449
+ return { ok: true, value };
450
+ }
451
+ function missing(message) {
452
+ return { ok: false, error: { code: 'not_found', message } };
453
+ }
454
+ function validationFailure(message) {
455
+ return { ok: false, error: { code: 'validation_failed', message } };
456
+ }
457
+ function fail(message, cause, code = 'validation_failed') {
458
+ const error = { code, message };
459
+ if (cause !== undefined)
460
+ error.cause = cause;
461
+ return { ok: false, error };
462
+ }
@@ -8,6 +8,7 @@ import { AllowlistedApplyProgressCommandExecutor } from '../workflows/command-al
8
8
  import { RunRepository, } from './repositories/runs.js';
9
9
  import { buildRunInsights, buildRunOperatorResumePlan } from './run-insights.js';
10
10
  import { buildResumeAfterApprovalResult } from './resume-after-approval-result.js';
11
+ import { grantEvaluationPayload, TaskScopedCommandGrantService } from './task-scoped-command-grant-service.js';
11
12
  const preflightSafety = {
12
13
  operationExecuted: false,
13
14
  executorInvoked: false,
@@ -28,8 +29,10 @@ const resumeGateSafety = {
28
29
  };
29
30
  export class RunService {
30
31
  runs;
32
+ taskScopedCommandGrants;
31
33
  constructor(database) {
32
34
  this.runs = new RunRepository(database);
35
+ this.taskScopedCommandGrants = new TaskScopedCommandGrantService(database);
33
36
  }
34
37
  createRun(input) {
35
38
  return this.runs.create({ ...input, phase: canonicalRunPhase(input.workflow, input.phase) ?? input.phase });
@@ -164,6 +167,9 @@ export class RunService {
164
167
  resolveApproval(input) {
165
168
  return this.runs.resolveApproval(input);
166
169
  }
170
+ taskScopedCommandGrantService() {
171
+ return this.taskScopedCommandGrants;
172
+ }
167
173
  abandonReservedOperationAttempt(input) {
168
174
  return this.runs.abandonOperationAttempt(input);
169
175
  }
@@ -781,6 +787,42 @@ export class RunService {
781
787
  : { ok: false, error: event.error };
782
788
  }
783
789
  }
790
+ const grantAuthorization = decision.decision === 'ask' ? this.taskScopedCommandGrants.tryAuthorizeAsk({ run: run.value, request: inputWithRunContext, baseDecision: decision }) : undefined;
791
+ if (grantAuthorization !== undefined) {
792
+ if (!grantAuthorization.ok)
793
+ return grantAuthorization;
794
+ if (grantAuthorization.value.authorized) {
795
+ const grantedDecision = grantAuthorization.value.decision;
796
+ const event = this.runs.appendEvent({
797
+ runId: inputWithRunContext.runId,
798
+ kind: 'permission-decision',
799
+ title: `Permission ${grantedDecision.decision}: ${inputWithRunContext.category} ${inputWithRunContext.operation}`,
800
+ payload: {
801
+ runId: inputWithRunContext.runId,
802
+ category: inputWithRunContext.category,
803
+ operation: inputWithRunContext.operation,
804
+ workflow: inputWithRunContext.workflow ?? null,
805
+ phase: inputWithRunContext.phase ?? null,
806
+ requestedOperation: operationMetadata(inputWithRunContext),
807
+ agent,
808
+ decision: grantedDecision.decision,
809
+ reasons: [{ code: grantedDecision.reason, message: grantedDecision.message }],
810
+ riskTier: grantedDecision.riskTier ?? null,
811
+ riskReasonCodes: grantedDecision.riskReasonCodes ?? [],
812
+ boundary: (grantedDecision.boundary ?? null),
813
+ auditEvidence: grantedDecision.auditEvidence ?? [],
814
+ requiresHumanApproval: false,
815
+ approvalStatus: 'not-required',
816
+ grantEvaluation: grantEvaluationPayload(grantAuthorization.value.evaluation),
817
+ grantUseEventId: grantAuthorization.value.event.id,
818
+ timestamp,
819
+ },
820
+ relatedType: 'task-scoped-command-grant',
821
+ relatedId: grantAuthorization.value.grant.id,
822
+ });
823
+ return event.ok ? { ok: true, value: { decision: grantedDecision, event: event.value } } : { ok: false, error: event.error };
824
+ }
825
+ }
784
826
  const event = this.runs.appendEvent({
785
827
  runId: inputWithRunContext.runId,
786
828
  kind: 'permission-decision',
@@ -801,6 +843,7 @@ export class RunService {
801
843
  auditEvidence: decision.auditEvidence ?? [],
802
844
  requiresHumanApproval: decision.decision === 'ask',
803
845
  approvalStatus: decision.decision === 'ask' ? 'pending' : 'not-required',
846
+ ...(grantAuthorization === undefined ? {} : { grantEvaluation: grantEvaluationPayload(grantAuthorization.value.evaluation) }),
804
847
  timestamp,
805
848
  },
806
849
  relatedType: 'permission',
@@ -1215,6 +1258,14 @@ function operationMetadata(input) {
1215
1258
  metadata.phase = input.phase;
1216
1259
  if (input.agentId !== undefined)
1217
1260
  metadata.agentId = input.agentId;
1261
+ if (input.taskFingerprint !== undefined)
1262
+ metadata.taskFingerprint = input.taskFingerprint;
1263
+ if (input.commandId !== undefined)
1264
+ metadata.commandId = input.commandId;
1265
+ if (input.cwd !== undefined)
1266
+ metadata.cwd = input.cwd;
1267
+ if (input.targetPaths !== undefined)
1268
+ metadata.targetPaths = input.targetPaths;
1218
1269
  if (input.destructive !== undefined)
1219
1270
  metadata.destructive = input.destructive;
1220
1271
  if (input.external !== undefined)
@@ -1253,7 +1304,7 @@ function operationMetadataCompatible(preflightOperation, requestedOperation) {
1253
1304
  return jsonEqual(preflightOperation, requestedOperation);
1254
1305
  if (preflightOperation.category !== requestedOperation.category || preflightOperation.name !== requestedOperation.name)
1255
1306
  return false;
1256
- const strictKeys = ['workspaceRoot', 'targetPath', 'providerToolName', 'sandboxStrategy', 'destructive', 'external', 'privileged', 'ambiguous'];
1307
+ const strictKeys = ['workspaceRoot', 'targetPath', 'providerToolName', 'sandboxStrategy', 'taskFingerprint', 'commandId', 'cwd', 'targetPaths', 'destructive', 'external', 'privileged', 'ambiguous'];
1257
1308
  for (const key of strictKeys) {
1258
1309
  if (key in preflightOperation && key in requestedOperation && !jsonEqual(preflightOperation[key], requestedOperation[key]))
1259
1310
  return false;
@@ -1336,6 +1387,14 @@ function operationFromPendingExecution(payload) {
1336
1387
  operation.phase = requested.phase;
1337
1388
  if (typeof requested.agentId === 'string')
1338
1389
  operation.agentId = requested.agentId;
1390
+ if (typeof requested.taskFingerprint === 'string')
1391
+ operation.taskFingerprint = requested.taskFingerprint;
1392
+ if (typeof requested.commandId === 'string')
1393
+ operation.commandId = requested.commandId;
1394
+ if (typeof requested.cwd === 'string')
1395
+ operation.cwd = requested.cwd;
1396
+ if (Array.isArray(requested.targetPaths) && requested.targetPaths.every((item) => typeof item === 'string'))
1397
+ operation.targetPaths = requested.targetPaths;
1339
1398
  if (typeof requested.destructive === 'boolean')
1340
1399
  operation.destructive = requested.destructive;
1341
1400
  if (typeof requested.external === 'boolean')