@aiwg/cli 2026.10.0 → 2026.10.3

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 (168) hide show
  1. package/agentic/code/providers/capability-matrix.yaml +270 -0
  2. package/agentic/code/providers/model-capabilities.v1.json +3091 -24
  3. package/bin/aiwg.mjs +86 -21
  4. package/dist/src/a2a/codecs.js +9 -0
  5. package/dist/src/artifacts/corpus-tools/vision-extract.js +105 -15
  6. package/dist/src/artifacts/repair.js +9 -4
  7. package/dist/src/channel/manager.mjs +59 -10
  8. package/dist/src/cli/child-process-result.js +40 -0
  9. package/dist/src/cli/env.js +38 -16
  10. package/dist/src/cli/flag-parsing.js +236 -0
  11. package/dist/src/cli/handlers/decision.js +297 -2
  12. package/dist/src/cli/handlers/diagnose.js +9 -2
  13. package/dist/src/cli/handlers/feedback.js +14 -5
  14. package/dist/src/cli/handlers/init.js +245 -30
  15. package/dist/src/cli/handlers/mc.js +23 -0
  16. package/dist/src/cli/handlers/models.js +69 -6
  17. package/dist/src/cli/handlers/ralph.js +3 -0
  18. package/dist/src/cli/handlers/refresh.js +16 -0
  19. package/dist/src/cli/handlers/regenerate.js +14 -0
  20. package/dist/src/cli/handlers/runtime-info.js +24 -9
  21. package/dist/src/cli/handlers/script-runner.js +16 -6
  22. package/dist/src/cli/handlers/session.js +396 -62
  23. package/dist/src/cli/handlers/sessions.js +146 -40
  24. package/dist/src/cli/handlers/setup-manifest.js +439 -39
  25. package/dist/src/cli/handlers/setup.js +139 -39
  26. package/dist/src/cli/handlers/subcommands.js +3 -1
  27. package/dist/src/cli/handlers/team.js +18 -8
  28. package/dist/src/cli/handlers/use.js +13 -0
  29. package/dist/src/cli/handlers/utilities.js +106 -23
  30. package/dist/src/cli/prompt-utils.js +72 -0
  31. package/dist/src/cli/provider-resolution.js +11 -0
  32. package/dist/src/config/aiwg-config.js +268 -4
  33. package/dist/src/config/cli.js +146 -19
  34. package/dist/src/config/dispatch-thresholds-runtime.mjs +38 -0
  35. package/dist/src/config/project-artifacts-health.mjs +14 -3
  36. package/dist/src/config/user-config.js +36 -10
  37. package/dist/src/decision/adapters/jev.js +80 -28
  38. package/dist/src/decision/ask.js +352 -0
  39. package/dist/src/decision/driver.js +4 -0
  40. package/dist/src/decision/entry.js +38 -6
  41. package/dist/src/decision/heldout/collector.js +10 -1
  42. package/dist/src/decision/index.js +3 -0
  43. package/dist/src/decision/interop.js +60 -8
  44. package/dist/src/decision/jev-credentials.js +121 -0
  45. package/dist/src/decision/ledger.js +839 -0
  46. package/dist/src/decision/validate.js +6 -3
  47. package/dist/src/effects/dispatch-verifier.js +252 -0
  48. package/dist/src/effects/index.js +1 -0
  49. package/dist/src/features/installer.js +2 -2
  50. package/dist/src/flow/graph-metadata.js +5 -0
  51. package/dist/src/gates/providers/loader.js +3 -1
  52. package/dist/src/installation/manager-command.mjs +1 -1
  53. package/dist/src/installation/manager.mjs +17 -2
  54. package/dist/src/lint/runner.js +2 -2
  55. package/dist/src/mcp/adapters/codex-runtime.js +360 -56
  56. package/dist/src/mcp/cli.mjs +587 -77
  57. package/dist/src/mcp/config-file.mjs +279 -0
  58. package/dist/src/mcp/config-layers.mjs +348 -0
  59. package/dist/src/mcp/credentials.mjs +484 -0
  60. package/dist/src/mcp/grok-build-config.mjs +16 -26
  61. package/dist/src/mcp/helpers.mjs +106 -2
  62. package/dist/src/mcp/omp-config.mjs +50 -29
  63. package/dist/src/mcp/profiles.js +97 -23
  64. package/dist/src/mcp/profiles.mjs +99 -27
  65. package/dist/src/mcp/registry.js +264 -57
  66. package/dist/src/mcp/registry.mjs +198 -45
  67. package/dist/src/mcp/server.mjs +403 -20
  68. package/dist/src/mcp/toml-string.mjs +9 -0
  69. package/dist/src/mcp/toml-strip-mcp.mjs +135 -0
  70. package/dist/src/mcp/tool-filters.mjs +357 -0
  71. package/dist/src/mcp/tools/agentic-sandbox.mjs +3 -1
  72. package/dist/src/mcp/tools/decision.mjs +58 -0
  73. package/dist/src/mcp/tools/orchestration.mjs +38 -0
  74. package/dist/src/mcp/tools/subsystems.mjs +39 -8
  75. package/dist/src/mcp/tools/worker-invoke.mjs +1539 -0
  76. package/dist/src/mcp/tools/worker-shell-control.mjs +258 -0
  77. package/dist/src/mcp/tools/worker-stdin-runtime.mjs +79 -0
  78. package/dist/src/memory/project-registry.js +2 -2
  79. package/dist/src/mission-protocol/codecs.js +639 -1
  80. package/dist/src/mission-protocol/types.js +110 -0
  81. package/dist/src/mission-protocol/worker-status.js +2 -0
  82. package/dist/src/models/config-loader.js +70 -13
  83. package/dist/src/models/grok-build-models.js +93 -41
  84. package/dist/src/models/model-capabilities.v1.json +3091 -24
  85. package/dist/src/models/model-discovery.js +862 -63
  86. package/dist/src/models/provider-policy.js +625 -11
  87. package/dist/src/models/qualification-digest.mjs +17 -0
  88. package/dist/src/models/resolver.js +259 -0
  89. package/dist/src/models/worker-direct-readiness.js +529 -0
  90. package/dist/src/models/worker-dispatch-offer.js +169 -0
  91. package/dist/src/models/worker-health-readiness.js +131 -0
  92. package/dist/src/models/worker-installation.mjs +879 -0
  93. package/dist/src/models/wrapper-route.js +18 -0
  94. package/dist/src/ops/registry.js +9 -14
  95. package/dist/src/plugins/standalone-packager.js +4 -3
  96. package/dist/src/process/bounded-child-process.mjs +387 -0
  97. package/dist/src/process/cli-command.mjs +75 -0
  98. package/dist/src/providers/capability-matrix.js +8 -0
  99. package/dist/src/providers/capability-matrix.yaml +270 -0
  100. package/dist/src/providers/grok-build-headless.js +43 -21
  101. package/dist/src/providers/provider-definitions.js +9 -1
  102. package/dist/src/providers/provider-definitions.mjs +9 -2
  103. package/dist/src/serve/executor-registry.js +16 -24
  104. package/dist/src/serve/fleet-mission-conductor.js +9 -0
  105. package/dist/src/serve/mission-conductor.js +9 -0
  106. package/dist/src/serve/shared-host-scheduler.js +2 -0
  107. package/dist/src/serve/worker-invocation.js +538 -0
  108. package/dist/src/serve/worker-stdin-frame.mjs +107 -0
  109. package/dist/src/sessions/adapter-registry.js +45 -0
  110. package/dist/src/sessions/adapters/generic.js +37 -3
  111. package/dist/src/sessions/adapters/grokbot-locator.js +111 -0
  112. package/dist/src/sessions/adapters/grokbot.js +423 -24
  113. package/dist/src/sessions/adapters/muse-workspace-scan.js +108 -0
  114. package/dist/src/sessions/adapters/muse.js +68 -29
  115. package/dist/src/sessions/adapters/pi.js +1 -0
  116. package/dist/src/sessions/batch-contracts.js +13 -1
  117. package/dist/src/sessions/batch-import.js +23 -42
  118. package/dist/src/sessions/candidates.js +47 -11
  119. package/dist/src/sessions/contracts.js +35 -0
  120. package/dist/src/sessions/coverage-preflight.js +134 -0
  121. package/dist/src/sessions/discovery.js +3 -0
  122. package/dist/src/sessions/grok-build-cli.js +67 -5
  123. package/dist/src/sessions/importer.js +24 -8
  124. package/dist/src/sessions/index.js +2 -0
  125. package/dist/src/sessions/repository.js +54 -13
  126. package/dist/src/sessions/workspace-discovery.js +99 -37
  127. package/dist/src/skills/adapters/clawhub.js +10 -8
  128. package/dist/src/skills/adapters/openclaw.js +10 -7
  129. package/dist/src/smiths/context-pipeline/parallelism-section.js +192 -3
  130. package/dist/src/smiths/context-pipeline/workspace-context.js +116 -9
  131. package/dist/src/smiths/hook-bridge/codex-translator.js +6 -5
  132. package/dist/src/smiths/mcpsmith/example.js +1 -1
  133. package/dist/src/smiths/toolsmith/runtime-discovery.mjs +10 -14
  134. package/dist/src/storage/backends/logseq.js +68 -7
  135. package/dist/src/storage/fortemi-qualification-receipt.js +2 -2
  136. package/dist/src/testing/generators/test-code-generator.js +57 -30
  137. package/dist/src/uhp/client.js +11 -1
  138. package/dist/src/uhp/mission.js +5 -0
  139. package/dist/src/update/checker.mjs +5 -5
  140. package/dist/src/update/notifier.mjs +54 -2
  141. package/dist/src/writing/fidelity.js +18 -1
  142. package/package.json +18 -1
  143. package/schemas/mission-v1.schema.json +2429 -0
  144. package/schemas/models/model-catalog.v1.schema.json +301 -0
  145. package/schemas/models/model-config-compatibility.v1.schema.json +281 -0
  146. package/schemas/models/model-policy.v1.schema.json +14 -0
  147. package/schemas/models/provider-model-capabilities.v1.schema.json +1066 -0
  148. package/tools/_resolve-impl.mjs +39 -0
  149. package/tools/agents/deploy-agents.mjs +15 -2
  150. package/tools/agents/providers/base.mjs +63 -43
  151. package/tools/agents/providers/grokbot.mjs +11 -12
  152. package/tools/agents/providers/hermes.mjs +6 -3
  153. package/tools/agents/providers/omp.mjs +5 -2
  154. package/tools/agents/providers/openclaw-translator.mjs +19 -5
  155. package/tools/providers/deepseek-harness-transport.mjs +358 -0
  156. package/tools/providers/omp-transport.mjs +192 -0
  157. package/tools/ralph-external/lib/claude-adapter.mjs +389 -0
  158. package/tools/ralph-external/lib/codex-adapter.mjs +432 -0
  159. package/tools/ralph-external/lib/deepseek-harness-adapter.mjs +397 -0
  160. package/tools/ralph-external/lib/factory-adapter.mjs +503 -0
  161. package/tools/ralph-external/lib/muse-adapter.mjs +364 -0
  162. package/tools/ralph-external/lib/omp-adapter.mjs +315 -0
  163. package/tools/ralph-external/lib/opencode-adapter.mjs +575 -0
  164. package/tools/ralph-external/lib/openhuman-adapter.mjs +75 -0
  165. package/tools/ralph-external/lib/pi-adapter.mjs +333 -0
  166. package/tools/ralph-external/lib/provider-adapter.mjs +690 -0
  167. package/tools/ralph-external/lib/worker-health.mjs +439 -0
  168. package/vscode-extension/schemas/aiwg.config.v1.json +1953 -0
