vgxness 1.20.3 → 1.20.5
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/dist/agents/canonical-agent-manifest.js +14 -12
- package/dist/agents/canonical-agent-projection.js +3 -2
- package/dist/agents/manager-profile-overlay-service.js +13 -5
- package/dist/cli/bun-bin.js +0 -0
- package/dist/cli/cli-flags.js +47 -1
- package/dist/cli/commands/run-permission-dispatcher.js +152 -1
- package/dist/cli/dispatcher.js +6 -1
- package/dist/governance/risk-classifier.js +2 -0
- package/dist/mcp/control-plane.js +34 -0
- package/dist/mcp/schema.js +144 -0
- package/dist/mcp/stdio-server.js +6 -0
- package/dist/mcp/validation.js +359 -2
- package/dist/memory/sqlite/migrations/019_task_scoped_command_grants.sql +37 -0
- package/dist/permissions/explicit-request-authorization.js +126 -0
- package/dist/permissions/path-boundaries.js +87 -0
- package/dist/permissions/policy-evaluator.js +42 -9
- package/dist/permissions/task-scoped-command-grant-matcher.js +117 -0
- package/dist/permissions/task-scoped-command-grant-validation.js +135 -0
- package/dist/runs/repositories/task-scoped-command-grants.js +462 -0
- package/dist/runs/run-service.js +91 -1
- package/dist/runs/sandbox-process-execution.js +41 -30
- package/dist/runs/task-scoped-command-grant-service.js +217 -0
- package/dist/workflows/command-allowlist-adapter.js +50 -0
- package/docs/safety.md +23 -0
- package/package.json +1 -1
package/dist/mcp/schema.js
CHANGED
|
@@ -50,6 +50,11 @@ export const SUPPORTED_VGX_MCP_TOOL_NAMES = [
|
|
|
50
50
|
'vgxness_run_list',
|
|
51
51
|
'vgxness_run_get',
|
|
52
52
|
'vgxness_run_preflight',
|
|
53
|
+
'vgxness_run_task_grant_create',
|
|
54
|
+
'vgxness_run_task_grant_list',
|
|
55
|
+
'vgxness_run_task_grant_get',
|
|
56
|
+
'vgxness_run_task_grant_revoke',
|
|
57
|
+
'vgxness_run_task_grant_preview_match',
|
|
53
58
|
'vgxness_run_start',
|
|
54
59
|
'vgxness_run_checkpoint',
|
|
55
60
|
'vgxness_run_finalize',
|
|
@@ -108,6 +113,11 @@ export const EXPOSED_VGX_MCP_TOOL_NAMES = [
|
|
|
108
113
|
'run_list',
|
|
109
114
|
'run_get',
|
|
110
115
|
'run_preflight',
|
|
116
|
+
'run_task_grant_create',
|
|
117
|
+
'run_task_grant_list',
|
|
118
|
+
'run_task_grant_get',
|
|
119
|
+
'run_task_grant_revoke',
|
|
120
|
+
'run_task_grant_preview_match',
|
|
111
121
|
'run_start',
|
|
112
122
|
'run_checkpoint',
|
|
113
123
|
'run_finalize',
|
|
@@ -132,6 +142,8 @@ const agentModes = ['agent', 'subagent'];
|
|
|
132
142
|
const memoryTypes = ['architecture', 'decision', 'bugfix', 'pattern', 'config', 'discovery', 'learning', 'preference', 'manual'];
|
|
133
143
|
const activityKinds = ['prompt', 'tool_call', 'artifact', 'summary', 'error'];
|
|
134
144
|
const runStatuses = ['created', 'planned', 'running', 'needs-human', 'completed', 'failed', 'blocked', 'cancelled'];
|
|
145
|
+
const taskGrantStatuses = ['active', 'revoked', 'expired', 'exhausted'];
|
|
146
|
+
const taskGrantCategories = ['shell', 'test-run', 'git'];
|
|
135
147
|
const finalRunStatuses = ['completed', 'failed', 'blocked', 'cancelled'];
|
|
136
148
|
const runOutcomes = ['success', 'partial', 'failure', 'blocked', 'cancelled'];
|
|
137
149
|
const payloadModes = ['compact', 'verbose'];
|
|
@@ -142,6 +154,63 @@ const skillSourceKinds = ['path', 'url', 'inline'];
|
|
|
142
154
|
const skillUsageOutcomes = ['selected', 'injected', 'helped', 'failed', 'neutral'];
|
|
143
155
|
const skillAttachmentTargetTypes = ['agent', 'subagent', 'workflow-phase', 'provider-adapter', 'intent-signal'];
|
|
144
156
|
const jsonRecordSchema = z.record(z.string(), z.unknown());
|
|
157
|
+
const explicitRequestInputSchema = z
|
|
158
|
+
.object({
|
|
159
|
+
version: z.literal(1),
|
|
160
|
+
actor: z
|
|
161
|
+
.object({
|
|
162
|
+
type: z.literal('human'),
|
|
163
|
+
id: z.string().min(1).optional(),
|
|
164
|
+
displayName: z.string().min(1).optional(),
|
|
165
|
+
})
|
|
166
|
+
.strict(),
|
|
167
|
+
request: z
|
|
168
|
+
.object({
|
|
169
|
+
id: z.string().min(1).optional(),
|
|
170
|
+
source: z.enum(['chat', 'mcp', 'cli', 'provider', 'unknown']),
|
|
171
|
+
utteranceRef: z.string().min(1).optional(),
|
|
172
|
+
excerpt: z.string().min(1).optional(),
|
|
173
|
+
requestedAt: z.string().min(1),
|
|
174
|
+
sessionId: z.string().min(1).optional(),
|
|
175
|
+
runId: z.string().min(1).optional(),
|
|
176
|
+
})
|
|
177
|
+
.strict(),
|
|
178
|
+
requestedOperation: z
|
|
179
|
+
.object({
|
|
180
|
+
category: z.enum(permissionCategories),
|
|
181
|
+
operation: z.string().min(1),
|
|
182
|
+
commandId: z.string().min(1).optional(),
|
|
183
|
+
providerToolName: z.string().min(1).optional(),
|
|
184
|
+
targetPath: z.string().min(1).optional(),
|
|
185
|
+
targetPaths: z.array(z.string().min(1)).optional(),
|
|
186
|
+
cwd: z.string().min(1).optional(),
|
|
187
|
+
workspaceRoot: z.string().min(1).optional(),
|
|
188
|
+
scopeSummary: z.string().min(1),
|
|
189
|
+
})
|
|
190
|
+
.strict(),
|
|
191
|
+
riskAttributes: z
|
|
192
|
+
.object({
|
|
193
|
+
destructive: z.boolean().optional(),
|
|
194
|
+
privileged: z.boolean().optional(),
|
|
195
|
+
external: z.boolean().optional(),
|
|
196
|
+
ambiguous: z.boolean().optional(),
|
|
197
|
+
secrets: z.boolean().optional(),
|
|
198
|
+
providerConfigMutation: z.boolean().optional(),
|
|
199
|
+
globalConfigMutation: z.boolean().optional(),
|
|
200
|
+
network: z.boolean().optional(),
|
|
201
|
+
install: z.boolean().optional(),
|
|
202
|
+
})
|
|
203
|
+
.strict(),
|
|
204
|
+
parser: z
|
|
205
|
+
.object({
|
|
206
|
+
exact: z.boolean(),
|
|
207
|
+
confidence: z.enum(['high', 'medium', 'low']).optional(),
|
|
208
|
+
ambiguityReasons: z.array(z.string().min(1)).optional(),
|
|
209
|
+
expansionReasons: z.array(z.string().min(1)).optional(),
|
|
210
|
+
})
|
|
211
|
+
.strict(),
|
|
212
|
+
})
|
|
213
|
+
.strict();
|
|
145
214
|
const skillLifecycleActorSchema = z
|
|
146
215
|
.object({
|
|
147
216
|
type: z.enum(['human', 'agent', 'system']),
|
|
@@ -163,6 +232,26 @@ const providerChangePlanTypes = ['opencode-mcp-install', 'claude-mcp-install', '
|
|
|
163
232
|
const sddPhaseInputSchema = z.union([z.enum(sddPhases), z.literal('apply')]);
|
|
164
233
|
const sddPhaseSchema = z.enum(sddPhases);
|
|
165
234
|
const jsonValueSchema = z.lazy(() => z.union([z.string(), z.number().finite(), z.boolean(), z.null(), z.array(jsonValueSchema), z.record(z.string(), jsonValueSchema)]));
|
|
235
|
+
const taskGrantHumanActorInputSchema = z
|
|
236
|
+
.object({
|
|
237
|
+
type: z.literal('human'),
|
|
238
|
+
id: z.string().min(1),
|
|
239
|
+
displayName: z.string().min(1).optional(),
|
|
240
|
+
})
|
|
241
|
+
.strict();
|
|
242
|
+
const taskGrantRevocationActorInputSchema = z
|
|
243
|
+
.object({
|
|
244
|
+
type: z.enum(['human', 'system']),
|
|
245
|
+
id: z.string().min(1),
|
|
246
|
+
displayName: z.string().min(1).optional(),
|
|
247
|
+
})
|
|
248
|
+
.strict();
|
|
249
|
+
const taskGrantCommandPolicyInputSchema = z
|
|
250
|
+
.object({
|
|
251
|
+
kind: z.literal('allowlisted-command-ids'),
|
|
252
|
+
ids: z.array(z.string().min(1)).min(1),
|
|
253
|
+
})
|
|
254
|
+
.strict();
|
|
166
255
|
const providerEvidenceOutputSchema = z
|
|
167
256
|
.object({
|
|
168
257
|
staticManifestKnown: z.boolean(),
|
|
@@ -810,6 +899,10 @@ export const VGX_MCP_TOOL_INPUT_SCHEMAS = {
|
|
|
810
899
|
agentId: z.string().min(1).optional(),
|
|
811
900
|
workspaceRoot: z.string().min(1).optional(),
|
|
812
901
|
targetPath: z.string().min(1).optional(),
|
|
902
|
+
taskFingerprint: z.string().min(1).optional(),
|
|
903
|
+
commandId: z.string().min(1).optional(),
|
|
904
|
+
cwd: z.string().min(1).optional(),
|
|
905
|
+
targetPaths: z.array(z.string().min(1)).optional(),
|
|
813
906
|
providerToolName: z.string().min(1).optional(),
|
|
814
907
|
agent: z
|
|
815
908
|
.object({
|
|
@@ -824,8 +917,59 @@ export const VGX_MCP_TOOL_INPUT_SCHEMAS = {
|
|
|
824
917
|
external: z.boolean().optional(),
|
|
825
918
|
privileged: z.boolean().optional(),
|
|
826
919
|
ambiguous: z.boolean().optional(),
|
|
920
|
+
providerConfigMutation: z.boolean().optional(),
|
|
921
|
+
globalConfigMutation: z.boolean().optional(),
|
|
922
|
+
explicitRequest: explicitRequestInputSchema.optional(),
|
|
827
923
|
})
|
|
828
924
|
.passthrough(),
|
|
925
|
+
vgxness_run_task_grant_create: z
|
|
926
|
+
.object({
|
|
927
|
+
runId: z.string().min(1),
|
|
928
|
+
agentId: z.string().min(1),
|
|
929
|
+
taskFingerprint: z.string().min(1),
|
|
930
|
+
categories: z.array(z.enum(taskGrantCategories)).min(1),
|
|
931
|
+
externalRoots: z.array(z.string().min(1)).min(1),
|
|
932
|
+
cwdPatterns: z.array(z.string().min(1)).optional(),
|
|
933
|
+
commandPolicy: taskGrantCommandPolicyInputSchema,
|
|
934
|
+
expiresAt: z.string().min(1),
|
|
935
|
+
maxUses: z.number().int().positive(),
|
|
936
|
+
createdBy: taskGrantHumanActorInputSchema,
|
|
937
|
+
creationReason: z.string().min(1),
|
|
938
|
+
approvedScopeSummary: z.string().min(1),
|
|
939
|
+
})
|
|
940
|
+
.strict(),
|
|
941
|
+
vgxness_run_task_grant_list: z
|
|
942
|
+
.object({
|
|
943
|
+
runId: z.string().min(1).optional(),
|
|
944
|
+
agentId: z.string().min(1).optional(),
|
|
945
|
+
status: z.enum(taskGrantStatuses).optional(),
|
|
946
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
947
|
+
})
|
|
948
|
+
.strict(),
|
|
949
|
+
vgxness_run_task_grant_get: z
|
|
950
|
+
.object({
|
|
951
|
+
grantId: z.string().min(1),
|
|
952
|
+
})
|
|
953
|
+
.strict(),
|
|
954
|
+
vgxness_run_task_grant_revoke: z
|
|
955
|
+
.object({
|
|
956
|
+
grantId: z.string().min(1),
|
|
957
|
+
actor: taskGrantRevocationActorInputSchema,
|
|
958
|
+
reason: z.string().min(1),
|
|
959
|
+
})
|
|
960
|
+
.strict(),
|
|
961
|
+
vgxness_run_task_grant_preview_match: z
|
|
962
|
+
.object({
|
|
963
|
+
runId: z.string().min(1),
|
|
964
|
+
agentId: z.string().min(1).optional(),
|
|
965
|
+
taskFingerprint: z.string().min(1),
|
|
966
|
+
category: z.enum(permissionCategories),
|
|
967
|
+
operation: z.string().min(1),
|
|
968
|
+
workspaceRoot: z.string().min(1).optional(),
|
|
969
|
+
cwd: z.string().min(1),
|
|
970
|
+
targetPaths: z.array(z.string().min(1)).optional(),
|
|
971
|
+
})
|
|
972
|
+
.strict(),
|
|
829
973
|
vgxness_run_start: z
|
|
830
974
|
.object({
|
|
831
975
|
project: z.string().min(1),
|
package/dist/mcp/stdio-server.js
CHANGED
|
@@ -93,6 +93,12 @@ function descriptionForTool(publicToolName) {
|
|
|
93
93
|
return 'Human-only mutating SDD reopen gate; records an explicit human decision to reopen an artifact and must not be called by agents on their own behalf. Use only after human instruction, then revise or re-review the phase.';
|
|
94
94
|
if (publicToolName === 'run_preflight')
|
|
95
95
|
return 'Agent-callable advisory/planning-only run preflight; evaluates permissions, records a plan, and returns a workspace strategy recommendation, but does not execute the checked shell/git/install/network/provider/secrets operation and does not create branches, worktrees, or pull requests. Use it to decide whether human approval or a separate executor step is required.';
|
|
96
|
+
if (publicToolName === 'run_task_grant_create')
|
|
97
|
+
return 'Human-approved lifecycle tool that creates a task-scoped command grant for bounded allowlisted external command operations.';
|
|
98
|
+
if (publicToolName === 'run_task_grant_list' || publicToolName === 'run_task_grant_get' || publicToolName === 'run_task_grant_preview_match')
|
|
99
|
+
return 'Read-only task-scoped command grant lifecycle tool; does not execute commands, write provider config, or consume grant usage.';
|
|
100
|
+
if (publicToolName === 'run_task_grant_revoke')
|
|
101
|
+
return 'Revokes a task-scoped command grant with an explicit human or system actor and reason.';
|
|
96
102
|
const toolName = toInternalVgxMcpToolName(publicToolName);
|
|
97
103
|
return `VGX control-plane tool ${toolName}`;
|
|
98
104
|
}
|
package/dist/mcp/validation.js
CHANGED
|
@@ -2,6 +2,7 @@ import { isRiskyPermissionCategory, permissionCategories } from '../permissions/
|
|
|
2
2
|
import { parseOperationRetryPolicy } from '../runs/operation-retry.js';
|
|
3
3
|
import { normalizeSddPhaseInput, sddPhases } from '../sdd/schema.js';
|
|
4
4
|
import { workflowIds } from '../workflows/schema.js';
|
|
5
|
+
import { commandAllowlistIds } from '../workflows/command-allowlist-adapter.js';
|
|
5
6
|
import { supportedTypeMessage, verificationChangeTypes } from '../verification/index.js';
|
|
6
7
|
import { errorEnvelope, isVgxMcpToolName, } from './schema.js';
|
|
7
8
|
const scopes = ['project', 'personal'];
|
|
@@ -9,6 +10,8 @@ const agentModes = ['agent', 'subagent'];
|
|
|
9
10
|
const memoryTypes = ['architecture', 'decision', 'bugfix', 'pattern', 'config', 'discovery', 'learning', 'preference', 'manual'];
|
|
10
11
|
const activityKinds = ['prompt', 'tool_call', 'artifact', 'summary', 'error'];
|
|
11
12
|
const runStatuses = ['created', 'planned', 'running', 'needs-human', 'completed', 'failed', 'blocked', 'cancelled'];
|
|
13
|
+
const taskGrantStatuses = ['active', 'revoked', 'expired', 'exhausted'];
|
|
14
|
+
const taskGrantCategories = ['shell', 'test-run', 'git'];
|
|
12
15
|
const finalRunStatuses = ['completed', 'failed', 'blocked', 'cancelled'];
|
|
13
16
|
const runOutcomes = ['success', 'partial', 'failure', 'blocked', 'cancelled'];
|
|
14
17
|
const permissionDecisions = ['allow', 'ask', 'deny'];
|
|
@@ -132,6 +135,16 @@ export function validateVgxMcpToolCall(call) {
|
|
|
132
135
|
return validationSuccess(tool.value, validateRunGetInput(input, tool.value));
|
|
133
136
|
case 'vgxness_run_preflight':
|
|
134
137
|
return validationSuccess(tool.value, validateRunPreflightInput(input, tool.value));
|
|
138
|
+
case 'vgxness_run_task_grant_create':
|
|
139
|
+
return validationSuccess(tool.value, validateRunTaskGrantCreateInput(input, tool.value));
|
|
140
|
+
case 'vgxness_run_task_grant_list':
|
|
141
|
+
return validationSuccess(tool.value, validateRunTaskGrantListInput(input, tool.value));
|
|
142
|
+
case 'vgxness_run_task_grant_get':
|
|
143
|
+
return validationSuccess(tool.value, validateRunTaskGrantGetInput(input, tool.value));
|
|
144
|
+
case 'vgxness_run_task_grant_revoke':
|
|
145
|
+
return validationSuccess(tool.value, validateRunTaskGrantRevokeInput(input, tool.value));
|
|
146
|
+
case 'vgxness_run_task_grant_preview_match':
|
|
147
|
+
return validationSuccess(tool.value, validateRunTaskGrantPreviewMatchInput(input, tool.value));
|
|
135
148
|
case 'vgxness_run_start':
|
|
136
149
|
return validationSuccess(tool.value, validateRunStartInput(input, tool.value));
|
|
137
150
|
case 'vgxness_run_checkpoint':
|
|
@@ -1565,6 +1578,10 @@ function validateRunPreflightInput(input, tool) {
|
|
|
1565
1578
|
'agentId',
|
|
1566
1579
|
'workspaceRoot',
|
|
1567
1580
|
'targetPath',
|
|
1581
|
+
'taskFingerprint',
|
|
1582
|
+
'commandId',
|
|
1583
|
+
'cwd',
|
|
1584
|
+
'targetPaths',
|
|
1568
1585
|
'providerToolName',
|
|
1569
1586
|
'agent',
|
|
1570
1587
|
'sandboxStrategy',
|
|
@@ -1572,6 +1589,9 @@ function validateRunPreflightInput(input, tool) {
|
|
|
1572
1589
|
'external',
|
|
1573
1590
|
'privileged',
|
|
1574
1591
|
'ambiguous',
|
|
1592
|
+
'providerConfigMutation',
|
|
1593
|
+
'globalConfigMutation',
|
|
1594
|
+
'explicitRequest',
|
|
1575
1595
|
]);
|
|
1576
1596
|
if (!record.ok)
|
|
1577
1597
|
return record;
|
|
@@ -1590,15 +1610,20 @@ function validateRunPreflightInput(input, tool) {
|
|
|
1590
1610
|
return workflow;
|
|
1591
1611
|
if (workflow.value !== undefined)
|
|
1592
1612
|
result.workflow = workflow.value;
|
|
1593
|
-
const copied = copyOptionalStrings(result, record.value, tool, ['phase', 'agentId', 'workspaceRoot', 'targetPath', 'providerToolName']);
|
|
1613
|
+
const copied = copyOptionalStrings(result, record.value, tool, ['phase', 'agentId', 'workspaceRoot', 'targetPath', 'taskFingerprint', 'commandId', 'cwd', 'providerToolName']);
|
|
1594
1614
|
if (!copied.ok)
|
|
1595
1615
|
return copied;
|
|
1616
|
+
const targetPaths = readOptionalStringArray(record.value, 'targetPaths', tool);
|
|
1617
|
+
if (!targetPaths.ok)
|
|
1618
|
+
return targetPaths;
|
|
1619
|
+
if (targetPaths.value !== undefined)
|
|
1620
|
+
result.targetPaths = targetPaths.value;
|
|
1596
1621
|
const sandboxStrategy = readOptionalOneOf(record.value, 'sandboxStrategy', ['worktree'], tool);
|
|
1597
1622
|
if (!sandboxStrategy.ok)
|
|
1598
1623
|
return sandboxStrategy;
|
|
1599
1624
|
if (sandboxStrategy.value !== undefined)
|
|
1600
1625
|
result.sandboxStrategy = sandboxStrategy.value;
|
|
1601
|
-
const booleans = copyOptionalBooleans(result, record.value, tool, ['destructive', 'external', 'privileged', 'ambiguous']);
|
|
1626
|
+
const booleans = copyOptionalBooleans(result, record.value, tool, ['destructive', 'external', 'privileged', 'ambiguous', 'providerConfigMutation', 'globalConfigMutation']);
|
|
1602
1627
|
if (!booleans.ok)
|
|
1603
1628
|
return booleans;
|
|
1604
1629
|
if (record.value.agent !== undefined) {
|
|
@@ -1607,6 +1632,12 @@ function validateRunPreflightInput(input, tool) {
|
|
|
1607
1632
|
return agent;
|
|
1608
1633
|
result.agent = agent.value;
|
|
1609
1634
|
}
|
|
1635
|
+
if (record.value.explicitRequest !== undefined) {
|
|
1636
|
+
const explicitRequest = readExplicitRequest(record.value.explicitRequest, tool);
|
|
1637
|
+
if (!explicitRequest.ok)
|
|
1638
|
+
return explicitRequest;
|
|
1639
|
+
result.explicitRequest = explicitRequest.value;
|
|
1640
|
+
}
|
|
1610
1641
|
if (isRiskyPermissionCategory(result.category)) {
|
|
1611
1642
|
if (result.phase !== undefined && result.agentId === undefined && result.agent === undefined) {
|
|
1612
1643
|
return validationFailure(`VGX-managed preflight for risky category ${result.category} requires agentId when phase metadata is provided`, tool);
|
|
@@ -1617,6 +1648,270 @@ function validateRunPreflightInput(input, tool) {
|
|
|
1617
1648
|
}
|
|
1618
1649
|
return { ok: true, value: result };
|
|
1619
1650
|
}
|
|
1651
|
+
function readExplicitRequest(value, tool) {
|
|
1652
|
+
const record = asRecord(value, tool, 'explicitRequest must be an object');
|
|
1653
|
+
if (!record.ok)
|
|
1654
|
+
return record;
|
|
1655
|
+
const unexpected = Object.keys(record.value).find((key) => !['version', 'actor', 'request', 'requestedOperation', 'riskAttributes', 'parser'].includes(key));
|
|
1656
|
+
if (unexpected !== undefined)
|
|
1657
|
+
return validationFailure(`Unexpected explicitRequest field: ${unexpected}`, tool);
|
|
1658
|
+
if (record.value.version !== 1)
|
|
1659
|
+
return validationFailure('explicitRequest.version must be 1', tool);
|
|
1660
|
+
const actor = readExplicitRequestActor(record.value.actor, tool);
|
|
1661
|
+
if (!actor.ok)
|
|
1662
|
+
return actor;
|
|
1663
|
+
const request = readExplicitRequestRequest(record.value.request, tool);
|
|
1664
|
+
if (!request.ok)
|
|
1665
|
+
return request;
|
|
1666
|
+
const requestedOperation = readExplicitRequestedOperation(record.value.requestedOperation, tool);
|
|
1667
|
+
if (!requestedOperation.ok)
|
|
1668
|
+
return requestedOperation;
|
|
1669
|
+
const riskAttributes = readExplicitRiskAttributes(record.value.riskAttributes, tool);
|
|
1670
|
+
if (!riskAttributes.ok)
|
|
1671
|
+
return riskAttributes;
|
|
1672
|
+
const parser = readExplicitParser(record.value.parser, tool);
|
|
1673
|
+
if (!parser.ok)
|
|
1674
|
+
return parser;
|
|
1675
|
+
return { ok: true, value: { version: 1, actor: actor.value, request: request.value, requestedOperation: requestedOperation.value, riskAttributes: riskAttributes.value, parser: parser.value } };
|
|
1676
|
+
}
|
|
1677
|
+
function readExplicitRequestActor(value, tool) {
|
|
1678
|
+
const record = asRecord(value, tool, 'explicitRequest.actor must be an object');
|
|
1679
|
+
if (!record.ok)
|
|
1680
|
+
return record;
|
|
1681
|
+
const unexpected = Object.keys(record.value).find((key) => !['type', 'id', 'displayName'].includes(key));
|
|
1682
|
+
if (unexpected !== undefined)
|
|
1683
|
+
return validationFailure(`Unexpected explicitRequest.actor field: ${unexpected}`, tool);
|
|
1684
|
+
const type = readRequiredOneOf(record.value, 'type', ['human'], tool);
|
|
1685
|
+
if (!type.ok)
|
|
1686
|
+
return type;
|
|
1687
|
+
const actor = { type: type.value };
|
|
1688
|
+
const copied = copyOptionalStrings(actor, record.value, tool, ['id', 'displayName']);
|
|
1689
|
+
if (!copied.ok)
|
|
1690
|
+
return copied;
|
|
1691
|
+
return { ok: true, value: actor };
|
|
1692
|
+
}
|
|
1693
|
+
function readExplicitRequestRequest(value, tool) {
|
|
1694
|
+
const record = asRecord(value, tool, 'explicitRequest.request must be an object');
|
|
1695
|
+
if (!record.ok)
|
|
1696
|
+
return record;
|
|
1697
|
+
const unexpected = Object.keys(record.value).find((key) => !['id', 'source', 'utteranceRef', 'excerpt', 'requestedAt', 'sessionId', 'runId'].includes(key));
|
|
1698
|
+
if (unexpected !== undefined)
|
|
1699
|
+
return validationFailure(`Unexpected explicitRequest.request field: ${unexpected}`, tool);
|
|
1700
|
+
const source = readRequiredOneOf(record.value, 'source', ['chat', 'mcp', 'cli', 'provider', 'unknown'], tool);
|
|
1701
|
+
if (!source.ok)
|
|
1702
|
+
return source;
|
|
1703
|
+
const requestedAt = readNonEmptyString(record.value, 'requestedAt', tool);
|
|
1704
|
+
if (!requestedAt.ok)
|
|
1705
|
+
return requestedAt;
|
|
1706
|
+
if (!validIsoTimestamp(requestedAt.value))
|
|
1707
|
+
return validationFailure('explicitRequest.request.requestedAt must be a valid ISO timestamp', tool);
|
|
1708
|
+
const request = { source: source.value, requestedAt: requestedAt.value };
|
|
1709
|
+
const copied = copyOptionalStrings(request, record.value, tool, ['id', 'utteranceRef', 'excerpt', 'sessionId', 'runId']);
|
|
1710
|
+
if (!copied.ok)
|
|
1711
|
+
return copied;
|
|
1712
|
+
return { ok: true, value: request };
|
|
1713
|
+
}
|
|
1714
|
+
function readExplicitRequestedOperation(value, tool) {
|
|
1715
|
+
const record = asRecord(value, tool, 'explicitRequest.requestedOperation must be an object');
|
|
1716
|
+
if (!record.ok)
|
|
1717
|
+
return record;
|
|
1718
|
+
const unexpected = Object.keys(record.value).find((key) => !['category', 'operation', 'commandId', 'providerToolName', 'targetPath', 'targetPaths', 'cwd', 'workspaceRoot', 'scopeSummary'].includes(key));
|
|
1719
|
+
if (unexpected !== undefined)
|
|
1720
|
+
return validationFailure(`Unexpected explicitRequest.requestedOperation field: ${unexpected}`, tool);
|
|
1721
|
+
const category = readRequiredOneOf(record.value, 'category', permissionCategories, tool);
|
|
1722
|
+
if (!category.ok)
|
|
1723
|
+
return category;
|
|
1724
|
+
const operation = readNonEmptyString(record.value, 'operation', tool);
|
|
1725
|
+
if (!operation.ok)
|
|
1726
|
+
return operation;
|
|
1727
|
+
const scopeSummary = readNonEmptyString(record.value, 'scopeSummary', tool);
|
|
1728
|
+
if (!scopeSummary.ok)
|
|
1729
|
+
return scopeSummary;
|
|
1730
|
+
const requestedOperation = { category: category.value, operation: operation.value, scopeSummary: scopeSummary.value };
|
|
1731
|
+
const copied = copyOptionalStrings(requestedOperation, record.value, tool, ['commandId', 'providerToolName', 'targetPath', 'cwd', 'workspaceRoot']);
|
|
1732
|
+
if (!copied.ok)
|
|
1733
|
+
return copied;
|
|
1734
|
+
const targetPaths = readOptionalStringArray(record.value, 'targetPaths', tool);
|
|
1735
|
+
if (!targetPaths.ok)
|
|
1736
|
+
return targetPaths;
|
|
1737
|
+
if (targetPaths.value !== undefined)
|
|
1738
|
+
requestedOperation.targetPaths = targetPaths.value;
|
|
1739
|
+
return { ok: true, value: requestedOperation };
|
|
1740
|
+
}
|
|
1741
|
+
function readExplicitRiskAttributes(value, tool) {
|
|
1742
|
+
const record = asRecord(value, tool, 'explicitRequest.riskAttributes must be an object');
|
|
1743
|
+
if (!record.ok)
|
|
1744
|
+
return record;
|
|
1745
|
+
const fields = ['destructive', 'privileged', 'external', 'ambiguous', 'secrets', 'providerConfigMutation', 'globalConfigMutation', 'network', 'install'];
|
|
1746
|
+
const unexpected = Object.keys(record.value).find((key) => !fields.includes(key));
|
|
1747
|
+
if (unexpected !== undefined)
|
|
1748
|
+
return validationFailure(`Unexpected explicitRequest.riskAttributes field: ${unexpected}`, tool);
|
|
1749
|
+
const riskAttributes = {};
|
|
1750
|
+
const copied = copyOptionalBooleans(riskAttributes, record.value, tool, fields);
|
|
1751
|
+
if (!copied.ok)
|
|
1752
|
+
return copied;
|
|
1753
|
+
return { ok: true, value: riskAttributes };
|
|
1754
|
+
}
|
|
1755
|
+
function readExplicitParser(value, tool) {
|
|
1756
|
+
const record = asRecord(value, tool, 'explicitRequest.parser must be an object');
|
|
1757
|
+
if (!record.ok)
|
|
1758
|
+
return record;
|
|
1759
|
+
const unexpected = Object.keys(record.value).find((key) => !['exact', 'confidence', 'ambiguityReasons', 'expansionReasons'].includes(key));
|
|
1760
|
+
if (unexpected !== undefined)
|
|
1761
|
+
return validationFailure(`Unexpected explicitRequest.parser field: ${unexpected}`, tool);
|
|
1762
|
+
if (typeof record.value.exact !== 'boolean')
|
|
1763
|
+
return validationFailure('explicitRequest.parser.exact must be a boolean', tool);
|
|
1764
|
+
const parser = { exact: record.value.exact };
|
|
1765
|
+
const confidence = readOptionalOneOf(record.value, 'confidence', ['high', 'medium', 'low'], tool);
|
|
1766
|
+
if (!confidence.ok)
|
|
1767
|
+
return confidence;
|
|
1768
|
+
if (confidence.value !== undefined)
|
|
1769
|
+
parser.confidence = confidence.value;
|
|
1770
|
+
const ambiguityReasons = readOptionalStringArray(record.value, 'ambiguityReasons', tool);
|
|
1771
|
+
if (!ambiguityReasons.ok)
|
|
1772
|
+
return ambiguityReasons;
|
|
1773
|
+
if (ambiguityReasons.value !== undefined)
|
|
1774
|
+
parser.ambiguityReasons = ambiguityReasons.value;
|
|
1775
|
+
const expansionReasons = readOptionalStringArray(record.value, 'expansionReasons', tool);
|
|
1776
|
+
if (!expansionReasons.ok)
|
|
1777
|
+
return expansionReasons;
|
|
1778
|
+
if (expansionReasons.value !== undefined)
|
|
1779
|
+
parser.expansionReasons = expansionReasons.value;
|
|
1780
|
+
return { ok: true, value: parser };
|
|
1781
|
+
}
|
|
1782
|
+
function validIsoTimestamp(value) {
|
|
1783
|
+
const timestamp = Date.parse(value);
|
|
1784
|
+
return Number.isFinite(timestamp) && new Date(timestamp).toISOString() === value;
|
|
1785
|
+
}
|
|
1786
|
+
function validateRunTaskGrantCreateInput(input, tool) {
|
|
1787
|
+
const record = inputRecord(input, tool, [
|
|
1788
|
+
'runId',
|
|
1789
|
+
'agentId',
|
|
1790
|
+
'taskFingerprint',
|
|
1791
|
+
'categories',
|
|
1792
|
+
'externalRoots',
|
|
1793
|
+
'cwdPatterns',
|
|
1794
|
+
'commandPolicy',
|
|
1795
|
+
'expiresAt',
|
|
1796
|
+
'maxUses',
|
|
1797
|
+
'createdBy',
|
|
1798
|
+
'creationReason',
|
|
1799
|
+
'approvedScopeSummary',
|
|
1800
|
+
]);
|
|
1801
|
+
if (!record.ok)
|
|
1802
|
+
return record;
|
|
1803
|
+
const result = {};
|
|
1804
|
+
const copied = copyRequiredStrings(result, record.value, tool, ['runId', 'agentId', 'taskFingerprint', 'expiresAt', 'creationReason', 'approvedScopeSummary']);
|
|
1805
|
+
if (!copied.ok)
|
|
1806
|
+
return copied;
|
|
1807
|
+
const categories = readRequiredOneOfArray(record.value, 'categories', taskGrantCategories, tool);
|
|
1808
|
+
if (!categories.ok)
|
|
1809
|
+
return categories;
|
|
1810
|
+
result.categories = categories.value;
|
|
1811
|
+
const externalRoots = readRequiredStringArray(record.value, 'externalRoots', tool);
|
|
1812
|
+
if (!externalRoots.ok)
|
|
1813
|
+
return externalRoots;
|
|
1814
|
+
result.externalRoots = externalRoots.value;
|
|
1815
|
+
const cwdPatterns = readOptionalStringArray(record.value, 'cwdPatterns', tool);
|
|
1816
|
+
if (!cwdPatterns.ok)
|
|
1817
|
+
return cwdPatterns;
|
|
1818
|
+
if (cwdPatterns.value !== undefined)
|
|
1819
|
+
result.cwdPatterns = cwdPatterns.value;
|
|
1820
|
+
const commandPolicy = readTaskGrantCommandPolicy(record.value.commandPolicy, tool);
|
|
1821
|
+
if (!commandPolicy.ok)
|
|
1822
|
+
return commandPolicy;
|
|
1823
|
+
result.commandPolicy = commandPolicy.value;
|
|
1824
|
+
const createdBy = readTaskGrantActor(record.value.createdBy, tool, ['human']);
|
|
1825
|
+
if (!createdBy.ok)
|
|
1826
|
+
return createdBy;
|
|
1827
|
+
result.createdBy = createdBy.value;
|
|
1828
|
+
if (typeof record.value.maxUses !== 'number' || !Number.isSafeInteger(record.value.maxUses) || record.value.maxUses < 1)
|
|
1829
|
+
return validationFailure('maxUses must be a positive safe integer', tool);
|
|
1830
|
+
result.maxUses = record.value.maxUses;
|
|
1831
|
+
const expiresAt = Date.parse(result.expiresAt ?? '');
|
|
1832
|
+
if (!Number.isFinite(expiresAt) || new Date(expiresAt).toISOString() !== result.expiresAt)
|
|
1833
|
+
return validationFailure('expiresAt must be a valid ISO date', tool);
|
|
1834
|
+
const expiryDelta = expiresAt - Date.now();
|
|
1835
|
+
if (expiryDelta <= 0)
|
|
1836
|
+
return validationFailure('expiresAt must be in the future', tool);
|
|
1837
|
+
if (expiryDelta > 24 * 60 * 60 * 1000)
|
|
1838
|
+
return validationFailure('expiresAt must be within 24 hours', tool);
|
|
1839
|
+
return { ok: true, value: result };
|
|
1840
|
+
}
|
|
1841
|
+
function validateRunTaskGrantListInput(input, tool) {
|
|
1842
|
+
const record = inputRecord(input, tool, ['runId', 'agentId', 'status', 'limit']);
|
|
1843
|
+
if (!record.ok)
|
|
1844
|
+
return record;
|
|
1845
|
+
const result = {};
|
|
1846
|
+
const copied = copyOptionalStrings(result, record.value, tool, ['runId', 'agentId']);
|
|
1847
|
+
if (!copied.ok)
|
|
1848
|
+
return copied;
|
|
1849
|
+
const status = readOptionalOneOf(record.value, 'status', taskGrantStatuses, tool);
|
|
1850
|
+
if (!status.ok)
|
|
1851
|
+
return status;
|
|
1852
|
+
if (status.value !== undefined)
|
|
1853
|
+
result.status = status.value;
|
|
1854
|
+
const limit = readOptionalBoundedLimit(record.value, tool);
|
|
1855
|
+
if (!limit.ok)
|
|
1856
|
+
return limit;
|
|
1857
|
+
if (limit.value !== undefined)
|
|
1858
|
+
result.limit = limit.value;
|
|
1859
|
+
return { ok: true, value: result };
|
|
1860
|
+
}
|
|
1861
|
+
function validateRunTaskGrantGetInput(input, tool) {
|
|
1862
|
+
const record = inputRecord(input, tool, ['grantId']);
|
|
1863
|
+
if (!record.ok)
|
|
1864
|
+
return record;
|
|
1865
|
+
const grantId = readNonEmptyString(record.value, 'grantId', tool);
|
|
1866
|
+
if (!grantId.ok)
|
|
1867
|
+
return grantId;
|
|
1868
|
+
return { ok: true, value: { grantId: grantId.value } };
|
|
1869
|
+
}
|
|
1870
|
+
function validateRunTaskGrantRevokeInput(input, tool) {
|
|
1871
|
+
const record = inputRecord(input, tool, ['grantId', 'actor', 'reason']);
|
|
1872
|
+
if (!record.ok)
|
|
1873
|
+
return record;
|
|
1874
|
+
const grantId = readNonEmptyString(record.value, 'grantId', tool);
|
|
1875
|
+
if (!grantId.ok)
|
|
1876
|
+
return grantId;
|
|
1877
|
+
const reason = readNonEmptyString(record.value, 'reason', tool);
|
|
1878
|
+
if (!reason.ok)
|
|
1879
|
+
return reason;
|
|
1880
|
+
const actor = readTaskGrantActor(record.value.actor, tool, ['human', 'system']);
|
|
1881
|
+
if (!actor.ok)
|
|
1882
|
+
return actor;
|
|
1883
|
+
return { ok: true, value: { grantId: grantId.value, actor: actor.value, reason: reason.value } };
|
|
1884
|
+
}
|
|
1885
|
+
function validateRunTaskGrantPreviewMatchInput(input, tool) {
|
|
1886
|
+
const record = inputRecord(input, tool, ['runId', 'agentId', 'taskFingerprint', 'category', 'operation', 'workspaceRoot', 'cwd', 'targetPaths']);
|
|
1887
|
+
if (!record.ok)
|
|
1888
|
+
return record;
|
|
1889
|
+
const runId = readNonEmptyString(record.value, 'runId', tool);
|
|
1890
|
+
if (!runId.ok)
|
|
1891
|
+
return runId;
|
|
1892
|
+
const taskFingerprint = readNonEmptyString(record.value, 'taskFingerprint', tool);
|
|
1893
|
+
if (!taskFingerprint.ok)
|
|
1894
|
+
return taskFingerprint;
|
|
1895
|
+
const category = readRequiredOneOf(record.value, 'category', permissionCategories, tool);
|
|
1896
|
+
if (!category.ok)
|
|
1897
|
+
return category;
|
|
1898
|
+
const operation = readNonEmptyString(record.value, 'operation', tool);
|
|
1899
|
+
if (!operation.ok)
|
|
1900
|
+
return operation;
|
|
1901
|
+
const cwd = readNonEmptyString(record.value, 'cwd', tool);
|
|
1902
|
+
if (!cwd.ok)
|
|
1903
|
+
return cwd;
|
|
1904
|
+
const result = { runId: runId.value, taskFingerprint: taskFingerprint.value, category: category.value, operation: operation.value, cwd: cwd.value };
|
|
1905
|
+
const copied = copyOptionalStrings(result, record.value, tool, ['agentId', 'workspaceRoot']);
|
|
1906
|
+
if (!copied.ok)
|
|
1907
|
+
return copied;
|
|
1908
|
+
const targetPaths = readOptionalStringArray(record.value, 'targetPaths', tool);
|
|
1909
|
+
if (!targetPaths.ok)
|
|
1910
|
+
return targetPaths;
|
|
1911
|
+
if (targetPaths.value !== undefined)
|
|
1912
|
+
result.targetPaths = targetPaths.value;
|
|
1913
|
+
return { ok: true, value: result };
|
|
1914
|
+
}
|
|
1620
1915
|
function validateRunStartInput(input, tool) {
|
|
1621
1916
|
const record = inputRecord(input, tool, ['project', 'userIntent', 'workflow', 'phase', 'selectedAgentId', 'providerAdapter', 'model']);
|
|
1622
1917
|
if (!record.ok)
|
|
@@ -1905,6 +2200,68 @@ function readRequiredOneOf(record, field, values, tool) {
|
|
|
1905
2200
|
return validationFailure(`${field} must be one of: ${values.join(', ')}`, tool);
|
|
1906
2201
|
return { ok: true, value: value.value };
|
|
1907
2202
|
}
|
|
2203
|
+
function readRequiredOneOfArray(record, field, values, tool) {
|
|
2204
|
+
const array = readRequiredStringArray(record, field, tool);
|
|
2205
|
+
if (!array.ok)
|
|
2206
|
+
return array;
|
|
2207
|
+
for (const item of array.value) {
|
|
2208
|
+
if (!values.includes(item))
|
|
2209
|
+
return validationFailure(`${field} must contain only: ${values.join(', ')}`, tool);
|
|
2210
|
+
}
|
|
2211
|
+
return { ok: true, value: array.value };
|
|
2212
|
+
}
|
|
2213
|
+
function readRequiredStringArray(record, field, tool) {
|
|
2214
|
+
if (record[field] === undefined)
|
|
2215
|
+
return validationFailure(`${field} is required`, tool);
|
|
2216
|
+
const values = readOptionalStringArray(record, field, tool);
|
|
2217
|
+
if (!values.ok)
|
|
2218
|
+
return values;
|
|
2219
|
+
if (values.value === undefined || values.value.length === 0)
|
|
2220
|
+
return validationFailure(`${field} must contain at least one item`, tool);
|
|
2221
|
+
if (new Set(values.value).size !== values.value.length)
|
|
2222
|
+
return validationFailure(`${field} must be unique`, tool);
|
|
2223
|
+
return { ok: true, value: values.value };
|
|
2224
|
+
}
|
|
2225
|
+
function readTaskGrantCommandPolicy(value, tool) {
|
|
2226
|
+
const record = asRecord(value, tool, 'commandPolicy must be an object');
|
|
2227
|
+
if (!record.ok)
|
|
2228
|
+
return record;
|
|
2229
|
+
const unexpected = Object.keys(record.value).find((key) => !['kind', 'ids'].includes(key));
|
|
2230
|
+
if (unexpected !== undefined)
|
|
2231
|
+
return validationFailure(`Unexpected commandPolicy field: ${unexpected}`, tool);
|
|
2232
|
+
const kind = readRequiredOneOf(record.value, 'kind', ['allowlisted-command-ids'], tool);
|
|
2233
|
+
if (!kind.ok)
|
|
2234
|
+
return kind;
|
|
2235
|
+
const ids = readRequiredStringArray(record.value, 'ids', tool);
|
|
2236
|
+
if (!ids.ok)
|
|
2237
|
+
return ids;
|
|
2238
|
+
const known = new Set(commandAllowlistIds());
|
|
2239
|
+
for (const id of ids.value)
|
|
2240
|
+
if (!known.has(id))
|
|
2241
|
+
return validationFailure(`Unknown allowlisted command id: ${id}`, tool);
|
|
2242
|
+
return { ok: true, value: { kind: kind.value, ids: ids.value } };
|
|
2243
|
+
}
|
|
2244
|
+
function readTaskGrantActor(value, tool, allowedTypes) {
|
|
2245
|
+
const record = asRecord(value, tool, 'actor must be an object');
|
|
2246
|
+
if (!record.ok)
|
|
2247
|
+
return record;
|
|
2248
|
+
const unexpected = Object.keys(record.value).find((key) => !['type', 'id', 'displayName'].includes(key));
|
|
2249
|
+
if (unexpected !== undefined)
|
|
2250
|
+
return validationFailure(`Unexpected actor field: ${unexpected}`, tool);
|
|
2251
|
+
const type = readRequiredOneOf(record.value, 'type', allowedTypes, tool);
|
|
2252
|
+
if (!type.ok)
|
|
2253
|
+
return type;
|
|
2254
|
+
const id = readNonEmptyString(record.value, 'id', tool);
|
|
2255
|
+
if (!id.ok)
|
|
2256
|
+
return id;
|
|
2257
|
+
const actor = { type: type.value, id: id.value };
|
|
2258
|
+
const displayName = readOptionalNonEmptyString(record.value, 'displayName', tool);
|
|
2259
|
+
if (!displayName.ok)
|
|
2260
|
+
return displayName;
|
|
2261
|
+
if (displayName.value !== undefined)
|
|
2262
|
+
actor.displayName = displayName.value;
|
|
2263
|
+
return { ok: true, value: actor };
|
|
2264
|
+
}
|
|
1908
2265
|
function readOptionalOneOf(record, field, values, tool) {
|
|
1909
2266
|
if (record[field] === undefined)
|
|
1910
2267
|
return { ok: true, value: undefined };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS task_scoped_command_grants (
|
|
2
|
+
id TEXT PRIMARY KEY,
|
|
3
|
+
run_id TEXT NOT NULL,
|
|
4
|
+
agent_id TEXT NOT NULL,
|
|
5
|
+
task_fingerprint TEXT NOT NULL,
|
|
6
|
+
|
|
7
|
+
categories_json TEXT NOT NULL,
|
|
8
|
+
external_roots_json TEXT NOT NULL,
|
|
9
|
+
canonical_external_roots_json TEXT NOT NULL,
|
|
10
|
+
cwd_patterns_json TEXT,
|
|
11
|
+
command_policy_json TEXT NOT NULL,
|
|
12
|
+
|
|
13
|
+
expires_at TEXT NOT NULL,
|
|
14
|
+
max_uses INTEGER NOT NULL,
|
|
15
|
+
used_count INTEGER NOT NULL DEFAULT 0,
|
|
16
|
+
status TEXT NOT NULL,
|
|
17
|
+
|
|
18
|
+
created_by_json TEXT NOT NULL,
|
|
19
|
+
created_at TEXT NOT NULL,
|
|
20
|
+
|
|
21
|
+
revoked_by_json TEXT,
|
|
22
|
+
revoked_at TEXT,
|
|
23
|
+
revoke_reason TEXT,
|
|
24
|
+
|
|
25
|
+
audit_json TEXT NOT NULL,
|
|
26
|
+
|
|
27
|
+
FOREIGN KEY(run_id) REFERENCES runs(id)
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
CREATE INDEX IF NOT EXISTS idx_task_scoped_command_grants_run
|
|
31
|
+
ON task_scoped_command_grants(run_id);
|
|
32
|
+
|
|
33
|
+
CREATE INDEX IF NOT EXISTS idx_task_scoped_command_grants_match
|
|
34
|
+
ON task_scoped_command_grants(run_id, agent_id, task_fingerprint, status);
|
|
35
|
+
|
|
36
|
+
CREATE INDEX IF NOT EXISTS idx_task_scoped_command_grants_status
|
|
37
|
+
ON task_scoped_command_grants(status);
|