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.
@@ -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/block; never invent 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\`, 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[]\`.
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)
@@ -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
@@ -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);
@@ -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 = ['workspaceRoot', 'targetPath', 'providerToolName', 'sandboxStrategy', 'taskFingerprint', 'commandId', 'cwd', 'targetPaths', 'destructive', 'external', 'privileged', 'ambiguous'];
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 } };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vgxness",
3
- "version": "1.20.4",
3
+ "version": "1.20.5",
4
4
  "description": "CLI and MCP control plane for guided AI-agent workflows, SDD, memory, and OpenCode setup.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "repository": {