@@ -1,7 +1,12 @@
1
+ import { tomlString } from './toml-string.mjs';
2
+ import { isLowerLayerEntry, loadLayered, lowerEntryQualified, resolveConfigLayers, saveLayerData } from './config-layers.mjs';
3
+ import { assertConfigDestination, assertProjectCredentials, classifyMcpDestination, projectCredentialRemedy, writeConfigAtomic, writeConfigTransaction } from './config-file.mjs';
1
4
  import { manageOmpMcp } from './omp-config.mjs';
2
5
  import { manageGrokBuildMcp } from './grok-build-config.mjs';
3
6
  import { replaceServer } from './toml-editor.mjs';
4
7
  import { resolveOmpPaths } from '../providers/omp-paths.mjs';
8
+ import { applyJsonToolFilterPlan, claudeSettingsPath, prepareClaudePermissions, planToolFilters } from './tool-filters.mjs';
9
+ import { assertCredentialPolicy, resolveCredentialPolicy, renderCredentialMaps, validateEnvReferenceName } from './credentials.mjs';
5
10
  /**
6
11
  * MCP Server Registry
7
12
  *
@@ -11,8 +16,11 @@ import { resolveOmpPaths } from '../providers/omp-paths.mjs';
11
16
  *
12
17
  * @implements #554
13
18
  */
