vgxness 1.9.3 → 1.9.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -34,8 +34,9 @@ export function planOpenCodeMcpInstall(input) {
34
34
  mcpOnly: input.mcpOnly,
35
35
  });
36
36
  const overwriteVgxness = input.overwriteVgxness === true;
37
+ const allowBash = true;
37
38
  if (scope === 'user') {
38
- return planUserOpenCodeMcpInstall(input, databasePathSource, server, agentPlan, overwriteVgxness);
39
+ return planUserOpenCodeMcpInstall(input, databasePathSource, server, agentPlan, overwriteVgxness, allowBash);
39
40
  }
40
41
  const existingTargets = supportedConfigTargets
41
42
  .map((relativePath) => ({
@@ -49,35 +50,35 @@ export function planOpenCodeMcpInstall(input) {
49
50
  kind: 'manual-check',
50
51
  message: 'Remove ambiguity by keeping exactly one OpenCode project config target before installing.',
51
52
  },
52
- ], agentPlan, overwriteVgxness);
53
+ ], agentPlan, overwriteVgxness, allowBash);
53
54
  }
54
55
  if (existingTargets.length === 0) {
55
56
  return {
56
- ...baseContract(input.databasePath, databasePathSource, scope, join(input.cwd, '.opencode', 'opencode.json'), false, 'create', agentPlan, overwriteVgxness),
57
+ ...baseContract(input.databasePath, databasePathSource, scope, join(input.cwd, '.opencode', 'opencode.json'), false, 'create', agentPlan, overwriteVgxness, allowBash),
57
58
  status: 'would_install',
58
59
  action: 'create',
59
60
  targetPath: join(input.cwd, '.opencode', 'opencode.json'),
60
61
  backupRequired: false,
61
62
  server,
62
- preservedTopLevelKeys: agentPlan.installsAgents ? ['$schema', 'instructions', 'default_agent', 'agent', 'mcp'] : ['$schema', 'mcp'],
63
+ preservedTopLevelKeys: createdConfigKeys(agentPlan),
63
64
  existingSchema: null,
64
65
  };
65
66
  }
66
67
  const [target] = existingTargets;
67
68
  if (target === undefined)
68
- return refusal('ambiguous_target', 'Unable to resolve OpenCode project config target.', input.databasePath, databasePathSource, scope, undefined, [], undefined, overwriteVgxness);
69
+ return refusal('ambiguous_target', 'Unable to resolve OpenCode project config target.', input.databasePath, databasePathSource, scope, undefined, [], undefined, overwriteVgxness, allowBash);
69
70
  if (target.relativePath.endsWith('.jsonc')) {
70
- 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);
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, allowBash);
71
72
  }
72
73
  const parsed = parseConfig(target.absolutePath);
73
74
  if (!parsed.ok)
74
- return refusal(parsed.reason, parsed.message, input.databasePath, databasePathSource, scope, target.absolutePath, [], agentPlan, overwriteVgxness);
75
+ return refusal(parsed.reason, parsed.message, input.databasePath, databasePathSource, scope, target.absolutePath, [], agentPlan, overwriteVgxness, allowBash);
75
76
  const config = parsed.value;
76
77
  if (config.mcp !== undefined && !isRecord(config.mcp)) {
77
- 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);
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, allowBash);
78
79
  }
79
80
  if (isRecord(config.mcp) && Object.hasOwn(config.mcp, 'vgxness') && !overwriteVgxness) {
80
- 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);
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, allowBash);
81
82
  }
82
83
  const agentConflict = findConflictingVgxnessAgents(config, agentPlan);
83
84
  if (agentConflict.length > 0 && !overwriteVgxness) {
@@ -86,13 +87,16 @@ export function planOpenCodeMcpInstall(input) {
86
87
  kind: 'manual-check',
87
88
  message: `Manually reconcile conflicting VGXNESS agent entries: ${agentConflict.join(', ')}.`,
88
89
  },
89
- ], agentPlan, overwriteVgxness);
90
+ ], agentPlan, overwriteVgxness, allowBash);
90
91
  }
91
92
  if (agentPlan.installsAgents && config.agent !== undefined && !isRecord(config.agent)) {
92
- 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);
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, allowBash);
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 permission.bash to allow by default.', input.databasePath, databasePathSource, scope, target.absolutePath, [], agentPlan, overwriteVgxness, allowBash);
93
97
  }
94
98
  return {
95
- ...baseContract(input.databasePath, databasePathSource, scope, target.absolutePath, true, 'merge-preserve-existing', agentPlan, overwriteVgxness),
99
+ ...baseContract(input.databasePath, databasePathSource, scope, target.absolutePath, true, 'merge-preserve-existing', agentPlan, overwriteVgxness, allowBash),
96
100
  status: 'would_install',
97
101
  action: 'merge',
98
102
  targetPath: target.absolutePath,
@@ -102,40 +106,40 @@ export function planOpenCodeMcpInstall(input) {
102
106
  existingSchema: typeof config.$schema === 'string' ? config.$schema : opencodeConfigSchema,
103
107
  };
104
108
  }
