vgxness 1.20.4 → 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 +3 -2
- package/dist/governance/risk-classifier.js +2 -0
- package/dist/mcp/schema.js +60 -0
- package/dist/mcp/validation.js +145 -1
- package/dist/permissions/explicit-request-authorization.js +126 -0
- package/dist/permissions/policy-evaluator.js +42 -9
- package/dist/runs/run-service.js +32 -1
- package/dist/runs/task-scoped-command-grant-service.js +1 -0
- package/package.json +1 -1
|
@@ -166,7 +166,7 @@ Coordinate SDD; keep chat thin, use MCP state, delegate to exact SDD subagent. C
|
|
|
166
166
|
- VGXNESS uses SQLite artifacts/control-plane; OpenCode as primary provider; artifacts are not openspec files. Do not write to \`openspec/\`.
|
|
167
167
|
- SDD artifact acceptance is human-only. Artifact presence is not acceptance. Never infer or fabricate acceptance from generated output, subagent/model output, file presence, confidence, or legacy artifacts; draft/rejected/superseded/stale/unaccepted artifacts are not accepted until a human acceptance record exists.
|
|
168
168
|
- Before phase advancement, call \`vgxness_sdd_status\`/\`vgxness_sdd_next\` plus \`vgxness_sdd_ready\` or \`vgxness_sdd_get_readiness\`.
|
|
169
|
-
- Before risky VGX-managed side effects (edit, shell/tests, git, network, provider-tool, secrets, external-directory, destructive, privileged, ambiguous), call \`vgxness_run_preflight\` with runId/workflow/phase/agent when available. Stop on approval
|
|
169
|
+
- Before risky VGX-managed side effects (edit, shell/tests, git, network, provider-tool, secrets, external-directory, destructive, privileged, ambiguous), call \`vgxness_run_preflight\` with runId/workflow/phase/agent when available. When the human explicitly requested the exact risky one-off action, pass structured \`explicitRequest\` (human actor, timestamp/source, exact category/operation/scope/risk/parser evidence). If preflight allows with \`authorizationMode: "explicit-request"\`, do not ask duplicate confirmation. Stop on approval-needed or blocked; never invent approval.
|
|
170
170
|
- Direct human acceptance via \`vgxness_sdd_accept_artifact\` is not a generic SDD write: do not preflight solely for it; require exact project/change/phase, \`acceptedBy.type\` \`"human"\`, non-empty \`acceptedBy.id\`, and eligible status/readiness. Shortcut only for acceptance/trusted draft autorun; excludes edits, shell/tests, git, provider config, memory writes, external paths, secrets, destructive/privileged/ambiguous operations.
|
|
171
171
|
- bounded read/search repo tools may be enabled for inspection; bash, edit, and write remain denied. Diagnostics are config-level evidence, not host runtime proof.
|
|
172
172
|
- Do not mutate provider/global OpenCode config. Do not publish packages unless explicitly requested. Never revert/overwrite unrelated user work. Preserve \`permission.task\` deny-by-default with only exact known SDD subagents.
|
|
@@ -190,7 +190,7 @@ SDD happens in OpenCode conversation + VGXNESS MCP/subagents. No terminal SDD ph
|
|
|
190
190
|
- Memory: search/get for prior work and duplicate avoidance. Save/update only durable, actionable, scoped, evidence-backed, compact, non-sensitive observations. Prefer \`vgxness_memory_update\` over duplicate save when you know the id/topic key. Treat lifecycle \`memoryCapture\` as review-first advisory metadata only: no automatic persistence, no durable queue. Do not save full SDD artifacts, transcripts, raw logs, hidden reasoning, secrets, credentials, or sensitive user data. Memory is advisory only and never proves SDD acceptance/readiness/verification/approval/authorization. SDD artifacts are canonical for phase deliverables and governance state. OpenCode previews/handoff are read-only and capture-free. Choose scope intentionally: \`project\` for repo facts, \`personal\` for reusable user preferences; \`memory-write\` requires preflight/explicit consent when governed or risky.
|
|
191
191
|
- Active-work memory uses existing memory APIs only: \`active-work/{change}/summary\` (+others). No secrets or hidden reasoning. Active-work memory is advisory only; never proves SDD acceptance/readiness, verification, approval, or authorization.
|
|
192
192
|
- Agents/skills: Use \`vgxness_skill_payload\` first for contextual skill guidance; preview/context-only, no provider execution or config writes. Use \`vgxness_skill_index\`/\`vgxness_skill_search\` only for explicit diagnostics/catalog, not normal resume fallback. Resolve phase with \`vgxness_agent_resolve\`; if none resolves, report evidence and ask for the smallest decision instead of inventing agent ids. \`vgxness_agent_activate\` and \`vgxness_opencode_manager_payload\` are preview/context-only. \`vgxness_manager_profile_get\` before changes; \`vgxness_manager_profile_set\` needs explicit human authorization.
|
|
193
|
-
- Runs/recovery: \`vgxness_run_start\`/\`vgxness_run_list\`/\`vgxness_run_get\`; checkpoint with \`vgxness_run_checkpoint\`, preflight with \`vgxness_run_preflight
|
|
193
|
+
- Runs/recovery: \`vgxness_run_start\`/\`vgxness_run_list\`/\`vgxness_run_get\`; checkpoint with \`vgxness_run_checkpoint\`, preflight with \`vgxness_run_preflight\` (include \`explicitRequest\` only for exact human-requested one-off actions), close with \`vgxness_run_finalize\`. Unknown runId: \`vgxness_run_resume_candidates\`. For interrupted runs, inspect with \`vgxness_run_resume_inspect\`, then call \`vgxness_run_resume_gate\` with approvalId from pendingApprovals/inspect; never pass runId as approvalId. Follow safe \`recommendedActions[]\`. Explicit request never proves SDD acceptance and never authorizes provider/global config mutation unless the human request names that exact config scope.
|
|
194
194
|
- Provider diagnostics: \`vgxness_provider_status\`/\`vgxness_provider_doctor\` read-only; report \`providerEvidence.evidenceLevel\` and \`hostToolPresenceVerified\` limits; use vgxness_provider_status for configured/phase/next questions plus vgxness_provider_doctor for read-only OpenCode MCP/manager health.
|
|
195
195
|
|
|
196
196
|
## Minimum flows
|
|
@@ -216,6 +216,7 @@ const registryManagerInstructionsV11 = [
|
|
|
216
216
|
'Use VGXNESS MCP as the durable control plane: for generic resume/continue prompts call vgxness_resume_context first, pass explicitChange only when the user gave an exact valid change, and do not infer a change from context_cockpit.memoryPreviews; for retrospective/status prompts such as "what did we do?" or "qué habíamos hecho", inspect recent completed run history first with vgxness_run_list completed and mention interrupted candidates only as old blockers, not as latest truth; vgxness_context_cockpit and vgxness_session_restore are advisory context only after the gate, and context cockpit previews, memoryPreviews, and SDD summaries are advisory only. Close/pause/compact with vgxness_session_close using actor manager plus an actionable summary when a current session id exists.',
|
|
217
217
|
'For generic continue/sigamos/continuemos/resume development without an explicit user change, call vgxness_resume_context before any SDD status/continuation. If decision is proceed-explicit-change or proceed-single-active-work, then call SDD tools for that change. If inspect-interrupted-run, call vgxness_run_resume_inspect. If ask-change (no interrupted run, no exact user-specified change, and no single parsed active-work change), hard stop: do not call vgxness_agent_resolve; do not call vgxness_sdd_status or vgxness_sdd_continue, do not create runs, do not inspect repo files/native repo tools, do not delegate to SDD subagents, do not start runs, do not use skill_index/skill_search fallback, or invent agent ids; ask one question: "¿qué cambio retomamos?".',
|
|
218
218
|
'Manager save-then-accept behavior: after a valid hidden phase subagent artifact, save it immediately as a draft with sdd_save_artifact, summarize the saved draft, then ask whether the human accepts it and wants to continue to the next phase. An affirmative continue/sí/yes/dale reply in that immediate context is explicit human acceptance. A reply of only save/no/revise keeps draft and must not advance. Never silently auto-accept.',
|
|
219
|
+
'Run preflight explicit-request behavior: when a human explicitly requests the exact risky one-off action, pass structured explicitRequest to vgxness_run_preflight with human actor, request source/timestamp, exact requested category/operation/scope, risk flags, and parser exactness. If preflight returns allowed with authorizationMode explicit-request, do not ask duplicate confirmation. If it returns approval-needed, ask/wait; if blocked, stop. Never use explicitRequest to infer SDD acceptance or to mutate provider/global config unless the human request names that exact config scope.',
|
|
219
220
|
'Check SDD status/next/ready/cockpit, read prerequisites with sdd_get_artifact or sdd_list_artifacts, use public sdd_continue/internal vgxness_sdd_continue first for advisory read-only continuation plans, use sdd_reopen_artifact only for rejected artifacts returning to draft with explicit human actor/audit context, save phase output with sdd_save_artifact only by governance; inspect delegated result-envelope completeness before SDD artifact persistence or phase advancement (status, executive_summary, artifacts_examined, skill_resolution, risks, next_recommended) as prompt guidance only, not runtime parsing/rejection; when proposal is accepted, spec/design/tasks may run sequentially as draft-only autorun without extra confirmation, while acceptance remains human-only. Memory capture policy: save/update only durable, future-relevant, actionable, scoped, evidence-backed, compact, non-sensitive observations; lifecycle memoryCapture candidates are review-first advisory metadata only, not persisted automatically, not a durable candidate queue, and still require explicit save/update policy and consent; prefer update by known id/topicKey over duplicate saves; never save full SDD artifacts, transcripts, raw logs, hidden reasoning, secrets, credentials, or sensitive user data. Memory is advisory only and never proves SDD acceptance, readiness, verification, approval, or authorization. SDD artifacts are canonical for phase deliverables/governance state. OpenCode previews/handoff are read-only and capture-free. Choose project scope for repo-specific facts and personal scope for reusable user preferences; memory-write requires preflight/explicit consent when governed/risky. Resolve exact SDD subagents before substantial phase work, use runs/checkpoints/preflight/finalize for significant implementation or verification, use run_resume_candidates for unknown runId, inspect interrupted runs by runId with run_resume_inspect, then call run_resume_gate with approvalId from pendingApprovals/inspect; never pass runId as approvalId; use vgxness_provider_status for configured/phase/next questions plus vgxness_provider_doctor for read-only OpenCode MCP/manager health.',
|
|
220
221
|
'When sdd_continue/vgxness_sdd_continue returns recommendedActions, prefer the first safe action with agentCallable true and humanOnly false; use targetTool plus suggestedArgs as the starting point, and stop for requiresHumanConfirmation, requiresProviderWriteConsent, requiresPreflight, ambiguous, destructive, privileged, external, or outside-approval actions. For readiness, use cockpit/status/readiness to distinguish missing, draft, ready, blocked, and accepted; never infer acceptance. For provider evidence, report providerEvidence.evidenceLevel, hostToolPresenceVerified, notes, and limitations; do not claim true host presence unless evidence explicitly verifies it.',
|
|
221
222
|
'Prefer payloadMode=compact for manager-facing status/context reads, SDD artifact reads/lists, and activation handoffs so the primary context stays clean; request payloadMode=verbose only when full artifact contents, provider payloads, or skill context are actually needed, preferably inside delegated phase subagents.',
|
|
@@ -55,6 +55,8 @@ export function classifyOperationRisk(input) {
|
|
|
55
55
|
evidence.push(`providerTool:${input.providerToolName}`);
|
|
56
56
|
if (input.destructive === true)
|
|
57
57
|
return classification(3, ['destructive_operation'], 'plan', 'ask', evidence);
|
|
58
|
+
if (input.providerConfigMutation === true || input.globalConfigMutation === true)
|
|
59
|
+
return classification(3, ['provider_config_write'], 'plan', 'ask', evidence);
|
|
58
60
|
if (input.external === true || input.category === 'network' || input.category === 'external-directory')
|
|
59
61
|
return classification(3, ['external_operation'], 'plan', 'ask', evidence);
|
|
60
62
|
if (input.privileged === true)
|
package/dist/mcp/schema.js
CHANGED
|
@@ -154,6 +154,63 @@ const skillSourceKinds = ['path', 'url', 'inline'];
|
|
|
154
154
|
const skillUsageOutcomes = ['selected', 'injected', 'helped', 'failed', 'neutral'];
|
|
155
155
|
const skillAttachmentTargetTypes = ['agent', 'subagent', 'workflow-phase', 'provider-adapter', 'intent-signal'];
|
|
156
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();
|
|
157
214
|
const skillLifecycleActorSchema = z
|
|
158
215
|
.object({
|
|
159
216
|
type: z.enum(['human', 'agent', 'system']),
|
|
@@ -860,6 +917,9 @@ export const VGX_MCP_TOOL_INPUT_SCHEMAS = {
|
|
|
860
917
|
external: z.boolean().optional(),
|
|
861
918
|
privileged: z.boolean().optional(),
|
|
862
919
|
ambiguous: z.boolean().optional(),
|
|
920
|
+
providerConfigMutation: z.boolean().optional(),
|
|
921
|
+
globalConfigMutation: z.boolean().optional(),
|
|
922
|
+
explicitRequest: explicitRequestInputSchema.optional(),
|
|
863
923
|
})
|
|
864
924
|
.passthrough(),
|
|
865
925
|
vgxness_run_task_grant_create: z
|
package/dist/mcp/validation.js
CHANGED
|
@@ -1589,6 +1589,9 @@ function validateRunPreflightInput(input, tool) {
|
|
|
1589
1589
|
'external',
|
|
1590
1590
|
'privileged',
|
|
1591
1591
|
'ambiguous',
|
|
1592
|
+
'providerConfigMutation',
|
|
1593
|
+
'globalConfigMutation',
|
|
1594
|
+
'explicitRequest',
|
|
1592
1595
|
]);
|
|
1593
1596
|
if (!record.ok)
|
|
1594
1597
|
return record;
|
|
@@ -1620,7 +1623,7 @@ function validateRunPreflightInput(input, tool) {
|
|
|
1620
1623
|
return sandboxStrategy;
|
|
1621
1624
|
if (sandboxStrategy.value !== undefined)
|
|
1622
1625
|
result.sandboxStrategy = sandboxStrategy.value;
|
|
1623
|
-
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']);
|
|
1624
1627
|
if (!booleans.ok)
|
|
1625
1628
|
return booleans;
|
|
1626
1629
|
if (record.value.agent !== undefined) {
|
|
@@ -1629,6 +1632,12 @@ function validateRunPreflightInput(input, tool) {
|
|
|
1629
1632
|
return agent;
|
|
1630
1633
|
result.agent = agent.value;
|
|
1631
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
|
+
}
|
|
1632
1641
|
if (isRiskyPermissionCategory(result.category)) {
|
|
1633
1642
|
if (result.phase !== undefined && result.agentId === undefined && result.agent === undefined) {
|
|
1634
1643
|
return validationFailure(`VGX-managed preflight for risky category ${result.category} requires agentId when phase metadata is provided`, tool);
|
|
@@ -1639,6 +1648,141 @@ function validateRunPreflightInput(input, tool) {
|
|
|
1639
1648
|
}
|
|
1640
1649
|
return { ok: true, value: result };
|
|
1641
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
|
+
}
|
|
1642
1786
|
function validateRunTaskGrantCreateInput(input, tool) {
|
|
1643
1787
|
const record = inputRecord(input, tool, [
|
|
1644
1788
|
'runId',
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
2
|
+
export function evaluateExplicitRequestAuthorization(input) {
|
|
3
|
+
const explicit = input.request.explicitRequest;
|
|
4
|
+
if (explicit === undefined)
|
|
5
|
+
return evaluation(input, 'ask', 'missing-context', ['explicit_request_missing']);
|
|
6
|
+
if (explicit.actor.type !== 'human')
|
|
7
|
+
return evaluation(input, 'ask', 'actor-mismatch', ['explicit_request_actor_mismatch']);
|
|
8
|
+
if (!validIsoTimestamp(explicit.request.requestedAt))
|
|
9
|
+
return evaluation(input, 'ask', 'timestamp-invalid', ['explicit_request_timestamp_invalid']);
|
|
10
|
+
if (explicit.request.runId !== undefined && input.request.runId !== undefined && explicit.request.runId !== input.request.runId)
|
|
11
|
+
return evaluation(input, 'ask', 'scope-mismatch', ['explicit_request_scope_mismatch']);
|
|
12
|
+
if (explicit.parser.exact !== true || explicit.parser.confidence === 'medium' || explicit.parser.confidence === 'low' || hasItems(explicit.parser.ambiguityReasons))
|
|
13
|
+
return evaluation(input, 'ask', 'ambiguous-request', ['explicit_request_ambiguous']);
|
|
14
|
+
if (hasItems(explicit.parser.expansionReasons))
|
|
15
|
+
return evaluation(input, 'ask', 'scope-expanded', ['explicit_request_scope_expanded']);
|
|
16
|
+
if (isSddAcceptanceAttempt(input.request))
|
|
17
|
+
return evaluation(input, 'deny', 'hard-stop', ['sdd_human_acceptance_required']);
|
|
18
|
+
if (input.request.category === 'secrets' || input.risk.reasonCodes.includes('secret_access') || explicit.riskAttributes.secrets === true)
|
|
19
|
+
return evaluation(input, 'deny', 'hard-stop', ['explicit_request_secret_access']);
|
|
20
|
+
if (explicit.requestedOperation.category !== input.request.category)
|
|
21
|
+
return evaluation(input, 'ask', 'category-mismatch', ['explicit_request_category_mismatch']);
|
|
22
|
+
if (normalizeOperation(explicit.requestedOperation.operation) !== normalizeOperation(input.request.operation))
|
|
23
|
+
return evaluation(input, 'ask', 'operation-mismatch', ['explicit_request_operation_mismatch']);
|
|
24
|
+
if (!optionalExact(explicit.requestedOperation.commandId, input.request.commandId))
|
|
25
|
+
return evaluation(input, 'ask', 'scope-mismatch', ['explicit_request_scope_mismatch']);
|
|
26
|
+
if (!optionalExact(explicit.requestedOperation.providerToolName, input.request.providerToolName))
|
|
27
|
+
return evaluation(input, 'ask', 'scope-mismatch', ['explicit_request_scope_mismatch']);
|
|
28
|
+
if (!optionalPathExact(explicit.requestedOperation.workspaceRoot, input.request.workspaceRoot, input.request.workspaceRoot))
|
|
29
|
+
return evaluation(input, 'ask', 'scope-mismatch', ['explicit_request_scope_mismatch']);
|
|
30
|
+
if (!optionalPathExact(explicit.requestedOperation.cwd, input.request.cwd, input.request.workspaceRoot))
|
|
31
|
+
return evaluation(input, 'ask', 'scope-mismatch', ['explicit_request_scope_mismatch']);
|
|
32
|
+
if (!targetSetMatches(input.request, explicit.requestedOperation))
|
|
33
|
+
return evaluation(input, 'ask', 'scope-mismatch', ['explicit_request_scope_mismatch']);
|
|
34
|
+
const riskMismatch = riskMismatchReasons(input.request, input.risk, explicit.riskAttributes);
|
|
35
|
+
if (riskMismatch.length > 0)
|
|
36
|
+
return evaluation(input, 'ask', 'risk-attribute-mismatch', riskMismatch);
|
|
37
|
+
return evaluation(input, 'allow', 'exact-match', ['explicit_request_exact_match']);
|
|
38
|
+
}
|
|
39
|
+
function evaluation(input, decision, matchStatus, reasonCodes) {
|
|
40
|
+
const explicit = input.request.explicitRequest;
|
|
41
|
+
return {
|
|
42
|
+
decision,
|
|
43
|
+
audit: {
|
|
44
|
+
authorization: 'explicit-request',
|
|
45
|
+
matched: decision === 'allow',
|
|
46
|
+
matchStatus,
|
|
47
|
+
...(explicit?.request.id === undefined ? {} : { requestRef: explicit.request.id }),
|
|
48
|
+
...(explicit?.actor === undefined ? {} : { actor: explicit.actor }),
|
|
49
|
+
...(explicit?.request.requestedAt === undefined ? {} : { requestedAt: explicit.request.requestedAt }),
|
|
50
|
+
evaluatedAt: input.evaluatedAt,
|
|
51
|
+
runId: input.request.runId ?? explicit?.request.runId ?? '',
|
|
52
|
+
category: input.request.category,
|
|
53
|
+
operation: input.request.operation,
|
|
54
|
+
scopeSummary: explicit?.requestedOperation.scopeSummary ?? '',
|
|
55
|
+
riskAttributes: explicitRiskAttributes(explicit?.riskAttributes),
|
|
56
|
+
reasonCodes,
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function riskMismatchReasons(request, risk, explicit) {
|
|
61
|
+
const reasons = [];
|
|
62
|
+
if (request.destructive === true && explicit.destructive !== true)
|
|
63
|
+
reasons.push('explicit_request_destructive_not_explicit');
|
|
64
|
+
if (request.privileged === true && explicit.privileged !== true)
|
|
65
|
+
reasons.push('explicit_request_privileged_not_explicit');
|
|
66
|
+
if ((request.external === true || risk.reasonCodes.includes('external_operation')) && explicit.external !== true && explicit.network !== true && explicit.install !== true)
|
|
67
|
+
reasons.push('explicit_request_external_not_explicit');
|
|
68
|
+
if (request.ambiguous === true && explicit.ambiguous !== true)
|
|
69
|
+
reasons.push('explicit_request_ambiguous');
|
|
70
|
+
if (request.globalConfigMutation === true && explicit.globalConfigMutation !== true)
|
|
71
|
+
reasons.push('explicit_request_global_config_not_explicit');
|
|
72
|
+
if (request.providerConfigMutation === true && explicit.providerConfigMutation !== true)
|
|
73
|
+
reasons.push('explicit_request_provider_config_not_explicit');
|
|
74
|
+
if (request.providerConfigMutation !== true && request.globalConfigMutation !== true && risk.reasonCodes.includes('provider_config_write') && explicit.providerConfigMutation !== true)
|
|
75
|
+
reasons.push('explicit_request_provider_config_not_explicit');
|
|
76
|
+
if (request.category === 'network' && explicit.network !== true)
|
|
77
|
+
reasons.push('explicit_request_risk_mismatch');
|
|
78
|
+
if (request.category === 'install' && explicit.install !== true)
|
|
79
|
+
reasons.push('explicit_request_risk_mismatch');
|
|
80
|
+
return [...new Set(reasons)];
|
|
81
|
+
}
|
|
82
|
+
function targetSetMatches(request, explicit) {
|
|
83
|
+
const requested = targetSet(request.targetPath, request.targetPaths, request.workspaceRoot);
|
|
84
|
+
const authorized = targetSet(explicit.targetPath, explicit.targetPaths, request.workspaceRoot ?? explicit.workspaceRoot);
|
|
85
|
+
if (requested.length === 0 && authorized.length === 0)
|
|
86
|
+
return true;
|
|
87
|
+
if (requested.length === 0 || authorized.length === 0)
|
|
88
|
+
return false;
|
|
89
|
+
if (requested.length !== authorized.length)
|
|
90
|
+
return false;
|
|
91
|
+
return requested.every((value, index) => value === authorized[index]);
|
|
92
|
+
}
|
|
93
|
+
function targetSet(targetPath, targetPaths, workspaceRoot) {
|
|
94
|
+
return [...new Set([...(targetPath === undefined ? [] : [targetPath]), ...(targetPaths ?? [])].map((path) => normalizePath(path, workspaceRoot)))].sort();
|
|
95
|
+
}
|
|
96
|
+
function optionalExact(explicit, actual) {
|
|
97
|
+
return actual === undefined ? explicit === undefined : explicit === actual;
|
|
98
|
+
}
|
|
99
|
+
function optionalPathExact(explicit, actual, workspaceRoot) {
|
|
100
|
+
return actual === undefined ? explicit === undefined : explicit !== undefined && normalizePath(explicit, workspaceRoot) === normalizePath(actual, workspaceRoot);
|
|
101
|
+
}
|
|
102
|
+
function normalizePath(path, workspaceRoot) {
|
|
103
|
+
const base = workspaceRoot === undefined ? undefined : resolve(workspaceRoot);
|
|
104
|
+
return resolve(isAbsolute(path) || base === undefined ? path : resolve(base, path));
|
|
105
|
+
}
|
|
106
|
+
function normalizeOperation(value) {
|
|
107
|
+
return value.trim().replace(/\s+/g, ' ');
|
|
108
|
+
}
|
|
109
|
+
function hasItems(values) {
|
|
110
|
+
return values !== undefined && values.length > 0;
|
|
111
|
+
}
|
|
112
|
+
function validIsoTimestamp(value) {
|
|
113
|
+
const timestamp = Date.parse(value);
|
|
114
|
+
return Number.isFinite(timestamp) && new Date(timestamp).toISOString() === value && timestamp <= Date.now() + 60_000;
|
|
115
|
+
}
|
|
116
|
+
function explicitRiskAttributes(value) {
|
|
117
|
+
const output = {};
|
|
118
|
+
for (const key of ['destructive', 'privileged', 'external', 'ambiguous', 'secrets', 'providerConfigMutation', 'globalConfigMutation', 'network', 'install']) {
|
|
119
|
+
output[key] = value?.[key] === true;
|
|
120
|
+
}
|
|
121
|
+
return output;
|
|
122
|
+
}
|
|
123
|
+
function isSddAcceptanceAttempt(request) {
|
|
124
|
+
const text = `${request.category} ${request.operation} ${request.providerToolName ?? ''}`.toLowerCase();
|
|
125
|
+
return /vgxness_sdd_accept_artifact|sdd_accept_artifact|artifact acceptance|accept artifact|acceptedby/.test(text);
|
|
126
|
+
}
|
|
@@ -3,6 +3,7 @@ import { dirname, isAbsolute, resolve, sep } from 'node:path';
|
|
|
3
3
|
import { classifyOperationRisk } from '../governance/risk-classifier.js';
|
|
4
4
|
import { isSddPhase, sddPhases } from '../sdd/schema.js';
|
|
5
5
|
import { commandAllowlistIds } from '../workflows/command-allowlist-adapter.js';
|
|
6
|
+
import { evaluateExplicitRequestAuthorization } from './explicit-request-authorization.js';
|
|
6
7
|
export { isRiskyPermissionCategory, permissionCategories, riskyPermissionCategories } from './schema.js';
|
|
7
8
|
export const permissionDecisions = ['allow', 'ask', 'deny'];
|
|
8
9
|
export const permissionModes = ['allow', 'audit', 'require-preflight', 'deny'];
|
|
@@ -85,30 +86,30 @@ const filesystemCategories = new Set([
|
|
|
85
86
|
export function evaluatePermission(request, policy = defaultPermissionPolicy) {
|
|
86
87
|
const risk = classifyOperationRisk(request);
|
|
87
88
|
if (request.category === 'secrets')
|
|
88
|
-
return result(request, 'deny', 'secret_access', 'Secret access is denied by default.', {}, risk);
|
|
89
|
+
return withExplicitRequestAuthorization(request, result(request, 'deny', 'secret_access', 'Secret access is denied by default.', {}, risk), risk);
|
|
89
90
|
if (request.category === 'external-directory') {
|
|
90
|
-
return result(request, 'deny', 'workspace_boundary', 'External directory access is denied by the foundation policy.', {}, risk);
|
|
91
|
+
return withExplicitRequestAuthorization(request, result(request, 'deny', 'workspace_boundary', 'External directory access is denied by the foundation policy.', {}, risk), risk);
|
|
91
92
|
}
|
|
92
93
|
const hardRisk = hardRiskDecision(request, risk);
|
|
93
94
|
if (hardRisk !== undefined)
|
|
94
|
-
return hardRisk;
|
|
95
|
+
return withExplicitRequestAuthorization(request, hardRisk, risk);
|
|
95
96
|
const workspaceEscape = workspaceEscapeDecision(request);
|
|
96
97
|
if (workspaceEscape !== undefined)
|
|
97
|
-
return workspaceEscape;
|
|
98
|
+
return withExplicitRequestAuthorization(request, workspaceEscape, risk);
|
|
98
99
|
const verifyAllowlistedCommand = verifyAllowlistedCommandDecision(request, risk);
|
|
99
100
|
if (verifyAllowlistedCommand !== undefined)
|
|
100
101
|
return verifyAllowlistedCommand;
|
|
101
102
|
const sddPhaseDecision = phaseMatrixDecision(request);
|
|
102
103
|
if (sddPhaseDecision !== undefined && sddPhaseDecision.mode !== 'allow' && sddPhaseDecision.mode !== 'audit')
|
|
103
|
-
return sddPhaseDecision;
|
|
104
|
+
return withExplicitRequestAuthorization(request, sddPhaseDecision, risk);
|
|
104
105
|
const explicitValidation = explicitValidationDecision(request, risk);
|
|
105
106
|
if (explicitValidation !== undefined)
|
|
106
|
-
return explicitValidation;
|
|
107
|
+
return withExplicitRequestAuthorization(request, explicitValidation, risk);
|
|
107
108
|
if (sddPhaseDecision !== undefined)
|
|
108
109
|
return sddPhaseDecision;
|
|
109
110
|
const boundary = workspaceBoundaryDecision(request);
|
|
110
111
|
if (boundary !== undefined)
|
|
111
|
-
return boundary;
|
|
112
|
+
return withExplicitRequestAuthorization(request, boundary, risk);
|
|
112
113
|
const agentDecision = request.agent?.permissions[request.category];
|
|
113
114
|
const baseRule = policy.rules.find((rule) => rule.category === request.category);
|
|
114
115
|
const baseDecision = agentDecision ?? baseRule?.decision ?? 'ask';
|
|
@@ -118,7 +119,7 @@ export function evaluatePermission(request, policy = defaultPermissionPolicy) {
|
|
|
118
119
|
: (baseRule?.reason ?? `No explicit rule exists for ${request.category}; ask by default.`);
|
|
119
120
|
const flagRisk = riskDecision(request);
|
|
120
121
|
if (flagRisk !== undefined && isLessRestrictive(baseDecision, flagRisk.decision)) {
|
|
121
|
-
return result(request, flagRisk.decision, flagRisk.reason, flagRisk.message, {}, risk);
|
|
122
|
+
return withExplicitRequestAuthorization(request, result(request, flagRisk.decision, flagRisk.reason, flagRisk.message, {}, risk), risk);
|
|
122
123
|
}
|
|
123
124
|
if (risk.defaultDecision === 'audit' && baseDecision !== 'deny') {
|
|
124
125
|
return result(request, 'allow', risk.reasonCodes.includes('provider_readonly_audit') ? 'provider_readonly_audit' : risk.reasonCodes.includes('allowlisted_verification') ? 'allowlisted_command' : baseReason, baseMessage, {
|
|
@@ -127,7 +128,39 @@ export function evaluatePermission(request, policy = defaultPermissionPolicy) {
|
|
|
127
128
|
warnings: risk.reasonCodes.includes('provider_readonly_audit') ? ['provider-readonly-audit-only'] : ['audit-only'],
|
|
128
129
|
}, risk);
|
|
129
130
|
}
|
|
130
|
-
return result(request, baseDecision, baseReason, baseMessage, {}, risk);
|
|
131
|
+
return withExplicitRequestAuthorization(request, result(request, baseDecision, baseReason, baseMessage, {}, risk), risk);
|
|
132
|
+
}
|
|
133
|
+
function withExplicitRequestAuthorization(request, decision, risk) {
|
|
134
|
+
if (request.explicitRequest === undefined)
|
|
135
|
+
return decision;
|
|
136
|
+
const explicit = evaluateExplicitRequestAuthorization({ request, risk, evaluatedAt: new Date().toISOString() });
|
|
137
|
+
if (decision.decision !== 'ask') {
|
|
138
|
+
return { ...decision, explicitRequestAudit: explicit.audit, auditEvidence: [...(decision.auditEvidence ?? []), `explicitRequest:${explicit.audit.matchStatus}`] };
|
|
139
|
+
}
|
|
140
|
+
if (explicit.decision === 'allow') {
|
|
141
|
+
return {
|
|
142
|
+
...decision,
|
|
143
|
+
decision: 'allow',
|
|
144
|
+
reason: 'explicit_request_authorization',
|
|
145
|
+
message: 'Explicit human request authorizes this exact one-off operation.',
|
|
146
|
+
explicitRequestAudit: explicit.audit,
|
|
147
|
+
authorizationMode: 'explicit-request',
|
|
148
|
+
auditEvidence: [...(decision.auditEvidence ?? []), 'explicitRequest:exact-match'],
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (explicit.decision === 'deny') {
|
|
152
|
+
return {
|
|
153
|
+
...decision,
|
|
154
|
+
decision: 'deny',
|
|
155
|
+
reason: explicit.audit.reasonCodes.includes('sdd_human_acceptance_required') ? 'sdd_phase_policy' : 'risk_tier_policy',
|
|
156
|
+
message: explicit.audit.reasonCodes.includes('sdd_human_acceptance_required')
|
|
157
|
+
? 'SDD artifact acceptance is human-only and cannot be inferred from explicit-request preflight.'
|
|
158
|
+
: 'Explicit request contains a hard-stop condition and cannot authorize this operation.',
|
|
159
|
+
explicitRequestAudit: explicit.audit,
|
|
160
|
+
auditEvidence: [...(decision.auditEvidence ?? []), `explicitRequest:${explicit.audit.matchStatus}`],
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
return { ...decision, explicitRequestAudit: explicit.audit, auditEvidence: [...(decision.auditEvidence ?? []), `explicitRequest:${explicit.audit.matchStatus}`] };
|
|
131
164
|
}
|
|
132
165
|
export function isPathInsideWorkspace(workspaceRoot, targetPath) {
|
|
133
166
|
const root = resolve(workspaceRoot);
|
package/dist/runs/run-service.js
CHANGED
|
@@ -364,6 +364,8 @@ export class RunService {
|
|
|
364
364
|
requestedOperation: operationMetadata(resolved.value),
|
|
365
365
|
decisionEventId: permission.value.event.id,
|
|
366
366
|
approvalId: permission.value.approval?.id ?? null,
|
|
367
|
+
authorizationMode: permission.value.decision.authorizationMode ?? null,
|
|
368
|
+
explicitRequestAudit: (permission.value.decision.explicitRequestAudit ?? null),
|
|
367
369
|
plan: plan,
|
|
368
370
|
workspaceStrategy: workspaceStrategy,
|
|
369
371
|
...(sandboxDecision === undefined ? {} : { sandboxDecision: sandboxDecision }),
|
|
@@ -396,6 +398,8 @@ export class RunService {
|
|
|
396
398
|
const audit = { permissionEventId: event.id, planEventId: planEvent.id };
|
|
397
399
|
if (approval !== undefined)
|
|
398
400
|
audit.approvalId = approval.id;
|
|
401
|
+
if (decision.explicitRequestAudit !== undefined)
|
|
402
|
+
audit.explicitRequestAudit = decision.explicitRequestAudit;
|
|
399
403
|
const sandbox = plan.sandbox;
|
|
400
404
|
const sandboxRejected = sandbox?.decision === 'rejected';
|
|
401
405
|
const sandboxReason = sandboxRejected ? [{ code: sandbox.reason ?? 'sandbox_boundary', message: sandbox.audit.validationSummary }] : [];
|
|
@@ -404,6 +408,7 @@ export class RunService {
|
|
|
404
408
|
permissionEventId: event.id,
|
|
405
409
|
planEventId: planEvent.id,
|
|
406
410
|
...(approval === undefined ? {} : { approvalId: approval.id }),
|
|
411
|
+
...(decision.explicitRequestAudit === undefined ? {} : { explicitRequestAudit: decision.explicitRequestAudit }),
|
|
407
412
|
});
|
|
408
413
|
return {
|
|
409
414
|
ok: true,
|
|
@@ -772,6 +777,8 @@ export class RunService {
|
|
|
772
777
|
riskReasonCodes: reusedDecision.riskReasonCodes ?? [],
|
|
773
778
|
boundary: (reusedDecision.boundary ?? null),
|
|
774
779
|
auditEvidence: reusedDecision.auditEvidence ?? [],
|
|
780
|
+
explicitRequestAudit: (reusedDecision.explicitRequestAudit ?? null),
|
|
781
|
+
authorizationMode: reusedDecision.authorizationMode ?? (reusedDecision.decision === 'allow' && decision.decision === 'ask' ? 'approval-reuse' : null),
|
|
775
782
|
requiresHumanApproval: reusedDecision.decision === 'ask',
|
|
776
783
|
approvalStatus: prior.value.approval.status,
|
|
777
784
|
reusedApprovalId: prior.value.approval.id,
|
|
@@ -811,6 +818,8 @@ export class RunService {
|
|
|
811
818
|
riskReasonCodes: grantedDecision.riskReasonCodes ?? [],
|
|
812
819
|
boundary: (grantedDecision.boundary ?? null),
|
|
813
820
|
auditEvidence: grantedDecision.auditEvidence ?? [],
|
|
821
|
+
explicitRequestAudit: (grantedDecision.explicitRequestAudit ?? null),
|
|
822
|
+
authorizationMode: grantedDecision.authorizationMode ?? null,
|
|
814
823
|
requiresHumanApproval: false,
|
|
815
824
|
approvalStatus: 'not-required',
|
|
816
825
|
grantEvaluation: grantEvaluationPayload(grantAuthorization.value.evaluation),
|
|
@@ -841,6 +850,8 @@ export class RunService {
|
|
|
841
850
|
riskReasonCodes: decision.riskReasonCodes ?? [],
|
|
842
851
|
boundary: (decision.boundary ?? null),
|
|
843
852
|
auditEvidence: decision.auditEvidence ?? [],
|
|
853
|
+
explicitRequestAudit: (decision.explicitRequestAudit ?? null),
|
|
854
|
+
authorizationMode: decision.authorizationMode ?? null,
|
|
844
855
|
requiresHumanApproval: decision.decision === 'ask',
|
|
845
856
|
approvalStatus: decision.decision === 'ask' ? 'pending' : 'not-required',
|
|
846
857
|
...(grantAuthorization === undefined ? {} : { grantEvaluation: grantEvaluationPayload(grantAuthorization.value.evaluation) }),
|
|
@@ -924,6 +935,7 @@ export class RunService {
|
|
|
924
935
|
permissionEventId: audit.permissionEventId,
|
|
925
936
|
planEventId: audit.planEventId,
|
|
926
937
|
approvalId: audit.approvalId ?? null,
|
|
938
|
+
explicitRequestAudit: (audit.explicitRequestAudit ?? null),
|
|
927
939
|
executorInvoked: false,
|
|
928
940
|
operationExecuted: false,
|
|
929
941
|
timestamp: new Date().toISOString(),
|
|
@@ -1274,6 +1286,10 @@ function operationMetadata(input) {
|
|
|
1274
1286
|
metadata.privileged = input.privileged;
|
|
1275
1287
|
if (input.ambiguous !== undefined)
|
|
1276
1288
|
metadata.ambiguous = input.ambiguous;
|
|
1289
|
+
if (input.providerConfigMutation !== undefined)
|
|
1290
|
+
metadata.providerConfigMutation = input.providerConfigMutation;
|
|
1291
|
+
if (input.globalConfigMutation !== undefined)
|
|
1292
|
+
metadata.globalConfigMutation = input.globalConfigMutation;
|
|
1277
1293
|
if (input.input !== undefined)
|
|
1278
1294
|
metadata.input = input.input;
|
|
1279
1295
|
return metadata;
|
|
@@ -1304,7 +1320,22 @@ function operationMetadataCompatible(preflightOperation, requestedOperation) {
|
|
|
1304
1320
|
return jsonEqual(preflightOperation, requestedOperation);
|
|
1305
1321
|
if (preflightOperation.category !== requestedOperation.category || preflightOperation.name !== requestedOperation.name)
|
|
1306
1322
|
return false;
|
|
1307
|
-
const strictKeys = [
|
|
1323
|
+
const strictKeys = [
|
|
1324
|
+
'workspaceRoot',
|
|
1325
|
+
'targetPath',
|
|
1326
|
+
'providerToolName',
|
|
1327
|
+
'sandboxStrategy',
|
|
1328
|
+
'taskFingerprint',
|
|
1329
|
+
'commandId',
|
|
1330
|
+
'cwd',
|
|
1331
|
+
'targetPaths',
|
|
1332
|
+
'destructive',
|
|
1333
|
+
'external',
|
|
1334
|
+
'privileged',
|
|
1335
|
+
'ambiguous',
|
|
1336
|
+
'providerConfigMutation',
|
|
1337
|
+
'globalConfigMutation',
|
|
1338
|
+
];
|
|
1308
1339
|
for (const key of strictKeys) {
|
|
1309
1340
|
if (key in preflightOperation && key in requestedOperation && !jsonEqual(preflightOperation[key], requestedOperation[key]))
|
|
1310
1341
|
return false;
|
|
@@ -101,6 +101,7 @@ export class TaskScopedCommandGrantService {
|
|
|
101
101
|
decision: 'allow',
|
|
102
102
|
reason: 'task_scoped_grant',
|
|
103
103
|
message: `Task-scoped grant ${consumed.value.id} authorizes this bounded external command preflight.`,
|
|
104
|
+
authorizationMode: 'task-scoped-grant',
|
|
104
105
|
auditEvidence: [...(input.baseDecision.auditEvidence ?? []), `taskScopedGrant:${consumed.value.id}`, ...(evaluation.commandId === undefined ? [] : [`commandAllowlistId:${evaluation.commandId}`])],
|
|
105
106
|
};
|
|
106
107
|
return { ok: true, value: { authorized: true, decision, grant: consumed.value, evaluation, event: event.value } };
|