vgxness 1.9.8 → 1.10.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.
Files changed (35) hide show
  1. package/README.md +5 -4
  2. package/dist/cli/cli-flags.js +15 -2
  3. package/dist/cli/cli-help.js +13 -13
  4. package/dist/cli/commands/mcp-dispatcher.js +8 -4
  5. package/dist/cli/commands/setup-dispatcher.js +2 -2
  6. package/dist/cli/tui/setup/setup-tui-read-model.js +17 -17
  7. package/dist/cli/tui/setup/setup-tui-state.js +1 -3
  8. package/dist/mcp/claude-code-config.js +2 -0
  9. package/dist/mcp/claude-code-scope.js +1 -0
  10. package/dist/mcp/claude-code-user-config.js +2 -0
  11. package/dist/mcp/client-install-claude-code-contract.js +15 -72
  12. package/dist/mcp/client-install-claude-code.js +4 -6
  13. package/dist/mcp/client-install-opencode-contract.js +5 -79
  14. package/dist/mcp/opencode-visibility.js +4 -3
  15. package/dist/mcp/provider-change-plan.js +10 -7
  16. package/dist/mcp/provider-doctor.js +31 -14
  17. package/dist/mcp/provider-health-types.js +19 -0
  18. package/dist/mcp/provider-status.js +19 -10
  19. package/dist/mcp/schema.js +5 -4
  20. package/dist/mcp/validation.js +11 -1
  21. package/dist/setup/backup-rollback-service.js +33 -2
  22. package/dist/setup/providers/claude-setup-adapter.js +13 -15
  23. package/dist/setup/providers/opencode-setup-adapter.js +12 -12
  24. package/dist/setup/setup-defaults.js +1 -0
  25. package/dist/setup/setup-plan.js +6 -6
  26. package/docs/architecture.md +3 -3
  27. package/docs/cli.md +29 -33
  28. package/docs/glossary.md +2 -2
  29. package/docs/mcp.md +1 -1
  30. package/docs/prd.md +3 -3
  31. package/docs/providers.md +14 -15
  32. package/docs/roadmap.md +1 -1
  33. package/docs/safety.md +1 -1
  34. package/docs/storage.md +1 -1
  35. package/package.json +1 -1