105
- function planUserOpenCodeMcpInstall(input, databasePathSource, server, agentPlan, overwriteVgxness) {
109
+ function planUserOpenCodeMcpInstall(input, databasePathSource, server, agentPlan, overwriteVgxness, allowBash) {
106
110
  const target = resolveOpenCodeMcpInstallTarget({
107
111
  cwd: input.cwd,
108
112
  scope: 'user',
109
113
  env: input.env,
110
114
  });
111
115
  if (!target.ok) {
112
- return refusal('unsupported_config_shape', target.message, input.databasePath, databasePathSource, 'user', undefined, [], undefined, overwriteVgxness);
116
+ return refusal('unsupported_config_shape', target.message, input.databasePath, databasePathSource, 'user', undefined, [], undefined, overwriteVgxness, allowBash);
113
117
  }
114
118
  const jsoncPath = `${target.path}c`;
115
119
  if (existsSync(jsoncPath)) {
116
- return refusal('unsupported_jsonc', 'OpenCode user JSONC config opencode.jsonc is not supported yet; use JSON or remove comments first.', input.databasePath, databasePathSource, 'user', jsoncPath, [], agentPlan, overwriteVgxness);
120
+ return refusal('unsupported_jsonc', 'OpenCode user JSONC config opencode.jsonc is not supported yet; use JSON or remove comments first.', input.databasePath, databasePathSource, 'user', jsoncPath, [], agentPlan, overwriteVgxness, allowBash);
117
121
  }
118
122
  if (!existsSync(target.path)) {
119
123
  return {
120
- ...baseContract(input.databasePath, databasePathSource, 'user', target.path, false, 'create', agentPlan, overwriteVgxness),
124
+ ...baseContract(input.databasePath, databasePathSource, 'user', target.path, false, 'create', agentPlan, overwriteVgxness, allowBash),
121
125
  status: 'would_install',
122
126
  action: 'create',
123
127
  targetPath: target.path,
124
128
  backupRequired: false,
125
129
  server,
126
- preservedTopLevelKeys: agentPlan.installsAgents ? ['$schema', 'instructions', 'default_agent', 'agent', 'mcp'] : ['$schema', 'mcp'],
130
+ preservedTopLevelKeys: createdConfigKeys(agentPlan),
127
131
  existingSchema: null,
128
132
  };
129
133
  }
130
134
  const parsed = parseConfig(target.path);
131
135
  if (!parsed.ok)
132
- return refusal(parsed.reason, parsed.message, input.databasePath, databasePathSource, 'user', target.path, [], agentPlan, overwriteVgxness);
136
+ return refusal(parsed.reason, parsed.message, input.databasePath, databasePathSource, 'user', target.path, [], agentPlan, overwriteVgxness, allowBash);
133
137
  const config = parsed.value;
134
138
  if (config.mcp !== undefined && !isRecord(config.mcp)) {
135
- return refusal('invalid_mcp_shape', 'Existing top-level mcp must be a JSON object before vgxness can be merged.', input.databasePath, databasePathSource, 'user', target.path, [], agentPlan, overwriteVgxness);
139
+ return refusal('invalid_mcp_shape', 'Existing top-level mcp must be a JSON object before vgxness can be merged.', input.databasePath, databasePathSource, 'user', target.path, [], agentPlan, overwriteVgxness, allowBash);
136
140
  }
137
141
  if (isRecord(config.mcp) && Object.hasOwn(config.mcp, 'vgxness') && !overwriteVgxness) {
138
- return refusal('existing_vgxness_mcp', 'Existing OpenCode config already contains mcp.vgxness; overwrite is refused by default.', input.databasePath, databasePathSource, 'user', target.path, [], agentPlan, overwriteVgxness);
142
+ return refusal('existing_vgxness_mcp', 'Existing OpenCode config already contains mcp.vgxness; overwrite is refused by default.', input.databasePath, databasePathSource, 'user', target.path, [], agentPlan, overwriteVgxness, allowBash);
139
143
  }
140
144
  const agentConflict = findConflictingVgxnessAgents(config, agentPlan);
141
145
  if (agentConflict.length > 0 && !overwriteVgxness) {
@@ -144,13 +148,16 @@ function planUserOpenCodeMcpInstall(input, databasePathSource, server, agentPlan
144
148
  kind: 'manual-check',
145
149
  message: `Manually reconcile conflicting VGXNESS agent entries: ${agentConflict.join(', ')}.`,
146
150
  },
147
- ], agentPlan, overwriteVgxness);
151
+ ], agentPlan, overwriteVgxness, allowBash);
148
152
  }
149
153
  if (agentPlan.installsAgents && config.agent !== undefined && !isRecord(config.agent)) {
150
- 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, 'user', target.path, [], agentPlan, overwriteVgxness);
154
+ 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, 'user', target.path, [], agentPlan, overwriteVgxness, allowBash);
155
+ }
156
+ if (config.permission !== undefined && !isRecord(config.permission)) {
157
+ return refusal('unsupported_config_shape', 'Existing top-level permission must be a JSON object before VGXNESS can set permission.bash to allow by default.', input.databasePath, databasePathSource, 'user', target.path, [], agentPlan, overwriteVgxness, allowBash);
151
158
  }