14
- import { readFile, writeFile, mkdir } from 'fs/promises';
15
- import { dirname, resolve } from 'path';
19
+ import { readFile } from 'fs/promises';
20
+ import { qualificationDigest } from '../models/qualification-digest.mjs';
21
+ import { qualifyWorkerInvocationProfile, requiredWorkerControls } from '../models/provider-policy.js';
22
+ import { dirname, isAbsolute, resolve } from 'path';
23
+ import { homedir } from 'os';
16
24
  import { resolveConfigDir } from '../config/user-config.js';
17
25
  import { getProviderDefinition } from '../providers/provider-definitions.js';
18
26
  // ============================================
@@ -33,12 +41,76 @@ function validateCredentialReferences(def) {
33
41
  throw new Error(`Invalid MCP header environment variable reference "${envName}"`);
34
42
  }
35
43
  }
44
+ for (const [key, envName] of Object.entries(def.envFrom ?? {})) {
45
+ if (!ENV_REFERENCE_NAME.test(key))
46
+ throw new Error(`Invalid MCP env-from variable name "${key}"`);
47
+ if (!ENV_REFERENCE_NAME.test(envName)) {
48
+ throw new Error(`Invalid MCP env-from environment variable reference "${envName}"`);
49
+ }
50
+ }
51
+ }
52
+ export function workerServerDigest(server) {
53
+ const { workerQualification, description, injectedProviders, addedAt, updatedAt, ...execution } = server;
54
+ return qualificationDigest(execution);
55
+ }
56
+ function workerQualificationBinding(server, input) {
57
+ if (!Array.isArray(input.requiredControls) || input.requiredControls.some(name => typeof name !== 'string' || !name || !Object.hasOwn(input.adapter.invocationProfiles?.[input.profile]?.controls ?? {}, name))) {
58
+ throw new Error('MCP worker must declare valid required controls');
59
+ }
60
+ if (server.enabled === false || input.mcp.serverRef !== server.name) {
61
+ throw new Error('MCP worker server is disabled or does not match the qualified profile');
62
+ }
63
+ if (input.authorizedServerDigest !== workerServerDigest(server)) {
64
+ throw new Error('MCP server command, endpoint or configuration lacks matching host authorization');
65
+ }
66
+ if (server.type === 'stdio') {
67
+ if (!server.command || !isAbsolute(server.command) || server.command.includes('\0')
68
+ || (server.args ?? []).some(arg => typeof arg !== 'string' || arg.includes('\0'))) {
69
+ throw new Error('MCP worker requires an authorized absolute local command and typed argv');
70
+ }
71
+ }
72
+ else {
73
+ let endpoint;
74
+ try {
75
+ endpoint = new URL(server.url ?? '');
76
+ }
77
+ catch {
78
+ throw new Error('MCP worker endpoint is invalid');
79
+ }
80
+ if (!['http:', 'https:'].includes(endpoint.protocol) || endpoint.username || endpoint.password || endpoint.hash) {
81
+ throw new Error('MCP worker endpoint must be an authorized HTTP URL without credentials or fragment');
82
+ }
83
+ }
84
+ const profile = qualifyWorkerInvocationProfile(input.adapter, { ...input, transport: 'mcp' });
85
+ const installed = input.installationQualification;
86
+ if (installed?.serverDigest !== input.authorizedServerDigest) {
87
+ throw new Error('MCP worker installation does not cover the authorized registration');
88
+ }
89
+ const evidence = profile.installationQualification;
90
+ if (!installed || installed.status !== 'ENABLED' || installed.platform !== 'linux' || installed.cliVersion !== input.cliVersion
91
+ || !evidence || ['scopeDigest', 'fixtureDigest', 'verifiedAt', 'evidenceRef'].some(key => installed[key] !== evidence[key])) {
92
+ throw new Error('MCP worker requires current trusted installation qualification');
93
+ }
94
+ if (Object.keys(server.env ?? {}).some(key => !profile.envAllowlist.includes(key))
95
+ || Object.values(server.headerEnv ?? {}).some(key => !profile.envAllowlist.includes(key))) {
96
+ throw new Error('MCP worker server environment exceeds the qualified profile allowlist');
97
+ }
98
+ return {
99
+ serverDigest: workerServerDigest(server), adapterDigest: qualificationDigest(input.adapter),
100
+ profile: input.profile, cliVersion: input.cliVersion,
101
+ scopeDigest: installed.scopeDigest, fixtureDigest: installed.fixtureDigest,
102
+ requiredControls: requiredWorkerControls(input).sort(),
103
+ };
36
104
  }