@@ -2,14 +2,15 @@ import { existsSync, readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { createOpenCodeDefaultAgentConfig, createOpenCodeDefaultAgentInstallPlan, } from './opencode-default-agent-config.js';
4
4
  const opencodeConfigSchema = 'https://opencode.ai/config.json';
5
- const supportedConfigTargets = ['.opencode/opencode.json', 'opencode.json', '.opencode/opencode.jsonc', 'opencode.jsonc'];
5
+ const opencodeUserGlobalOnlyMessage = 'VGX-managed OpenCode configuration is user-global only. Project/local OpenCode files are treated as external/manual diagnostics and will not be written by VGXNESS. Re-run without the project/local install scope.';
6
6
  export function resolveOpenCodeMcpInstallTarget(input) {
7
7
  const scope = input.scope ?? 'user';
8
8
  if (scope === 'project')
9
9
  return {
10
- ok: true,
10
+ ok: false,
11
11
  scope,
12
- path: join(input.cwd, '.opencode', 'opencode.json'),
12
+ reason: 'legacy_project_scope',
13
+ message: opencodeUserGlobalOnlyMessage,
13
14
  };
14
15
  const home = input.env?.HOME;
15
16
  if (typeof home !== 'string' || home.trim().length === 0) {
@@ -38,73 +39,7 @@ export function planOpenCodeMcpInstall(input) {
38
39
  if (scope === 'user') {
39
40
  return planUserOpenCodeMcpInstall(input, databasePathSource, server, agentPlan, overwriteVgxness, bashPermissionPolicy);
40
41
  }
41
- const existingTargets = supportedConfigTargets
42
- .map((relativePath) => ({
43
- relativePath,
44
- absolutePath: join(input.cwd, relativePath),
45
- }))
46
- .filter((target) => existsSync(target.absolutePath));
47
- if (existingTargets.length > 1) {
48
- return refusal('ambiguous_target', `Multiple OpenCode project config targets exist: ${existingTargets.map((target) => target.relativePath).join(', ')}. Remove ambiguity before installing.`, input.databasePath, databasePathSource, scope, undefined, [
49
- {
50
- kind: 'manual-check',
51
- message: 'Remove ambiguity by keeping exactly one OpenCode project config target before installing.',
52
- },
53
- ], agentPlan, overwriteVgxness, bashPermissionPolicy);
54
- }
55
- if (existingTargets.length === 0) {
56
- return {
57
- ...baseContract(input.databasePath, databasePathSource, scope, join(input.cwd, '.opencode', 'opencode.json'), false, 'create', agentPlan, overwriteVgxness, bashPermissionPolicy),
58
- status: 'would_install',
59
- action: 'create',
60
- targetPath: join(input.cwd, '.opencode', 'opencode.json'),
61
- backupRequired: false,
62
- server,
63
- preservedTopLevelKeys: createdConfigKeys(agentPlan),
64
- existingSchema: null,
65
- };
66
- }
67
- const [target] = existingTargets;
68
- if (target === undefined)
69
- return refusal('ambiguous_target', 'Unable to resolve OpenCode project config target.', input.databasePath, databasePathSource, scope, undefined, [], undefined, overwriteVgxness, bashPermissionPolicy);
70
- if (target.relativePath.endsWith('.jsonc')) {
71
- return refusal('unsupported_jsonc', `OpenCode JSONC config ${target.relativePath} is not supported yet; use JSON or remove comments first.`, input.databasePath, databasePathSource, scope, target.absolutePath, [], agentPlan, overwriteVgxness, bashPermissionPolicy);
72
- }
73
- const parsed = parseConfig(target.absolutePath);
74
- if (!parsed.ok)
75
- return refusal(parsed.reason, parsed.message, input.databasePath, databasePathSource, scope, target.absolutePath, [], agentPlan, overwriteVgxness, bashPermissionPolicy);
76
- const config = parsed.value;
77
- if (config.mcp !== undefined && !isRecord(config.mcp)) {
78
- return refusal('invalid_mcp_shape', 'Existing top-level mcp must be a JSON object before vgxness can be merged.', input.databasePath, databasePathSource, scope, target.absolutePath, [], agentPlan, overwriteVgxness, bashPermissionPolicy);
79
- }
80
- if (isRecord(config.mcp) && Object.hasOwn(config.mcp, 'vgxness') && !overwriteVgxness) {
81
- return refusal('existing_vgxness_mcp', 'Existing OpenCode config already contains mcp.vgxness; overwrite is refused by default.', input.databasePath, databasePathSource, scope, target.absolutePath, [], agentPlan, overwriteVgxness, bashPermissionPolicy);
82
- }
83
- const agentConflict = findConflictingVgxnessAgents(config, agentPlan);
84
- if (agentConflict.length > 0 && !overwriteVgxness) {
85
- return refusal('existing_vgxness_agent', `Existing OpenCode config contains custom VGXNESS agent entries that would be overwritten: ${agentConflict.join(', ')}. Remove, rename, or manually reconcile them before installing.`, input.databasePath, databasePathSource, scope, target.absolutePath, [
86
- {
87
- kind: 'manual-check',
88
- message: `Manually reconcile conflicting VGXNESS agent entries: ${agentConflict.join(', ')}.`,
89
- },
90
- ], agentPlan, overwriteVgxness, bashPermissionPolicy);
91
- }
92
- if (agentPlan.installsAgents && config.agent !== undefined && !isRecord(config.agent)) {
93
- return refusal('unsupported_config_shape', 'Existing top-level agent must be a JSON object before VGXNESS agent entries can be merged or overwritten.', input.databasePath, databasePathSource, scope, target.absolutePath, [], agentPlan, overwriteVgxness, bashPermissionPolicy);
94
- }
95
- if (config.permission !== undefined && !isRecord(config.permission)) {
96
- return refusal('unsupported_config_shape', 'Existing top-level permission must be a JSON object before VGXNESS can set top-level permission.bash to ask.', input.databasePath, databasePathSource, scope, target.absolutePath, [], agentPlan, overwriteVgxness, bashPermissionPolicy);
97
- }
98
- return {
99
- ...baseContract(input.databasePath, databasePathSource, scope, target.absolutePath, true, 'merge-preserve-existing', agentPlan, overwriteVgxness, bashPermissionPolicy),
100
- status: 'would_install',
101
- action: 'merge',
102
- targetPath: target.absolutePath,
103
- backupRequired: true,
104
- server,
105
- preservedTopLevelKeys: Object.keys(config),
106
- existingSchema: typeof config.$schema === 'string' ? config.$schema : opencodeConfigSchema,
107
- };
42
+ return refusal('unsupported_config_shape', opencodeUserGlobalOnlyMessage, input.databasePath, databasePathSource, scope, undefined, [], agentPlan, overwriteVgxness, bashPermissionPolicy);
108
43
  }
109
44
  function planUserOpenCodeMcpInstall(input, databasePathSource, server, agentPlan, overwriteVgxness, bashPermissionPolicy) {
110
45
  const target = resolveOpenCodeMcpInstallTarget({
@@ -263,8 +198,6 @@ function warningsForScope(scope, overwriteVgxness, agentPlan, bashPermissionPoli
263
198
  ]
264
199
  : [];
265
200
  const bashWarnings = bashPermissionPolicy.manager === 'allow' ? ['OpenCode top-level permission.bash is set to ask; the VGXNESS manager agent allows bash while SDD subagents keep explicit permissions.'] : ['OpenCode top-level permission.bash is set to ask.'];
266
- if (scope === 'project')
267
- return ['Restart OpenCode after installation so it reloads the project MCP config.', ...overwriteWarnings, ...bashWarnings];
268
201
  return [
269
202
  'Restart OpenCode after installation so it reloads the user MCP config.',
270
203
  'OpenCode project config may override user config for a workspace; check project-level config if vgxness is not visible.',
@@ -280,13 +213,6 @@ function createdConfigKeys(agentPlan) {
280
213
  return [...keys, 'permission'];
281
214
  }
282
215
  function manualTestForScope(scope, databasePath, source) {
283
- if (scope === 'project') {
284
- return {
285
- restart: 'Restart OpenCode after the config is installed.',
286
- verify: 'Open the project in OpenCode and verify the vgxness MCP server is available.',
287
- doctorCommand: createVgxnessMcpDoctorCommand(databasePath, source),
288
- };
289
- }
290
216
  return {
291
217
  restart: 'Restart OpenCode after the user config is installed.',
292
218
  verify: 'Open OpenCode and verify the vgxness MCP server is available from the user OpenCode config; project config may override it.',
@@ -15,7 +15,7 @@ export function createOpenCodeMcpVisibilityReport(options) {
15
15
  const projectVisibleFromCurrentCwd = cwdInsideProject && projectConfigExists && projectHasVgxnessServer;
16
16
  const userTarget = resolveOpenCodeMcpInstallTarget({ cwd: projectRoot, scope: 'user', env: options.env });
17
17
  const userReport = userTarget.ok ? userVisibilityReport(userTarget.path, projectVisibleFromCurrentCwd) : undefined;
18
- const selectedTargetPath = targetScope === 'user' && userTarget.ok ? userTarget.path : projectTargetPath;
18
+ const selectedTargetPath = userTarget.ok ? userTarget.path : '$HOME/.config/opencode/opencode.json';
19
19
  const selectedReady = targetScope === 'user' ? userReport?.configExists === true && userReport.hasVgxnessServer : projectVisibleFromCurrentCwd;
20
20
  return {
21
21
  version: 1,
@@ -57,6 +57,9 @@ function buildGuidance(state) {
57
57
  guidance.push('User scope targets `$HOME/.config/opencode/opencode.json` and this diagnostic reads it without writing provider config.');
58
58
  guidance.push('Project OpenCode config can override user config for a workspace; inspect project config first when both define `mcp.vgxness`.');
59
59
  }
60
+ if (state.targetScope === 'project') {
61
+ guidance.push('VGX-managed OpenCode installs are user-global only; project OpenCode config is read as external/manual diagnostics and will not be written by VGXNESS.');
62
+ }
60
63
  if (state.userTargetMessage !== undefined)
61
64
  guidance.push(state.userTargetMessage);
62
65
  if (state.userOverridden)
@@ -65,8 +68,6 @@ function buildGuidance(state) {
65
68
  guidance.push('The project-local `vgxness` server is not expected to appear from outside the worktree.');
66
69
  guidance.push('Use `mcp install opencode --scope user --plan` to inspect the user config target without writing it.');
67
70
  }
68
- if (!state.configExists && state.targetScope === 'project')
69
- guidance.push('Expected project-local config `.opencode/opencode.json` was not found.');
70
71
  if (state.configExists && !state.hasVgxnessServer)
71
72
  guidance.push('Project-local config exists, but it does not define `mcp.vgxness`.');
72
73
  return guidance;
@@ -2,8 +2,8 @@ import { resolveMemoryDatabasePath } from '../memory/storage-paths.js';
2
2
  import { planClaudeCodeMcpInstall } from './client-install-claude-code-contract.js';
3
3
  import { planOpenCodeMcpInstall } from './client-install-opencode-contract.js';
4
4
  import { ProviderDoctorService } from './provider-doctor.js';
5
+ import { providerRuntimeContext } from './provider-health-types.js';
5
6
  import { ProviderStatusService } from './provider-status.js';
6
- import { resolveClaudeCodeScope } from './claude-code-scope.js';
7
7
  export const providerChangePlanProviders = ['opencode', 'claude', 'antigravity', 'custom'];
8
8
  export const providerChangePlanTypes = ['opencode-mcp-install', 'claude-mcp-install', 'setup', 'install', 'config-preparation'];
9
9
  const safety = {
@@ -76,13 +76,10 @@ export class ProviderChangePlanService {
76
76
  const doctor = (this.deps.providerDoctor ?? new ProviderDoctorService()).getDoctor({ project: normalized.project, scope: normalized.scope, providerAdapter: 'claude', workspaceRoot: normalized.workspaceRoot, env: this.deps.env ?? process.env, payloadMode: normalized.payloadMode });
77
77
  if (!doctor.ok)
78
78
  return doctor;
79
- const claudeScope = resolveClaudeCodeScope(normalized.scope);
80
- if (!claudeScope.ok)
81
- return claudeScope;
82
79
  const databasePath = resolveMemoryDatabasePath({ cwd: normalized.workspaceRoot, env: this.deps.env ?? process.env });
83
80
  if (!databasePath.ok)
84
81
  return { ok: true, value: blockedPlanEnvelope(normalized, status.value, doctor.value, databasePath.error.message) };
85
- const installPlan = planClaudeCodeMcpInstall({ cwd: normalized.workspaceRoot, databasePath: databasePath.value.path, databasePathSource: databasePath.value.source, scope: normalized.scope, env: this.deps.env ?? process.env });
82
+ const installPlan = planClaudeCodeMcpInstall({ cwd: normalized.workspaceRoot, databasePath: databasePath.value.path, databasePathSource: databasePath.value.source, scope: 'user', env: this.deps.env ?? process.env });
86
83
  return { ok: true, value: claudeEnvelope(normalized, status.value, doctor.value, installPlan, databasePath.value.source) };
87
84
  }
88
85
  }
@@ -90,6 +87,7 @@ function normalizeInput(input) {
90
87
  return {
91
88
  project: input.project?.trim() || 'vgxness',
92
89
  scope: input.scope ?? 'project',
90
+ installTarget: input.installTarget ?? 'user-global',
93
91
  provider: input.provider,
94
92
  changeType: input.changeType,
95
93
  workspaceRoot: input.workspaceRoot.trim(),
@@ -134,6 +132,7 @@ function blockedPlanEnvelope(input, status, doctor, message) {
134
132
  },
135
133
  statusSummary: statusSummary(status),
136
134
  statusFindings: status.config,
135
+ configClassification: status.configClassification,
137
136
  doctorSummary: doctorSummary(doctor),
138
137
  doctorFindings: doctor.checks,
139
138
  previewEffects: {
@@ -162,6 +161,7 @@ function opencodeEnvelope(input, status, doctor, installPlan, source) {
162
161
  },
163
162
  statusSummary: statusSummary(status),
164
163
  statusFindings: status.config,
164
+ configClassification: status.configClassification,
165
165
  doctorSummary: doctorSummary(doctor),
166
166
  doctorFindings: doctor.checks,
167
167
  previewEffects: previewEffects(installPlan),
@@ -185,10 +185,11 @@ function claudeEnvelope(input, status, doctor, installPlan, source) {
185
185
  supported: true,
186
186
  status: installPlan.status === 'refused' ? 'blocked' : 'planned',
187
187
  ...(installPlan.status === 'refused' ? { code: 'PLAN_UNAVAILABLE' } : {}),
188
- summary: installPlan.status === 'refused' ? `Read-only Claude ${input.changeType} planning completed; future install is currently refused: ${installPlan.message}` : `Read-only Claude ${input.changeType} planning completed; future confirmed write would update ${installPlan.canonicalClaudeScope === 'user' ? '~/.claude.json, user agents, and ~/.claude/CLAUDE.md' : 'project .mcp.json, agent files, and project-root CLAUDE.md'} as needed.`,
188
+ summary: installPlan.status === 'refused' ? `Read-only Claude ${input.changeType} planning completed; future install is currently refused: ${installPlan.message}` : 'Read-only Claude planning completed; future confirmed write would update ~/.claude.json, user agents, and ~/.claude/CLAUDE.md as needed. Project/local Claude files are diagnostics only.',
189
189
  providerIdentity: { provider: 'claude', adapter: 'claude', support: 'supported' },
190
190
  statusSummary: statusSummary(status),
191
191
  statusFindings: status.config,
192
+ configClassification: status.configClassification,
192
193
  doctorSummary: doctorSummary(doctor),
193
194
  doctorFindings: doctor.checks,
194
195
  previewEffects: previewEffectsClaude(installPlan),
@@ -205,11 +206,13 @@ function baseEnvelope(input) {
205
206
  kind: 'provider-change-plan',
206
207
  project: input.project,
207
208
  scope: input.scope,
209
+ installTarget: input.installTarget,
208
210
  provider: input.provider,
209
211
  requestedChangeType: input.changeType,
210
212
  normalizedChangeType: normalizeChangeType(input.provider, input.changeType),
211
213
  payloadMode: input.payloadMode,
212
214
  workspaceRoot: input.workspaceRoot,
215
+ runtimeContext: providerRuntimeContext(input),
213
216
  safety,
214
217
  };
215
218
  }
@@ -323,7 +326,7 @@ function risksForOpenCode(status, doctor, plan, source) {
323
326
  return risks;
324
327
  }
325
328
  function risksForClaude(status, doctor, plan, source) {
326
- const risks = [plan.canonicalClaudeScope === 'user' ? 'Claude user/global files affect this OS user; backups are required before merging existing files.' : 'Claude project .mcp.json and project-root CLAUDE.md may affect collaborators if committed; review before committing.', 'Claude Code runtime MCP approval happens in Claude Code; this plan does not prove runtime connection.'];
329
+ const risks = ['Claude user/global files affect this OS user; backups are required before merging existing files.', 'Claude Code runtime MCP approval happens in Claude Code; this plan does not prove runtime connection.'];
327
330
  if (status.status !== 'ready')
328
331
  risks.push(`Provider status is ${status.status}; future writes should review status findings first.`);
329
332
  if (doctor.status !== 'healthy')
@@ -7,7 +7,7 @@ import { inspectClaudeCodeUserMcpConfig } from './claude-code-user-config.js';
7
7
  import { inspectClaudeUserMemory } from './claude-code-user-memory.js';
8
8
  import { vgxnessOpenCodeDefaultAgent, vgxnessOpenCodeSddSubagents } from './opencode-default-agent-config.js';
9
9
  import { buildCanonicalAgentManifestDiagnostic } from './provider-canonical-agent-manifest.js';
10
- import { normalizeProviderHealthInput, PROVIDER_HEALTH_SAFETY, CLAUDE_USER_GLOBAL_SCOPE_CAPABILITIES, providerHealthFailure, isUserGlobalScope, REQUIRED_PROVIDER_NATIVE_MCP_TOOLS, rollupProviderDoctor, } from './provider-health-types.js';
10
+ import { normalizeProviderHealthInput, PROVIDER_HEALTH_SAFETY, CLAUDE_USER_GLOBAL_SCOPE_CAPABILITIES, classifyProviderConfigPaths, providerHealthFailure, providerRuntimeContext, isUserGlobalScope, REQUIRED_PROVIDER_NATIVE_MCP_TOOLS, rollupProviderDoctor, } from './provider-health-types.js';
11
11
  import { inspectOpenCodeConfigPaths } from './provider-status.js';
12
12
  export class ProviderDoctorService {
13
13
  deps;
@@ -65,6 +65,8 @@ export class ProviderDoctorService {
65
65
  providerAdapter: normalized.providerAdapter,
66
66
  scope: normalized.scope,
67
67
  workspaceRoot: normalized.workspaceRoot,
68
+ runtimeContext: providerRuntimeContext(normalized),
69
+ configClassification: classifyProviderConfigPaths(paths),
68
70
  status,
69
71
  payloadMode: normalized.payloadMode,
70
72
  overallStatus: status,
@@ -105,24 +107,24 @@ export class ProviderDoctorService {
105
107
  const memoryFiles = advisoryPaths.filter((path) => existsSync(path));
106
108
  const checks = [
107
109
  { id: 'workspace-root', status: existsSync(normalized.workspaceRoot) ? 'pass' : 'fail', detail: `Workspace root ${existsSync(normalized.workspaceRoot) ? 'exists' : 'does not exist'}: ${normalized.workspaceRoot}` },
108
- { id: 'provider-supported', status: 'pass', detail: 'Claude provider adapter selected for CLI MCP registration diagnostics, project .mcp.json compatibility, and Claude agents support.' },
110
+ { id: 'provider-supported', status: 'pass', detail: 'Claude provider adapter selected. VGX-managed Claude writes are user-global only; project Claude files are advisory external/manual diagnostics.' },
109
111
  { id: 'claude-scope', status: 'pass', detail: 'Claude scope project resolved to canonical project.' },
110
112
  { id: 'claude-cli-presence', status: 'warn', detail: 'Read-only doctor does not execute `claude --version`; CLI availability is diagnosed during explicit apply/preflight only.' },
111
- { id: 'claude-project-mcp-readable', status: mcp.status === 'invalid' ? 'fail' : mcp.status === 'missing' ? 'not-configured' : 'pass', detail: mcp.message, ...(mcp.status === 'invalid' ? { remediation: 'Fix malformed .mcp.json before installing VGXNESS Claude support.' } : {}) },
112
- { id: 'claude-vgxness-mcp-entry', status: mcp.status === 'configured' ? 'pass' : mcp.status === 'conflicting' ? 'fail' : 'not-configured', detail: mcp.message, ...(mcp.status === 'conflicting' ? { remediation: 'Manually reconcile mcpServers.vgxness before applying VGXNESS Claude support.' } : {}) },
113
- { id: 'claude-agents-directory', status: agents.directoryExists ? 'pass' : 'not-configured', detail: agents.directoryExists ? 'Claude project agents directory exists.' : 'Claude project agents directory is missing; confirmed apply may create it.' },
113
+ { id: 'claude-project-mcp-readable', status: mcp.status === 'invalid' ? 'warn' : 'pass', detail: `${mcp.message} Advisory only; VGXNESS will not write project .mcp.json.`, ...(mcp.status === 'invalid' ? { remediation: 'Fix malformed project .mcp.json manually if Claude Code should read it.' } : {}) },
114
+ { id: 'claude-vgxness-mcp-entry', status: mcp.status === 'conflicting' ? 'warn' : 'pass', detail: `${mcp.message} Advisory only; managed Claude MCP registration is user-global.`, ...(mcp.status === 'conflicting' ? { remediation: 'Manually reconcile project mcpServers.vgxness only if you intend to keep external project Claude config.' } : {}) },
115
+ { id: 'claude-agents-directory', status: 'pass', detail: agents.directoryExists ? 'Claude project agents directory exists as external/manual diagnostics.' : 'Claude project agents directory is absent; VGXNESS will not create project-local Claude agents.' },
114
116
  {
115
117
  id: 'claude-vgxness-agents',
116
- status: blockingAgents.length > 0 ? 'fail' : missingAgents.length > 0 ? 'not-configured' : 'pass',
118
+ status: blockingAgents.length > 0 ? 'warn' : 'pass',
117
119
  detail: blockingAgents.length > 0
118
- ? `Conflicting or invalid VGXNESS Claude agents: ${blockingAgents.map((agent) => agent.agentName).join(', ')}.`
120
+ ? `External/manual project Claude agents conflict or are invalid; VGXNESS will not repair them: ${blockingAgents.map((agent) => agent.agentName).join(', ')}.`
119
121
  : missingAgents.length > 0
120
- ? `Missing VGXNESS Claude agents: ${missingAgents.map((agent) => agent.agentName).join(', ')}.`
121
- : 'Expected VGXNESS Claude agent targets were inspected.',
122
+ ? `Project-local VGXNESS Claude agents are absent; they are no longer managed targets.`
123
+ : 'External/manual project Claude agent files were inspected.',
122
124
  },
123
- { id: 'claude-agent-frontmatter', status: badFrontmatter.length === 0 ? 'pass' : 'fail', detail: badFrontmatter.length === 0 ? 'Expected Claude agent frontmatter is valid.' : `Invalid or missing frontmatter for: ${badFrontmatter.map((agent) => agent.agentName).join(', ')}.` },
124
- { id: 'claude-agent-generated-metadata', status: badMarkers.length === 0 ? 'pass' : 'fail', detail: badMarkers.length === 0 ? 'Existing VGXNESS Claude agent files include generated metadata markers.' : `Missing VGXNESS generated marker for: ${badMarkers.map((agent) => agent.agentName).join(', ')}.` },
125
- claudeProjectMemoryCheck(projectMemory),
125
+ { id: 'claude-agent-frontmatter', status: badFrontmatter.length === 0 ? 'pass' : 'warn', detail: badFrontmatter.length === 0 ? 'No invalid project Claude agent frontmatter was detected.' : `External/manual project Claude agent frontmatter is invalid for: ${badFrontmatter.map((agent) => agent.agentName).join(', ')}.` },
126
+ { id: 'claude-agent-generated-metadata', status: badMarkers.length === 0 ? 'pass' : 'warn', detail: badMarkers.length === 0 ? 'No project Claude agent metadata marker issues were detected.' : `External/manual project Claude agents are missing VGXNESS generated markers: ${badMarkers.map((agent) => agent.agentName).join(', ')}.` },
127
+ advisoryProjectMemoryCheck(projectMemory),
126
128
  { id: 'claude-project-memory-advisory', status: memoryFiles.length === 0 ? 'pass' : 'warn', detail: memoryFiles.length === 0 ? 'No Claude project memory/settings advisory files were found.' : `Advisory only; VGXNESS will not modify: ${memoryFiles.join(', ')}.` },
127
129
  readonlySafetyCheck(before, snapshotPaths(checkedPathList, normalized.workspaceRoot)),
128
130
  ];
@@ -131,7 +133,7 @@ export class ProviderDoctorService {
131
133
  const checkedPaths = normalized.payloadMode === 'verbose' ? checkedPathList : checkedPathList.filter((path) => existsSync(path) || path === mcp.path);
132
134
  const failedChecks = checks.filter((check) => check.status === 'fail').map((check) => check.id);
133
135
  const recommendations = checks.flatMap((check) => (check.remediation === undefined ? [] : [check.remediation]));
134
- return { ok: true, value: { version: 1, kind: 'provider-doctor', project: normalized.project, providerAdapter: 'claude', scope: normalized.scope, workspaceRoot: normalized.workspaceRoot, status, payloadMode: normalized.payloadMode, overallStatus: status, checkCount: checks.length, passedCount: checks.filter((check) => check.status === 'pass').length, warningCount: checks.filter((check) => check.status === 'warn' || check.status === 'not-configured').length, errorCount: failedChecks.length, skippedCount: checks.filter((check) => check.status === 'skip').length, failedChecks, summary: summarizeDoctor(status, failedChecks.length, recommendations.length), recommendations, checks: compactChecksValue, checkedPaths, bytes: { originalBytes: Buffer.byteLength(JSON.stringify({ checks, checkedPaths: checkedPathList }), 'utf8'), compactBytes: Buffer.byteLength(JSON.stringify({ checks: compactChecksValue, checkedPaths }), 'utf8') }, verboseAvailable: normalized.payloadMode === 'compact', fullContentRef: `provider-doctor:claude:${normalized.workspaceRoot}`, generatedAt: 'read-only-snapshot', safety: PROVIDER_HEALTH_SAFETY } };
136
+ return { ok: true, value: { version: 1, kind: 'provider-doctor', project: normalized.project, providerAdapter: 'claude', scope: normalized.scope, workspaceRoot: normalized.workspaceRoot, runtimeContext: providerRuntimeContext(normalized), configClassification: externalProjectClassification(checkedPathList), status, payloadMode: normalized.payloadMode, overallStatus: status, checkCount: checks.length, passedCount: checks.filter((check) => check.status === 'pass').length, warningCount: checks.filter((check) => check.status === 'warn' || check.status === 'not-configured').length, errorCount: failedChecks.length, skippedCount: checks.filter((check) => check.status === 'skip').length, failedChecks, summary: summarizeDoctor(status, failedChecks.length, recommendations.length), recommendations, checks: compactChecksValue, checkedPaths, bytes: { originalBytes: Buffer.byteLength(JSON.stringify({ checks, checkedPaths: checkedPathList }), 'utf8'), compactBytes: Buffer.byteLength(JSON.stringify({ checks: compactChecksValue, checkedPaths }), 'utf8') }, verboseAvailable: normalized.payloadMode === 'compact', fullContentRef: `provider-doctor:claude:${normalized.workspaceRoot}`, generatedAt: 'read-only-snapshot', safety: PROVIDER_HEALTH_SAFETY } };
135
137
  }
136
138
  getClaudeUserGlobalDoctor(normalized, canonicalScope = 'user', scopeWarnings = []) {
137
139
  const mcp = inspectClaudeCodeUserMcpConfig(normalized.env);
@@ -162,9 +164,15 @@ export class ProviderDoctorService {
162
164
  const failedChecks = checks.filter((check) => check.status === 'fail').map((check) => check.id);
163
165
  const recommendations = checks.flatMap((check) => (check.remediation === undefined ? [] : [check.remediation]));
164
166
  const checkedPaths = normalized.payloadMode === 'verbose' ? checkedPathList : checkedPathList.filter((path) => existsSync(path) || path === mcp.path || path === userMemory.path);
165
- return { ok: true, value: { version: 1, kind: 'provider-doctor', project: normalized.project, providerAdapter: 'claude', scope: normalized.scope, workspaceRoot: normalized.workspaceRoot, status, payloadMode: normalized.payloadMode, overallStatus: status, checkCount: checks.length, passedCount: checks.filter((check) => check.status === 'pass').length, warningCount: checks.filter((check) => check.status === 'warn' || check.status === 'not-configured').length, errorCount: failedChecks.length, skippedCount: checks.filter((check) => check.status === 'skip').length, failedChecks, summary: summarizeDoctor(status, failedChecks.length, recommendations.length), recommendations, checks: compactChecksValue, checkedPaths, bytes: { originalBytes: Buffer.byteLength(JSON.stringify({ checks, checkedPaths: checkedPathList }), 'utf8'), compactBytes: Buffer.byteLength(JSON.stringify({ checks: compactChecksValue, checkedPaths }), 'utf8') }, verboseAvailable: normalized.payloadMode === 'compact', fullContentRef: `provider-doctor:claude:${canonicalScope}:${normalized.workspaceRoot}`, generatedAt: 'read-only-snapshot', safety: { ...PROVIDER_HEALTH_SAFETY, scopeCapabilities: CLAUDE_USER_GLOBAL_SCOPE_CAPABILITIES } } };
167
+ return { ok: true, value: { version: 1, kind: 'provider-doctor', project: normalized.project, providerAdapter: 'claude', scope: normalized.scope, workspaceRoot: normalized.workspaceRoot, runtimeContext: providerRuntimeContext(normalized), configClassification: managedUserGlobalClassification(checkedPathList), status, payloadMode: normalized.payloadMode, overallStatus: status, checkCount: checks.length, passedCount: checks.filter((check) => check.status === 'pass').length, warningCount: checks.filter((check) => check.status === 'warn' || check.status === 'not-configured').length, errorCount: failedChecks.length, skippedCount: checks.filter((check) => check.status === 'skip').length, failedChecks, summary: summarizeDoctor(status, failedChecks.length, recommendations.length), recommendations, checks: compactChecksValue, checkedPaths, bytes: { originalBytes: Buffer.byteLength(JSON.stringify({ checks, checkedPaths: checkedPathList }), 'utf8'), compactBytes: Buffer.byteLength(JSON.stringify({ checks: compactChecksValue, checkedPaths }), 'utf8') }, verboseAvailable: normalized.payloadMode === 'compact', fullContentRef: `provider-doctor:claude:${canonicalScope}:${normalized.workspaceRoot}`, generatedAt: 'read-only-snapshot', safety: { ...PROVIDER_HEALTH_SAFETY, scopeCapabilities: CLAUDE_USER_GLOBAL_SCOPE_CAPABILITIES } } };
166
168
  }
167
169
  }
170
+ function externalProjectClassification(paths) {
171
+ return { managedUserGlobalConfig: [], detectedExternalProjectConfig: paths.filter((path) => existsSync(path)), detectedExternalUserConfig: [] };
172
+ }
173
+ function managedUserGlobalClassification(paths) {
174
+ return { managedUserGlobalConfig: [...paths], detectedExternalProjectConfig: [], detectedExternalUserConfig: [] };
175
+ }
168
176
  function summarizeDoctor(status, failedCount, recommendationCount) {
169
177
  if (status === 'healthy')
170
178
  return 'Provider doctor checks passed; no configuration was changed.';
@@ -289,6 +297,15 @@ function claudeProjectMemoryCheck(state) {
289
297
  : { remediation: 'Run confirmed Claude project install when ready.' }),
290
298
  };
291
299
  }
300
+ function advisoryProjectMemoryCheck(state) {
301
+ const status = state.status === 'blocked' || state.status === 'managed-stale' ? 'warn' : 'pass';
302
+ return {
303
+ id: 'claude-project-memory-managed-block',
304
+ status,
305
+ detail: `${state.message} Advisory only; VGXNESS will not write project-root CLAUDE.md.`,
306
+ ...(status === 'warn' ? { remediation: 'Manually reconcile project-root CLAUDE.md only if you intend to keep external project Claude memory.' } : {}),
307
+ };
308
+ }
292
309
  function snapshotPaths(paths, workspaceRoot) {
293
310
  const unique = [...paths, `${workspaceRoot}/.vgx`];
294
311
  return Object.fromEntries(unique.map((path) => [path, existsSync(path) ? statSync(path).mtimeMs : false]));
@@ -12,9 +12,28 @@ export const PROVIDER_HEALTH_SAFETY = {
12
12
  executesProvider: false,
13
13
  };
14
14
  export const CLAUDE_USER_GLOBAL_SCOPE_CAPABILITIES = ['status', 'doctor', 'change-plan'];
15
+ export function providerConfigPathDiagnostics(ownership, exists) {
16
+ return {
17
+ ownership,
18
+ managed: ownership === 'managed-user-global',
19
+ readOnlyDiagnostic: true,
20
+ mayAffectProviderBehaviorExternally: ownership.startsWith('external-') && exists,
21
+ ...(ownership === 'managed-user-global' ? { installTarget: 'user-global' } : {}),
22
+ };
23
+ }
15
24
  export function isUserGlobalScope(scope) {
16
25
  return scope === 'personal' || scope === 'user';
17
26
  }
27
+ export function providerRuntimeContext(input) {
28
+ return { project: input.project, workspaceRoot: input.workspaceRoot, diagnosticScope: input.scope };
29
+ }
30
+ export function classifyProviderConfigPaths(paths) {
31
+ return {
32
+ managedUserGlobalConfig: paths.filter((path) => path.diagnostics.ownership === 'managed-user-global').map((path) => path.path),
33
+ detectedExternalProjectConfig: paths.filter((path) => path.diagnostics.ownership === 'external-project' && path.exists).map((path) => path.path),
34
+ detectedExternalUserConfig: paths.filter((path) => path.diagnostics.ownership === 'external-user' && path.exists).map((path) => path.path),
35
+ };
36
+ }
18
37
  export const REQUIRED_PROVIDER_MCP_TOOLS = ['vgxness_provider_status', 'vgxness_provider_doctor', 'vgxness_provider_change_plan'];
19
38
  export const REQUIRED_PROVIDER_NATIVE_MCP_TOOLS = [
20
39
  'vgxness_provider_status',
@@ -9,7 +9,7 @@ import { inspectClaudeUserMemory } from './claude-code-user-memory.js';
9
9
  import { resolveOpenCodeMcpInstallTarget } from './client-install-opencode-contract.js';
10
10
  import { vgxnessOpenCodeDefaultAgent, vgxnessOpenCodePromptContractVersion, vgxnessOpenCodeSddSubagents } from './opencode-default-agent-config.js';
11
11
  import { buildCanonicalAgentManifestDiagnostic } from './provider-canonical-agent-manifest.js';
12
- import { normalizeProviderHealthInput, PROVIDER_HEALTH_SAFETY, CLAUDE_USER_GLOBAL_SCOPE_CAPABILITIES, providerHealthFailure, isUserGlobalScope, REQUIRED_PROVIDER_MCP_TOOLS, rollupProviderHealth, } from './provider-health-types.js';
12
+ import { normalizeProviderHealthInput, PROVIDER_HEALTH_SAFETY, CLAUDE_USER_GLOBAL_SCOPE_CAPABILITIES, classifyProviderConfigPaths, providerConfigPathDiagnostics, providerRuntimeContext, providerHealthFailure, isUserGlobalScope, REQUIRED_PROVIDER_MCP_TOOLS, rollupProviderHealth, } from './provider-health-types.js';
13
13
  const projectConfigTargets = ['.opencode/opencode.json', 'opencode.json', '.opencode/opencode.jsonc', 'opencode.jsonc'];
14
14
  export class ProviderStatusService {
15
15
  deps;
@@ -59,6 +59,8 @@ export class ProviderStatusService {
59
59
  providerAdapter: normalized.providerAdapter,
60
60
  scope: normalized.scope,
61
61
  workspaceRoot: normalized.workspaceRoot,
62
+ runtimeContext: providerRuntimeContext(normalized),
63
+ configClassification: classifyProviderConfigPaths(paths),
62
64
  status,
63
65
  overallStatus: status,
64
66
  inspectedPaths: checkedPaths,
@@ -123,6 +125,8 @@ export class ProviderStatusService {
123
125
  providerAdapter: 'claude',
124
126
  scope: normalized.scope,
125
127
  workspaceRoot: normalized.workspaceRoot,
128
+ runtimeContext: providerRuntimeContext(normalized),
129
+ configClassification: classifyProviderConfigPaths(paths),
126
130
  status,
127
131
  overallStatus: status,
128
132
  inspectedPaths: checkedPaths,
@@ -153,7 +157,7 @@ export class ProviderStatusService {
153
157
  const mcpState = inspectClaudeCodeUserMcpConfig(normalized.env);
154
158
  const agents = inspectClaudeCodeAgents({ workspaceRoot: normalized.workspaceRoot, scope: 'user', env: normalized.env });
155
159
  const userMemory = inspectClaudeUserMemory(normalized.env);
156
- const userMemoryPathStatus = { ...claudeProjectMemoryPathStatus(userMemory), label: 'user ~/.claude/CLAUDE.md managed block' };
160
+ const userMemoryPathStatus = { ...claudeProjectMemoryPathStatus(userMemory, 'managed-user-global'), label: 'user ~/.claude/CLAUDE.md managed block' };
157
161
  const paths = [claudeUserMcpConfigPathStatus(mcpState), userMemoryPathStatus];
158
162
  const mcpEntry = claudeUserMcpEntryStatus(mcpState);
159
163
  const agentStatuses = agents.agents.map((agent) => (agent.status === 'managed' ? 'pass' : agent.status === 'missing' ? 'not-configured' : 'fail'));
@@ -173,6 +177,8 @@ export class ProviderStatusService {
173
177
  providerAdapter: 'claude',
174
178
  scope: normalized.scope,
175
179
  workspaceRoot: normalized.workspaceRoot,
180
+ runtimeContext: providerRuntimeContext(normalized),
181
+ configClassification: classifyProviderConfigPaths(paths),
176
182
  status,
177
183
  overallStatus: status,
178
184
  inspectedPaths: checkedPaths,
@@ -211,7 +217,7 @@ function claudeConfigHealthStatus(statuses) {
211
217
  return 'warn';
212
218
  return 'pass';
213
219
  }
214
- function claudeProjectMemoryPathStatus(state) {
220
+ function claudeProjectMemoryPathStatus(state, ownership = 'external-project') {
215
221
  const status = state.status === 'managed-current' ? 'pass' : state.status === 'managed-stale' ? 'warn' : state.status === 'blocked' ? 'fail' : 'not-configured';
216
222
  return {
217
223
  label: 'project CLAUDE.md managed block',
@@ -221,12 +227,13 @@ function claudeProjectMemoryPathStatus(state) {
221
227
  parsed: state.status === 'managed-current' || state.status === 'managed-stale',
222
228
  status,
223
229
  detail: state.message,
230
+ diagnostics: providerConfigPathDiagnostics(ownership, state.exists),
224
231
  };
225
232
  }
226
233
  function compactPaths(paths, mode) {
227
234
  if (mode === 'verbose')
228
235
  return paths;
229
- return paths.map((path) => ({ label: path.label, path: path.path, status: path.status }));
236
+ return paths.map((path) => ({ label: path.label, path: path.path, status: path.status, diagnostics: path.diagnostics }));
230
237
  }
231
238
  function compactMcpEntry(entry, mode) {
232
239
  if (mode === 'verbose')
@@ -260,12 +267,12 @@ function readFirstParsedConfig(paths) {
260
267
  }
261
268
  }
262
269
  export function inspectProjectConfigPaths(workspaceRoot) {
263
- return projectConfigTargets.map((relativePath) => inspectConfigPath(relativePath, join(workspaceRoot, relativePath)));
270
+ return projectConfigTargets.map((relativePath) => inspectConfigPath(relativePath, join(workspaceRoot, relativePath), 'external-project'));
264
271
  }
265
272
  export function inspectOpenCodeConfigPaths(workspaceRoot, env = process.env) {
266
273
  const userTarget = resolveOpenCodeMcpInstallTarget({ cwd: workspaceRoot, scope: 'user', env });
267
274
  const userPaths = userTarget.ok
268
- ? [inspectConfigPath('user ~/.config/opencode/opencode.json', userTarget.path)]
275
+ ? [inspectConfigPath('user ~/.config/opencode/opencode.json', userTarget.path, 'managed-user-global')]
269
276
  : [
270
277
  {
271
278
  label: 'user ~/.config/opencode/opencode.json',
@@ -275,13 +282,14 @@ export function inspectOpenCodeConfigPaths(workspaceRoot, env = process.env) {
275
282
  parsed: false,
276
283
  status: 'warn',
277
284
  detail: userTarget.message,
285
+ diagnostics: providerConfigPathDiagnostics('managed-user-global', false),
278
286
  },
279
287
  ];
280
288
  return [...userPaths, ...inspectProjectConfigPaths(workspaceRoot)];
281
289
  }
282
- function inspectConfigPath(label, path) {
290
+ function inspectConfigPath(label, path, ownership = 'not-managed') {
283
291
  if (!existsSync(path))
284
- return { label, path, exists: false, readable: false, parsed: false, status: 'not-configured', detail: 'Config path does not exist.' };
292
+ return { label, path, exists: false, readable: false, parsed: false, status: 'not-configured', detail: 'Config path does not exist.', diagnostics: providerConfigPathDiagnostics(ownership, false) };
285
293
  if (label.endsWith('.jsonc'))
286
294
  return {
287
295
  label,
@@ -291,14 +299,15 @@ function inspectConfigPath(label, path) {
291
299
  parsed: false,
292
300
  status: 'warn',
293
301
  detail: 'JSONC config exists but is not parsed by this read-only status check.',
302
+ diagnostics: providerConfigPathDiagnostics(ownership, true),
294
303
  };
295
304
  try {
296
305
  JSON.parse(readFileSync(path, 'utf8'));
297
- return { label, path, exists: true, readable: true, parsed: true, status: 'pass', detail: 'Config exists and parses as JSON.' };
306
+ return { label, path, exists: true, readable: true, parsed: true, status: 'pass', detail: 'Config exists and parses as JSON.', diagnostics: providerConfigPathDiagnostics(ownership, true) };
298
307
  }
299
308
  catch (cause) {
300
309
  const message = cause instanceof Error ? cause.message : String(cause);
301
- return { label, path, exists: true, readable: false, parsed: false, status: 'fail', detail: `Config could not be read or parsed: ${message}` };
310
+ return { label, path, exists: true, readable: false, parsed: false, status: 'fail', detail: `Config could not be read or parsed: ${message}`, diagnostics: providerConfigPathDiagnostics(ownership, true) };
302
311
  }
303
312
  }
304
313
  function inspectOpenCodeMcpEntry(paths) {
@@ -97,7 +97,7 @@ export function toInternalVgxMcpToolName(toolName) {
97
97
  return EXPOSED_TO_INTERNAL_TOOL_NAMES[toolName];
98
98
  }
99
99
  const scopes = ['project', 'personal'];
100
- const providerScopes = ['project', 'personal', 'local', 'user'];
100
+ const providerInstallTargets = ['user-global'];
101
101
  const agentModes = ['agent', 'subagent'];
102
102
  const memoryTypes = ['architecture', 'decision', 'bugfix', 'pattern', 'config', 'discovery', 'learning', 'preference', 'manual'];
103
103
  const activityKinds = ['prompt', 'tool_call', 'artifact', 'summary', 'error'];
@@ -469,7 +469,7 @@ export const VGX_MCP_TOOL_INPUT_SCHEMAS = {
469
469
  vgxness_provider_status: z
470
470
  .object({
471
471
  project: z.string().min(1).optional(),
472
- scope: z.enum(providerScopes).optional(),
472
+ scope: z.enum(scopes).optional(),
473
473
  providerAdapter: z.enum(['opencode', 'claude']).optional(),
474
474
  workspaceRoot: z.string().min(1).optional(),
475
475
  change: z.string().min(1).optional(),
@@ -479,7 +479,7 @@ export const VGX_MCP_TOOL_INPUT_SCHEMAS = {
479
479
  vgxness_provider_doctor: z
480
480
  .object({
481
481
  project: z.string().min(1).optional(),
482
- scope: z.enum(providerScopes).optional(),
482
+ scope: z.enum(scopes).optional(),
483
483
  providerAdapter: z.enum(['opencode', 'claude']).optional(),
484
484
  workspaceRoot: z.string().min(1).optional(),
485
485
  expectedPromptContractVersion: z.number().int().positive().optional(),
@@ -489,7 +489,8 @@ export const VGX_MCP_TOOL_INPUT_SCHEMAS = {
489
489
  vgxness_provider_change_plan: z
490
490
  .object({
491
491
  project: z.string().min(1).optional(),
492
- scope: z.enum(providerScopes).optional(),
492
+ scope: z.enum(scopes).optional(),
493
+ installTarget: z.enum(providerInstallTargets).optional(),
493
494
  provider: z.enum(providerChangePlanProviders),
494
495
  changeType: z.enum(providerChangePlanTypes),
495
496
  workspaceRoot: z.string().min(1),
@@ -16,6 +16,8 @@ const payloadModes = ['compact', 'verbose'];
16
16
  const contextCockpitLevels = ['compact', 'expanded', 'verbose'];
17
17
  const providerChangePlanProviders = ['opencode', 'claude', 'antigravity', 'custom'];
18
18
  const providerChangePlanTypes = ['opencode-mcp-install', 'claude-mcp-install', 'setup', 'install', 'config-preparation'];
19
+ const providerInstallTargets = ['user-global'];
20
+ const providerUserGlobalOnlyMigrationMessage = 'VGX-managed provider configuration is user-global only for OpenCode and Claude. Project/local provider files are treated as external/manual diagnostics and will not be written by VGXNESS. Use installTarget: user-global.';
19
21
  const validChangePattern = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
20
22
  const validMemoryTopicKeyPattern = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;
21
23
  const maxMemoryTitleLength = 200;
@@ -139,7 +141,7 @@ function readVerificationChangeType(record, tool) {
139
141
  return { ok: true, value: trimmed };
140
142
  }
141
143
  function validateProviderChangePlanInput(input, tool) {
142
- const record = inputRecord(input, tool, ['project', 'scope', 'provider', 'changeType', 'workspaceRoot', 'payloadMode']);
144
+ const record = inputRecord(input, tool, ['project', 'scope', 'installTarget', 'provider', 'changeType', 'workspaceRoot', 'payloadMode']);
143
145
  if (!record.ok)
144
146
  return record;
145
147
  const provider = readRequiredOneOf(record.value, 'provider', providerChangePlanProviders, tool);
@@ -170,6 +172,14 @@ function validateProviderChangePlanInput(input, tool) {
170
172
  return scope;
171
173
  if (scope.value !== undefined)
172
174
  result.scope = scope.value;
175
+ if (typeof record.value.installTarget === 'string' && record.value.installTarget.trim() !== 'user-global') {
176
+ return validationFailure(providerUserGlobalOnlyMigrationMessage, tool);
177
+ }
178
+ const installTarget = readOptionalOneOf(record.value, 'installTarget', providerInstallTargets, tool);
179
+ if (!installTarget.ok)
180
+ return installTarget;
181
+ if (installTarget.value !== undefined)
182
+ result.installTarget = installTarget.value;
173
183
  return { ok: true, value: result };
174
184
  }
175
185
  function validateProviderHealthInput(input, tool) {
@@ -1,6 +1,7 @@
1
1
  import { createHash, randomBytes } from 'node:crypto';
2
2
  import { accessSync, constants, copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
3
- import { basename, dirname, join, resolve } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
4
5
  export function createConfigBackupPath(targetPath, label = 'backup') {
5
6
  return `${targetPath}.${label}-${new Date().toISOString().replaceAll(/[-:.]/g, '')}`;
6
7
  }
@@ -202,12 +203,18 @@ function classifyBackup(backupPath) {
202
203
  return managedSummary(backupPath, metadataPath, parsed.value, validation.blockers, validation.warnings);
203
204
  }
204
205
  const target = inferRollbackTarget(backupPath);
206
+ const blockers = target.ok ? [] : [target.error.message];
207
+ if (target.ok) {
208
+ const rollbackTargetBlocker = validateProviderRollbackTarget(target.value);
209
+ if (rollbackTargetBlocker !== undefined)
210
+ blockers.push(rollbackTargetBlocker);
211
+ }
205
212
  return {
206
213
  kind: 'legacy',
207
214
  backupPath,
208
215
  provider: 'opencode',
209
216
  ...(target.ok ? { targetPath: target.value } : {}),
210
- blockers: target.ok ? [] : [target.error.message],
217
+ blockers,
211
218
  warnings: ['Legacy backup has no VGXNESS sidecar metadata; target is inferred from the filename.'],
212
219
  };
213
220
  }
@@ -238,8 +245,32 @@ function validateMetadataBinding(backupPath, metadata) {
238
245
  }
239
246
  else
240
247
  blockers.push(`Backup file does not exist: ${backupPath}`);
248
+ const rollbackTargetBlocker = validateProviderRollbackTarget(metadata.target.path);
249
+ if (rollbackTargetBlocker !== undefined)
250
+ blockers.push(rollbackTargetBlocker);
241
251
  return { blockers, warnings };
242
252
  }
253
+ function validateProviderRollbackTarget(targetPath) {
254
+ const normalizedTarget = resolve(targetPath);
255
+ if (isManagedUserGlobalProviderTarget(normalizedTarget))
256
+ return undefined;
257
+ return `Refusing to restore provider config backup to ${normalizedTarget}. VGX-managed provider rollback writes user-global OpenCode/Claude targets only; project-local provider files are external/manual diagnostics and will not be restored.`;
258
+ }
259
+ function isManagedUserGlobalProviderTarget(targetPath) {
260
+ const home = resolve(homedir());
261
+ if (targetPath === resolve(home, '.config', 'opencode', 'opencode.json'))
262
+ return true;
263
+ if (targetPath === resolve(home, '.claude.json'))
264
+ return true;
265
+ if (targetPath === resolve(home, '.claude', 'CLAUDE.md'))
266
+ return true;
267
+ const claudeAgentsRoot = resolve(home, '.claude', 'agents');
268
+ return isPathInside(claudeAgentsRoot, targetPath) && dirname(targetPath) === claudeAgentsRoot && basename(targetPath).endsWith('.md');
269
+ }
270
+ function isPathInside(parentPath, candidatePath) {
271
+ const relation = relative(parentPath, candidatePath);
272
+ return relation === '' || (relation !== '' && !relation.startsWith('..') && !isAbsolute(relation));
273
+ }
243
274
  function managedSummary(backupPath, metadataPath, metadata, blockers, warnings) {
244
275
  return {
245
276
  kind: 'managed',