vgxness 1.7.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.
- package/dist/agents/canonical-agent-manifest.js +4 -1
- package/dist/cli/cli-help.js +2 -2
- package/dist/cli/setup-status-renderer.js +6 -1
- package/dist/cli/tui/main-menu/main-menu-read-model.js +1 -1
- package/dist/cli/tui/setup/setup-tui-read-model.js +7 -7
- package/dist/governance/index.js +1 -0
- package/dist/governance/risk-classifier.js +116 -0
- package/dist/mcp/provider-change-plan.js +19 -8
- package/dist/mcp/schema.js +3 -1
- package/dist/mcp/validation.js +8 -1
- package/dist/orchestrator/natural-language-planner.js +34 -17
- package/dist/permissions/policy-evaluator.js +88 -11
- package/dist/runs/execution-planning.js +1 -1
- package/dist/runs/run-service.js +129 -35
- package/dist/setup/providers/claude-setup-adapter.js +48 -5
- package/dist/setup/providers/opencode-setup-adapter.js +2 -2
- package/dist/setup/providers/provider-setup-adapter.js +3 -0
- package/dist/setup/setup-lifecycle-service.js +2 -1
- package/dist/setup/setup-plan.js +2 -2
- package/package.json +1 -1
|
@@ -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
|
|
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
|
|
package/dist/cli/cli-help.js
CHANGED
|
@@ -69,7 +69,7 @@ Areas:
|
|
|
69
69
|
Use --overwrite-vgxness (alias --reinstall) to reinstall only VGXNESS-managed OpenCode entries while preserving unrelated config; --yes is still required to write.
|
|
70
70
|
It writes only after --yes. The default target is $HOME/.config/opencode/opencode.json; use --scope project to target .opencode/opencode.json explicitly.
|
|
71
71
|
Project OpenCode config can override user config. Plans are read-only; applies refuse unsafe existing config and create backups before merge.
|
|
72
|
-
Claude support is
|
|
72
|
+
Claude support is first-class for guarded MCP setup. Claude scopes are local|project|user; compatibility aliases personal/global map to user with warnings. Plans show Claude CLI argv, project .mcp.json compatibility, user ~/.claude.json MCP merge, agents, and guarded CLAUDE.md managed memory separately. Project scope confirmed applies write .mcp.json, .claude/agents/*.md, and the project-root CLAUDE.md managed block as needed. User/global confirmed applies narrowly merge only mcpServers.vgxness in ~/.claude.json, write ~/.claude/agents/*.md, and manage only the VGXNESS block in ~/.claude/CLAUDE.md. Confirmed Claude writes/CLI execution require VGXNESS run preflight metadata (--run-id, with optional --phase/--agent-id). Status/doctor/change-plan are read-only and do not execute Claude Code.
|
|
73
73
|
|
|
74
74
|
skills register --project <name> --name <name> --description <text>
|
|
75
75
|
skills list [--project <name>] [--scope project|personal]
|
|
@@ -98,7 +98,7 @@ Areas:
|
|
|
98
98
|
No args in an interactive TTY opens the OpenTUI main menu.
|
|
99
99
|
No args without a TTY prints static safe setup guidance and exits 0 without opening project state.
|
|
100
100
|
Setup TUI may launch without --project; Installation remains available and project-scoped checks are deferred while project screens render project-required recovery states.
|
|
101
|
-
Provider setup support: OpenCode supported
|
|
101
|
+
Provider setup support: OpenCode first-class supported default guided install; Claude first-class supported guarded install for CLI MCP registration, project compatibility, and project/user agent planning; Antigravity placeholder; Custom/future extension point.
|
|
102
102
|
Provider config writes/install/apply are external-only and require explicit confirmation.
|
|
103
103
|
|
|
104
104
|
sdd status --project <name> --change <id> [--json]
|
|
@@ -41,5 +41,10 @@ function renderDefaults(status) {
|
|
|
41
41
|
}
|
|
42
42
|
function renderProvider(provider) {
|
|
43
43
|
const safety = provider.actions.some((action) => action.safety.writesProviderConfig) ? 'external write action flagged' : 'no provider writes';
|
|
44
|
-
return `${provider.displayName} ${provider.status}/${provider.supportLevel} (${safety})`;
|
|
44
|
+
return `${provider.displayName} ${provider.status}/${renderSupportLevel(provider.supportLevel)} (${safety})`;
|
|
45
|
+
}
|
|
46
|
+
function renderSupportLevel(supportLevel) {
|
|
47
|
+
if (supportLevel === 'supported-primary' || supportLevel === 'supported-secondary')
|
|
48
|
+
return 'supported';
|
|
49
|
+
return supportLevel;
|
|
45
50
|
}
|
|
@@ -14,7 +14,7 @@ const statCards = [
|
|
|
14
14
|
{ label: 'SDD', value: 'guided', badge: '', description: 'MCP flow' },
|
|
15
15
|
];
|
|
16
16
|
const statusSnapshotLines = [
|
|
17
|
-
'OpenCode
|
|
17
|
+
'OpenCode supported; dashboard does not call providers.',
|
|
18
18
|
'Advanced checks stay explicit: setup status, mcp doctor opencode.',
|
|
19
19
|
];
|
|
20
20
|
const optionCopy = {
|
|
@@ -31,7 +31,7 @@ export function setupTuiViewModelFromPlan(plan, status, state) {
|
|
|
31
31
|
workspaceRootLabel: compactPath(workspaceRoot, 72),
|
|
32
32
|
readinessLabel: label(readiness),
|
|
33
33
|
readinessBadge: readinessBadge(readiness),
|
|
34
|
-
providerLabel: isOpenCode ? 'OpenCode' : isClaude ? 'Claude (supported
|
|
34
|
+
providerLabel: isOpenCode ? 'OpenCode' : isClaude ? 'Claude (first-class supported)' : 'Manual / none',
|
|
35
35
|
databaseLabel: plan === undefined ? 'pending' : plan.db.mode,
|
|
36
36
|
databasePathLabel: compactPath(databasePath, 72),
|
|
37
37
|
databaseSourceLabel: String(databaseSource),
|
|
@@ -47,8 +47,8 @@ export function setupTuiViewModelFromPlan(plan, status, state) {
|
|
|
47
47
|
opencodeActionLabel: isOpenCode ? opencodeActionLabel(opencode?.action) : isClaude ? 'No guided setup write; use guarded Claude install with run preflight.' : 'No automatic provider write; use manual setup guidance.',
|
|
48
48
|
memoryPathExplanation: memoryExplanation,
|
|
49
49
|
providerInstallabilityLabel: isOpenCode
|
|
50
|
-
? 'OpenCode is the default guided install provider. Claude is supported
|
|
51
|
-
: 'Manual / none is read-only guidance. Claude remains supported
|
|
50
|
+
? 'OpenCode is the default guided install provider. Claude is first-class supported via guarded mcp install claude --scope project --yes --run-id <id>.'
|
|
51
|
+
: 'Manual / none is read-only guidance. Claude remains supported through explicit guarded apply outside this guided OpenCode flow.',
|
|
52
52
|
agentReadinessLabel: agentReadiness.label,
|
|
53
53
|
agentReadinessDetail: agentReadiness.detail,
|
|
54
54
|
plannedActions: plan?.actions.map((action) => ({
|
|
@@ -79,7 +79,7 @@ export function setupTuiViewModelFromPlan(plan, status, state) {
|
|
|
79
79
|
],
|
|
80
80
|
providerChoices: [
|
|
81
81
|
choice('provider:opencode', 'OpenCode', 'Default guided install provider; writes still require final confirmation.', (selections?.provider ?? plan?.provider ?? 'opencode') === 'opencode', state?.focusedChoiceId, [badgeLabels.recommended]),
|
|
82
|
-
choice('provider:claude-supported
|
|
82
|
+
choice('provider:claude-supported', 'Claude (first-class supported)', 'Claude CLI MCP registration and project compatibility apply only via guarded mcp install claude --scope project --yes --run-id <id>; explicit install path.', (selections?.provider ?? plan?.provider) === 'claude', state?.focusedChoiceId, ['[supported]', badgeLabels.readOnly]),
|
|
83
83
|
choice('provider:none', 'Manual / none', 'No automatic provider config write; follow manual setup guidance.', (selections?.provider ?? plan?.provider) === 'none', state?.focusedChoiceId, [badgeLabels.manual, badgeLabels.readOnly]),
|
|
84
84
|
],
|
|
85
85
|
scopeChoices: isOpenCode
|
|
@@ -106,13 +106,13 @@ function previewDetailLines(input) {
|
|
|
106
106
|
const opencode = plan?.opencode;
|
|
107
107
|
const actions = plan?.actions.map((action) => `Planned action: ${action.description}${action.targetPath === undefined ? '' : ` Target: ${compactPath(action.targetPath, 72)}`}${action.backupRequired ? ' Backup required.' : ''}`) ?? ['Planned action: create read-only preview plan.'];
|
|
108
108
|
return [
|
|
109
|
-
`Provider: ${input.provider === 'opencode' ? 'OpenCode [recommended default]' : input.provider === 'claude' ? 'Claude [supported
|
|
109
|
+
`Provider: ${input.provider === 'opencode' ? 'OpenCode [recommended default]' : input.provider === 'claude' ? 'Claude [first-class supported] [guarded explicit apply]' : 'Manual / none [manual] [read-only]'}`,
|
|
110
110
|
`Memory path: ${plan?.db.mode ?? 'pending'} at ${compactPath(input.databasePath, 72)} (source: ${String(input.databaseSource)})`,
|
|
111
111
|
`Memory guidance: ${memoryPathExplanation(plan, input.databasePath, input.databaseSource)}`,
|
|
112
112
|
`Scope: ${input.isOpenCode ? (opencode?.scope ?? 'user') : 'disabled for manual/none provider'}`,
|
|
113
113
|
`Install mode: ${input.isOpenCode ? (opencode?.installsAgents === false ? 'mcp-only' : 'mcp-plus-agents') : 'disabled for manual/none provider'}`,
|
|
114
114
|
`Reinstall VGXNESS entries: ${input.isOpenCode ? String(opencode?.overwriteVgxness ?? false) : 'disabled for manual/none provider'}`,
|
|
115
|
-
`Provider installability: ${input.isOpenCode ? 'OpenCode installable after final confirmation; Claude
|
|
115
|
+
`Provider installability: ${input.isOpenCode ? 'OpenCode installable after final confirmation; Claude first-class support requires guarded mcp install claude --scope project --yes --run-id <id>.' : 'No guided provider install from this selection; Claude uses guarded explicit apply.'}`,
|
|
116
116
|
`Agent readiness: ${agentReadinessFromPlan(plan)}`,
|
|
117
117
|
`Target config: ${input.isOpenCode && opencode?.targetPath !== undefined ? compactPath(opencode.targetPath, 72) : 'none; no provider config will be written'}`,
|
|
118
118
|
`Safety: ${input.isOpenCode ? '[will write after confirm] only on final confirmation' : '[read-only] manual/no-provider-write mode'}`,
|
|
@@ -132,7 +132,7 @@ function helpLines(screen) {
|
|
|
132
132
|
'Next/back: Tab continues; Shift+Tab goes back. Enter continues on review and confirms only on final confirmation.',
|
|
133
133
|
'Cancel/close: q or Esc cancels setup; when help is open, ?/h toggles it closed.',
|
|
134
134
|
reviewLine,
|
|
135
|
-
'Provider support: OpenCode is the default guided install provider; Claude is supported
|
|
135
|
+
'Provider support: OpenCode is the default guided install provider; Claude is first-class supported for guarded explicit apply via mcp install claude --scope project --yes --run-id <id>; Manual / none writes no provider config.',
|
|
136
136
|
'Agent readiness: the preview checks vgxness-manager/SDD readiness guidance; preview screens never seed agents.',
|
|
137
137
|
'No-write guarantee: no provider config is written before explicit final confirmation.',
|
|
138
138
|
];
|
package/dist/governance/index.js
CHANGED
|
@@ -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
|
+
}
|
|
@@ -5,7 +5,7 @@ import { ProviderDoctorService } from './provider-doctor.js';
|
|
|
5
5
|
import { ProviderStatusService } from './provider-status.js';
|
|
6
6
|
import { resolveClaudeCodeScope } from './claude-code-scope.js';
|
|
7
7
|
export const providerChangePlanProviders = ['opencode', 'claude', 'antigravity', 'custom'];
|
|
8
|
-
export const providerChangePlanTypes = ['opencode-mcp-install', 'setup', 'install', 'config-preparation'];
|
|
8
|
+
export const providerChangePlanTypes = ['opencode-mcp-install', 'claude-mcp-install', 'setup', 'install', 'config-preparation'];
|
|
9
9
|
const safety = {
|
|
10
10
|
readOnly: true,
|
|
11
11
|
willWrite: false,
|
|
@@ -102,7 +102,7 @@ function unsupportedProviderEnvelope(input) {
|
|
|
102
102
|
supported: false,
|
|
103
103
|
status: 'unsupported',
|
|
104
104
|
code: 'UNSUPPORTED_PROVIDER',
|
|
105
|
-
summary: `${input.provider} is a recognized provider value, but provider change planning
|
|
105
|
+
summary: `${input.provider} is a recognized provider value, but read-only provider change planning currently supports OpenCode and Claude.`,
|
|
106
106
|
providerIdentity: {
|
|
107
107
|
provider: input.provider,
|
|
108
108
|
adapter: input.provider,
|
|
@@ -120,15 +120,16 @@ function unsupportedProviderEnvelope(input) {
|
|
|
120
120
|
};
|
|
121
121
|
}
|
|
122
122
|
function blockedPlanEnvelope(input, status, doctor, message) {
|
|
123
|
+
const providerName = input.provider === 'claude' ? 'Claude' : 'OpenCode';
|
|
123
124
|
return {
|
|
124
125
|
...baseEnvelope(input),
|
|
125
126
|
supported: true,
|
|
126
127
|
status: 'blocked',
|
|
127
128
|
code: 'PLAN_UNAVAILABLE',
|
|
128
|
-
summary:
|
|
129
|
+
summary: `${providerName} change planning could not resolve the future MCP server command: ${message}`,
|
|
129
130
|
providerIdentity: {
|
|
130
|
-
provider:
|
|
131
|
-
adapter:
|
|
131
|
+
provider: input.provider,
|
|
132
|
+
adapter: input.provider,
|
|
132
133
|
support: 'supported',
|
|
133
134
|
},
|
|
134
135
|
statusSummary: statusSummary(status),
|
|
@@ -206,14 +207,24 @@ function baseEnvelope(input) {
|
|
|
206
207
|
scope: input.scope,
|
|
207
208
|
provider: input.provider,
|
|
208
209
|
requestedChangeType: input.changeType,
|
|
209
|
-
normalizedChangeType: normalizeChangeType(input.changeType),
|
|
210
|
+
normalizedChangeType: normalizeChangeType(input.provider, input.changeType),
|
|
210
211
|
payloadMode: input.payloadMode,
|
|
211
212
|
workspaceRoot: input.workspaceRoot,
|
|
212
213
|
safety,
|
|
213
214
|
};
|
|
214
215
|
}
|
|
215
|
-
function normalizeChangeType(changeType) {
|
|
216
|
-
|
|
216
|
+
function normalizeChangeType(provider, changeType) {
|
|
217
|
+
if (provider === 'opencode') {
|
|
218
|
+
return changeType === 'setup' || changeType === 'install' || changeType === 'config-preparation' || changeType === 'claude-mcp-install'
|
|
219
|
+
? 'opencode-mcp-install'
|
|
220
|
+
: changeType;
|
|
221
|
+
}
|
|
222
|
+
if (provider === 'claude') {
|
|
223
|
+
return changeType === 'setup' || changeType === 'install' || changeType === 'config-preparation' || changeType === 'opencode-mcp-install'
|
|
224
|
+
? 'claude-mcp-install'
|
|
225
|
+
: changeType;
|
|
226
|
+
}
|
|
227
|
+
return changeType;
|
|
217
228
|
}
|
|
218
229
|
function statusSummary(status) {
|
|
219
230
|
return {
|
package/dist/mcp/schema.js
CHANGED
|
@@ -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',
|
|
@@ -99,7 +100,7 @@ const finalRunStatuses = ['completed', 'failed', 'blocked', 'cancelled'];
|
|
|
99
100
|
const runOutcomes = ['success', 'partial', 'failure', 'blocked', 'cancelled'];
|
|
100
101
|
const payloadModes = ['compact', 'verbose'];
|
|
101
102
|
const providerChangePlanProviders = ['opencode', 'claude', 'antigravity', 'custom'];
|
|
102
|
-
const providerChangePlanTypes = ['opencode-mcp-install', 'setup', 'install', 'config-preparation'];
|
|
103
|
+
const providerChangePlanTypes = ['opencode-mcp-install', 'claude-mcp-install', 'setup', 'install', 'config-preparation'];
|
|
103
104
|
const jsonValueSchema = z.lazy(() => z.union([z.string(), z.number().finite(), z.boolean(), z.null(), z.array(jsonValueSchema), z.record(z.string(), jsonValueSchema)]));
|
|
104
105
|
export const VGX_MCP_TOOL_INPUT_SCHEMAS = {
|
|
105
106
|
vgxness_sdd_status: z
|
|
@@ -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(),
|
package/dist/mcp/validation.js
CHANGED
|
@@ -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'];
|
|
@@ -13,7 +14,7 @@ const runOutcomes = ['success', 'partial', 'failure', 'blocked', 'cancelled'];
|
|
|
13
14
|
const permissionDecisions = ['allow', 'ask', 'deny'];
|
|
14
15
|
const payloadModes = ['compact', 'verbose'];
|
|
15
16
|
const providerChangePlanProviders = ['opencode', 'claude', 'antigravity', 'custom'];
|
|
16
|
-
const providerChangePlanTypes = ['opencode-mcp-install', 'setup', 'install', 'config-preparation'];
|
|
17
|
+
const providerChangePlanTypes = ['opencode-mcp-install', 'claude-mcp-install', 'setup', 'install', 'config-preparation'];
|
|
17
18
|
const validChangePattern = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
18
19
|
const validMemoryTopicKeyPattern = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;
|
|
19
20
|
const maxMemoryTitleLength = 200;
|
|
@@ -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
|
|
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._/
|
|
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 (
|
|
129
|
-
return '
|
|
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) =>
|
|
137
|
-
return '
|
|
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
|
|
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
|
|
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
|
|
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 ??
|
|
234
|
-
|
|
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
|
|
106
|
-
if (
|
|
107
|
-
return result(request,
|
|
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
|
|
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 {
|
|
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: '
|
|
110
|
+
return { status: 'compatible', reason: 'workspace has no git metadata to conflict with a planned worktree boundary' };
|
|
111
111
|
};
|
package/dist/runs/run-service.js
CHANGED
|
@@ -233,22 +233,25 @@ export class RunService {
|
|
|
233
233
|
};
|
|
234
234
|
}
|
|
235
235
|
planOperationExecution(input) {
|
|
236
|
-
const
|
|
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:
|
|
243
|
+
operation: resolved.value,
|
|
241
244
|
decision: permission.value.decision,
|
|
242
|
-
...(
|
|
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:
|
|
249
|
+
runId: resolved.value.runId,
|
|
247
250
|
kind: 'execution-plan',
|
|
248
|
-
title: `Execution plan: ${
|
|
251
|
+
title: `Execution plan: ${resolved.value.category} ${resolved.value.operation}`,
|
|
249
252
|
payload: {
|
|
250
|
-
runId:
|
|
251
|
-
requestedOperation: operationMetadata(
|
|
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:
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
575
|
+
const contextConflict = workflowConflictDecision(inputWithRunContext, run.value.workflow);
|
|
576
|
+
const decision = contextConflict ?? evaluatePermission(inputWithRunContext);
|
|
566
577
|
const timestamp = new Date().toISOString();
|
|
567
|
-
const agent =
|
|
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(
|
|
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:
|
|
595
|
+
runId: inputWithRunContext.runId,
|
|
585
596
|
kind: 'permission-decision',
|
|
586
|
-
title: `Permission ${reusedDecision.decision}: ${
|
|
597
|
+
title: `Permission ${reusedDecision.decision}: ${inputWithRunContext.category} ${inputWithRunContext.operation}`,
|
|
587
598
|
payload: {
|
|
588
|
-
runId:
|
|
589
|
-
category:
|
|
590
|
-
operation:
|
|
591
|
-
|
|
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:
|
|
628
|
+
runId: inputWithRunContext.runId,
|
|
612
629
|
kind: 'permission-decision',
|
|
613
|
-
title: `Permission ${decision.decision}: ${
|
|
630
|
+
title: `Permission ${decision.decision}: ${inputWithRunContext.category} ${inputWithRunContext.operation}`,
|
|
614
631
|
payload: {
|
|
615
|
-
runId:
|
|
616
|
-
category:
|
|
617
|
-
operation:
|
|
618
|
-
|
|
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:
|
|
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:
|
|
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 (!
|
|
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 &&
|
|
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');
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { planClaudeCodeMcpInstall } from '../../mcp/client-install-claude-code-contract.js';
|
|
1
2
|
import { createMcpClientSetupPreview } from '../../mcp/client-setup-preview.js';
|
|
2
|
-
import { noWriteActionSafety } from './provider-setup-adapter.js';
|
|
3
|
+
import { externalProviderWriteSafety, noWriteActionSafety } from './provider-setup-adapter.js';
|
|
3
4
|
export const claudeSetupAdapter = {
|
|
4
5
|
id: 'claude',
|
|
5
6
|
displayName: 'Claude',
|
|
6
|
-
supportLevel: 'supported
|
|
7
|
+
supportLevel: 'supported',
|
|
7
8
|
capabilities: ['mcp-preview', 'mcp-install-plan', 'cli-mcp-plan', 'agent-preview', 'doctor', 'manual-guidance'],
|
|
8
9
|
targets: [
|
|
9
10
|
{ kind: 'project-config', label: 'Project .mcp.json', path: '.mcp.json', writableBySetup: false },
|
|
@@ -14,10 +15,11 @@ export const claudeSetupAdapter = {
|
|
|
14
15
|
{ kind: 'user-config', label: 'User Claude memory', path: '~/.claude/CLAUDE.md', writableBySetup: false },
|
|
15
16
|
],
|
|
16
17
|
getStatus(context) {
|
|
18
|
+
const plan = this.getInstallPlan?.(context);
|
|
17
19
|
return {
|
|
18
20
|
providerId: 'claude',
|
|
19
|
-
status: '
|
|
20
|
-
summary: 'Claude
|
|
21
|
+
status: plan?.status === 'blocked' ? 'blocked' : 'available',
|
|
22
|
+
summary: 'Claude provider setup is available for CLI MCP registration planning/apply, project .mcp.json compatibility, guarded CLAUDE.md memory, user ~/.claude.json MCP merge, and project/user agents; explicit guarded apply is required for writes or CLI execution.',
|
|
21
23
|
evidence: context.databasePath !== undefined
|
|
22
24
|
? ['Claude MCP preview can be generated from the selected database path.']
|
|
23
25
|
: ['Claude MCP preview uses a placeholder until a database path is selected.'],
|
|
@@ -27,9 +29,10 @@ export const claudeSetupAdapter = {
|
|
|
27
29
|
id: 'claude-manual-guidance',
|
|
28
30
|
label: 'Review guarded Claude install guidance',
|
|
29
31
|
kind: 'manual-guidance',
|
|
30
|
-
description: '
|
|
32
|
+
description: 'Claude install guidance only in setup status; writes are available only through the guarded mcp install claude flow with run preflight metadata.',
|
|
31
33
|
safety: noWriteActionSafety,
|
|
32
34
|
},
|
|
35
|
+
...(plan?.actions ?? []),
|
|
33
36
|
],
|
|
34
37
|
};
|
|
35
38
|
},
|
|
@@ -48,4 +51,44 @@ export const claudeSetupAdapter = {
|
|
|
48
51
|
warnings: preview.warnings,
|
|
49
52
|
};
|
|
50
53
|
},
|
|
54
|
+
getInstallPlan(context) {
|
|
55
|
+
const contract = planClaudeCodeMcpInstall({
|
|
56
|
+
cwd: context.workspaceRoot,
|
|
57
|
+
databasePath: context.databasePath ?? '<database-path>',
|
|
58
|
+
...(context.databasePathSource !== undefined ? { databasePathSource: context.databasePathSource } : {}),
|
|
59
|
+
scope: 'project',
|
|
60
|
+
...(context.env !== undefined ? { env: context.env } : {}),
|
|
61
|
+
...(context.overwriteVgxness !== undefined ? { overwriteVgxness: context.overwriteVgxness } : {}),
|
|
62
|
+
});
|
|
63
|
+
return claudeInstallPlan(context, contract);
|
|
64
|
+
},
|
|
51
65
|
};
|
|
66
|
+
function claudeInstallPlan(_context, contract) {
|
|
67
|
+
const actions = contract.status === 'would_install' ? [externalInstallAction(contract.targetPath, contract.overwriteVgxness)] : [];
|
|
68
|
+
return {
|
|
69
|
+
providerId: 'claude',
|
|
70
|
+
status: contract.status === 'would_install' ? 'available' : 'blocked',
|
|
71
|
+
readOnly: true,
|
|
72
|
+
mutating: false,
|
|
73
|
+
writesProviderConfig: false,
|
|
74
|
+
summary: contract.status === 'would_install'
|
|
75
|
+
? 'Claude project install plan is available for external guarded application; the read-only setup status does not write provider config.'
|
|
76
|
+
: contract.message,
|
|
77
|
+
targetPath: contract.targetPath,
|
|
78
|
+
warnings: contract.warnings,
|
|
79
|
+
actions,
|
|
80
|
+
source: contract,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function externalInstallAction(targetPath, overwriteVgxness = false) {
|
|
84
|
+
return {
|
|
85
|
+
id: 'claude-mcp-install-project',
|
|
86
|
+
label: 'Copy external Claude project install command',
|
|
87
|
+
kind: 'copy-command',
|
|
88
|
+
command: ['vgxness', 'mcp', 'install', 'claude', '--scope', 'project', '--yes', ...(overwriteVgxness ? ['--overwrite-vgxness'] : [])],
|
|
89
|
+
description: overwriteVgxness
|
|
90
|
+
? 'Copy and run this outside setup status only after reviewing that VGXNESS entries will be overwritten and unrelated config preserved.'
|
|
91
|
+
: 'Copy and run this outside setup status only after reviewing the read-only plan.',
|
|
92
|
+
safety: externalProviderWriteSafety(targetPath),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -5,7 +5,7 @@ import { externalProviderWriteSafety, noWriteActionSafety, } from './provider-se
|
|
|
5
5
|
export const openCodeSetupAdapter = {
|
|
6
6
|
id: 'opencode',
|
|
7
7
|
displayName: 'OpenCode',
|
|
8
|
-
supportLevel: 'supported
|
|
8
|
+
supportLevel: 'supported',
|
|
9
9
|
capabilities: ['mcp-preview', 'mcp-install-plan', 'mcp-install-apply-external', 'agent-preview', 'doctor', 'visibility-check'],
|
|
10
10
|
targets: [
|
|
11
11
|
{ kind: 'user-config', label: 'User/global OpenCode config (default)', path: '$HOME/.config/opencode/opencode.json', writableBySetup: false },
|
|
@@ -19,7 +19,7 @@ export const openCodeSetupAdapter = {
|
|
|
19
19
|
status: visibility?.ready === true ? 'ready' : 'available',
|
|
20
20
|
summary: visibility?.ready === true
|
|
21
21
|
? 'OpenCode MCP visibility is ready.'
|
|
22
|
-
: 'OpenCode
|
|
22
|
+
: 'OpenCode provider setup is available; install/apply remains external and explicit.',
|
|
23
23
|
evidence: visibility?.evidence ?? ['OpenCode setup preview and install planning are available.'],
|
|
24
24
|
guidance: visibility?.guidance ?? ['Preview setup first, then copy and run external install commands only after review.'],
|
|
25
25
|
actions: plan?.actions ?? [],
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isSupportedProviderLevel } from './providers/provider-setup-adapter.js';
|
|
1
2
|
import { listProviderSetupAdapters } from './providers/provider-setup-registry.js';
|
|
2
3
|
const agentName = 'vgxness-manager';
|
|
3
4
|
export class SetupLifecycleService {
|
|
@@ -151,7 +152,7 @@ function verificationSummary(environment, project, providers, agents, mcp) {
|
|
|
151
152
|
{
|
|
152
153
|
id: 'providers',
|
|
153
154
|
label: 'Providers',
|
|
154
|
-
status: providers.some((provider) => provider.supportLevel
|
|
155
|
+
status: providers.some((provider) => isSupportedProviderLevel(provider.supportLevel)) ? 'ready' : 'unknown',
|
|
155
156
|
detail: `${providers.length} provider targets are available for read-only setup status.`,
|
|
156
157
|
},
|
|
157
158
|
{
|
package/dist/setup/setup-plan.js
CHANGED
|
@@ -87,7 +87,7 @@ export function createSetupPlan(input) {
|
|
|
87
87
|
actions: [
|
|
88
88
|
{
|
|
89
89
|
id: 'claude-guarded-install',
|
|
90
|
-
description: `Review supported
|
|
90
|
+
description: `Review first-class supported Claude plan for CLI MCP registration, .mcp.json compatibility, ${claude.agentNames.length} .claude/agents/*.md file(s), and guarded project-root CLAUDE.md managed block; apply only with vgxness mcp install claude --scope project --yes --run-id <id>.`,
|
|
91
91
|
mutating: false,
|
|
92
92
|
targetPath,
|
|
93
93
|
backupRequired: claude.backupRequired,
|
|
@@ -100,7 +100,7 @@ export function createSetupPlan(input) {
|
|
|
100
100
|
targetPath: target.path,
|
|
101
101
|
reason: target.kind === 'project-memory' ? 'A future guarded Claude apply would create a managed VGXNESS backup before appending or updating the project-root CLAUDE.md managed block.' : 'A future guarded Claude apply would create a managed VGXNESS backup before merging or updating Claude project configuration.',
|
|
102
102
|
})),
|
|
103
|
-
nextCommands: ['vgxness mcp install claude --plan --scope project', 'vgxness mcp install claude --scope project --yes --run-id <id>', 'OpenCode remains the default
|
|
103
|
+
nextCommands: ['vgxness mcp install claude --plan --scope project', 'vgxness mcp install claude --scope project --yes --run-id <id>', 'OpenCode remains the default guided setup provider.'],
|
|
104
104
|
},
|
|
105
105
|
};
|
|
106
106
|
}
|