152
159
  return {
153
- ...baseContract(input.databasePath, databasePathSource, 'user', target.path, true, 'merge-preserve-existing', agentPlan, overwriteVgxness),
160
+ ...baseContract(input.databasePath, databasePathSource, 'user', target.path, true, 'merge-preserve-existing', agentPlan, overwriteVgxness, allowBash),
154
161
  status: 'would_install',
155
162
  action: 'merge',
156
163
  targetPath: target.path,
@@ -195,10 +202,10 @@ function parseConfig(path) {
195
202
  function isRecord(value) {
196
203
  return typeof value === 'object' && value !== null && !Array.isArray(value);
197
204
  }
198
- export function findConflictingVgxnessAgents(config, agentPlan) {
205
+ export function findConflictingVgxnessAgents(config, agentPlan, input = {}) {
199
206
  if (!agentPlan.installsAgents || !isRecord(config.agent))
200
207
  return [];
201
- const defaults = createOpenCodeDefaultAgentConfig().agents;
208
+ const defaults = createOpenCodeDefaultAgentConfig(input).agents;
202
209
  return agentPlan.agentNames.filter((name) => Object.hasOwn(config.agent, name) && !deepEqual(config.agent[name], defaults[name]));
203
210
  }
204
211
  function deepEqual(left, right) {
@@ -213,7 +220,7 @@ function deepEqual(left, right) {
213
220
  const rightKeys = Object.keys(right).sort();
214
221
  return leftKeys.length === rightKeys.length && leftKeys.every((key, index) => key === rightKeys[index] && deepEqual(left[key], right[key]));
215
222
  }
216
- function baseContract(databasePath, source, scope, targetPath, backupRequired, mergePolicy, agentPlan = createOpenCodeDefaultAgentInstallPlan({ mcpOnly: true }), overwriteVgxness = false) {
223
+ function baseContract(databasePath, source, scope, targetPath, backupRequired, mergePolicy, agentPlan = createOpenCodeDefaultAgentInstallPlan({ mcpOnly: true }), overwriteVgxness = false, allowBash = true) {
217
224
  return {
218
225
  version: 1,
219
226
  kind: 'mcp-client-install-opencode',
@@ -228,17 +235,18 @@ function baseContract(databasePath, source, scope, targetPath, backupRequired, m
228
235
  mergePolicy,
229
236
  },
230
237
  scope,
231
- warnings: warningsForScope(scope, overwriteVgxness, agentPlan),
238
+ warnings: warningsForScope(scope, overwriteVgxness, agentPlan, allowBash),
232
239
  verificationHints: verificationHints(databasePath, source),
233
240
  manualTest: manualTestForScope(scope, databasePath, source),
234
241
  installsAgents: agentPlan.installsAgents,
235
242
  agentNames: agentPlan.agentNames,
236
243
  overwriteVgxness,
244
+ allowBash,
237
245
  ...(agentPlan.defaultAgent !== undefined ? { defaultAgent: agentPlan.defaultAgent } : {}),
238
246
  };
239
247
  }
240
- function refusal(reason, message, databasePath, source, scope, targetPath, extraVerificationHints = [], agentPlan = createOpenCodeDefaultAgentInstallPlan({ mcpOnly: true }), overwriteVgxness = false) {
241
- const contract = baseContract(databasePath, source, scope, targetPath, false, 'refuse-no-clobber', agentPlan, overwriteVgxness);
248
+ function refusal(reason, message, databasePath, source, scope, targetPath, extraVerificationHints = [], agentPlan = createOpenCodeDefaultAgentInstallPlan({ mcpOnly: true }), overwriteVgxness = false, allowBash = true) {
249
+ const contract = baseContract(databasePath, source, scope, targetPath, false, 'refuse-no-clobber', agentPlan, overwriteVgxness, allowBash);
242
250
  return {
243
251
  ...contract,
244
252
  verificationHints: [...extraVerificationHints, ...contract.verificationHints],
@@ -248,20 +256,26 @@ function refusal(reason, message, databasePath, source, scope, targetPath, extra
248
256
  ...(targetPath !== undefined ? { targetPath } : {}),
249
257
  };
250
258
  }
251
- function warningsForScope(scope, overwriteVgxness, agentPlan) {
259
+ function warningsForScope(scope, overwriteVgxness, agentPlan, allowBash) {
252
260
  const overwriteWarnings = overwriteVgxness
253
261
  ? [
254
262
  `Reinstall/overwrite is enabled: existing VGXNESS-managed OpenCode entries will be replaced (${agentPlan.installsAgents ? 'mcp.vgxness, default_agent, instructions AGENTS.md, and known VGXNESS agents' : 'mcp.vgxness only'}); unrelated OpenCode config is preserved.`,
255
263
  ]
256
264
  : [];
265
+ const bashWarnings = allowBash ? ['OpenCode permission.bash is set to allow by default; terminal commands may run without prompting.'] : [];
257
266
  if (scope === 'project')
258
- return ['Restart OpenCode after installation so it reloads the project MCP config.', ...overwriteWarnings];
267
+ return ['Restart OpenCode after installation so it reloads the project MCP config.', ...overwriteWarnings, ...bashWarnings];
259
268
  return [
260
269
  'Restart OpenCode after installation so it reloads the user MCP config.',
261
270
  'OpenCode project config may override user config for a workspace; check project-level config if vgxness is not visible.',
262
271
  ...overwriteWarnings,
272
+ ...bashWarnings,
263
273
  ];
264
274
  }
275
+ function createdConfigKeys(agentPlan) {
276
+ const keys = agentPlan.installsAgents ? ['$schema', 'instructions', 'default_agent', 'agent', 'mcp'] : ['$schema', 'mcp'];
277
+ return [...keys, 'permission'];
278
+ }
265
279
  function manualTestForScope(scope, databasePath, source) {
266
280
  if (scope === 'project') {
267
281
  return {
@@ -28,7 +28,7 @@ export async function installOpenCodeMcpClient(input) {
28
28
  ...(input.overwriteVgxness !== undefined ? { overwriteVgxness: input.overwriteVgxness } : {}),
29
29
  });
30
30
  if (!input.confirmed) {
31
- return refusal('confirmation_required', confirmationRequiredMessage(plan.scope), input.databasePath, databasePathSource, server, confirmationRequiredSafety(plan), 'targetPath' in plan ? plan.targetPath : undefined, plan.verificationHints, plan.warnings, plan.manualTest, agentPlan, plan.overwriteVgxness);
31
+ return refusal('confirmation_required', confirmationRequiredMessage(plan.scope), input.databasePath, databasePathSource, server, confirmationRequiredSafety(plan), 'targetPath' in plan ? plan.targetPath : undefined, plan.verificationHints, plan.warnings, plan.manualTest, agentPlan, plan.overwriteVgxness, plan.allowBash);
32
32
  }
33
33
  if (plan.status === 'refused')
34
34
  return refusalFromPlan(plan, input.databasePath, databasePathSource, server);
@@ -36,41 +36,46 @@ export async function installOpenCodeMcpClient(input) {
36
36
  if (existsSync(plan.targetPath)) {
37
37
  const reparsed = parseConfig(plan.targetPath);
38
38
  if (!reparsed.ok)
39
- return refusal(reparsed.reason, reparsed.message, input.databasePath, databasePathSource, server, applySafety(plan), plan.targetPath, plan.verificationHints, plan.warnings, plan.manualTest, agentPlan, plan.overwriteVgxness);
40
- const conflictingAgents = findConflictingVgxnessAgents(reparsed.value, agentPlan);
39
+ return refusal(reparsed.reason, reparsed.message, input.databasePath, databasePathSource, server, applySafety(plan), plan.targetPath, plan.verificationHints, plan.warnings, plan.manualTest, agentPlan, plan.overwriteVgxness, plan.allowBash);
40
+ const conflictingAgents = findConflictingVgxnessAgents(reparsed.value, agentPlan, {
41
+ effectiveManagerInstructions: input.effectiveManagerInstructions,
42
+ });
41
43
  if (conflictingAgents.length > 0 && input.overwriteVgxness !== true)
42
- return agentConflictRefusal(conflictingAgents, input.databasePath, databasePathSource, server, applySafety(plan), plan.targetPath, plan.verificationHints, plan.warnings, plan.manualTest, agentPlan);
43
- return refusal('unsupported_config_shape', 'OpenCode config appeared after planning; rerun setup apply so it can be merged safely without overwriting user config.', input.databasePath, databasePathSource, server, applySafety(plan), plan.targetPath, plan.verificationHints, plan.warnings, plan.manualTest, agentPlan, plan.overwriteVgxness);
44
+ return agentConflictRefusal(conflictingAgents, input.databasePath, databasePathSource, server, applySafety(plan), plan.targetPath, plan.verificationHints, plan.warnings, plan.manualTest, agentPlan, plan.allowBash);
45
+ return refusal('unsupported_config_shape', 'OpenCode config appeared after planning; rerun setup apply so it can be merged safely without overwriting user config.', input.databasePath, databasePathSource, server, applySafety(plan), plan.targetPath, plan.verificationHints, plan.warnings, plan.manualTest, agentPlan, plan.overwriteVgxness, plan.allowBash);
44
46
  }
45
- writeConfig(plan.targetPath, mergeVgxnessOpenCodeConfig({ $schema: opencodeConfigSchema }, plan.server, plan.installsAgents));
46
- return validateInstalledResult(plan.targetPath, undefined, server, input.databasePath, databasePathSource, applySafety(plan), plan.verificationHints, plan.warnings, plan.manualTest, agentPlan, plan.overwriteVgxness);
47
+ writeConfig(plan.targetPath, mergeVgxnessOpenCodeConfig({ $schema: opencodeConfigSchema }, plan.server, plan.installsAgents, input.effectiveManagerInstructions));
48
+ return validateInstalledResult(plan.targetPath, undefined, server, input.databasePath, databasePathSource, applySafety(plan), plan.verificationHints, plan.warnings, plan.manualTest, agentPlan, plan.overwriteVgxness, plan.allowBash);
47
49
  }
48
50
  const parsed = parseConfig(plan.targetPath);
49
51
  if (!parsed.ok)
50
- return refusal(parsed.reason, parsed.message, input.databasePath, databasePathSource, server, applySafety(plan), plan.targetPath, plan.verificationHints, plan.warnings, plan.manualTest, agentPlan, plan.overwriteVgxness);
51
- const conflictingAgents = findConflictingVgxnessAgents(parsed.value, agentPlan);
52
+ return refusal(parsed.reason, parsed.message, input.databasePath, databasePathSource, server, applySafety(plan), plan.targetPath, plan.verificationHints, plan.warnings, plan.manualTest, agentPlan, plan.overwriteVgxness, plan.allowBash);
53
+ const conflictingAgents = findConflictingVgxnessAgents(parsed.value, agentPlan, {
54
+ effectiveManagerInstructions: input.effectiveManagerInstructions,
55
+ });
52
56
  if (conflictingAgents.length > 0 && input.overwriteVgxness !== true)
53
- return agentConflictRefusal(conflictingAgents, input.databasePath, databasePathSource, server, applySafety(plan), plan.targetPath, plan.verificationHints, plan.warnings, plan.manualTest, agentPlan);
57
+ return agentConflictRefusal(conflictingAgents, input.databasePath, databasePathSource, server, applySafety(plan), plan.targetPath, plan.verificationHints, plan.warnings, plan.manualTest, agentPlan, plan.allowBash);
54
58
  const backup = createBackup(plan.targetPath, plan.scope);
55
59
  if (!backup.ok)
56
- return refusal('post_write_validation_failed', backup.error.message, input.databasePath, databasePathSource, server, applySafety(plan), plan.targetPath, plan.verificationHints, plan.warnings, plan.manualTest, agentPlan, plan.overwriteVgxness);
60
+ return refusal('post_write_validation_failed', backup.error.message, input.databasePath, databasePathSource, server, applySafety(plan), plan.targetPath, plan.verificationHints, plan.warnings, plan.manualTest, agentPlan, plan.overwriteVgxness, plan.allowBash);
57
61
  const config = parsed.value;
58
- const mergedConfig = mergeVgxnessOpenCodeConfig({ ...config, $schema: plan.existingSchema ?? opencodeConfigSchema }, plan.server, plan.installsAgents);
62
+ const mergedConfig = mergeVgxnessOpenCodeConfig({ ...config, $schema: plan.existingSchema ?? opencodeConfigSchema }, plan.server, plan.installsAgents, input.effectiveManagerInstructions);
59
63
  writeConfig(plan.targetPath, mergedConfig);
60
- return validateInstalledResult(plan.targetPath, backup.value, server, input.databasePath, databasePathSource, applySafety(plan), plan.verificationHints, plan.warnings, plan.manualTest, agentPlan, plan.overwriteVgxness);
64
+ return validateInstalledResult(plan.targetPath, backup.value, server, input.databasePath, databasePathSource, applySafety(plan), plan.verificationHints, plan.warnings, plan.manualTest, agentPlan, plan.overwriteVgxness, plan.allowBash);
61
65
  }
62
- function mergeVgxnessOpenCodeConfig(config, server, installsAgents) {
66
+ function mergeVgxnessOpenCodeConfig(config, server, installsAgents, effectiveManagerInstructions) {
63
67
  const merged = {
64
68
  ...config,
65
69
  $schema: typeof config.$schema === 'string' ? config.$schema : opencodeConfigSchema,
66
70
  mcp: { ...(isRecord(config.mcp) ? config.mcp : {}), vgxness: server },
67
71
  };
68
72
  if (installsAgents) {
69
- const defaults = createOpenCodeDefaultAgentConfig();
73
+ const defaults = createOpenCodeDefaultAgentConfig({ effectiveManagerInstructions });
70
74
  merged.instructions = mergeOpenCodeInstructions(config.instructions);
71
75
  merged.default_agent = defaults.defaultAgent;
72
76
  merged.agent = { ...(isRecord(config.agent) ? config.agent : {}), ...defaults.agents };
73
77
  }
78
+ merged.permission = { ...(isRecord(config.permission) ? config.permission : {}), bash: 'allow' };
74
79
  return merged;
75
80
  }
76
81
  function mergeOpenCodeInstructions(existing) {
@@ -102,21 +107,25 @@ function createBackup(path, scope) {
102
107
  description: 'Backup existing OpenCode config before merging VGXNESS MCP configuration.',
103
108
  });
104
109
  }
105
- function validateInstalledResult(targetPath, backup, server, databasePath, source, safety, verificationHints, warningMessages, manualTestGuidance, agentPlan, overwriteVgxness = false) {
110
+ function validateInstalledResult(targetPath, backup, server, databasePath, source, safety, verificationHints, warningMessages, manualTestGuidance, agentPlan, overwriteVgxness = false, allowBash = true) {
106
111
  const parsed = parseConfig(targetPath);
107
112
  if (!parsed.ok)
108
- return refusal('post_write_validation_failed', 'OpenCode config could not be re-read after write.', databasePath, source, server, safety, targetPath, verificationHints, warningMessages, manualTestGuidance, agentPlan);
113
+ return refusal('post_write_validation_failed', 'OpenCode config could not be re-read after write.', databasePath, source, server, safety, targetPath, verificationHints, warningMessages, manualTestGuidance, agentPlan, overwriteVgxness, allowBash);
109
114
  const mcp = parsed.value.mcp;
110
115
  const installed = isRecord(mcp) ? mcp.vgxness : undefined;
111
116
  if (!isOpenCodeLocalMcpServerConfig(installed)) {
112
- return refusal('post_write_validation_failed', 'OpenCode config was written but mcp.vgxness did not validate.', databasePath, source, server, safety, targetPath, verificationHints, warningMessages, manualTestGuidance, agentPlan);
117
+ return refusal('post_write_validation_failed', 'OpenCode config was written but mcp.vgxness did not validate.', databasePath, source, server, safety, targetPath, verificationHints, warningMessages, manualTestGuidance, agentPlan, overwriteVgxness, allowBash);
113
118
  }
114
119
  if (agentPlan.installsAgents) {
115
120
  const agent = parsed.value.agent;
116
121
  if (!isRecord(agent) || parsed.value.default_agent !== agentPlan.defaultAgent || agentPlan.agentNames.some((name) => !isRecord(agent[name]))) {
117
- return refusal('post_write_validation_failed', 'OpenCode config was written but VGXNESS agent entries did not validate.', databasePath, source, server, safety, targetPath, verificationHints, warningMessages, manualTestGuidance, agentPlan);
122
+ return refusal('post_write_validation_failed', 'OpenCode config was written but VGXNESS agent entries did not validate.', databasePath, source, server, safety, targetPath, verificationHints, warningMessages, manualTestGuidance, agentPlan, overwriteVgxness, allowBash);
118
123
  }
119
124
  }
125
+ const permission = parsed.value.permission;
126
+ if (!isRecord(permission) || permission.bash !== 'allow') {
127
+ return refusal('post_write_validation_failed', 'OpenCode config was written but permission.bash did not validate as allow.', databasePath, source, server, safety, targetPath, verificationHints, warningMessages, manualTestGuidance, agentPlan, overwriteVgxness, allowBash);
128
+ }
120
129
  return {
121
130
  version: 1,
122
131
  kind: 'mcp-client-install-opencode',
@@ -131,6 +140,7 @@ function validateInstalledResult(targetPath, backup, server, databasePath, sourc
131
140
  installsAgents: agentPlan.installsAgents,
132
141
  agentNames: agentPlan.agentNames,
133
142
  overwriteVgxness,
143
+ allowBash,
134
144
  ...(agentPlan.defaultAgent !== undefined ? { defaultAgent: agentPlan.defaultAgent } : {}),
135
145
  };
136
146
  }
@@ -155,7 +165,7 @@ function isOpenCodeLocalMcpServerConfig(value) {
155
165
  function isRecord(value) {
156
166
  return typeof value === 'object' && value !== null && !Array.isArray(value);
157
167
  }
158
- function refusal(reason, message, databasePath, source, server, safety, targetPath, verificationHints = defaultVerificationHints(databasePath, source), warningMessages = warnings(), manualTestGuidance = manualTest(databasePath, source), agentPlan = createOpenCodeDefaultAgentInstallPlan({ mcpOnly: true }), overwriteVgxness = false) {
168
+ function refusal(reason, message, databasePath, source, server, safety, targetPath, verificationHints = defaultVerificationHints(databasePath, source), warningMessages = warnings(), manualTestGuidance = manualTest(databasePath, source), agentPlan = createOpenCodeDefaultAgentInstallPlan({ mcpOnly: true }), overwriteVgxness = false, allowBash = true) {
159
169
  return {
160
170
  version: 1,
161
171
  kind: 'mcp-client-install-opencode',
@@ -171,14 +181,15 @@ function refusal(reason, message, databasePath, source, server, safety, targetPa
171
181
  installsAgents: agentPlan.installsAgents,
172
182
  agentNames: agentPlan.agentNames,
173
183
  overwriteVgxness,
184
+ allowBash,
174
185
  ...(agentPlan.defaultAgent !== undefined ? { defaultAgent: agentPlan.defaultAgent } : {}),
175
186
  };
176
187
  }
177
188
  function refusalFromPlan(plan, databasePath, source, server) {
178
- return refusal(plan.reason, plan.message, databasePath, source, server, plan.safety, plan.targetPath, plan.verificationHints, plan.warnings, plan.manualTest, plan, plan.overwriteVgxness);
189
+ return refusal(plan.reason, plan.message, databasePath, source, server, plan.safety, plan.targetPath, plan.verificationHints, plan.warnings, plan.manualTest, plan, plan.overwriteVgxness, plan.allowBash);
179
190
  }
180
- function agentConflictRefusal(conflictingAgents, databasePath, source, server, safety, targetPath, verificationHints, warningMessages, manualTestGuidance, agentPlan) {
181
- return refusal('existing_vgxness_agent', `Existing OpenCode config contains custom VGXNESS agent entries that would be overwritten: ${conflictingAgents.join(', ')}. Remove, rename, or manually reconcile them before installing.`, databasePath, source, server, safety, targetPath, verificationHints, warningMessages, manualTestGuidance, agentPlan);
191
+ function agentConflictRefusal(conflictingAgents, databasePath, source, server, safety, targetPath, verificationHints, warningMessages, manualTestGuidance, agentPlan, allowBash = true) {
192
+ return refusal('existing_vgxness_agent', `Existing OpenCode config contains custom VGXNESS agent entries that would be overwritten: ${conflictingAgents.join(', ')}. Remove, rename, or manually reconcile them before installing.`, databasePath, source, server, safety, targetPath, verificationHints, warningMessages, manualTestGuidance, agentPlan, false, allowBash);
182
193
  }
183
194
  function applySafety(plan) {
184
195
  return {
@@ -202,7 +213,7 @@ function confirmationRequiredMessage(scope) {
202
213
  return `\`mcp install opencode\` requires explicit --yes before any ${scope} config write.`;
203
214
  }
204
215
  function warnings() {
205
- return ['Restart OpenCode after installation so it reloads the project MCP config.'];
216
+ return ['Restart OpenCode after installation so it reloads the project MCP config.', 'OpenCode permission.bash is set to allow by default; terminal commands may run without prompting.'];
206
217
  }
207
218
  function manualTest(databasePath, source) {
208
219
  return {
@@ -9,6 +9,7 @@ import { prepareMemoryDatabasePath, resolveMemoryDatabasePath } from '../memory/
9
9
  import { OpenCodeManagerPayloadService } from '../providers/opencode/manager-payload.js';
10
10
  import { RunService } from '../runs/run-service.js';
11
11
  import { sddContinuationPlanFrom } from '../sdd/sdd-continuation-plan.js';
12
+ import { buildSddCockpitSurfaceResponse } from '../sdd/cockpit-read-model.js';
12
13
  import { SddWorkflowService } from '../sdd/sdd-workflow-service.js';
13
14
  import { SkillRegistryService } from '../skills/skill-registry-service.js';
14
15
  import { VerificationPlanService } from '../verification/index.js';
@@ -43,7 +44,7 @@ export function callVgxTool(call, services) {
43
44
  case 'vgxness_sdd_next':
44
45
  return toEnvelope(validated.tool, services.sdd.getNext(validated.input));
45
46
  case 'vgxness_sdd_cockpit':
46
- return toEnvelope(validated.tool, services.sdd.getCockpit(validated.input));
47
+ return toEnvelope(validated.tool, mapMemoryResult(services.sdd.getCockpit(validated.input), buildSddCockpitSurfaceResponse));
47
48
  case 'vgxness_sdd_continue':
48
49
  return sddContinueEnvelope(validated.input, services);
49
50
  case 'vgxness_governance_report':
@@ -405,6 +406,9 @@ function runResumeCandidatesEnvelope(input, services) {
405
406
  function toEnvelope(tool, result) {
406
407
  return result.ok ? successEnvelope(tool, result.value) : errorEnvelope(result.error.code, result.error.message, tool);
407
408
  }
409
+ function mapMemoryResult(result, project) {
410
+ return result.ok ? { ok: true, value: project(result.value) } : result;
411
+ }
408
412
  function resolveControlPlaneDatabasePath(options) {
409
413
  if (options.databasePath !== undefined)
410
414
  return { ok: true, value: options.databasePath };
@@ -1,5 +1,5 @@
1
- import { projectCanonicalAgentManifestToOpenCode, } from '../agents/canonical-agent-projection.js';
2
- import { canonicalDefaultAgentName, canonicalPromptContractVersion, canonicalSddSubagentNames, createCanonicalOpenCodeSddTaskPermissions, } from '../agents/canonical-agent-manifest.js';
1
+ import { projectCanonicalAgentManifestToOpenCode, withEffectiveOpenCodeManagerInstructions, } from '../agents/canonical-agent-projection.js';
2
+ import { canonicalDefaultAgentName, canonicalPromptContractVersion, canonicalSddSubagentNames, createCanonicalOpenCodeSddMcpToolPermissions, createCanonicalOpenCodeSddTaskPermissions, } from '../agents/canonical-agent-manifest.js';
3
3
  export const vgxnessOpenCodeDefaultAgent = canonicalDefaultAgentName;
4
4
  export const vgxnessOpenCodePromptContractVersion = canonicalPromptContractVersion;
5
5
  export const vgxnessOpenCodeInstructionsPath = 'AGENTS.md';
@@ -7,11 +7,14 @@ export const vgxnessOpenCodeSddSubagents = canonicalSddSubagentNames;
7
7
  export function createOpenCodeSddTaskPermissions() {
8
8
  return createCanonicalOpenCodeSddTaskPermissions();
9
9
  }
10
+ export function createOpenCodeSddMcpToolPermissions() {
11
+ return createCanonicalOpenCodeSddMcpToolPermissions();
12
+ }
10
13
  export function createOpenCodeDefaultAgentInstallPlan(input = {}) {
11
14
  if (input.mcpOnly === true)
12
15
  return { installsAgents: false, agentNames: [] };
13
16
  return { installsAgents: true, agentNames: [vgxnessOpenCodeDefaultAgent, ...vgxnessOpenCodeSddSubagents], defaultAgent: vgxnessOpenCodeDefaultAgent };
14
17
  }
15
- export function createOpenCodeDefaultAgentConfig() {
16
- return projectCanonicalAgentManifestToOpenCode();
18
+ export function createOpenCodeDefaultAgentConfig(input = {}) {
19
+ return withEffectiveOpenCodeManagerInstructions(projectCanonicalAgentManifestToOpenCode(), input.effectiveManagerInstructions);
17
20
  }
@@ -66,7 +66,7 @@ function descriptionForTool(publicToolName) {
66
66
  if (publicToolName === 'context_cockpit')
67
67
  return 'Read-only context cockpit for start/resume/recovery; returns latest restorable session plus bounded memory previews without traces, provider config writes, repository writes, runs, artifacts, or session mutations.';
68
68
  if (publicToolName === 'sdd_cockpit')
69
- return 'Read-only SDD cockpit summary with next decision, explicit acceptance state, metadata-only artifact summaries, and aggregate blockers.';
69
+ return 'Read-only SDD cockpit summary with raw compatibility fields plus additive readModel; metadata-only artifact summaries, no artifact bodies, and explicit human acceptance semantics.';
70
70
  if (publicToolName === 'sdd_continue')
71
71
  return 'Read-only SDD continuation planner; returns blocker actions, safe suggested commands, related interrupted run context, and safety notes without provider execution, run creation, artifact mutation, provider config writes, or openspec writes.';
72
72
  const toolName = toInternalVgxMcpToolName(publicToolName);
@@ -0,0 +1,192 @@
1
+ import { sddPhases, sddPrerequisites } from './schema.js';
2
+ export class SddCockpitReadModelService {
3
+ workflow;
4
+ constructor(workflow) {
5
+ this.workflow = workflow;
6
+ }
7
+ getReadModel(input) {
8
+ const cockpit = this.workflow.getCockpit(input);
9
+ if (!cockpit.ok)
10
+ return cockpit;
11
+ return { ok: true, value: buildSddCockpitReadModel(cockpit.value) };
12
+ }
13
+ getSurfaceResponse(input) {
14
+ const cockpit = this.workflow.getCockpit(input);
15
+ if (!cockpit.ok)
16
+ return cockpit;
17
+ return { ok: true, value: buildSddCockpitSurfaceResponse(cockpit.value) };
18
+ }
19
+ }
20
+ export function buildSddCockpitSurfaceResponse(cockpit) {
21
+ return { ...cockpit, readModel: buildSddCockpitReadModel(cockpit) };
22
+ }
23
+ export function buildSddCockpitReadModel(cockpit) {
24
+ const phases = cockpit.phases.map((phase) => toPhaseReadModel(phase));
25
+ const blockers = toReadBlockers(cockpit);
26
+ const nextAction = chooseNextAction(cockpit, phases);
27
+ return {
28
+ project: cockpit.project,
29
+ change: cockpit.change,
30
+ phases,
31
+ nextAction,
32
+ blockers,
33
+ guidance: guidanceFor(cockpit, nextAction, blockers),
34
+ summary: summarize(phases),
35
+ contentIncluded: false,
36
+ };
37
+ }
38
+ function toPhaseReadModel(phase) {
39
+ const status = artifactReadStatus(phase);
40
+ const contentAvailable = phase.present && phase.artifact !== undefined;
41
+ const reasons = phase.blockers.map((blocker) => blocker.action ?? blocker.reason);
42
+ const canAccept = phase.present && !phase.accepted && status === 'draft';
43
+ const canReview = contentAvailable && status === 'draft';
44
+ return {
45
+ phase: phase.phase,
46
+ label: phaseLabel(phase.phase),
47
+ topicKey: phase.topicKey,
48
+ artifact: {
49
+ exists: phase.present,
50
+ status,
51
+ contentAvailable,
52
+ topicKey: phase.topicKey,
53
+ ...(phase.artifact?.artifactId === undefined ? {} : { artifactId: phase.artifact.artifactId }),
54
+ ...(phase.artifact?.createdAt === undefined ? {} : { createdAt: phase.artifact.createdAt }),
55
+ ...(phase.artifact?.updatedAt === undefined ? {} : { updatedAt: phase.artifact.updatedAt }),
56
+ },
57
+ acceptance: {
58
+ accepted: phase.accepted,
59
+ acceptedByHuman: phase.artifact?.acceptance?.actor.type === 'human',
60
+ ...(phase.artifact?.acceptance === undefined ? {} : { acceptance: phase.artifact.acceptance }),
61
+ requiredForNextPhase: isRequiredForLaterPhase(phase.phase),
62
+ },
63
+ actionability: {
64
+ canGenerate: !phase.present && phase.readiness.ready,
65
+ canReview,
66
+ canAccept,
67
+ canReopen: phase.present && status === 'rejected',
68
+ blocked: !phase.readiness.ready || phase.blockers.length > 0,
69
+ reasons,
70
+ },
71
+ guidance: guidanceForPhase(phase, status, canAccept),
72
+ };
73
+ }
74
+ function artifactReadStatus(phase) {
75
+ if (!phase.present)
76
+ return 'missing';
77
+ if (phase.legacy)
78
+ return 'legacy';
79
+ const state = phase.state;
80
+ if (state === 'draft' || state === 'accepted' || state === 'rejected' || state === 'superseded' || state === 'missing')
81
+ return state;
82
+ return 'unknown';
83
+ }
84
+ function chooseNextAction(cockpit, phases) {
85
+ const reviewable = phases.find((phase) => phase.actionability.canReview);
86
+ if (reviewable !== undefined) {
87
+ return {
88
+ kind: 'review-artifact',
89
+ phase: reviewable.phase,
90
+ label: `Review ${reviewable.phase} artifact and accept it only after explicit human approval.`,
91
+ requiresConfirmation: false,
92
+ };
93
+ }
94
+ const generatable = phases.find((phase) => phase.actionability.canGenerate);
95
+ if (generatable !== undefined) {
96
+ return {
97
+ kind: 'generate-artifact',
98
+ phase: generatable.phase,
99
+ label: `Generate or create the ${generatable.phase} SDD artifact.`,
100
+ requiresConfirmation: false,
101
+ };
102
+ }
103
+ if (cockpit.next.status === 'complete') {
104
+ return { kind: 'none', label: 'All SDD phases are human-accepted.', requiresConfirmation: false };
105
+ }
106
+ if (cockpit.next.nextPhase !== undefined) {
107
+ const nextPhase = phases.find((phase) => phase.phase === cockpit.next.nextPhase);
108
+ return {
109
+ kind: 'continue-workflow',
110
+ phase: cockpit.next.nextPhase,
111
+ label: safeRecommendedAction(cockpit.recommendedAction, nextPhase),
112
+ requiresConfirmation: false,
113
+ };
114
+ }
115
+ return { kind: 'none', label: cockpit.recommendedAction, requiresConfirmation: false };
116
+ }
117
+ function safeRecommendedAction(recommendedAction, phase) {
118
+ if (phase === undefined)
119
+ return recommendedAction;
120
+ if (!/accept/i.test(recommendedAction))
121
+ return recommendedAction;
122
+ if (phase.artifact.status === 'draft')
123
+ return recommendedAction;
124
+ if (phase.artifact.status === 'rejected')
125
+ return `Reopen the ${phase.phase} artifact before further review.`;
126
+ if (phase.artifact.status === 'superseded')
127
+ return `Generate or inspect the current canonical ${phase.phase} artifact; superseded metadata is not actionable.`;
128
+ if (phase.artifact.status === 'legacy')
129
+ return `Inspect legacy ${phase.phase} metadata before using it in the current SDD flow.`;
130
+ return recommendedAction;
131
+ }
132
+ function toReadBlockers(cockpit) {
133
+ return cockpit.aggregateBlockers.map((blocker) => ({
134
+ phase: blocker.phase,
135
+ topicKey: blocker.topicKey,
136
+ code: blocker.kind,
137
+ message: blocker.reason,
138
+ severity: blocker.kind === 'missing-topic-key' ? 'info' : 'blocking',
139
+ ...(blocker.action === undefined ? {} : { action: blocker.action }),
140
+ ...(blocker.artifactId === undefined ? {} : { artifactId: blocker.artifactId }),
141
+ }));
142
+ }
143
+ function summarize(phases) {
144
+ return {
145
+ totalPhases: phases.length,
146
+ presentArtifacts: phases.filter((phase) => phase.artifact.exists).length,
147
+ acceptedArtifacts: phases.filter((phase) => phase.acceptance.accepted).length,
148
+ missingArtifacts: phases.filter((phase) => phase.artifact.status === 'missing').length,
149
+ draftArtifacts: phases.filter((phase) => phase.artifact.status === 'draft').length,
150
+ rejectedArtifacts: phases.filter((phase) => phase.artifact.status === 'rejected').length,
151
+ supersededArtifacts: phases.filter((phase) => phase.artifact.status === 'superseded').length,
152
+ legacyArtifacts: phases.filter((phase) => phase.artifact.status === 'legacy').length,
153
+ isComplete: phases.length === sddPhases.length && phases.every((phase) => phase.acceptance.accepted),
154
+ };
155
+ }
156
+ function guidanceFor(cockpit, nextAction, blockers) {
157
+ const phaseStatusByName = new Map(cockpit.phases.map((phase) => [phase.phase, artifactReadStatus(phase)]));
158
+ const guidance = [nextAction.label];
159
+ if (cockpit.phases.some((phase) => artifactReadStatus(phase) === 'draft' && !phase.accepted)) {
160
+ guidance.push('Artifact presence alone does not count as acceptance; explicit human acceptance is required for drafts.');
161
+ }
162
+ if (cockpit.next.blockerGuidance !== undefined) {
163
+ guidance.push(...cockpit.next.blockerGuidance
164
+ .filter((blocker) => phaseStatusByName.get(blocker.phase) === 'draft' || !/accept/i.test(blocker.action))
165
+ .map((blocker) => blocker.action));
166
+ }
167
+ return [...new Set(guidance)];
168
+ }
169
+ function guidanceForPhase(phase, status, canAccept) {
170
+ if (status === 'missing')
171
+ return [`Create the ${phase.phase} artifact when prerequisites are ready.`];
172
+ if (canAccept)
173
+ return [`Review ${phase.topicKey}; accept only after explicit human approval.`];
174
+ if (status === 'accepted')
175
+ return ['Accepted artifact metadata is available; full content is omitted from this read model.'];
176
+ if (status === 'rejected')
177
+ return ['Rejected artifact metadata is available; reopen it before further review.'];
178
+ if (status === 'superseded')
179
+ return ['Superseded artifact metadata is available; generate or inspect the current canonical artifact instead.'];
180
+ if (status === 'legacy')
181
+ return ['Legacy artifact metadata requires inspection before it can be used for current SDD flow.'];
182
+ return phase.blockers.map((blocker) => blocker.action ?? blocker.reason);
183
+ }
184
+ function phaseLabel(phase) {
185
+ return phase
186
+ .split('-')
187
+ .map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`)
188
+ .join(' ');
189
+ }
190
+ function isRequiredForLaterPhase(phase) {
191
+ return sddPhases.some((candidate) => sddPrerequisites[candidate].includes(phase));
192
+ }
@@ -0,0 +1 @@
1
+ export {};