vgxness 1.8.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -74,7 +74,7 @@ function managerDefinition() {
74
74
  mode: 'agent',
75
75
  builtIn: true,
76
76
  name: canonicalDefaultAgentName,
77
- description: 'Coordinates VGXNESS MCP state and SDD sub-agents while avoiding unsafe inline execution.',
77
+ description: 'Coordinates VGXNESS MCP state and SDD sub-agents while routing Tier 0-2 lightweight work, Tier 3 preflight validation, and Tier 4 formal SDD.',
78
78
  instructions: { kind: 'inline', value: registryManagerInstructions },
79
79
  capabilities: ['sdd-orchestration', 'agent-routing', 'mcp-coordination', 'project-local-automation'],
80
80
  permissions: { read: 'allow', edit: 'ask', shell: 'ask', git: 'ask', memory: 'allow', 'provider-tool': 'deny', secrets: 'deny' },
@@ -131,6 +131,9 @@ Coach while coordinating: teach briefly when helpful, explain practical tradeoff
131
131
  - Do not publish packages unless explicitly requested.
132
132
  - Do not change model or reasoning effort.
133
133
 
134
+ ## Flexible governance routing
135
+ Use the lightest safe path: Tier 0-2 direct/explore/plan/debug/quickfix/build; keywords alone do not force SDD. Tier 3 needs preflight/explicit validation but not automatic SDD. Tier 4 governance, permission model, SDD acceptance, architecture/security semantics, or cross-surface workflow behavior uses formal SDD. Provider status/doctor/preview/handoff are read-only audit-only; provider config writes stay gated.
136
+
134
137
  ## Provider-native daily flow
135
138
  Normal SDD progression happens inside OpenCode through conversation, VGXNESS MCP, and hidden SDD subagents. Do not tell users to run terminal SDD phase commands for daily flow. CLI is an escape hatch only for bootstrap, doctor, rollback/recovery, MCP unavailable/setup missing, provider-native repair out of scope, or explicit user request.
136
139
 
@@ -1,3 +1,4 @@
1
1
  export * from './governance-report-builder.js';
2
2
  export * from './overlay-fingerprint.js';
3
+ export * from './risk-classifier.js';
3
4
  export * from './schema.js';
@@ -0,0 +1,116 @@
1
+ import { isWorkflowId } from '../workflows/schema.js';
2
+ const readOnlyIntentTerms = [
3
+ /\b(explain|what|why|how|describe|read|inspect|search|status|doctor|preview|diagnos(?:e|tic|tics)|logs?)\b/,
4
+ /\b(explicar|diagnosticar)\b/,
5
+ ];
6
+ const planningTerms = [/\b(plan|review|checklist|preview|dry[- ]run)\b/, /\btarea simple\b/];
7
+ const quickfixTerms = [/\b(fix|patch|typo|small|bounded|arreglar)\b/];
8
+ const buildTerms = [/\b(build|add|create|implement|feature|capability)\b/];
9
+ const tier3Terms = [
10
+ /\b(install|repair|write config|provider config|\.opencode|\.claude|global config)\b/,
11
+ /\b(commit|push|tag|amend|force[- ]push|publish|release|delete|remove|destroy|drop|reset|secret|token|sudo|external|network)\b/,
12
+ /\b(aprobaci[oó]n|permiso)\b/,
13
+ ];
14
+ const tier4MutationTerms = [
15
+ /\b(build|add|create|implement)\b.*\bnew\b.*\bworkflow\b/,
16
+ /\b(change|update|modify|refactor|implement|build|add|create|fix|arreglar)\b.*\b(governance|permission model|permissions?|sdd acceptance|artifact acceptance|workflow routing|workflow capability|routing|security semantics|architecture|cross[- ]surface)\b/,
17
+ /\b(governance|permission model|permissions?|sdd acceptance|artifact acceptance|workflow routing|workflow capability|routing|security semantics|architecture|cross[- ]surface)\b.*\b(change|update|modify|refactor|implement|build|add|create|fix|arreglar)\b/,
18
+ ];
19
+ const keywordOnlyProtectedTerms = /\b(governance|permissions?|workflow|provider|sdd|security|architecture)\b/;
20
+ export function classifyIntentRisk(input) {
21
+ const intent = normalize(input.intent);
22
+ const evidence = [`intent:${intent}`];
23
+ if (tier4MutationTerms.some((term) => term.test(intent))) {
24
+ const reasonCodes = [];
25
+ if (/\bpermissions?|permission model|permiso\b/.test(intent))
26
+ reasonCodes.push('permission_model_change');
27
+ if (/\bsdd acceptance|artifact acceptance|sdd\b/.test(intent))
28
+ reasonCodes.push('sdd_governance_change');
29
+ if (/\bgovernance\b/.test(intent))
30
+ reasonCodes.push('governance_change');
31
+ if (/\barchitecture\b/.test(intent))
32
+ reasonCodes.push('architecture_change');
33
+ if (/\bcross[- ]surface|workflow routing|workflow capability|routing\b/.test(intent))
34
+ reasonCodes.push('cross_surface_change');
35
+ return classification(4, unique(reasonCodes.length === 0 ? ['governance_change'] : reasonCodes), 'sdd', 'ask', evidence);
36
+ }
37
+ if (readOnlyIntentTerms.some((term) => term.test(intent)) || (/\b(no uses sdd)\b/.test(intent) && keywordOnlyProtectedTerms.test(intent))) {
38
+ const workflow = /\bdiagnos(?:e|tic|tics)|diagnosticar|doctor|status|logs?\b/.test(intent) ? 'debug' : 'direct';
39
+ return classification(0, ['read_only'], workflow, 'audit', evidence);
40
+ }
41
+ if (tier3Terms.some((term) => term.test(intent)))
42
+ return classification(3, ['unknown_conservative'], workflowFromHint(input.workflowHint, 'plan'), 'ask', evidence);
43
+ if (buildTerms.some((term) => term.test(intent)))
44
+ return classification(2, ['moderate_project_mutation'], 'build', 'ask', evidence);
45
+ if (planningTerms.some((term) => term.test(intent)))
46
+ return classification(1, ['read_only'], 'plan', 'audit', evidence);
47
+ if (quickfixTerms.some((term) => term.test(intent)))
48
+ return classification(1, ['small_local_change'], 'quickfix', 'allow', evidence);
49
+ return classification(0, ['read_only'], 'explore', 'audit', evidence);
50
+ }
51
+ export function classifyOperationRisk(input) {
52
+ const op = normalize([input.operation, input.providerToolName].filter(Boolean).join(' '));
53
+ const evidence = [`category:${input.category}`, `operation:${input.operation}`];
54
+ if (input.providerToolName !== undefined)
55
+ evidence.push(`providerTool:${input.providerToolName}`);
56
+ if (input.destructive === true)
57
+ return classification(3, ['destructive_operation'], 'plan', 'ask', evidence);
58
+ if (input.external === true || input.category === 'network' || input.category === 'external-directory')
59
+ return classification(3, ['external_operation'], 'plan', 'ask', evidence);
60
+ if (input.privileged === true)
61
+ return classification(3, ['privileged_operation'], 'plan', 'ask', evidence);
62
+ if (input.ambiguous === true)
63
+ return classification(3, ['ambiguous_mutation'], 'plan', 'ask', evidence);
64
+ if (input.category === 'secrets')
65
+ return classification(3, ['secret_access'], 'plan', 'deny', evidence);
66
+ if (input.category === 'provider-tool')
67
+ return classifyProviderTool(op, evidence);
68
+ if (input.category === 'git-write' || (input.category === 'git' && /\b(commit|amend|tag|push|delete[- ]branch|force[- ]push|merge|rebase)\b/.test(op)))
69
+ return classification(3, ['git_write'], 'plan', 'ask', evidence);
70
+ if (input.category === 'git')
71
+ return classification(0, ['read_only'], 'direct', 'audit', evidence);
72
+ if (/\b(publish|release|npm publish|package distribution)\b/.test(op))
73
+ return classification(3, ['publish_release'], 'plan', 'ask', evidence);
74
+ if (input.category === 'shell' && /^command-allowlist[ :-][a-z0-9._/-]+$/.test(op))
75
+ return classification(0, ['allowlisted_verification'], 'debug', 'allow', evidence);
76
+ if (input.category === 'shell')
77
+ return classification(3, ['unknown_conservative'], 'plan', 'ask', evidence);
78
+ if (input.category === 'test-run')
79
+ return classification(0, ['allowlisted_verification'], 'debug', 'audit', evidence);
80
+ if (input.category === 'read')
81
+ return classification(0, [input.workspaceRoot !== undefined ? 'workspace_read' : 'read_only'], 'direct', 'audit', evidence);
82
+ if (input.category === 'edit' || input.category === 'implementation-edit' || input.category === 'spec-write' || input.category === 'design-write' || input.category === 'task-write')
83
+ return classification(input.category === 'implementation-edit' ? 2 : 1, [input.category === 'implementation-edit' ? 'moderate_project_mutation' : 'small_local_change'], workflowFromHint(input.workflow, 'quickfix'), 'ask', evidence);
84
+ if (input.category === 'install')
85
+ return classification(3, ['external_operation'], 'plan', 'ask', evidence);
86
+ if (input.category === 'memory' || input.category === 'memory-write')
87
+ return classification(1, ['small_local_change'], 'explore', 'audit', evidence);
88
+ return classification(3, ['unknown_conservative'], 'plan', 'ask', evidence);
89
+ }
90
+ function classifyProviderTool(operation, evidence) {
91
+ if (/\b(status|doctor|preview|handoff|change[- ]plan|health)\b/.test(operation))
92
+ return classification(0, ['provider_readonly_audit'], 'direct', 'audit', evidence);
93
+ if (/\b(install|setup|repair|write|apply|config|mcp[- ]install)\b/.test(operation))
94
+ return classification(3, ['provider_config_write'], 'plan', 'ask', evidence);
95
+ return classification(3, ['unknown_conservative'], 'plan', 'ask', evidence);
96
+ }
97
+ function classification(tier, reasonCodes, recommendedWorkflow, defaultDecision, evidence) {
98
+ return {
99
+ tier,
100
+ reasonCodes,
101
+ recommendedWorkflow,
102
+ defaultDecision,
103
+ requiresSdd: tier === 4,
104
+ requiresExplicitValidation: tier >= 3,
105
+ evidence,
106
+ };
107
+ }
108
+ function workflowFromHint(value, fallback) {
109
+ return value !== undefined && isWorkflowId(value) ? value : fallback;
110
+ }
111
+ function normalize(value) {
112
+ return value.toLowerCase().replace(/[^a-z0-9._/ áéíóúüñ-]+/g, ' ').replace(/\s+/g, ' ').trim();
113
+ }
114
+ function unique(values) {
115
+ return [...new Set(values)];
116
+ }
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { permissionCategories } from '../permissions/schema.js';
3
3
  import { sddPhases } from '../sdd/schema.js';
4
+ import { workflowIds } from '../workflows/schema.js';
4
5
  import { verificationChangeTypes } from '../verification/index.js';
5
6
  export const SUPPORTED_VGX_MCP_TOOL_NAMES = [
6
7
  'vgxness_sdd_status',
@@ -364,6 +365,7 @@ export const VGX_MCP_TOOL_INPUT_SCHEMAS = {
364
365
  runId: z.string().min(1),
365
366
  category: z.enum(permissionCategories),
366
367
  operation: z.string().min(1),
368
+ workflow: z.enum(workflowIds).optional(),
367
369
  phase: z.string().min(1).optional(),
368
370
  agentId: z.string().min(1).optional(),
369
371
  workspaceRoot: z.string().min(1).optional(),
@@ -1,6 +1,7 @@
1
1
  import { isRiskyPermissionCategory, permissionCategories } from '../permissions/schema.js';
2
2
  import { parseOperationRetryPolicy } from '../runs/operation-retry.js';
3
3
  import { isSddPhase, sddPhases } from '../sdd/schema.js';
4
+ import { workflowIds } from '../workflows/schema.js';
4
5
  import { supportedTypeMessage, verificationChangeTypes } from '../verification/index.js';
5
6
  import { errorEnvelope, isVgxMcpToolName, } from './schema.js';
6
7
  const scopes = ['project', 'personal'];
@@ -822,6 +823,7 @@ function validateRunPreflightInput(input, tool) {
822
823
  'runId',
823
824
  'category',
824
825
  'operation',
826
+ 'workflow',
825
827
  'phase',
826
828
  'agentId',
827
829
  'workspaceRoot',
@@ -846,6 +848,11 @@ function validateRunPreflightInput(input, tool) {
846
848
  if (!operation.ok)
847
849
  return operation;
848
850
  const result = { runId: runId.value, category: category.value, operation: operation.value };
851
+ const workflow = readOptionalOneOf(record.value, 'workflow', workflowIds, tool);
852
+ if (!workflow.ok)
853
+ return workflow;
854
+ if (workflow.value !== undefined)
855
+ result.workflow = workflow.value;
849
856
  const copied = copyOptionalStrings(result, record.value, tool, ['phase', 'agentId', 'workspaceRoot', 'targetPath', 'providerToolName']);
850
857
  if (!copied.ok)
851
858
  return copied;
@@ -1,9 +1,11 @@
1
+ import { classifyIntentRisk } from '../governance/risk-classifier.js';
1
2
  const stopWords = new Set(['a', 'an', 'the', 'to', 'for', 'of', 'and', 'or', 'with', 'we', 'before', 'new']);
2
3
  const signalRules = [
3
4
  {
4
5
  signal: 'diagnostic-request',
5
6
  terms: [
6
7
  /\bdiagnos(e|tic|tics)\b/,
8
+ /\bdiagnosticar\b/,
7
9
  /\bfail(ing|ure|ed)?\b/,
8
10
  /\bcrash(es|ing)?\b/,
9
11
  /\berrors?\b/,
@@ -18,17 +20,17 @@ const signalRules = [
18
20
  { signal: 'planning-request', terms: [/\bplan\b/, /\bpreview\b/, /\breview\b/, /\bdry[- ]run\b/] },
19
21
  {
20
22
  signal: 'answer-only',
21
- terms: [/\bexplain\b/, /\bwhat\b/, /\bhow\b/, /\bwhy\b/, /\bdescribe\b/, /\bunderstand\b/, /\binvestigat(e|ion)\b/, /\binspect\b/, /\bread[- ]only\b/],
23
+ terms: [/\bexplain\b/, /\bexplicar\b/, /\bwhat\b/, /\bhow\b/, /\bwhy\b/, /\bdescribe\b/, /\bunderstand\b/, /\binvestigat(e|ion)\b/, /\binspect\b/, /\bread[- ]only\b/],
22
24
  },
23
25
  { signal: 'new-capability', terms: [/\bbuild\b/, /\badd\b/, /\bcreate\b/, /\bnew\b/, /\bcapabilit(y|ies)\b/, /\bfeature\b/] },
24
26
  { signal: 'architecture-change', terms: [/\barchitecture\b/, /\barchitectural\b/, /\brefactor\b/, /\bboundar(y|ies)\b/] },
25
27
  { signal: 'workflow-change', terms: [/\bworkflow\b/, /\borchestrat(e|ion|or)\b/, /\bsdd\b/, /\bphase\b/] },
26
28
  { signal: 'persistence-change', terms: [/\bpersist(ent|ence)?\b/, /\bstorage\b/, /\bsqlite\b/, /\bdatabase\b/, /\bmemory\b/, /\bmigration\b/] },
27
- { signal: 'security-sensitive', terms: [/\bsecurity\b/, /\bauth\b/, /\btoken\b/, /\bsecret\b/, /\bpermission\b/] },
29
+ { signal: 'security-sensitive', terms: [/\bsecurity\b/, /\bauth\b/, /\btoken\b/, /\bsecret\b/, /\bpermission\b/, /\bpermiso\b/, /\baprobaci[oó]n\b/] },
28
30
  { signal: 'broad-change', terms: [/\bmulti[- ]file\b/, /\bacross\b/, /\bend[- ]to[- ]end\b/, /\bsystem\b/] },
29
31
  { signal: 'execution-request', terms: [/\brun\b/, /\bexecute\b/, /\bapply\b/, /\bstart\b/, /\binstall\b/, /\bpush\b/] },
30
32
  { signal: 'provider-execution', terms: [/\bprovider\b/, /\bopencode\b/, /\bclaude\b/, /\bmodel\b/, /\bllm\b/] },
31
- { signal: 'file-edit-request', terms: [/\bedit\b/, /\bwrite\b/, /\bmodify\b/, /\bpatch\b/] },
33
+ { signal: 'file-edit-request', terms: [/\bedit\b/, /\bwrite\b/, /\bmodify\b/, /\bpatch\b/, /\barreglar\b/] },
32
34
  { signal: 'provider-config-write', terms: [/\bconfig\b/, /\.opencode\b/, /\.claude\b/, /\bglobal\b/] },
33
35
  { signal: 'run-recording', terms: [/\brun record\b/, /\brecord a run\b/, /\bcheckpoint\b/] },
34
36
  { signal: 'destructive', terms: [/\bdelete\b/, /\bremove\b/, /\bdestroy\b/, /\bdrop\b/, /\breset\b/] },
@@ -41,7 +43,8 @@ export function createNaturalLanguagePlan(input) {
41
43
  const ambiguous = isAmbiguous(normalizedIntent);
42
44
  if (ambiguous)
43
45
  addSignal(signals, 'ambiguous');
44
- const flow = chooseFlow(signals);
46
+ const risk = classifyIntentRisk({ project: input.project, intent: input.intent });
47
+ const flow = chooseFlow(signals, risk);
45
48
  const workflow = workflowFor(flow);
46
49
  const needsClarification = ambiguous;
47
50
  const suggestedChangeId = suggestedChangeFor(input, flow);
@@ -67,13 +70,17 @@ export function createNaturalLanguagePlan(input) {
67
70
  ...(suggestedChangeId !== undefined ? { suggestedChangeId } : {}),
68
71
  previewActions,
69
72
  safety,
73
+ riskTier: risk.tier,
74
+ riskReasonCodes: risk.reasonCodes,
75
+ requiresSdd: risk.requiresSdd,
76
+ requiresExplicitValidation: risk.requiresExplicitValidation,
70
77
  ...(input.sdd !== undefined ? { sdd: input.sdd } : {}),
71
78
  };
72
79
  }
73
80
  function normalizeText(value) {
74
81
  return value
75
82
  .toLowerCase()
76
- .replace(/[^a-z0-9._/ -]+/g, ' ')
83
+ .replace(/[^a-z0-9._/ áéíóúüñ-]+/g, ' ')
77
84
  .replace(/\s+/g, ' ')
78
85
  .trim();
79
86
  }
@@ -118,23 +125,35 @@ function isAmbiguous(intent) {
118
125
  return true;
119
126
  return /^(fix|update|change|do|handle) (it|this|that)$/.test(intent);
120
127
  }
121
- function chooseFlow(signals) {
128
+ function chooseFlow(signals, risk) {
129
+ if (risk.requiresSdd)
130
+ return 'sdd';
131
+ if (risk.recommendedWorkflow === 'direct')
132
+ return 'explore';
133
+ if (risk.recommendedWorkflow === 'debug')
134
+ return 'debug';
135
+ if (risk.recommendedWorkflow === 'plan')
136
+ return 'plan';
137
+ if (risk.recommendedWorkflow === 'quickfix')
138
+ return 'quickfix';
139
+ if (risk.recommendedWorkflow === 'build')
140
+ return 'build';
122
141
  if (signals.includes('answer-only') &&
123
142
  !signals.includes('diagnostic-request') &&
124
143
  !signals.includes('new-capability') &&
125
144
  !signals.includes('planning-request') &&
126
145
  !signals.some((signal) => unsafeSignals.has(signal)))
127
146
  return 'explore';
128
- if (signals.some((signal) => strongSddSignals.has(signal) || strongUnsafeSignals.has(signal)))
129
- return 'sdd';
147
+ if (risk.tier >= 3)
148
+ return signals.includes('diagnostic-request') ? 'debug' : signals.includes('planning-request') ? 'plan' : 'plan';
130
149
  if (signals.includes('diagnostic-request'))
131
150
  return 'debug';
132
151
  if (signals.includes('answer-only') && !signals.includes('new-capability') && !signals.some((signal) => unsafeSignals.has(signal)))
133
152
  return 'explore';
134
153
  if (signals.includes('planning-request') && !signals.some((signal) => strongSddSignals.has(signal) || strongUnsafeSignals.has(signal)))
135
154
  return 'plan';
136
- if (signals.some((signal) => sddSignals.has(signal)) || signals.some((signal) => unsafeSignals.has(signal)))
137
- return 'sdd';
155
+ if (signals.some((signal) => unsafeSignals.has(signal)))
156
+ return 'plan';
138
157
  if (signals.includes('planning-request'))
139
158
  return 'plan';
140
159
  if (signals.includes('new-capability'))
@@ -171,7 +190,7 @@ function reasonFor(flow, signals, needsClarification) {
171
190
  if (flow === 'debug' || flow === 'diagnose')
172
191
  return 'The request asks for inspection or failure/status diagnosis, so the preview stays read-only.';
173
192
  if (flow === 'sdd')
174
- return 'The request includes capability, architecture, workflow, persistence, safety, or execution risk signals, so SDD is the safest preview path.';
193
+ return 'The request changes governance, permission, SDD, architecture, or cross-surface safety semantics, so SDD is required.';
175
194
  if (flow === 'plan')
176
195
  return 'The request asks for planning or review without immediate mutation, so SDD is not required for this preview.';
177
196
  if (flow === 'build')
@@ -198,14 +217,14 @@ function buildSafety(signals) {
198
217
  'Preview only: this preview only classifies intent; no providers are called, no files are edited, no provider config is written, and no run is recorded.',
199
218
  ];
200
219
  if (signals.includes('execution-request') || signals.includes('provider-execution') || signals.includes('file-edit-request')) {
201
- notes.push('Execution was requested but refused in this preview flow; continue manually or through an approved SDD apply phase.');
220
+ notes.push('Execution was requested but refused in this preview flow; continue manually or through an approved preflight/apply flow.');
202
221
  }
203
222
  if (signals.includes('destructive'))
204
223
  notes.push('Destructive impact was detected; review and approval are required before any real operation.');
205
224
  if (signals.includes('provider-config-write'))
206
225
  notes.push('Provider configuration impact was detected; this preview will not write provider config.');
207
226
  if (signals.includes('persistence-change'))
208
- notes.push('Persistent behavior or storage impact was detected; SDD review is recommended.');
227
+ notes.push('Persistent behavior or storage impact was detected; use the lightest workflow that matches risk tier and require SDD only for Tier 4.');
209
228
  if (signals.includes('external'))
210
229
  notes.push('External or network impact was detected; this preview does not perform external calls.');
211
230
  if (signals.includes('privileged'))
@@ -225,13 +244,11 @@ function buildPreviewActions(input, flow, needsClarification) {
225
244
  return [{ kind: 'workflow-preview', description: 'Preview a small localized quickfix; no execution occurs in this planner response.' }];
226
245
  if (flow === 'build')
227
246
  return [{ kind: 'workflow-preview', description: 'Preview a scoped build workflow; no execution occurs in this planner response.' }];
228
- const change = input.change;
229
- const command = change === undefined ? undefined : `vgxness sdd next --project ${input.project} --change ${change}`;
230
247
  return [
231
248
  {
232
249
  kind: 'sdd-preview',
233
- description: input.sdd?.next?.recommendedAction ?? 'Preview the SDD handoff for this substantial or risky request; do not execute it here.',
234
- ...(command !== undefined ? { command } : {}),
250
+ description: input.sdd?.next?.recommendedAction ??
251
+ 'Continue formal SDD progression through the provider-native manager flow and VGXNESS MCP; do not execute terminal SDD phase commands from this preview.',
235
252
  },
236
253
  ];
237
254
  }
@@ -1,5 +1,6 @@
1
1
  import { existsSync, realpathSync } from 'node:fs';
2
2
  import { dirname, isAbsolute, resolve, sep } from 'node:path';
3
+ import { classifyOperationRisk } from '../governance/risk-classifier.js';
3
4
  import { isSddPhase, sddPhases } from '../sdd/schema.js';
4
5
  export { isRiskyPermissionCategory, permissionCategories, riskyPermissionCategories } from './schema.js';
5
6
  export const permissionDecisions = ['allow', 'ask', 'deny'];
@@ -81,15 +82,24 @@ const filesystemCategories = new Set([
81
82
  'external-directory',
82
83
  ]);
83
84
  export function evaluatePermission(request, policy = defaultPermissionPolicy) {
85
+ const risk = classifyOperationRisk(request);
84
86
  if (request.category === 'secrets')
85
- return result(request, 'deny', 'secret_access', 'Secret access is denied by default.');
87
+ return result(request, 'deny', 'secret_access', 'Secret access is denied by default.', {}, risk);
86
88
  if (request.category === 'external-directory') {
87
- return result(request, 'deny', 'workspace_boundary', 'External directory access is denied by the foundation policy.');
89
+ return result(request, 'deny', 'workspace_boundary', 'External directory access is denied by the foundation policy.', {}, risk);
88
90
  }
91
+ const hardRisk = hardRiskDecision(request, risk);
92
+ if (hardRisk !== undefined)
93
+ return hardRisk;
89
94
  const workspaceEscape = workspaceEscapeDecision(request);
90
95
  if (workspaceEscape !== undefined)
91
96
  return workspaceEscape;
92
97
  const sddPhaseDecision = phaseMatrixDecision(request);
98
+ if (sddPhaseDecision !== undefined && sddPhaseDecision.mode !== 'allow' && sddPhaseDecision.mode !== 'audit')
99
+ return sddPhaseDecision;
100
+ const explicitValidation = explicitValidationDecision(request, risk);
101
+ if (explicitValidation !== undefined)
102
+ return explicitValidation;
93
103
  if (sddPhaseDecision !== undefined)
94
104
  return sddPhaseDecision;
95
105
  const boundary = workspaceBoundaryDecision(request);
@@ -102,11 +112,18 @@ export function evaluatePermission(request, policy = defaultPermissionPolicy) {
102
112
  const baseMessage = agentDecision !== undefined
103
113
  ? `${request.agent?.mode ?? 'agent'} ${request.agent?.name ?? request.agent?.id ?? 'unknown'} sets ${request.category} to ${agentDecision}.`
104
114
  : (baseRule?.reason ?? `No explicit rule exists for ${request.category}; ask by default.`);
105
- const risk = riskDecision(request);
106
- if (risk !== undefined && isLessRestrictive(baseDecision, risk.decision)) {
107
- return result(request, risk.decision, risk.reason, risk.message);
115
+ const flagRisk = riskDecision(request);
116
+ if (flagRisk !== undefined && isLessRestrictive(baseDecision, flagRisk.decision)) {
117
+ return result(request, flagRisk.decision, flagRisk.reason, flagRisk.message, {}, risk);
118
+ }
119
+ if (risk.defaultDecision === 'audit' && baseDecision !== 'deny') {
120
+ return result(request, 'allow', risk.reasonCodes.includes('provider_readonly_audit') ? 'provider_readonly_audit' : risk.reasonCodes.includes('allowlisted_verification') ? 'allowlisted_command' : baseReason, baseMessage, {
121
+ mode: 'audit',
122
+ auditOnly: true,
123
+ warnings: risk.reasonCodes.includes('provider_readonly_audit') ? ['provider-readonly-audit-only'] : ['audit-only'],
124
+ }, risk);
108
125
  }
109
- return result(request, baseDecision, baseReason, baseMessage);
126
+ return result(request, baseDecision, baseReason, baseMessage, {}, risk);
110
127
  }
111
128
  export function isPathInsideWorkspace(workspaceRoot, targetPath) {
112
129
  const root = resolve(workspaceRoot);
@@ -148,11 +165,27 @@ function canonicalExistingPath(targetPath) {
148
165
  function workspaceBoundaryDecision(request) {
149
166
  if (!filesystemCategories.has(request.category))
150
167
  return undefined;
168
+ if (request.category === 'read' && request.workspaceRoot !== undefined && request.targetPath === undefined) {
169
+ return result(request, 'allow', 'workspace_boundary', 'Broad read-only workspace access is allowed when workspaceRoot is known.', {
170
+ mode: 'audit',
171
+ auditOnly: true,
172
+ boundary: { workspaceRoot: request.workspaceRoot, insideWorkspace: true, targetPathRequired: false },
173
+ warnings: ['broad-workspace-read'],
174
+ });
175
+ }
151
176
  if (request.workspaceRoot === undefined || request.targetPath === undefined) {
152
- return result(request, 'ask', 'ambiguous_operation', 'Filesystem operation needs workspaceRoot and targetPath before it can be safely decided.');
177
+ return result(request, 'ask', 'ambiguous_operation', 'Filesystem mutation needs workspaceRoot and targetPath before it can be safely decided.', {
178
+ boundary: {
179
+ ...(request.workspaceRoot === undefined ? {} : { workspaceRoot: request.workspaceRoot }),
180
+ ...(request.targetPath === undefined ? {} : { targetPath: request.targetPath }),
181
+ targetPathRequired: request.category !== 'read',
182
+ },
183
+ });
153
184
  }
154
185
  if (!isPathInsideWorkspace(request.workspaceRoot, request.targetPath)) {
155
- return result(request, 'deny', 'workspace_boundary', `Path escapes workspace boundary: ${request.targetPath}`);
186
+ return result(request, 'deny', 'workspace_boundary', `Path escapes workspace boundary: ${request.targetPath}`, {
187
+ boundary: { workspaceRoot: request.workspaceRoot, targetPath: request.targetPath, insideWorkspace: false, targetPathRequired: true },
188
+ });
156
189
  }
157
190
  return undefined;
158
191
  }
@@ -162,7 +195,9 @@ function workspaceEscapeDecision(request) {
162
195
  if (request.workspaceRoot === undefined || request.targetPath === undefined)
163
196
  return undefined;
164
197
  if (!isPathInsideWorkspace(request.workspaceRoot, request.targetPath)) {
165
- return result(request, 'deny', 'workspace_boundary', `Path escapes workspace boundary: ${request.targetPath}`);
198
+ return result(request, 'deny', 'workspace_boundary', `Path escapes workspace boundary: ${request.targetPath}`, {
199
+ boundary: { workspaceRoot: request.workspaceRoot, targetPath: request.targetPath, insideWorkspace: false, targetPathRequired: true },
200
+ });
166
201
  }
167
202
  return undefined;
168
203
  }
@@ -212,6 +247,37 @@ function riskDecision(request) {
212
247
  return { decision: 'ask', reason: 'ambiguous_operation', message: 'Ambiguous operations require human approval.' };
213
248
  return undefined;
214
249
  }
250
+ function hardRiskDecision(request, risk) {
251
+ if (request.category === 'shell' && /\b(arbitrary|shell-string)\b|[;&|`$()]/.test(request.operation))
252
+ return result(request, 'deny', 'risk_tier_policy', 'Arbitrary shell execution is denied unless represented as an allowlisted category.', {}, risk);
253
+ if (risk.defaultDecision === 'deny')
254
+ return result(request, 'deny', 'risk_tier_policy', 'Risk classifier denies this operation by default.', {}, risk);
255
+ return undefined;
256
+ }
257
+ function explicitValidationDecision(request, risk) {
258
+ if (!risk.requiresExplicitValidation)
259
+ return undefined;
260
+ if (request.category === 'secrets')
261
+ return undefined;
262
+ return result(request, 'ask', reasonForRisk(request, risk), 'Risk tier policy requires explicit validation before this operation.', {}, risk);
263
+ }
264
+ function reasonForRisk(request, risk) {
265
+ if (risk.reasonCodes.includes('provider_config_write'))
266
+ return 'risk_tier_policy';
267
+ if (risk.reasonCodes.includes('git_write'))
268
+ return 'risk_tier_policy';
269
+ if (risk.reasonCodes.includes('publish_release'))
270
+ return 'risk_tier_policy';
271
+ if (request.destructive === true)
272
+ return 'destructive_operation';
273
+ if (request.external === true)
274
+ return 'external_operation';
275
+ if (request.privileged === true)
276
+ return 'privileged_operation';
277
+ if (request.ambiguous === true)
278
+ return 'ambiguous_operation';
279
+ return 'risk_tier_policy';
280
+ }
215
281
  function isLessRestrictive(candidate, floor) {
216
282
  return rank(candidate) < rank(floor);
217
283
  }
@@ -222,6 +288,17 @@ function rank(decision) {
222
288
  return 1;
223
289
  return 2;
224
290
  }
225
- function result(request, decision, reason, message, extras = {}) {
226
- return { decision, category: request.category, operation: request.operation, reason, message, ...extras };
291
+ function result(request, decision, reason, message, extras = {}, risk = classifyOperationRisk(request)) {
292
+ return {
293
+ decision,
294
+ category: request.category,
295
+ operation: request.operation,
296
+ reason,
297
+ message,
298
+ riskTier: risk.tier,
299
+ riskReasonCodes: risk.reasonCodes,
300
+ ...(request.workflow === undefined ? {} : { workflow: request.workflow }),
301
+ auditEvidence: risk.evidence,
302
+ ...extras,
303
+ };
227
304
  }
@@ -107,5 +107,5 @@ const defaultGitBoundaryInspector = ({ workspaceRoot }) => {
107
107
  if (existsSync(join(workspaceRoot, '.git'))) {
108
108
  return { status: 'compatible', reason: 'workspace git metadata is present' };
109
109
  }
110
- return { status: 'uncertain', reason: 'workspace git metadata could not be proven safe without shell execution' };
110
+ return { status: 'compatible', reason: 'workspace has no git metadata to conflict with a planned worktree boundary' };
111
111
  };
@@ -233,22 +233,25 @@ export class RunService {
233
233
  };
234
234
  }
235
235
  planOperationExecution(input) {
236
- const permission = this.evaluatePermissionForRun(input);
236
+ const resolved = this.buildPermissionContextForRun(input);
237
+ if (!resolved.ok)
238
+ return resolved;
239
+ const permission = this.evaluatePermissionForRun(resolved.value);
237
240
  if (!permission.ok)
238
241
  return permission;
239
242
  const plan = planExecutionIsolation({
240
- operation: input,
243
+ operation: resolved.value,
241
244
  decision: permission.value.decision,
242
- ...(input.gitBoundaryInspector === undefined ? {} : { gitBoundaryInspector: input.gitBoundaryInspector }),
245
+ ...(resolved.value.gitBoundaryInspector === undefined ? {} : { gitBoundaryInspector: resolved.value.gitBoundaryInspector }),
243
246
  });
244
247
  const sandboxDecision = sandboxDecisionSummary(plan.sandbox);
245
248
  const planEvent = this.runs.appendEvent({
246
- runId: input.runId,
249
+ runId: resolved.value.runId,
247
250
  kind: 'execution-plan',
248
- title: `Execution plan: ${input.category} ${input.operation}`,
251
+ title: `Execution plan: ${resolved.value.category} ${resolved.value.operation}`,
249
252
  payload: {
250
- runId: input.runId,
251
- requestedOperation: operationMetadata(input),
253
+ runId: resolved.value.runId,
254
+ requestedOperation: operationMetadata(resolved.value),
252
255
  decisionEventId: permission.value.event.id,
253
256
  approvalId: permission.value.approval?.id ?? null,
254
257
  plan: plan,
@@ -262,17 +265,20 @@ export class RunService {
262
265
  timestamp: new Date().toISOString(),
263
266
  },
264
267
  relatedType: 'operation',
265
- relatedId: input.operation,
268
+ relatedId: resolved.value.operation,
266
269
  });
267
270
  if (!planEvent.ok)
268
271
  return { ok: false, error: planEvent.error };
269
272
  return { ok: true, value: { ...permission.value, plan, planEvent: planEvent.value } };
270
273
  }
271
274
  preflightOperation(input) {
272
- const metadataValidation = this.validateVgxManagedPreflightMetadata(input);
275
+ const resolved = this.buildPermissionContextForRun(input);
276
+ if (!resolved.ok)
277
+ return resolved;
278
+ const metadataValidation = this.validateVgxManagedPreflightMetadata(resolved.value);
273
279
  if (!metadataValidation.ok)
274
280
  return metadataValidation;
275
- const planned = this.planOperationExecution(input);
281
+ const planned = this.planOperationExecution(resolved.value);
276
282
  if (!planned.ok)
277
283
  return planned;
278
284
  const { decision, event, approval, plan, planEvent } = planned.value;
@@ -283,7 +289,7 @@ export class RunService {
283
289
  const sandboxRejected = sandbox?.decision === 'rejected';
284
290
  const sandboxReason = sandboxRejected ? [{ code: sandbox.reason ?? 'sandbox_boundary', message: sandbox.audit.validationSummary }] : [];
285
291
  const outcome = sandboxRejected ? 'blocked' : preflightOutcome(decision);
286
- this.appendPreflightAuditEvent(input, outcome, {
292
+ this.appendPreflightAuditEvent(resolved.value, outcome, {
287
293
  permissionEventId: event.id,
288
294
  planEventId: planEvent.id,
289
295
  ...(approval === undefined ? {} : { approvalId: approval.id }),
@@ -313,14 +319,14 @@ export class RunService {
313
319
  const details = this.runs.getDetails(approval.value.runId);
314
320
  if (!details.ok)
315
321
  return details;
316
- const permissionEvent = details.value.events.find((event) => event.id === approval.value.decisionEventId);
322
+ const pendingExecutionEvent = details.value.events.find((event) => isPendingExecutionEventForApproval(event, approval.value.id));
323
+ if (pendingExecutionEvent === undefined)
324
+ return validationFailure('Approved approval has no resumable pending operation event');
325
+ const permissionEvent = permissionEventForPendingExecution(details.value, approval.value, pendingExecutionEvent);
317
326
  if (permissionEvent === undefined)
318
327
  return validationFailure('Approval permission-decision event is missing');
319
328
  if (!isAskPermissionEvent(permissionEvent))
320
329
  return validationFailure('Approval is not linked to an ask permission decision');
321
- const pendingExecutionEvent = details.value.events.find((event) => isPendingExecutionEventForApproval(event, approval.value.id));
322
- if (pendingExecutionEvent === undefined)
323
- return validationFailure('Approved approval has no resumable pending operation event');
324
330
  const operation = operationFromPendingExecution(pendingExecutionEvent.payload);
325
331
  if (operation === undefined)
326
332
  return validationFailure('Pending operation metadata is incomplete and cannot be resumed');
@@ -559,14 +565,19 @@ export class RunService {
559
565
  : { ok: false, error: executionEvent.error };
560
566
  }
561
567
  evaluatePermissionForRun(input) {
562
- const run = this.runs.getById(input.runId);
568
+ const resolved = this.buildPermissionContextForRun(input);
569
+ if (!resolved.ok)
570
+ return resolved;
571
+ const inputWithRunContext = resolved.value;
572
+ const run = this.runs.getById(inputWithRunContext.runId);
563
573
  if (!run.ok)
564
574
  return { ok: false, error: run.error };
565
- const decision = evaluatePermission(input);
575
+ const contextConflict = workflowConflictDecision(inputWithRunContext, run.value.workflow);
576
+ const decision = contextConflict ?? evaluatePermission(inputWithRunContext);
566
577
  const timestamp = new Date().toISOString();
567
- const agent = input.agent === undefined ? { id: run.value.selectedAgentId } : { id: input.agent.id, name: input.agent.name, mode: input.agent.mode };
578
+ const agent = inputWithRunContext.agent === undefined ? { id: run.value.selectedAgentId } : { id: inputWithRunContext.agent.id, name: inputWithRunContext.agent.name, mode: inputWithRunContext.agent.mode };
568
579
  if (decision.decision === 'ask') {
569
- const prior = this.findMatchingApproval(input, agent, input.reusePendingApproval === true);
580
+ const prior = this.findMatchingApproval(inputWithRunContext, agent, inputWithRunContext.reusePendingApproval === true);
570
581
  if (!prior.ok)
571
582
  return prior;
572
583
  if (prior.value !== undefined) {
@@ -581,17 +592,23 @@ export class RunService {
581
592
  message: `Previously ${prior.value.approval.status} approval ${prior.value.approval.id} denies this exact operation.`,
582
593
  };
583
594
  const event = this.runs.appendEvent({
584
- runId: input.runId,
595
+ runId: inputWithRunContext.runId,
585
596
  kind: 'permission-decision',
586
- title: `Permission ${reusedDecision.decision}: ${input.category} ${input.operation}`,
597
+ title: `Permission ${reusedDecision.decision}: ${inputWithRunContext.category} ${inputWithRunContext.operation}`,
587
598
  payload: {
588
- runId: input.runId,
589
- category: input.category,
590
- operation: input.operation,
591
- requestedOperation: operationMetadata(input),
599
+ runId: inputWithRunContext.runId,
600
+ category: inputWithRunContext.category,
601
+ operation: inputWithRunContext.operation,
602
+ workflow: inputWithRunContext.workflow ?? null,
603
+ phase: inputWithRunContext.phase ?? null,
604
+ requestedOperation: operationMetadata(inputWithRunContext),
592
605
  agent,
593
606
  decision: reusedDecision.decision,
594
607
  reasons: [{ code: reusedDecision.reason, message: reusedDecision.message }],
608
+ riskTier: reusedDecision.riskTier ?? null,
609
+ riskReasonCodes: reusedDecision.riskReasonCodes ?? [],
610
+ boundary: (reusedDecision.boundary ?? null),
611
+ auditEvidence: reusedDecision.auditEvidence ?? [],
595
612
  requiresHumanApproval: reusedDecision.decision === 'ask',
596
613
  approvalStatus: prior.value.approval.status,
597
614
  reusedApprovalId: prior.value.approval.id,
@@ -608,29 +625,35 @@ export class RunService {
608
625
  }
609
626
  }
610
627
  const event = this.runs.appendEvent({
611
- runId: input.runId,
628
+ runId: inputWithRunContext.runId,
612
629
  kind: 'permission-decision',
613
- title: `Permission ${decision.decision}: ${input.category} ${input.operation}`,
630
+ title: `Permission ${decision.decision}: ${inputWithRunContext.category} ${inputWithRunContext.operation}`,
614
631
  payload: {
615
- runId: input.runId,
616
- category: input.category,
617
- operation: input.operation,
618
- requestedOperation: operationMetadata(input),
632
+ runId: inputWithRunContext.runId,
633
+ category: inputWithRunContext.category,
634
+ operation: inputWithRunContext.operation,
635
+ workflow: inputWithRunContext.workflow ?? null,
636
+ phase: inputWithRunContext.phase ?? null,
637
+ requestedOperation: operationMetadata(inputWithRunContext),
619
638
  agent,
620
639
  decision: decision.decision,
621
640
  reasons: [{ code: decision.reason, message: decision.message }],
641
+ riskTier: decision.riskTier ?? null,
642
+ riskReasonCodes: decision.riskReasonCodes ?? [],
643
+ boundary: (decision.boundary ?? null),
644
+ auditEvidence: decision.auditEvidence ?? [],
622
645
  requiresHumanApproval: decision.decision === 'ask',
623
646
  approvalStatus: decision.decision === 'ask' ? 'pending' : 'not-required',
624
647
  timestamp,
625
648
  },
626
649
  relatedType: 'permission',
627
- relatedId: input.category,
650
+ relatedId: inputWithRunContext.category,
628
651
  });
629
652
  if (!event.ok)
630
653
  return { ok: false, error: event.error };
631
654
  if (decision.decision !== 'ask')
632
655
  return { ok: true, value: { decision, event: event.value } };
633
- const approval = this.runs.createApproval({ runId: input.runId, decisionEventId: event.value.id });
656
+ const approval = this.runs.createApproval({ runId: inputWithRunContext.runId, decisionEventId: event.value.id });
634
657
  return approval.ok ? { ok: true, value: { decision, event: event.value, approval: approval.value } } : { ok: false, error: approval.error };
635
658
  }
636
659
  validateVgxManagedPreflightMetadata(input) {
@@ -668,6 +691,19 @@ export class RunService {
668
691
  }
669
692
  return { ok: true, value: undefined };
670
693
  }
694
+ buildPermissionContextForRun(input) {
695
+ const run = this.runs.getById(input.runId);
696
+ if (!run.ok)
697
+ return { ok: false, error: run.error };
698
+ const phase = input.phase ?? canonicalRunPhase(run.value.phase);
699
+ const merged = {
700
+ ...input,
701
+ workflow: input.workflow ?? run.value.workflow,
702
+ ...(phase === undefined ? {} : { phase }),
703
+ agentId: input.agentId ?? input.agent?.id ?? run.value.selectedAgentId,
704
+ };
705
+ return { ok: true, value: merged };
706
+ }
671
707
  appendPreflightAuditEvent(input, outcome, audit) {
672
708
  const run = this.runs.getById(input.runId);
673
709
  if (!run.ok || !isVgxManagedWorkflow(run.value.workflow) || !isRiskyPermissionCategory(input.category))
@@ -774,6 +810,23 @@ function isVgxManagedWorkflow(workflow) {
774
810
  const normalized = workflow.toLowerCase();
775
811
  return normalized === 'sdd' || normalized.startsWith('sdd-') || normalized.includes('sdd') || normalized.includes('vgx');
776
812
  }
813
+ function workflowConflictDecision(input, runWorkflow) {
814
+ if (input.workflow === undefined || input.workflow === runWorkflow)
815
+ return undefined;
816
+ const eitherSdd = input.workflow === 'sdd' || runWorkflow === 'sdd' || input.workflow.includes('sdd') || runWorkflow.includes('sdd');
817
+ if (!eitherSdd)
818
+ return undefined;
819
+ return {
820
+ decision: 'ask',
821
+ category: input.category,
822
+ operation: input.operation,
823
+ reason: 'workflow_context',
824
+ message: `Explicit workflow ${input.workflow} conflicts with run workflow ${runWorkflow}; human validation is required before crossing SDD/non-SDD context.`,
825
+ workflow: input.workflow,
826
+ warnings: [`workflow-conflict:${runWorkflow}->${input.workflow}`],
827
+ auditEvidence: [`runWorkflow:${runWorkflow}`, `explicitWorkflow:${input.workflow}`],
828
+ };
829
+ }
777
830
  function executionPayload(operation, executorName, status, extra) {
778
831
  return {
779
832
  status,
@@ -830,6 +883,11 @@ function resumeGateManualNextCommands(approval, details, operation) {
830
883
  commands.push(`Review operation manually: ${operation.category} ${operation.operation}`);
831
884
  return commands;
832
885
  }
886
+ function canonicalRunPhase(phase) {
887
+ if (phase === undefined || phase.trim().length === 0)
888
+ return undefined;
889
+ return phase === 'apply' ? 'apply-progress' : phase;
890
+ }
833
891
  function operationMetadata(input) {
834
892
  const metadata = { category: input.category, name: input.operation };
835
893
  if (input.workspaceRoot !== undefined)
@@ -840,6 +898,12 @@ function operationMetadata(input) {
840
898
  metadata.providerToolName = input.providerToolName;
841
899
  if (input.sandboxStrategy !== undefined)
842
900
  metadata.sandboxStrategy = input.sandboxStrategy;
901
+ if (input.workflow !== undefined)
902
+ metadata.workflow = input.workflow;
903
+ if (input.phase !== undefined)
904
+ metadata.phase = input.phase;
905
+ if (input.agentId !== undefined)
906
+ metadata.agentId = input.agentId;
843
907
  if (input.destructive !== undefined)
844
908
  metadata.destructive = input.destructive;
845
909
  if (input.external !== undefined)
@@ -859,7 +923,7 @@ function approvalEventMatches(event, input, requestedOperation, agent) {
859
923
  return false;
860
924
  if (event.payload.category !== input.category || event.payload.operation !== input.operation)
861
925
  return false;
862
- if (!jsonEqual(event.payload.requestedOperation, requestedOperation))
926
+ if (!operationMetadataCompatible(event.payload.requestedOperation, requestedOperation))
863
927
  return false;
864
928
  if (!jsonEqual(event.payload.agent, agent))
865
929
  return false;
@@ -869,7 +933,23 @@ function isMatchingPreflightPlanEvent(event, operation) {
869
933
  if (event.kind !== 'execution-plan' || !isObject(event.payload))
870
934
  return false;
871
935
  const requestedOperation = event.payload.requestedOperation;
872
- return requestedOperation !== undefined && jsonEqual(requestedOperation, operationMetadata(operation));
936
+ return requestedOperation !== undefined && operationMetadataCompatible(requestedOperation, operationMetadata(operation));
937
+ }
938
+ function operationMetadataCompatible(preflightOperation, requestedOperation) {
939
+ if (preflightOperation === undefined || requestedOperation === undefined)
940
+ return jsonEqual(preflightOperation, requestedOperation);
941
+ if (!isObject(preflightOperation) || !isObject(requestedOperation))
942
+ return jsonEqual(preflightOperation, requestedOperation);
943
+ if (preflightOperation.category !== requestedOperation.category || preflightOperation.name !== requestedOperation.name)
944
+ return false;
945
+ const strictKeys = ['workspaceRoot', 'targetPath', 'providerToolName', 'sandboxStrategy', 'destructive', 'external', 'privileged', 'ambiguous'];
946
+ for (const key of strictKeys) {
947
+ if (key in preflightOperation && key in requestedOperation && !jsonEqual(preflightOperation[key], requestedOperation[key]))
948
+ return false;
949
+ }
950
+ if ('input' in preflightOperation && 'input' in requestedOperation && !jsonEqual(preflightOperation.input, requestedOperation.input))
951
+ return false;
952
+ return true;
873
953
  }
874
954
  function preflightPlanStatus(event) {
875
955
  if (!isObject(event.payload))
@@ -919,6 +999,12 @@ function isAskPermissionEvent(event) {
919
999
  function isPendingExecutionEventForApproval(event, approvalId) {
920
1000
  return (event.kind === 'operation-execution' && isObject(event.payload) && event.payload.status === 'pending-approval' && event.payload.approvalId === approvalId);
921
1001
  }
1002
+ function permissionEventForPendingExecution(details, approval, pendingExecutionEvent) {
1003
+ const decisionEventId = isObject(pendingExecutionEvent.payload) && typeof pendingExecutionEvent.payload.decisionEventId === 'string'
1004
+ ? pendingExecutionEvent.payload.decisionEventId
1005
+ : approval.decisionEventId;
1006
+ return details.events.find((event) => event.id === decisionEventId) ?? details.events.find((event) => event.id === approval.decisionEventId);
1007
+ }
922
1008
  function operationFromPendingExecution(payload) {
923
1009
  if (!isObject(payload))
924
1010
  return undefined;
@@ -955,10 +1041,18 @@ function isObject(value) {
955
1041
  function isPermissionCategory(value) {
956
1042
  return (value === 'read' ||
957
1043
  value === 'edit' ||
1044
+ value === 'implementation-edit' ||
1045
+ value === 'spec-write' ||
1046
+ value === 'design-write' ||
1047
+ value === 'task-write' ||
958
1048
  value === 'shell' ||
1049
+ value === 'test-run' ||
1050
+ value === 'install' ||
959
1051
  value === 'network' ||
960
1052
  value === 'git' ||
1053
+ value === 'git-write' ||
961
1054
  value === 'memory' ||
1055
+ value === 'memory-write' ||
962
1056
  value === 'external-directory' ||
963
1057
  value === 'provider-tool' ||
964
1058
  value === 'secrets');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vgxness",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
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": {