vgxness 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +9 -0
- package/README.md +110 -0
- package/dist/agents/agent-activation-service.js +144 -0
- package/dist/agents/agent-registry-service.js +46 -0
- package/dist/agents/agent-resolver.js +249 -0
- package/dist/agents/agent-seed-service.js +146 -0
- package/dist/agents/manager-profile-overlay-service.js +34 -0
- package/dist/agents/profile-model-routing.js +26 -0
- package/dist/agents/renderers/claude-renderer.js +98 -0
- package/dist/agents/renderers/index.js +16 -0
- package/dist/agents/renderers/json-renderer.js +87 -0
- package/dist/agents/renderers/opencode-renderer.js +100 -0
- package/dist/agents/renderers/provider-adapter.js +6 -0
- package/dist/agents/repositories/agents.js +185 -0
- package/dist/agents/repositories/manager-profile-overlays.js +81 -0
- package/dist/agents/schema.js +1 -0
- package/dist/cli/dashboard-operational-read-models.js +153 -0
- package/dist/cli/dashboard-renderer.js +109 -0
- package/dist/cli/dashboard-screen-renderers.js +332 -0
- package/dist/cli/dashboard-tui-read-model.js +71 -0
- package/dist/cli/dashboard-tui-state.js +218 -0
- package/dist/cli/dispatcher.js +2880 -0
- package/dist/cli/index.js +27 -0
- package/dist/cli/interactive-dashboard.js +29 -0
- package/dist/cli/mcp-start-path.js +21 -0
- package/dist/cli/setup-status-renderer.js +29 -0
- package/dist/cli/setup-wizard-read-model.js +56 -0
- package/dist/cli/setup-wizard-renderer.js +148 -0
- package/dist/cli/setup-wizard-state.js +82 -0
- package/dist/cli/tui-render-helpers.js +192 -0
- package/dist/export/redaction.js +71 -0
- package/dist/harness/tools/agents.js +245 -0
- package/dist/harness/tools/memory.js +29 -0
- package/dist/mcp/client-install-opencode-contract.js +227 -0
- package/dist/mcp/client-install-opencode.js +194 -0
- package/dist/mcp/client-setup-preview.js +38 -0
- package/dist/mcp/control-plane.js +175 -0
- package/dist/mcp/doctor.js +193 -0
- package/dist/mcp/index.js +10 -0
- package/dist/mcp/opencode-default-agent-config.js +156 -0
- package/dist/mcp/opencode-visibility.js +102 -0
- package/dist/mcp/schema.js +234 -0
- package/dist/mcp/stdio-server.js +56 -0
- package/dist/mcp/validation.js +761 -0
- package/dist/memory/import/dry-run-planner.js +58 -0
- package/dist/memory/import/index.js +3 -0
- package/dist/memory/import/observation-writer.js +220 -0
- package/dist/memory/import/package.js +178 -0
- package/dist/memory/memory-service.js +126 -0
- package/dist/memory/repositories/artifacts.js +41 -0
- package/dist/memory/repositories/observations.js +133 -0
- package/dist/memory/repositories/sessions.js +105 -0
- package/dist/memory/repositories/traces.js +58 -0
- package/dist/memory/schema.js +1 -0
- package/dist/memory/search.js +11 -0
- package/dist/memory/sqlite/database.js +97 -0
- package/dist/memory/sqlite/migrations/001_initial.sql +128 -0
- package/dist/memory/sqlite/migrations/002_observation_revisions.sql +14 -0
- package/dist/memory/sqlite/migrations/003_agent_registry.sql +26 -0
- package/dist/memory/sqlite/migrations/004_run_runtime.sql +62 -0
- package/dist/memory/sqlite/migrations/005_run_approvals.sql +20 -0
- package/dist/memory/sqlite/migrations/006_run_operation_attempts.sql +32 -0
- package/dist/memory/sqlite/migrations/007_abandoned_operation_attempts.sql +46 -0
- package/dist/memory/sqlite/migrations/008_run_execution_plan_events.sql +105 -0
- package/dist/memory/sqlite/migrations/009_multiple_operation_attempts.sql +73 -0
- package/dist/memory/sqlite/migrations/010_skill_registry.sql +66 -0
- package/dist/memory/sqlite/migrations/011_skill_usage_resolution_outcomes.sql +21 -0
- package/dist/memory/sqlite/migrations/012_skill_improvement_proposals.sql +37 -0
- package/dist/memory/sqlite/migrations/013_skill_evaluation_scenarios.sql +43 -0
- package/dist/memory/sqlite/migrations/014_manager_profile_overlays.sql +14 -0
- package/dist/memory/storage-paths.js +72 -0
- package/dist/orchestrator/natural-language-planner.js +191 -0
- package/dist/orchestrator/schema.js +1 -0
- package/dist/permissions/index.js +2 -0
- package/dist/permissions/policy-evaluator.js +109 -0
- package/dist/permissions/schema.js +1 -0
- package/dist/providers/opencode/injection-preview.js +134 -0
- package/dist/providers/opencode/manager-payload.js +129 -0
- package/dist/runs/execution-planning.js +117 -0
- package/dist/runs/operation-execution.js +1 -0
- package/dist/runs/operation-retry.js +124 -0
- package/dist/runs/repositories/runs.js +611 -0
- package/dist/runs/run-insights.js +145 -0
- package/dist/runs/run-service.js +713 -0
- package/dist/runs/run-snapshot-export-service.js +31 -0
- package/dist/runs/sandbox-process-execution.js +218 -0
- package/dist/runs/sandbox-worktree-planning.js +59 -0
- package/dist/runs/schema.js +1 -0
- package/dist/sdd/artifact-portability-service.js +118 -0
- package/dist/sdd/schema.js +17 -0
- package/dist/sdd/sdd-workflow-service.js +217 -0
- package/dist/setup/backup-rollback-service.js +76 -0
- package/dist/setup/index.js +3 -0
- package/dist/setup/providers/antigravity-setup-adapter.js +18 -0
- package/dist/setup/providers/claude-setup-adapter.js +30 -0
- package/dist/setup/providers/custom-setup-adapter.js +18 -0
- package/dist/setup/providers/index.js +6 -0
- package/dist/setup/providers/opencode-setup-adapter.js +104 -0
- package/dist/setup/providers/provider-setup-adapter.js +15 -0
- package/dist/setup/providers/provider-setup-registry.js +11 -0
- package/dist/setup/schema.js +1 -0
- package/dist/setup/setup-defaults.js +11 -0
- package/dist/setup/setup-lifecycle-service.js +175 -0
- package/dist/setup/setup-plan.js +105 -0
- package/dist/skills/repositories/skill-evaluation-scenarios.js +289 -0
- package/dist/skills/repositories/skill-improvement-proposals.js +288 -0
- package/dist/skills/repositories/skills.js +430 -0
- package/dist/skills/schema.js +1 -0
- package/dist/skills/skill-payload.js +94 -0
- package/dist/skills/skill-registry-service.js +92 -0
- package/dist/skills/skill-resolver.js +191 -0
- package/dist/workflows/command-allowlist-adapter.js +70 -0
- package/dist/workflows/schema.js +4 -0
- package/dist/workflows/workflow-executor.js +345 -0
- package/dist/workflows/workflow-registry.js +66 -0
- package/docs/architecture.md +698 -0
- package/docs/cli.md +741 -0
- package/docs/funcionamiento-del-sistema.md +868 -0
- package/docs/harness-gap-analysis.md +229 -0
- package/docs/prd.md +372 -0
- package/package.json +57 -0
|
@@ -0,0 +1,713 @@
|
|
|
1
|
+
import { evaluatePermission } from '../permissions/policy-evaluator.js';
|
|
2
|
+
import { planExecutionIsolation } from './execution-planning.js';
|
|
3
|
+
import { evaluateOperationRetry as evaluateOperationRetryPolicy } from './operation-retry.js';
|
|
4
|
+
import { RunRepository } from './repositories/runs.js';
|
|
5
|
+
import { buildRunInsights, buildRunOperatorResumePlan } from './run-insights.js';
|
|
6
|
+
const preflightSafety = {
|
|
7
|
+
operationExecuted: false,
|
|
8
|
+
executorInvoked: false,
|
|
9
|
+
executesProvider: false,
|
|
10
|
+
writesProviderConfig: false,
|
|
11
|
+
createsSandbox: false,
|
|
12
|
+
createsWorktree: false,
|
|
13
|
+
};
|
|
14
|
+
const resumeGateSafety = {
|
|
15
|
+
readOnly: true,
|
|
16
|
+
preflightExecuted: false,
|
|
17
|
+
retryAdmitted: false,
|
|
18
|
+
executorInvoked: false,
|
|
19
|
+
operationExecuted: false,
|
|
20
|
+
writesProviderConfig: false,
|
|
21
|
+
createsSandbox: false,
|
|
22
|
+
createsWorktree: false,
|
|
23
|
+
};
|
|
24
|
+
export class RunService {
|
|
25
|
+
runs;
|
|
26
|
+
constructor(database) {
|
|
27
|
+
this.runs = new RunRepository(database);
|
|
28
|
+
}
|
|
29
|
+
createRun(input) { return this.runs.create(input); }
|
|
30
|
+
getRun(id) { return this.runs.getDetails(id); }
|
|
31
|
+
getRunInsights(id) {
|
|
32
|
+
const details = this.runs.getDetails(id);
|
|
33
|
+
return details.ok ? { ok: true, value: buildRunInsights(details.value) } : details;
|
|
34
|
+
}
|
|
35
|
+
getRunOperatorResumePlan(id) {
|
|
36
|
+
const details = this.runs.getDetails(id);
|
|
37
|
+
return details.ok ? { ok: true, value: buildRunOperatorResumePlan(details.value) } : details;
|
|
38
|
+
}
|
|
39
|
+
listRuns(filters = {}) { return this.runs.list(filters); }
|
|
40
|
+
appendEvent(input) { return this.runs.appendEvent(input); }
|
|
41
|
+
appendEvidence(input) { return this.runs.appendEvent({ ...input, kind: 'evidence' }); }
|
|
42
|
+
appendCheckpoint(input) { return this.runs.appendCheckpoint(input); }
|
|
43
|
+
updateFinalStatus(input) { return this.runs.updateFinalStatus(input); }
|
|
44
|
+
listApprovals(runId) { return this.runs.listApprovals(runId); }
|
|
45
|
+
listOperationAttempts(runId) { return this.runs.listOperationAttempts(runId); }
|
|
46
|
+
getApproval(id) { return this.runs.getApproval(id); }
|
|
47
|
+
resolveApproval(input) { return this.runs.resolveApproval(input); }
|
|
48
|
+
abandonReservedOperationAttempt(input) { return this.runs.abandonOperationAttempt(input); }
|
|
49
|
+
evaluateOperationRetry(input) {
|
|
50
|
+
const approval = this.runs.getApproval(input.approvalId);
|
|
51
|
+
if (!approval.ok)
|
|
52
|
+
return { ok: false, error: approval.error };
|
|
53
|
+
const details = this.runs.getDetails(approval.value.runId);
|
|
54
|
+
if (!details.ok)
|
|
55
|
+
return { ok: false, error: details.error };
|
|
56
|
+
const attempts = details.value.operationAttempts.filter((attempt) => attempt.approvalId === input.approvalId);
|
|
57
|
+
return {
|
|
58
|
+
ok: true,
|
|
59
|
+
value: evaluateOperationRetryPolicy(input.policy === undefined ? { attempts } : { attempts, policy: input.policy }),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
getRunResumeOrchestrationPlan(input) {
|
|
63
|
+
const approval = this.runs.getApproval(input.approvalId);
|
|
64
|
+
if (!approval.ok)
|
|
65
|
+
return approval;
|
|
66
|
+
const details = this.runs.getDetails(approval.value.runId);
|
|
67
|
+
if (!details.ok)
|
|
68
|
+
return details;
|
|
69
|
+
const blockers = [];
|
|
70
|
+
if (approval.value.status !== 'approved') {
|
|
71
|
+
blockers.push({
|
|
72
|
+
code: 'approval-not-approved',
|
|
73
|
+
message: `Approval must be approved before manual continuation guidance is available: ${approval.value.status}`,
|
|
74
|
+
relatedId: approval.value.id,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
const permissionEvent = details.value.events.find((event) => event.id === approval.value.decisionEventId);
|
|
78
|
+
if (permissionEvent === undefined || !isAskPermissionEvent(permissionEvent)) {
|
|
79
|
+
blockers.push({
|
|
80
|
+
code: 'missing-permission-event',
|
|
81
|
+
message: 'Approval permission-decision event is missing or is not an ask decision',
|
|
82
|
+
relatedId: approval.value.decisionEventId,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
const pendingExecutionEvent = details.value.events.find((event) => isPendingExecutionEventForApproval(event, approval.value.id));
|
|
86
|
+
let operation;
|
|
87
|
+
if (pendingExecutionEvent === undefined) {
|
|
88
|
+
blockers.push({
|
|
89
|
+
code: 'missing-pending-operation',
|
|
90
|
+
message: 'Approval has no pending operation-execution event to reconstruct',
|
|
91
|
+
relatedId: approval.value.id,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
operation = operationFromPendingExecution(pendingExecutionEvent.payload);
|
|
96
|
+
if (operation === undefined) {
|
|
97
|
+
blockers.push({
|
|
98
|
+
code: 'operation-metadata-incomplete',
|
|
99
|
+
message: 'Pending operation metadata is incomplete and cannot be planned',
|
|
100
|
+
relatedId: pendingExecutionEvent.id,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const retryDecision = operation === undefined
|
|
105
|
+
? null
|
|
106
|
+
: evaluateOperationRetryPolicy(input.policy === undefined
|
|
107
|
+
? { attempts: details.value.operationAttempts.filter((attempt) => attempt.approvalId === approval.value.id) }
|
|
108
|
+
: { attempts: details.value.operationAttempts.filter((attempt) => attempt.approvalId === approval.value.id), policy: input.policy });
|
|
109
|
+
if (retryDecision !== null && !retryDecision.allowed) {
|
|
110
|
+
const relatedId = retryDecision.latestAttempt?.id ?? retryDecision.activeAttempt?.id;
|
|
111
|
+
blockers.push({
|
|
112
|
+
code: 'retry-blocked',
|
|
113
|
+
message: retryDecision.reason,
|
|
114
|
+
...(relatedId === undefined ? {} : { relatedId }),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
ok: true,
|
|
119
|
+
value: {
|
|
120
|
+
kind: 'run-resume-orchestration-plan',
|
|
121
|
+
version: 1,
|
|
122
|
+
run: {
|
|
123
|
+
id: details.value.id,
|
|
124
|
+
project: details.value.project,
|
|
125
|
+
status: details.value.status,
|
|
126
|
+
workflow: details.value.workflow,
|
|
127
|
+
phase: details.value.phase,
|
|
128
|
+
},
|
|
129
|
+
approval: {
|
|
130
|
+
id: approval.value.id,
|
|
131
|
+
runId: approval.value.runId,
|
|
132
|
+
status: approval.value.status,
|
|
133
|
+
decisionEventId: approval.value.decisionEventId,
|
|
134
|
+
},
|
|
135
|
+
operation: operation === undefined ? null : summarizeOperation(operation),
|
|
136
|
+
retryDecision,
|
|
137
|
+
blockers,
|
|
138
|
+
nextAction: resumeGateNextAction(approval.value, details.value, blockers),
|
|
139
|
+
manualNextCommands: resumeGateManualNextCommands(approval.value, details.value, operation),
|
|
140
|
+
safety: resumeGateSafety,
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
admitOperationRetryAttempt(input) {
|
|
145
|
+
const approval = this.runs.getApproval(input.approvalId);
|
|
146
|
+
if (!approval.ok)
|
|
147
|
+
return approval;
|
|
148
|
+
if (approval.value.status !== 'approved')
|
|
149
|
+
return validationFailure(`Only approved approvals can admit retry attempts: ${approval.value.status}`);
|
|
150
|
+
const details = this.runs.getDetails(approval.value.runId);
|
|
151
|
+
if (!details.ok)
|
|
152
|
+
return details;
|
|
153
|
+
const permissionEvent = details.value.events.find((event) => event.id === approval.value.decisionEventId);
|
|
154
|
+
if (permissionEvent === undefined)
|
|
155
|
+
return validationFailure('Approval permission-decision event is missing');
|
|
156
|
+
if (!isAskPermissionEvent(permissionEvent))
|
|
157
|
+
return validationFailure('Approval is not linked to an ask permission decision');
|
|
158
|
+
const pendingExecutionEvent = details.value.events.find((event) => isPendingExecutionEventForApproval(event, approval.value.id));
|
|
159
|
+
if (pendingExecutionEvent === undefined)
|
|
160
|
+
return validationFailure('Approved approval has no retryable pending operation event');
|
|
161
|
+
const operation = operationFromPendingExecution(pendingExecutionEvent.payload);
|
|
162
|
+
if (operation === undefined)
|
|
163
|
+
return validationFailure('Pending operation metadata is incomplete and cannot be retried');
|
|
164
|
+
const attempts = details.value.operationAttempts.filter((attempt) => attempt.approvalId === approval.value.id);
|
|
165
|
+
const retryDecision = evaluateOperationRetryPolicy(input.policy === undefined ? { attempts } : { attempts, policy: input.policy });
|
|
166
|
+
if (!retryDecision.allowed || retryDecision.reasonCode !== 'status_allowed_by_policy')
|
|
167
|
+
return validationFailure(retryDecision.reason);
|
|
168
|
+
const reserved = this.runs.reserveOperationAttempt({
|
|
169
|
+
runId: approval.value.runId,
|
|
170
|
+
approvalId: approval.value.id,
|
|
171
|
+
decisionEventId: permissionEvent.id,
|
|
172
|
+
pendingExecutionEventId: pendingExecutionEvent.id,
|
|
173
|
+
category: operation.category,
|
|
174
|
+
operation: operation.operation,
|
|
175
|
+
operationMetadata: operationMetadata(operation),
|
|
176
|
+
executorName: input.executorName ?? 'retry-admission',
|
|
177
|
+
});
|
|
178
|
+
if (!reserved.ok)
|
|
179
|
+
return reserved;
|
|
180
|
+
const admissionEvent = this.runs.appendEvent({
|
|
181
|
+
runId: approval.value.runId,
|
|
182
|
+
kind: 'operation-execution',
|
|
183
|
+
title: `Retry attempt admitted: ${operation.category} ${operation.operation}`,
|
|
184
|
+
payload: executionPayload(operation, reserved.value.executorName, 'pending-approval', {
|
|
185
|
+
admissionStatus: 'reserved',
|
|
186
|
+
retryAdmission: true,
|
|
187
|
+
attemptId: reserved.value.id,
|
|
188
|
+
attemptSequence: reserved.value.attemptSequence,
|
|
189
|
+
approvalId: approval.value.id,
|
|
190
|
+
decisionEventId: permissionEvent.id,
|
|
191
|
+
originalPendingExecutionEventId: pendingExecutionEvent.id,
|
|
192
|
+
retryPolicy: retryDecision.policy,
|
|
193
|
+
retryReasonCode: retryDecision.reasonCode,
|
|
194
|
+
retryReason: retryDecision.reason,
|
|
195
|
+
evaluatedAttemptCount: retryDecision.evaluatedAttemptCount,
|
|
196
|
+
retryableStatuses: [...retryDecision.retryableStatuses],
|
|
197
|
+
executorInvoked: false,
|
|
198
|
+
operationExecuted: false,
|
|
199
|
+
}),
|
|
200
|
+
relatedType: 'operation-attempt',
|
|
201
|
+
relatedId: reserved.value.id,
|
|
202
|
+
});
|
|
203
|
+
if (!admissionEvent.ok)
|
|
204
|
+
return { ok: false, error: admissionEvent.error };
|
|
205
|
+
return { ok: true, value: { approval: approval.value, retryDecision, permissionEvent, pendingExecutionEvent, attempt: reserved.value, admissionEvent: admissionEvent.value } };
|
|
206
|
+
}
|
|
207
|
+
planOperationExecution(input) {
|
|
208
|
+
const permission = this.evaluatePermissionForRun(input);
|
|
209
|
+
if (!permission.ok)
|
|
210
|
+
return permission;
|
|
211
|
+
const plan = planExecutionIsolation({
|
|
212
|
+
operation: input,
|
|
213
|
+
decision: permission.value.decision,
|
|
214
|
+
...(input.gitBoundaryInspector === undefined ? {} : { gitBoundaryInspector: input.gitBoundaryInspector }),
|
|
215
|
+
});
|
|
216
|
+
const sandboxDecision = sandboxDecisionSummary(plan.sandbox);
|
|
217
|
+
const planEvent = this.runs.appendEvent({
|
|
218
|
+
runId: input.runId,
|
|
219
|
+
kind: 'execution-plan',
|
|
220
|
+
title: `Execution plan: ${input.category} ${input.operation}`,
|
|
221
|
+
payload: {
|
|
222
|
+
runId: input.runId,
|
|
223
|
+
requestedOperation: operationMetadata(input),
|
|
224
|
+
decisionEventId: permission.value.event.id,
|
|
225
|
+
approvalId: permission.value.approval?.id ?? null,
|
|
226
|
+
plan: plan,
|
|
227
|
+
...(sandboxDecision === undefined ? {} : { sandboxDecision: sandboxDecision }),
|
|
228
|
+
operationExecuted: false,
|
|
229
|
+
executorInvoked: false,
|
|
230
|
+
executesProvider: false,
|
|
231
|
+
writesProviderConfig: false,
|
|
232
|
+
createsSandbox: false,
|
|
233
|
+
createsWorktree: false,
|
|
234
|
+
timestamp: new Date().toISOString(),
|
|
235
|
+
},
|
|
236
|
+
relatedType: 'operation',
|
|
237
|
+
relatedId: input.operation,
|
|
238
|
+
});
|
|
239
|
+
if (!planEvent.ok)
|
|
240
|
+
return { ok: false, error: planEvent.error };
|
|
241
|
+
return { ok: true, value: { ...permission.value, plan, planEvent: planEvent.value } };
|
|
242
|
+
}
|
|
243
|
+
preflightOperation(input) {
|
|
244
|
+
const planned = this.planOperationExecution(input);
|
|
245
|
+
if (!planned.ok)
|
|
246
|
+
return planned;
|
|
247
|
+
const { decision, event, approval, plan, planEvent } = planned.value;
|
|
248
|
+
const audit = { permissionEventId: event.id, planEventId: planEvent.id };
|
|
249
|
+
if (approval !== undefined)
|
|
250
|
+
audit.approvalId = approval.id;
|
|
251
|
+
const sandbox = plan.sandbox;
|
|
252
|
+
const sandboxRejected = sandbox?.decision === 'rejected';
|
|
253
|
+
const sandboxReason = sandboxRejected
|
|
254
|
+
? [{ code: sandbox.reason ?? 'sandbox_boundary', message: sandbox.audit.validationSummary }]
|
|
255
|
+
: [];
|
|
256
|
+
return {
|
|
257
|
+
ok: true,
|
|
258
|
+
value: {
|
|
259
|
+
outcome: sandboxRejected ? 'blocked' : preflightOutcome(decision),
|
|
260
|
+
decision,
|
|
261
|
+
reasons: [...sandboxReason, { code: decision.reason, message: decision.message }],
|
|
262
|
+
permissionEvent: event,
|
|
263
|
+
...(approval === undefined ? {} : { approval }),
|
|
264
|
+
plan,
|
|
265
|
+
planEvent,
|
|
266
|
+
audit,
|
|
267
|
+
eligibleForFutureExecution: plan.executable && !sandboxRejected,
|
|
268
|
+
safety: preflightSafety,
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
resumeApprovedOperation(input) {
|
|
273
|
+
const approval = this.runs.getApproval(input.approvalId);
|
|
274
|
+
if (!approval.ok)
|
|
275
|
+
return approval;
|
|
276
|
+
if (approval.value.status !== 'approved')
|
|
277
|
+
return validationFailure(`Only approved approvals can be resumed: ${approval.value.status}`);
|
|
278
|
+
const details = this.runs.getDetails(approval.value.runId);
|
|
279
|
+
if (!details.ok)
|
|
280
|
+
return details;
|
|
281
|
+
const permissionEvent = details.value.events.find((event) => event.id === approval.value.decisionEventId);
|
|
282
|
+
if (permissionEvent === undefined)
|
|
283
|
+
return validationFailure('Approval permission-decision event is missing');
|
|
284
|
+
if (!isAskPermissionEvent(permissionEvent))
|
|
285
|
+
return validationFailure('Approval is not linked to an ask permission decision');
|
|
286
|
+
const pendingExecutionEvent = details.value.events.find((event) => isPendingExecutionEventForApproval(event, approval.value.id));
|
|
287
|
+
if (pendingExecutionEvent === undefined)
|
|
288
|
+
return validationFailure('Approved approval has no resumable pending operation event');
|
|
289
|
+
const operation = operationFromPendingExecution(pendingExecutionEvent.payload);
|
|
290
|
+
if (operation === undefined)
|
|
291
|
+
return validationFailure('Pending operation metadata is incomplete and cannot be resumed');
|
|
292
|
+
const retry = this.evaluateOperationRetry({ approvalId: approval.value.id });
|
|
293
|
+
if (!retry.ok)
|
|
294
|
+
return retry;
|
|
295
|
+
if (!retry.value.allowed)
|
|
296
|
+
return validationFailure(retry.value.reason);
|
|
297
|
+
const reserved = this.runs.reserveOperationAttempt({
|
|
298
|
+
runId: approval.value.runId,
|
|
299
|
+
approvalId: approval.value.id,
|
|
300
|
+
decisionEventId: permissionEvent.id,
|
|
301
|
+
pendingExecutionEventId: pendingExecutionEvent.id,
|
|
302
|
+
category: operation.category,
|
|
303
|
+
operation: operation.operation,
|
|
304
|
+
operationMetadata: operationMetadata(operation),
|
|
305
|
+
executorName: input.executor.name,
|
|
306
|
+
});
|
|
307
|
+
if (!reserved.ok)
|
|
308
|
+
return reserved;
|
|
309
|
+
const executed = input.executor.execute({ runId: approval.value.runId, operation });
|
|
310
|
+
if (!executed.ok) {
|
|
311
|
+
const error = { code: executed.error.code, message: executed.error.message };
|
|
312
|
+
const attempt = this.runs.completeOperationAttempt({ attemptId: reserved.value.id, status: 'failed', error });
|
|
313
|
+
if (!attempt.ok)
|
|
314
|
+
return attempt;
|
|
315
|
+
const executionEvent = this.runs.appendEvent({
|
|
316
|
+
runId: approval.value.runId,
|
|
317
|
+
kind: 'operation-execution',
|
|
318
|
+
title: `Approved operation failed: ${operation.category} ${operation.operation}`,
|
|
319
|
+
payload: executionPayload(operation, input.executor.name, 'failed', {
|
|
320
|
+
attemptId: attempt.value.id,
|
|
321
|
+
decisionEventId: permissionEvent.id,
|
|
322
|
+
approvalId: approval.value.id,
|
|
323
|
+
originalPendingExecutionEventId: pendingExecutionEvent.id,
|
|
324
|
+
originalDecision: 'ask',
|
|
325
|
+
approvalStatus: 'approved',
|
|
326
|
+
policyReevaluated: false,
|
|
327
|
+
resumedFromApprovalId: approval.value.id,
|
|
328
|
+
executorInvoked: true,
|
|
329
|
+
error,
|
|
330
|
+
}),
|
|
331
|
+
relatedType: 'approval',
|
|
332
|
+
relatedId: approval.value.id,
|
|
333
|
+
});
|
|
334
|
+
if (!executionEvent.ok)
|
|
335
|
+
return { ok: false, error: executionEvent.error };
|
|
336
|
+
const linkedAttempt = this.runs.attachAttemptResultEvent(attempt.value.id, executionEvent.value.id);
|
|
337
|
+
return { ok: true, value: { approval: approval.value, permissionEvent, pendingExecutionEvent, attempt: linkedAttempt.ok ? linkedAttempt.value : attempt.value, executionEvent: executionEvent.value, status: 'failed' } };
|
|
338
|
+
}
|
|
339
|
+
const output = executed.value.output;
|
|
340
|
+
const attempt = this.runs.completeOperationAttempt({ attemptId: reserved.value.id, status: 'succeeded', ...(output === undefined ? {} : { output }) });
|
|
341
|
+
if (!attempt.ok)
|
|
342
|
+
return attempt;
|
|
343
|
+
const executionEvent = this.runs.appendEvent({
|
|
344
|
+
runId: approval.value.runId,
|
|
345
|
+
kind: 'operation-execution',
|
|
346
|
+
title: `Approved operation executed: ${operation.category} ${operation.operation}`,
|
|
347
|
+
payload: executionPayload(operation, input.executor.name, 'succeeded', {
|
|
348
|
+
attemptId: attempt.value.id,
|
|
349
|
+
decisionEventId: permissionEvent.id,
|
|
350
|
+
approvalId: approval.value.id,
|
|
351
|
+
originalPendingExecutionEventId: pendingExecutionEvent.id,
|
|
352
|
+
originalDecision: 'ask',
|
|
353
|
+
approvalStatus: 'approved',
|
|
354
|
+
policyReevaluated: false,
|
|
355
|
+
resumedFromApprovalId: approval.value.id,
|
|
356
|
+
executorInvoked: true,
|
|
357
|
+
output: output ?? null,
|
|
358
|
+
}),
|
|
359
|
+
relatedType: 'approval',
|
|
360
|
+
relatedId: approval.value.id,
|
|
361
|
+
});
|
|
362
|
+
if (!executionEvent.ok)
|
|
363
|
+
return { ok: false, error: executionEvent.error };
|
|
364
|
+
const linkedAttempt = this.runs.attachAttemptResultEvent(attempt.value.id, executionEvent.value.id);
|
|
365
|
+
return { ok: true, value: { approval: approval.value, permissionEvent, pendingExecutionEvent, attempt: linkedAttempt.ok ? linkedAttempt.value : attempt.value, executionEvent: executionEvent.value, status: 'succeeded', ...(output === undefined ? {} : { output }) } };
|
|
366
|
+
}
|
|
367
|
+
executeOperation(input) {
|
|
368
|
+
const permission = this.evaluatePermissionForRun({ runId: input.runId, ...input.operation });
|
|
369
|
+
if (!permission.ok)
|
|
370
|
+
return permission;
|
|
371
|
+
if (permission.value.decision.decision === 'deny') {
|
|
372
|
+
const executionEvent = this.runs.appendEvent({
|
|
373
|
+
runId: input.runId,
|
|
374
|
+
kind: 'operation-execution',
|
|
375
|
+
title: `Operation blocked: ${input.operation.category} ${input.operation.operation}`,
|
|
376
|
+
payload: executionPayload(input.operation, input.executor.name, 'blocked', {
|
|
377
|
+
decisionEventId: permission.value.event.id,
|
|
378
|
+
decision: 'deny',
|
|
379
|
+
reason: permission.value.decision.reason,
|
|
380
|
+
message: permission.value.decision.message,
|
|
381
|
+
executorInvoked: false,
|
|
382
|
+
}),
|
|
383
|
+
relatedType: 'operation',
|
|
384
|
+
relatedId: input.operation.operation,
|
|
385
|
+
});
|
|
386
|
+
return executionEvent.ok
|
|
387
|
+
? { ok: true, value: { decision: permission.value.decision, permissionEvent: permission.value.event, executionEvent: executionEvent.value, status: 'blocked' } }
|
|
388
|
+
: { ok: false, error: executionEvent.error };
|
|
389
|
+
}
|
|
390
|
+
if (permission.value.decision.decision === 'ask') {
|
|
391
|
+
const executionEvent = this.runs.appendEvent({
|
|
392
|
+
runId: input.runId,
|
|
393
|
+
kind: 'operation-execution',
|
|
394
|
+
title: `Operation pending approval: ${input.operation.category} ${input.operation.operation}`,
|
|
395
|
+
payload: executionPayload(input.operation, input.executor.name, 'pending-approval', {
|
|
396
|
+
decisionEventId: permission.value.event.id,
|
|
397
|
+
approvalId: permission.value.approval?.id ?? null,
|
|
398
|
+
decision: 'ask',
|
|
399
|
+
reason: permission.value.decision.reason,
|
|
400
|
+
message: permission.value.decision.message,
|
|
401
|
+
executorInvoked: false,
|
|
402
|
+
operationExecuted: false,
|
|
403
|
+
}),
|
|
404
|
+
relatedType: 'operation',
|
|
405
|
+
relatedId: input.operation.operation,
|
|
406
|
+
});
|
|
407
|
+
return executionEvent.ok
|
|
408
|
+
? { ok: true, value: { decision: permission.value.decision, permissionEvent: permission.value.event, executionEvent: executionEvent.value, status: 'pending-approval', ...(permission.value.approval === undefined ? {} : { approval: permission.value.approval }) } }
|
|
409
|
+
: { ok: false, error: executionEvent.error };
|
|
410
|
+
}
|
|
411
|
+
const executed = input.executor.execute({ runId: input.runId, operation: input.operation });
|
|
412
|
+
if (!executed.ok) {
|
|
413
|
+
const executionEvent = this.runs.appendEvent({
|
|
414
|
+
runId: input.runId,
|
|
415
|
+
kind: 'operation-execution',
|
|
416
|
+
title: `Operation failed: ${input.operation.category} ${input.operation.operation}`,
|
|
417
|
+
payload: executionPayload(input.operation, input.executor.name, 'failed', {
|
|
418
|
+
decisionEventId: permission.value.event.id,
|
|
419
|
+
decision: 'allow',
|
|
420
|
+
executorInvoked: true,
|
|
421
|
+
error: { code: executed.error.code, message: executed.error.message },
|
|
422
|
+
}),
|
|
423
|
+
relatedType: 'operation',
|
|
424
|
+
relatedId: input.operation.operation,
|
|
425
|
+
});
|
|
426
|
+
return executionEvent.ok
|
|
427
|
+
? { ok: true, value: { decision: permission.value.decision, permissionEvent: permission.value.event, executionEvent: executionEvent.value, status: 'failed' } }
|
|
428
|
+
: { ok: false, error: executionEvent.error };
|
|
429
|
+
}
|
|
430
|
+
const output = executed.value.output;
|
|
431
|
+
const executionEvent = this.runs.appendEvent({
|
|
432
|
+
runId: input.runId,
|
|
433
|
+
kind: 'operation-execution',
|
|
434
|
+
title: `Operation executed: ${input.operation.category} ${input.operation.operation}`,
|
|
435
|
+
payload: executionPayload(input.operation, input.executor.name, 'succeeded', {
|
|
436
|
+
decisionEventId: permission.value.event.id,
|
|
437
|
+
decision: 'allow',
|
|
438
|
+
executorInvoked: true,
|
|
439
|
+
output: output ?? null,
|
|
440
|
+
}),
|
|
441
|
+
relatedType: 'operation',
|
|
442
|
+
relatedId: input.operation.operation,
|
|
443
|
+
});
|
|
444
|
+
return executionEvent.ok
|
|
445
|
+
? { ok: true, value: { decision: permission.value.decision, permissionEvent: permission.value.event, executionEvent: executionEvent.value, status: 'succeeded', ...(output === undefined ? {} : { output }) } }
|
|
446
|
+
: { ok: false, error: executionEvent.error };
|
|
447
|
+
}
|
|
448
|
+
evaluatePermissionForRun(input) {
|
|
449
|
+
const run = this.runs.getById(input.runId);
|
|
450
|
+
if (!run.ok)
|
|
451
|
+
return { ok: false, error: run.error };
|
|
452
|
+
const decision = evaluatePermission(input);
|
|
453
|
+
const timestamp = new Date().toISOString();
|
|
454
|
+
const agent = input.agent === undefined
|
|
455
|
+
? { id: run.value.selectedAgentId }
|
|
456
|
+
: { id: input.agent.id, name: input.agent.name, mode: input.agent.mode };
|
|
457
|
+
if (decision.decision === 'ask') {
|
|
458
|
+
const prior = this.findResolvedMatchingApproval(input, agent);
|
|
459
|
+
if (!prior.ok)
|
|
460
|
+
return prior;
|
|
461
|
+
if (prior.value !== undefined) {
|
|
462
|
+
const reusedDecision = prior.value.approval.status === 'approved'
|
|
463
|
+
? { ...decision, decision: 'allow', message: `Previously approved approval ${prior.value.approval.id} authorizes this exact operation.` }
|
|
464
|
+
: { ...decision, decision: 'deny', reason: 'default_policy', message: `Previously ${prior.value.approval.status} approval ${prior.value.approval.id} denies this exact operation.` };
|
|
465
|
+
const event = this.runs.appendEvent({
|
|
466
|
+
runId: input.runId,
|
|
467
|
+
kind: 'permission-decision',
|
|
468
|
+
title: `Permission ${reusedDecision.decision}: ${input.category} ${input.operation}`,
|
|
469
|
+
payload: {
|
|
470
|
+
runId: input.runId,
|
|
471
|
+
category: input.category,
|
|
472
|
+
operation: input.operation,
|
|
473
|
+
requestedOperation: operationMetadata(input),
|
|
474
|
+
agent,
|
|
475
|
+
decision: reusedDecision.decision,
|
|
476
|
+
reasons: [{ code: reusedDecision.reason, message: reusedDecision.message }],
|
|
477
|
+
requiresHumanApproval: false,
|
|
478
|
+
approvalStatus: prior.value.approval.status,
|
|
479
|
+
reusedApprovalId: prior.value.approval.id,
|
|
480
|
+
originalDecisionEventId: prior.value.event.id,
|
|
481
|
+
approvalMatched: true,
|
|
482
|
+
timestamp,
|
|
483
|
+
},
|
|
484
|
+
relatedType: 'approval',
|
|
485
|
+
relatedId: prior.value.approval.id,
|
|
486
|
+
});
|
|
487
|
+
return event.ok
|
|
488
|
+
? { ok: true, value: { decision: reusedDecision, event: event.value, approval: prior.value.approval } }
|
|
489
|
+
: { ok: false, error: event.error };
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
const event = this.runs.appendEvent({
|
|
493
|
+
runId: input.runId,
|
|
494
|
+
kind: 'permission-decision',
|
|
495
|
+
title: `Permission ${decision.decision}: ${input.category} ${input.operation}`,
|
|
496
|
+
payload: {
|
|
497
|
+
runId: input.runId,
|
|
498
|
+
category: input.category,
|
|
499
|
+
operation: input.operation,
|
|
500
|
+
requestedOperation: operationMetadata(input),
|
|
501
|
+
agent,
|
|
502
|
+
decision: decision.decision,
|
|
503
|
+
reasons: [{ code: decision.reason, message: decision.message }],
|
|
504
|
+
requiresHumanApproval: decision.decision === 'ask',
|
|
505
|
+
approvalStatus: decision.decision === 'ask' ? 'pending' : 'not-required',
|
|
506
|
+
timestamp,
|
|
507
|
+
},
|
|
508
|
+
relatedType: 'permission',
|
|
509
|
+
relatedId: input.category,
|
|
510
|
+
});
|
|
511
|
+
if (!event.ok)
|
|
512
|
+
return { ok: false, error: event.error };
|
|
513
|
+
if (decision.decision !== 'ask')
|
|
514
|
+
return { ok: true, value: { decision, event: event.value } };
|
|
515
|
+
const approval = this.runs.createApproval({ runId: input.runId, decisionEventId: event.value.id });
|
|
516
|
+
return approval.ok
|
|
517
|
+
? { ok: true, value: { decision, event: event.value, approval: approval.value } }
|
|
518
|
+
: { ok: false, error: approval.error };
|
|
519
|
+
}
|
|
520
|
+
findResolvedMatchingApproval(input, agent) {
|
|
521
|
+
const details = this.runs.getDetails(input.runId);
|
|
522
|
+
if (!details.ok)
|
|
523
|
+
return details;
|
|
524
|
+
const requestedOperation = operationMetadata(input);
|
|
525
|
+
const resolved = details.value.approvals
|
|
526
|
+
.filter((approval) => approval.status === 'approved' || approval.status === 'rejected' || approval.status === 'cancelled')
|
|
527
|
+
.map((approval) => ({ approval, event: details.value.events.find((event) => event.id === approval.decisionEventId) }))
|
|
528
|
+
.filter((entry) => entry.event !== undefined && isAskPermissionEvent(entry.event))
|
|
529
|
+
.filter((entry) => approvalEventMatches(entry.event, input, requestedOperation, agent))
|
|
530
|
+
.sort((left, right) => (right.approval.resolvedAt ?? right.approval.requestedAt).localeCompare(left.approval.resolvedAt ?? left.approval.requestedAt));
|
|
531
|
+
return { ok: true, value: resolved[0] };
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
function preflightOutcome(decision) {
|
|
535
|
+
if (decision.decision === 'allow')
|
|
536
|
+
return 'allowed';
|
|
537
|
+
if (decision.decision === 'deny')
|
|
538
|
+
return 'blocked';
|
|
539
|
+
return 'approval-needed';
|
|
540
|
+
}
|
|
541
|
+
function executionPayload(operation, executorName, status, extra) {
|
|
542
|
+
return {
|
|
543
|
+
status,
|
|
544
|
+
executorName,
|
|
545
|
+
requestedOperation: operationMetadata(operation),
|
|
546
|
+
input: operation.input ?? null,
|
|
547
|
+
timestamp: new Date().toISOString(),
|
|
548
|
+
...extra,
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
function summarizeOperation(operation) {
|
|
552
|
+
return {
|
|
553
|
+
category: operation.category,
|
|
554
|
+
operation: operation.operation,
|
|
555
|
+
...(operation.input === undefined ? {} : { input: operation.input }),
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
function resumeGateNextAction(approval, details, blockers) {
|
|
559
|
+
const approvalBlocker = blockers.find((blocker) => blocker.code === 'approval-not-approved');
|
|
560
|
+
if (approvalBlocker !== undefined) {
|
|
561
|
+
return {
|
|
562
|
+
type: 'resolve-approval',
|
|
563
|
+
command: `runs approve --approval ${approval.id}`,
|
|
564
|
+
reason: approvalBlocker.message,
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
const missingContext = blockers.find((blocker) => blocker.code === 'missing-permission-event' || blocker.code === 'missing-pending-operation' || blocker.code === 'operation-metadata-incomplete');
|
|
568
|
+
if (missingContext !== undefined) {
|
|
569
|
+
return {
|
|
570
|
+
type: 'inspect-run',
|
|
571
|
+
command: `runs resume-inspect --id ${details.id}`,
|
|
572
|
+
reason: missingContext.message,
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
const retryBlocker = blockers.find((blocker) => blocker.code === 'retry-blocked');
|
|
576
|
+
if (retryBlocker !== undefined) {
|
|
577
|
+
return {
|
|
578
|
+
type: 'retry-check',
|
|
579
|
+
command: `runs retry-check --approval ${approval.id}`,
|
|
580
|
+
reason: retryBlocker.message,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
return {
|
|
584
|
+
type: 'manual-recovery',
|
|
585
|
+
command: `runs retry-check --approval ${approval.id}`,
|
|
586
|
+
reason: 'The operator may manually continue after reviewing retry-check guidance; no execution is performed by this plan.',
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
function resumeGateManualNextCommands(approval, details, operation) {
|
|
590
|
+
const commands = [`runs resume-inspect --id ${details.id}`, `runs retry-check --approval ${approval.id}`];
|
|
591
|
+
if (approval.status !== 'approved')
|
|
592
|
+
commands.unshift(`runs approve --approval ${approval.id}`);
|
|
593
|
+
if (operation !== undefined)
|
|
594
|
+
commands.push(`Review operation manually: ${operation.category} ${operation.operation}`);
|
|
595
|
+
return commands;
|
|
596
|
+
}
|
|
597
|
+
function operationMetadata(input) {
|
|
598
|
+
const metadata = { category: input.category, name: input.operation };
|
|
599
|
+
if (input.workspaceRoot !== undefined)
|
|
600
|
+
metadata.workspaceRoot = input.workspaceRoot;
|
|
601
|
+
if (input.targetPath !== undefined)
|
|
602
|
+
metadata.targetPath = input.targetPath;
|
|
603
|
+
if (input.providerToolName !== undefined)
|
|
604
|
+
metadata.providerToolName = input.providerToolName;
|
|
605
|
+
if (input.sandboxStrategy !== undefined)
|
|
606
|
+
metadata.sandboxStrategy = input.sandboxStrategy;
|
|
607
|
+
if (input.destructive !== undefined)
|
|
608
|
+
metadata.destructive = input.destructive;
|
|
609
|
+
if (input.external !== undefined)
|
|
610
|
+
metadata.external = input.external;
|
|
611
|
+
if (input.privileged !== undefined)
|
|
612
|
+
metadata.privileged = input.privileged;
|
|
613
|
+
if (input.ambiguous !== undefined)
|
|
614
|
+
metadata.ambiguous = input.ambiguous;
|
|
615
|
+
if (input.input !== undefined)
|
|
616
|
+
metadata.input = input.input;
|
|
617
|
+
return metadata;
|
|
618
|
+
}
|
|
619
|
+
function approvalEventMatches(event, input, requestedOperation, agent) {
|
|
620
|
+
if (!isObject(event.payload))
|
|
621
|
+
return false;
|
|
622
|
+
if (event.payload.runId !== input.runId)
|
|
623
|
+
return false;
|
|
624
|
+
if (event.payload.category !== input.category || event.payload.operation !== input.operation)
|
|
625
|
+
return false;
|
|
626
|
+
if (!jsonEqual(event.payload.requestedOperation, requestedOperation))
|
|
627
|
+
return false;
|
|
628
|
+
if (!jsonEqual(event.payload.agent, agent))
|
|
629
|
+
return false;
|
|
630
|
+
return true;
|
|
631
|
+
}
|
|
632
|
+
function jsonEqual(left, right) {
|
|
633
|
+
return stableJson(left ?? null) === stableJson(right ?? null);
|
|
634
|
+
}
|
|
635
|
+
function stableJson(value) {
|
|
636
|
+
if (Array.isArray(value))
|
|
637
|
+
return `[${value.map(stableJson).join(',')}]`;
|
|
638
|
+
if (isObject(value))
|
|
639
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key] ?? null)}`).join(',')}}`;
|
|
640
|
+
return JSON.stringify(value);
|
|
641
|
+
}
|
|
642
|
+
function sandboxDecisionSummary(sandbox) {
|
|
643
|
+
if (sandbox === undefined)
|
|
644
|
+
return undefined;
|
|
645
|
+
return {
|
|
646
|
+
strategy: sandbox.strategy,
|
|
647
|
+
decision: sandbox.decision,
|
|
648
|
+
workspaceRoot: sandbox.workspaceRoot,
|
|
649
|
+
targetPath: sandbox.targetPath ?? null,
|
|
650
|
+
reason: sandbox.reason ?? null,
|
|
651
|
+
validationSummary: sandbox.audit.validationSummary,
|
|
652
|
+
gitBoundary: sandbox.gitBoundary,
|
|
653
|
+
providerConfigMutation: sandbox.providerConfigMutation,
|
|
654
|
+
createsWorktree: sandbox.createsWorktree,
|
|
655
|
+
executesProvider: sandbox.executesProvider,
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
function isAskPermissionEvent(event) {
|
|
659
|
+
return event.kind === 'permission-decision' && isObject(event.payload) && event.payload.decision === 'ask';
|
|
660
|
+
}
|
|
661
|
+
function isPendingExecutionEventForApproval(event, approvalId) {
|
|
662
|
+
return event.kind === 'operation-execution'
|
|
663
|
+
&& isObject(event.payload)
|
|
664
|
+
&& event.payload.status === 'pending-approval'
|
|
665
|
+
&& event.payload.approvalId === approvalId;
|
|
666
|
+
}
|
|
667
|
+
function operationFromPendingExecution(payload) {
|
|
668
|
+
if (!isObject(payload))
|
|
669
|
+
return undefined;
|
|
670
|
+
const requestedOperation = payload.requestedOperation;
|
|
671
|
+
if (requestedOperation === undefined || !isObject(requestedOperation))
|
|
672
|
+
return undefined;
|
|
673
|
+
const requested = requestedOperation;
|
|
674
|
+
if (!isPermissionCategory(requested.category) || typeof requested.name !== 'string')
|
|
675
|
+
return undefined;
|
|
676
|
+
const operation = { category: requested.category, operation: requested.name };
|
|
677
|
+
if (typeof requested.workspaceRoot === 'string')
|
|
678
|
+
operation.workspaceRoot = requested.workspaceRoot;
|
|
679
|
+
if (typeof requested.targetPath === 'string')
|
|
680
|
+
operation.targetPath = requested.targetPath;
|
|
681
|
+
if (typeof requested.providerToolName === 'string')
|
|
682
|
+
operation.providerToolName = requested.providerToolName;
|
|
683
|
+
if (typeof requested.destructive === 'boolean')
|
|
684
|
+
operation.destructive = requested.destructive;
|
|
685
|
+
if (typeof requested.external === 'boolean')
|
|
686
|
+
operation.external = requested.external;
|
|
687
|
+
if (typeof requested.privileged === 'boolean')
|
|
688
|
+
operation.privileged = requested.privileged;
|
|
689
|
+
if (typeof requested.ambiguous === 'boolean')
|
|
690
|
+
operation.ambiguous = requested.ambiguous;
|
|
691
|
+
if ('input' in requested)
|
|
692
|
+
operation.input = requested.input;
|
|
693
|
+
if ('input' in payload)
|
|
694
|
+
operation.input = payload.input;
|
|
695
|
+
return operation;
|
|
696
|
+
}
|
|
697
|
+
function isObject(value) {
|
|
698
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
699
|
+
}
|
|
700
|
+
function isPermissionCategory(value) {
|
|
701
|
+
return value === 'read'
|
|
702
|
+
|| value === 'edit'
|
|
703
|
+
|| value === 'shell'
|
|
704
|
+
|| value === 'network'
|
|
705
|
+
|| value === 'git'
|
|
706
|
+
|| value === 'memory'
|
|
707
|
+
|| value === 'external-directory'
|
|
708
|
+
|| value === 'provider-tool'
|
|
709
|
+
|| value === 'secrets';
|
|
710
|
+
}
|
|
711
|
+
function validationFailure(message) {
|
|
712
|
+
return { ok: false, error: { code: 'validation_failed', message } };
|
|
713
|
+
}
|