37
105
  export class McpServerRegistry {
38
106
  configDir;
39
107
  cache = null;
108
+ /** Configuration layers, lowest precedence first; null for a single directory */
109
+ layers;
110
+ layering = null;
40
111
  constructor(configDirOverride) {
41
- this.configDir = resolveConfigDir(configDirOverride);
112
+ this.layers = resolveConfigLayers(configDirOverride);
113
+ this.configDir = this.layers ? this.layers[this.layers.length - 1] : resolveConfigDir(configDirOverride);
42
114
  }
43
115
  /** Get the registry file path */
44
116
  getPath() {
@@ -48,6 +120,12 @@ export class McpServerRegistry {
48
120
  async load() {
49
121
  if (this.cache)
50
122
  return this.cache;
123
+ if (this.layers) {
124
+ const layered = await loadLayered(this.layers, REGISTRY_FILENAME, 'servers', DEFAULT_REGISTRY);
125
+ this.cache = layered.data;
126
+ this.layering = layered.layering;
127
+ return this.cache;
128
+ }
51
129
  const filePath = this.getPath();
52
130
  try {
53
131
  const content = await readFile(filePath, 'utf-8');
@@ -61,43 +139,67 @@ export class McpServerRegistry {
61
139
  catch {
62
140
  this.cache = { ...DEFAULT_REGISTRY, servers: {} };
63
141
  }
142
+ try {
143
+ resolveCredentialPolicy({ registryPolicy: this.cache.credentialPolicy, env: {} });
144
+ }
145
+ catch (error) {
146
+ this.cache = null;
147
+ throw new Error(`Invalid MCP credential policy in ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
148
+ }
64
149
  return this.cache;
65
150
  }
66
151
  /** Save the registry to disk */
67
- async save() {
68
- if (!this.cache)
152
+ async save(data = this.cache, explicitPolicy = false) {
153
+ if (!data)
69
154
  return;
70
- await mkdir(this.configDir, { recursive: true });
71
- const filePath = this.getPath();
72
- await writeFile(filePath, JSON.stringify(this.cache, null, 2) + '\n', 'utf-8');
155
+ // A fresh instance has no ownership record yet; load it first so inherited
156
+ // entries and inherited policy are never copied into the write layer.
157
+ let layering = this.layering;
158
+ if (this.layers && !layering) {
159
+ await this.load();
160
+ layering = this.layering;
161
+ }
162
+ await saveLayerData(this.getPath(), data, 'servers', this.layers, layering, explicitPolicy);
163
+ this.cache = data;
164
+ if (this.layers) {
165
+ this.clearCache();
166
+ await this.load();
167
+ }
73
168
  }
74
169
  /** Add a new MCP server definition */
75
170
  async add(def) {
76
171
  validateCredentialReferences(def);
77
- const data = await this.load();
172
+ const data = structuredClone(await this.load());
78
173
  if (data.servers[def.name]) {
79
174
  throw new Error(`Server "${def.name}" already exists. Use "update" to modify it.`);
80
175
  }
81
176
  data.servers[def.name] = {
82
177
  ...def,
178
+ workerQualification: undefined,
83
179
  injectedProviders: [],
84
180
  addedAt: new Date().toISOString(),
85
181
  updatedAt: new Date().toISOString(),
86
182
  };
87
- await this.save();
183
+ await this.save(data);
88
184
  }
89
185
  /** Remove an MCP server definition */
90
186
  async remove(name) {
91
- const data = await this.load();
187
+ const data = structuredClone(await this.load());
92
188
  if (!data.servers[name]) {
93
189
  throw new Error(`Server "${name}" not found.`);
94
190
  }
191
+ if (this.layering && isLowerLayerEntry(this.layering, name)) {
192
+ throw new Error(`Server "${name}" is defined in a lower configuration layer; remove it there.`);
193
+ }
194
+ if (this.layering && lowerEntryQualified(this.layering, name)) {
195
+ throw new Error(`Server "${name}" overrides a qualified lower-layer entry; removing the override would silently re-expose its qualification. Remove the lower-layer entry in its own layer instead.`);
196
+ }
95
197
  delete data.servers[name];
96
- await this.save();
198
+ await this.save(data);
97
199
  }
98
200
  /** Update an existing MCP server definition */
99
201
  async update(name, updates) {
100
- const data = await this.load();
202
+ const data = structuredClone(await this.load());
101
203
  if (!data.servers[name]) {
102
204
  throw new Error(`Server "${name}" not found.`);
103
205
  }
@@ -108,8 +210,35 @@ export class McpServerRegistry {
108
210
  updatedAt: new Date().toISOString(),
109
211
  };
110
212
  validateCredentialReferences(next);
213
+ // Ordinary registration edits cannot mint qualification, including a copied binding.
214
+ next.workerQualification = workerServerDigest(next) === workerServerDigest(data.servers[name])
215
+ ? data.servers[name].workerQualification : undefined;
111
216
  data.servers[name] = next;
217
+ await this.save(data);
218
+ }
219
+ /** Bind an already qualified host installation to this exact MCP registration. */
220
+ async recordWorkerQualification(name, input) {
221
+ this.clearCache();
222
+ const server = await this.get(name);
223
+ if (!server)
224
+ throw new Error(`Server "${name}" not found.`);
225
+ const binding = workerQualificationBinding(server, input);
226
+ server.workerQualification = binding;
112
227
  await this.save();
228
+ return structuredClone(binding);
229
+ }
230
+ /** Recheck immediately before use; direct shell invocations do not use this registry. */
231
+ async assertWorkerQualified(name, input) {
232
+ // Other trusted registry writers may have changed the registration since load().
233
+ this.clearCache();
234
+ const server = await this.get(name);
235
+ if (!server)
236
+ throw new Error(`Server "${name}" not found.`);
237
+ const current = workerQualificationBinding(server, input);
238
+ if (!server.workerQualification || qualificationDigest(server.workerQualification) !== qualificationDigest(current)) {
239
+ throw new Error('MCP worker registration qualification is missing or changed');
240
+ }
241
+ return structuredClone(server);
113
242
  }
114
243
  /** Get a specific server definition */
115
244
  async get(name) {
@@ -123,7 +252,7 @@ export class McpServerRegistry {
123
252
  }
124
253
  /** Record that a server was injected into a provider */
125
254
  async recordInjection(name, provider) {
126
- const data = await this.load();
255
+ const data = structuredClone(await this.load());
127
256
  const server = data.servers[name];
128
257
  if (!server)
129
258
  return;
@@ -133,7 +262,7 @@ export class McpServerRegistry {
133
262
  if (!server.injectedProviders.includes(provider)) {
134
263
  server.injectedProviders.push(provider);
135
264
  }
136
- await this.save();
265
+ await this.save(data);
137
266
  }
138
267
  /** Get all providers that have had servers injected */
139
268
  async getInjectedProviders() {
@@ -146,6 +275,33 @@ export class McpServerRegistry {
146
275
  }
147
276
  return [...providers];
148
277
  }
278
+ /** Registry-wide default credential policy, if one is set */
279
+ async getCredentialPolicy() {
280
+ const data = await this.load();
281
+ return data.credentialPolicy;
282
+ }
283
+ /** The enforced organisation floor for this registry, if any layer below the write layer sets one. */
284
+ async getCredentialPolicyFloor() {
285
+ await this.load();
286
+ return this.layering?.lowerPolicy;
287
+ }
288
+ /** The config file of the layer that set the enforced floor, for diagnostics. */
289
+ async getCredentialPolicyFloorSource() {
290
+ await this.load();
291
+ return this.layering?.lowerPolicySource;
292
+ }
293
+ async setCredentialPolicy(policy) {
294
+ const data = structuredClone(await this.load());
295
+ const floorPolicy = this.layering?.lowerPolicy;
296
+ const floorSource = this.layering?.lowerPolicySource;
297
+ const effective = resolveCredentialPolicy({ flag: policy, floorPolicy, floorSource, env: {} });
298
+ if (effective !== policy) {
299
+ const location = floorSource ? ` (set in ${floorSource})` : '';
300
+ throw new Error(`Cannot relax MCP credential policy floor "${effective}"${location} to "${policy}".`);
301
+ }
302
+ data.credentialPolicy = policy;
303
+ await this.save(data, true);
304
+ }
149
305
  /** Clear the in-memory cache */
150
306
  clearCache() {
151
307
  this.cache = null;
@@ -157,27 +313,31 @@ export class McpServerRegistry {
157
313
  /**
158
314
  * Build the MCP config block for a single server in a given provider's format.
159
315
  */
160
- function buildServerConfig(server, provider) {
316
+ export function buildServerConfig(server, provider) {
161
317
  const adapter = getProviderDefinition(provider)?.adapters.mcpInjection;
318
+ const { env, headers } = adapter && adapter !== 'codex'
319
+ ? renderCredentialMaps(server, adapter)
320
+ : { env: server.env, headers: server.headers };
162
321
  switch (adapter) {
163
322
  case 'antigravity': {
164
323
  if (server.type === 'stdio') {
165
- return { command: server.command, args: server.args || [], ...(server.env ? { env: server.env } : {}) };
324
+ return { command: server.command, args: server.args || [], ...(env ? { env } : {}) };
166
325
  }
167
- return { serverUrl: server.url, ...(server.headers ? { headers: server.headers } : {}) };
326
+ return { serverUrl: server.url, ...(headers ? { headers } : {}) };
168
327
  }
169
328
  case 'claude-code': {
170
329
  if (server.type === 'stdio') {
171
330
  return {
172
331
  command: server.command,
173
332
  args: server.args || [],
174
- ...(server.env ? { env: server.env } : {}),
333
+ ...(env ? { env } : {}),
175
334
  };
176
335
  }
177
- // http/sse
336
+ // Claude Code skips a url entry that has no type.
178
337
  return {
338
+ type: server.type,
179
339
  url: server.url,
180
- ...(server.headers ? { headers: server.headers } : {}),
340
+ ...(headers ? { headers } : {}),
181
341
  };
182
342
  }
183
343
  case 'cursor': {
@@ -185,12 +345,12 @@ function buildServerConfig(server, provider) {
185
345
  return {
186
346
  command: server.command,
187
347
  args: server.args || [],
188
- ...(server.env ? { env: server.env } : {}),
348
+ ...(env ? { env } : {}),
189
349
  };
190
350
  }
191
351
  return {
192
352
  url: server.url,
193
- ...(server.headers ? { headers: server.headers } : {}),
353
+ ...(headers ? { headers } : {}),
194
354
  };
195
355
  }
196
356
  case 'factory': {
@@ -200,14 +360,14 @@ function buildServerConfig(server, provider) {
200
360
  command: server.command,
201
361
  args: server.args || [],
202
362
  disabled: false,
203
- ...(server.env ? { env: server.env } : {}),
363
+ ...(env ? { env } : {}),
204
364
  };
205
365
  }
206
366
  return {
207
367
  type: server.type,
208
368
  url: server.url,
209
369
  disabled: false,
210
- ...(server.headers ? { headers: server.headers } : {}),
370
+ ...(headers ? { headers } : {}),
211
371
  };
212
372
  }
213
373
  case 'opencode': {
@@ -215,13 +375,14 @@ function buildServerConfig(server, provider) {
215
375
  return {
216
376
  type: 'local',
217
377
  command: [server.command, ...(server.args || [])],
218
- ...(server.env ? { env: server.env } : {}),
378
+ // opencode names a local server's variables `environment`.
379
+ ...(env ? { environment: env } : {}),
219
380
  };
220
381
  }
221
382
  return {
222
383
  type: 'remote',
223
384
  url: server.url,
224
- ...(server.headers ? { headers: server.headers } : {}),
385
+ ...(headers ? { headers } : {}),
225
386
  };
226
387
  }
227
388
  case 'windsurf':
@@ -230,12 +391,12 @@ function buildServerConfig(server, provider) {
230
391
  return {
231
392
  command: server.command,
232
393
  args: server.args || [],
233
- ...(server.env ? { env: server.env } : {}),
394
+ ...(env ? { env } : {}),
234
395
  };
235
396
  }
236
397
  return {
237
398
  url: server.url,
238
- ...(server.headers ? { headers: server.headers } : {}),
399
+ ...(headers ? { headers } : {}),
239
400
  };
240
401
  }
241
402
  case 'codex':
@@ -245,22 +406,13 @@ function buildServerConfig(server, provider) {
245
406
  return {};
246
407
  }
247
408
  }
248
- /**
249
- * Build a TOML section for a server (Codex/OpenAI provider).
250
- */
251
- function tomlString(value) {
252
- if (typeof value !== 'string' || [...value].some(char => {
253
- const point = char.codePointAt(0);
254
- return point >= 0xd800 && point <= 0xdfff;
255
- }))
256
- throw new Error('TOML values must be strings containing valid Unicode scalar values');
257
- // JSON escapes align with TOML basic strings except DEL must also be escaped.
258
- return JSON.stringify(value).replace(/\u007f/g, '\\u007f');
259
- }
260
409
  function tomlKey(value) {
261
410
  return typeof value === 'string' && /^[A-Za-z0-9_-]+$/.test(value) ? value : tomlString(value);
262
411
  }
263
- function buildServerToml(server) {
412
+ function tomlInlineTable(values) {
413
+ return `{ ${Object.entries(values).map(([name, value]) => `${tomlKey(name)} = ${tomlString(value)}`).join(', ')} }`;
414
+ }
415
+ export function buildServerToml(server) {
264
416
  const lines = [];
265
417
  lines.push(`[mcp_servers.${tomlKey(server.name)}]`);
266
418
  if (server.type === 'stdio') {
@@ -269,9 +421,27 @@ function buildServerToml(server) {
269
421
  const argsStr = server.args.map(a => tomlString(a)).join(', ');
270
422
  lines.push(`args = [${argsStr}]`);
271
423
  }
424
+ if (server.env && Object.keys(server.env).length > 0)
425
+ lines.push(`env = ${tomlInlineTable(server.env)}`);
426
+ // Codex forwards a variable only under its own name (env_vars).
427
+ const forwarded = Object.entries(server.envFrom ?? {});
428
+ for (const [key, name] of forwarded) {
429
+ validateEnvReferenceName(name);
430
+ if (key !== name) {
431
+ throw new Error(`MCP server "${server.name}": Codex forwards environment variables under their own name only; env-from ${key}=${name} cannot be rendered. Use ${name}=${name}.`);
432
+ }
433
+ }
434
+ if (forwarded.length > 0)
435
+ lines.push(`env_vars = [${forwarded.map(([name]) => tomlString(name)).join(', ')}]`);
272
436
  }
273
437
  else {
274
438
  lines.push(`url = ${tomlString(server.url)}`);
439
+ if (server.headers && Object.keys(server.headers).length > 0)
440
+ lines.push(`http_headers = ${tomlInlineTable(server.headers)}`);
441
+ for (const name of Object.values(server.headerEnv ?? {}))
442
+ validateEnvReferenceName(name);
443
+ if (server.headerEnv && Object.keys(server.headerEnv).length > 0)
444
+ lines.push(`env_http_headers = ${tomlInlineTable(server.headerEnv)}`);
275
445
  }
276
446
  lines.push(`startup_timeout_sec = 10.0`);
277
447
  lines.push(`tool_timeout_sec = 60.0`);
@@ -285,6 +455,9 @@ export function getProviderConfigPath(provider, projectDir = '.', options = {})
285
455
  const homeDir = process.env.HOME || process.env.USERPROFILE || '';
286
456
  return resolve(homeDir, '.gemini/config/mcp_config.json');
287
457
  }
458
+ if ((provider === 'claude-code' || provider === 'claude') && options.scope === 'user') {
459
+ return resolve(process.env.HOME || process.env.USERPROFILE || homedir(), '.claude.json');
460
+ }
288
461
  if ((provider === 'omp' || provider === 'oh-my-pi') && options.scope !== undefined && !['user', 'project'].includes(options.scope))
289
462
  throw new Error('OMP MCP scope must be user or project');
290
463
  if ((provider === 'omp' || provider === 'oh-my-pi') && options.scope === 'user')
@@ -295,12 +468,12 @@ export function getProviderConfigPath(provider, projectDir = '.', options = {})
295
468
  agy: resolve(projectDir, '.agents/mcp_config.json'),
296
469
  omp: resolve(projectDir, '.omp/mcp.json'),
297
470
  'oh-my-pi': resolve(projectDir, '.omp/mcp.json'),
298
- 'claude-code': resolve(projectDir, '.claude/settings.local.json'),
299
- claude: resolve(projectDir, '.claude/settings.local.json'),
471
+ 'claude-code': resolve(projectDir, '.mcp.json'),
472
+ claude: resolve(projectDir, '.mcp.json'),
300
473
  cursor: resolve(projectDir, '.cursor/mcp.json'),
301
474
  factory: resolve(homeDir, '.factory/mcp.json'),
302
- codex: resolve(homeDir, '.codex/config.toml'),
303
- openai: resolve(homeDir, '.codex/config.toml'),
475
+ codex: process.env.CODEX_HOME ? resolve(process.env.CODEX_HOME, 'config.toml') : resolve(homeDir, '.codex/config.toml'),
476
+ openai: process.env.CODEX_HOME ? resolve(process.env.CODEX_HOME, 'config.toml') : resolve(homeDir, '.codex/config.toml'),
304
477
  'grok-build': options.scope === 'user'
305
478
  ? resolve(process.env.GROK_HOME || resolve(homeDir, '.grok'), 'config.toml')
306
479
  : resolve(projectDir, '.grok/config.toml'),
@@ -327,16 +500,37 @@ export async function injectServers(registry, provider, options = {}) {
327
500
  };
328
501
  // Get servers to inject
329
502
  let allServers = await registry.list();
330
- if (serverFilter && serverFilter.length > 0) {
503
+ if (serverFilter !== undefined) {
331
504
  allServers = allServers.filter(s => serverFilter.includes(s.name));
332
505
  }
506
+ const toolPlan = options.toolFilters
507
+ ? planToolFilters(provider, allServers.map(server => server.name), options.toolFilters)
508
+ : null;
509
+ if (toolPlan)
510
+ result.warnings = toolPlan.warnings;
333
511
  if (allServers.length === 0) {
334
512
  result.error = 'No servers to inject. Use "aiwg mcp add" first.';
335
513
  return result;
336
514
  }
515
+ assertCredentialPolicy(allServers, resolveCredentialPolicy({
516
+ flag: options.credentialPolicy, registryPolicy: await registry.getCredentialPolicy(),
517
+ floorPolicy: await registry.getCredentialPolicyFloor(),
518
+ floorSource: await registry.getCredentialPolicyFloorSource(),
519
+ }));
520
+ // The shareable-project decision follows the actual resolved destination,
521
+ // never the requested scope: a `--scope user` request for a provider without
522
+ // a user-scope destination still resolves to the project file and is refused.
523
+ const { userScope, projectRoot } = classifyMcpDestination(provider, options.scope, configPath, projectDir);
524
+ await assertConfigDestination(configPath, projectRoot);
525
+ // Every destination inside the project directory is meant to be committed to
526
+ // version control; home destinations are private, so literals there are not
527
+ // copied into a shared file.
528
+ if (projectRoot) {
529
+ assertProjectCredentials(allServers, configPath, projectCredentialRemedy(provider, projectDir));
530
+ }
337
531
  if (provider === 'omp' || provider === 'oh-my-pi') {
338
532
  try {
339
- const managed = await manageOmpMcp(configPath, allServers, { dryRun });
533
+ const managed = await manageOmpMcp(configPath, allServers, { dryRun, userScope, projectRoot });
340
534
  if (!dryRun)
341
535
  for (const server of allServers)
342
536
  await registry.recordInjection(server.name, 'omp');
@@ -350,6 +544,7 @@ export async function injectServers(registry, provider, options = {}) {
350
544
  try {
351
545
  const managed = await manageGrokBuildMcp(configPath, allServers, {
352
546
  dryRun,
547
+ userScope,
353
548
  root: options.scope === 'user' ? dirname(dirname(configPath)) : resolve(projectDir),
354
549
  });
355
550
  if (!dryRun && !managed.error)
@@ -363,12 +558,12 @@ export async function injectServers(registry, provider, options = {}) {
363
558
  }
364
559
  // Handle TOML-based providers (Codex/OpenAI) separately
365
560
  if (provider === 'codex' || provider === 'openai') {
366
- return injectToml(registry, allServers, configPath, provider, dryRun, result);
561
+ return injectToml(registry, allServers, configPath, provider, dryRun, result, userScope, projectRoot, toolPlan);
367
562
  }
368
563
  // JSON-based providers
369
- return injectJson(registry, allServers, configPath, provider, dryRun, result);
564
+ return injectJson(registry, allServers, configPath, provider, dryRun, result, userScope, projectRoot, toolPlan, { projectDir, scope: options.scope });
370
565
  }
371
- async function injectJson(registry, servers, configPath, provider, dryRun, result) {
566
+ async function injectJson(registry, servers, configPath, provider, dryRun, result, userScope, projectRoot, toolPlan = null, location = {}) {
372
567
  // Load existing config
373
568
  let existing = {};
374
569
  try {
@@ -406,9 +601,21 @@ async function injectJson(registry, servers, configPath, provider, dryRun, resul
406
601
  }
407
602
  // Merge back
408
603
  const merged = { ...existing, [mcpKey]: newServers };
604
+ if (toolPlan) {
605
+ applyJsonToolFilterPlan(merged, mcpKey, toolPlan);
606
+ }
607
+ const isClaude = provider === 'claude-code' || provider === 'claude';
608
+ const settingsPath = isClaude ? claudeSettingsPath(location.projectDir, location.scope) : null;
609
+ const settings = settingsPath ? await prepareClaudePermissions(settingsPath, toolPlan?.claudePermissions || { deny: [], allow: [] }, { userScope, projectRoot, managedDir: dirname(registry.getPath()), mcpPath: configPath }) : null;
610
+ if (settings?.active && settingsPath)
611
+ result.settingsPath = settingsPath;
612
+ if (settings?.warnings.length)
613
+ result.warnings = [...(result.warnings || []), ...settings.warnings];
409
614
  if (!dryRun) {
410
- await mkdir(resolve(configPath, '..'), { recursive: true });
411
- await writeFile(configPath, JSON.stringify(merged, null, 2) + '\n', 'utf-8');
615
+ await writeConfigTransaction([
616
+ { file: configPath, content: JSON.stringify(merged, null, 2) + '\n', options: { userScope, projectRoot } },
617
+ ...(settings?.writes || []),
618
+ ]);
412
619
  // Record injection in registry
413
620
  for (const server of servers) {
414
621
  if ((provider === 'antigravity' || provider === 'agy') && !result.serversInjected.includes(server.name))
@@ -418,7 +625,7 @@ async function injectJson(registry, servers, configPath, provider, dryRun, resul
418
625
  }
419
626
  return result;
420
627
  }
421
- async function injectToml(registry, servers, configPath, provider, dryRun, result) {
628
+ async function injectToml(registry, servers, configPath, provider, dryRun, result, userScope, projectRoot, toolPlan = null) {
422
629
  let existing = '';
423
630
  try {
424
631
  existing = await readFile(configPath, 'utf-8');
@@ -428,15 +635,15 @@ async function injectToml(registry, servers, configPath, provider, dryRun, resul
428
635
  throw error;
429
636
  }
430
637
  for (const server of servers) {
431
- const edited = replaceServer(existing, server.name, buildServerToml(server));
638
+ const filterLines = toolPlan?.tomlLines[server.name] || [];
639
+ const edited = replaceServer(existing, server.name, [buildServerToml(server), ...filterLines].join('\n'));
432
640
  existing = edited.text;
433
641
  if (edited.alreadyPresent)
434
642
  result.alreadyPresent.push(server.name);
435
643
  result.serversInjected.push(server.name);
436
644
  }
437
645
  if (!dryRun) {
438
- await mkdir(resolve(configPath, '..'), { recursive: true });
439
- await writeFile(configPath, existing, 'utf-8');
646
+ await writeConfigAtomic(configPath, existing, { userScope, projectRoot });
440
647
  for (const server of servers) {
441
648
  await registry.recordInjection(server.name, provider);
442
649
  }