@aiwg/cli 2026.10.5 → 2026.10.7

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 (263) hide show
  1. package/agentic/code/addons/composition-engine/lib/runtime.mjs +1401 -0
  2. package/agentic/code/addons/composition-engine/lib/validator.mjs +513 -0
  3. package/agentic/code/addons/composition-engine/schemas/flow-graph.schema.json +372 -0
  4. package/agentic/code/providers/model-capabilities.v1.json +74 -50
  5. package/bin/aiwg.mjs +7 -3
  6. package/dist/src/a2a/agent-card.js +11 -0
  7. package/dist/src/api/index.d.ts +1 -0
  8. package/dist/src/api/index.js +1 -0
  9. package/dist/src/artifacts/browser-export.js +1 -0
  10. package/dist/src/artifacts/citation-parser.js +8 -1
  11. package/dist/src/artifacts/cli.js +57 -6
  12. package/dist/src/artifacts/fortemi-core-query-adapter.js +2 -0
  13. package/dist/src/artifacts/fortemi-core-sync.js +26 -8
  14. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/contract.json +103 -0
  15. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-config.schema.json +212 -0
  16. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-set.schema.json +240 -0
  17. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-manifest.schema.json +451 -0
  18. package/dist/src/artifacts/fortemi-shard-export.js +284 -0
  19. package/dist/src/artifacts/graph-query.js +18 -8
  20. package/dist/src/artifacts/index-adapters.js +339 -0
  21. package/dist/src/artifacts/index-builder.js +143 -14
  22. package/dist/src/artifacts/index-files.js +15 -4
  23. package/dist/src/artifacts/index-preparation.js +2 -0
  24. package/dist/src/artifacts/index-reader.js +44 -0
  25. package/dist/src/artifacts/index-refresh.js +114 -55
  26. package/dist/src/artifacts/index-status.js +20 -5
  27. package/dist/src/artifacts/memory-maintenance.js +503 -0
  28. package/dist/src/artifacts/prebuilt-relations.js +3 -1
  29. package/dist/src/artifacts/query-engine.js +263 -108
  30. package/dist/src/artifacts/relations-builder.js +2 -2
  31. package/dist/src/artifacts/source-graph.js +2 -2
  32. package/dist/src/artifacts/types.js +9 -1
  33. package/dist/src/auth/client.js +5 -1
  34. package/dist/src/auth/credential-store.js +5 -1
  35. package/dist/src/auth/sensitive-input.js +151 -0
  36. package/dist/src/channel/manager.mjs +3 -0
  37. package/dist/src/cli/handlers/agentcard.js +47 -25
  38. package/dist/src/cli/handlers/auth.js +22 -2
  39. package/dist/src/cli/handlers/decision.js +22 -3
  40. package/dist/src/cli/handlers/effect.js +4 -84
  41. package/dist/src/cli/handlers/guided-setup.js +470 -0
  42. package/dist/src/cli/handlers/help.js +2 -2
  43. package/dist/src/cli/handlers/housekeeper.js +1110 -0
  44. package/dist/src/cli/handlers/index.js +4 -2
  45. package/dist/src/cli/handlers/init.js +41 -6
  46. package/dist/src/cli/handlers/install.js +6 -0
  47. package/dist/src/cli/handlers/local-executor.js +5 -3
  48. package/dist/src/cli/handlers/marketplace.js +15 -4
  49. package/dist/src/cli/handlers/mc.js +509 -53
  50. package/dist/src/cli/handlers/models.js +32 -2
  51. package/dist/src/cli/handlers/ralph-launcher.js +76 -4
  52. package/dist/src/cli/handlers/rules.js +1 -1
  53. package/dist/src/cli/handlers/script-runner.js +73 -2
  54. package/dist/src/cli/handlers/serve.js +5 -3
  55. package/dist/src/cli/handlers/sessions.js +1 -0
  56. package/dist/src/cli/handlers/setup.js +432 -1
  57. package/dist/src/cli/handlers/skill-lint.js +1 -1
  58. package/dist/src/cli/handlers/subcommands.js +118 -8
  59. package/dist/src/cli/handlers/use.js +335 -107
  60. package/dist/src/cli/handlers/workspace.js +3 -1
  61. package/dist/src/cli/prompt-utils.js +81 -0
  62. package/dist/src/cli/router.js +16 -1
  63. package/dist/src/config/aiwg-config.js +42 -0
  64. package/dist/src/config/cli.js +7 -3
  65. package/dist/src/config/decision-config.js +14 -1
  66. package/dist/src/config/gitignore.js +4 -0
  67. package/dist/src/config/native-sandbox-warnings.js +25 -0
  68. package/dist/src/config/workspace.js +2 -1
  69. package/dist/src/contributors/validation.js +6 -5
  70. package/dist/src/decision/adapters/jev.js +20 -31
  71. package/dist/src/decision/admission.js +7 -3
  72. package/dist/src/decision/ask.js +6 -1
  73. package/dist/src/decision/benchmark.js +482 -0
  74. package/dist/src/decision/ledger.js +22 -3
  75. package/dist/src/decision/pricing.js +147 -0
  76. package/dist/src/decision/qualification/gate-evidence.js +23 -18
  77. package/dist/src/decision/qualification/load-staged.js +576 -0
  78. package/dist/src/decision/qualification/load.js +109 -33
  79. package/dist/src/decision/qualification/runner.js +12 -5
  80. package/dist/src/decision/review/service.js +6 -4
  81. package/dist/src/decision/review/validate.js +23 -2
  82. package/dist/src/decision/savings.js +8 -3
  83. package/dist/src/discovery/index.js +492 -0
  84. package/dist/src/discovery/interview.js +238 -0
  85. package/dist/src/discovery/plan-apply.js +567 -0
  86. package/dist/src/discovery/plan-store.js +171 -0
  87. package/dist/src/discovery/plan-types.js +2 -0
  88. package/dist/src/discovery/plan-validation.js +139 -0
  89. package/dist/src/discovery/types.js +2 -0
  90. package/dist/src/effects/host.js +91 -0
  91. package/dist/src/extensions/commands/definitions.js +49 -9
  92. package/dist/src/extensions/manifest.js +71 -44
  93. package/dist/src/extensions/project-local-discovery.js +1 -1
  94. package/dist/src/extensions/project-local-gitignore.js +2 -0
  95. package/dist/src/extensions/project-quickref.js +18 -17
  96. package/dist/src/extensions/validation.js +32 -29
  97. package/dist/src/housekeeper/daemon-config.js +148 -0
  98. package/dist/src/housekeeper/ipc.js +84 -0
  99. package/dist/src/housekeeper/lifecycle.js +148 -0
  100. package/dist/src/housekeeper/platform.js +35 -0
  101. package/dist/src/housekeeper/status.js +70 -0
  102. package/dist/src/housekeeper/units.js +114 -0
  103. package/dist/src/issues/cli.js +16 -5
  104. package/dist/src/issues/live.js +44 -12
  105. package/dist/src/jobs/flow.js +22 -21
  106. package/dist/src/lint/rule-activation.js +20 -5
  107. package/dist/src/lint/runner.js +1 -1
  108. package/dist/src/marketplace/exchange.js +18 -6
  109. package/dist/src/marketplace/provenance.js +3 -2
  110. package/dist/src/mcp/adapters/codex-runtime.js +46 -76
  111. package/dist/src/mcp/cli.mjs +22 -96
  112. package/dist/src/mcp/config-file.mjs +6 -2
  113. package/dist/src/mcp/config-layers.mjs +23 -7
  114. package/dist/src/mcp/grok-build-config.mjs +2 -1
  115. package/dist/src/mcp/muse-native-config.mjs +2 -1
  116. package/dist/src/mcp/profiles.js +18 -31
  117. package/dist/src/mcp/profiles.mjs +18 -32
  118. package/dist/src/mcp/registry.js +7 -27
  119. package/dist/src/mcp/registry.mjs +7 -28
  120. package/dist/src/mcp/server.mjs +34 -1
  121. package/dist/src/mcp/tools/agentic-sandbox.mjs +8 -7
  122. package/dist/src/mcp/tools/command-run.mjs +3 -2
  123. package/dist/src/mcp/tools/decision.mjs +49 -48
  124. package/dist/src/mcp/tools/discovery.mjs +46 -21
  125. package/dist/src/mcp/tools/orchestration.mjs +22 -22
  126. package/dist/src/mcp/tools/subsystems.mjs +9 -8
  127. package/dist/src/mcp/tools/worker-invoke.mjs +30 -6
  128. package/dist/src/mcp/tools/writer-profiles.mjs +1 -1
  129. package/dist/src/memory/audit.js +719 -0
  130. package/dist/src/memory/canonical-context.js +392 -13
  131. package/dist/src/memory/canonical-maintenance.js +95 -0
  132. package/dist/src/memory/cli-result.js +22 -0
  133. package/dist/src/memory/cli.js +176 -0
  134. package/dist/src/memory/context-pack.js +57 -6
  135. package/dist/src/memory/cost.js +512 -0
  136. package/dist/src/memory/firewall-classification.d.mts +7 -0
  137. package/dist/src/memory/firewall-classification.mjs +32 -0
  138. package/dist/src/memory/index.js +8 -0
  139. package/dist/src/memory/line-maintenance.js +68 -0
  140. package/dist/src/memory/maintenance-cli.js +286 -0
  141. package/dist/src/memory/maintenance-contract.js +136 -0
  142. package/dist/src/memory/maintenance-index.js +18 -0
  143. package/dist/src/memory/maintenance-owner.js +2 -0
  144. package/dist/src/memory/maintenance-record.js +138 -0
  145. package/dist/src/memory/maintenance.js +254 -0
  146. package/dist/src/memory/project-registry.js +2 -1
  147. package/dist/src/memory/provider-memory-adapter.js +154 -0
  148. package/dist/src/memory/recommend-cli.js +83 -0
  149. package/dist/src/memory/recommend.js +269 -0
  150. package/dist/src/memory/scheduled.js +81 -0
  151. package/dist/src/memory/taxonomy.js +44 -0
  152. package/dist/src/mission-protocol/codecs.js +19 -9
  153. package/dist/src/mission-protocol/worker-status.js +91 -1
  154. package/dist/src/models/config-loader.js +11 -1
  155. package/dist/src/models/model-capabilities.v1.json +74 -50
  156. package/dist/src/models/provider-policy.js +125 -62
  157. package/dist/src/models/resolver.js +45 -8
  158. package/dist/src/models/worker-installation.d.mts +8 -1
  159. package/dist/src/models/worker-installation.mjs +231 -38
  160. package/dist/src/models/wrapper-route.js +1 -1
  161. package/dist/src/output-modes/registry.js +8 -7
  162. package/dist/src/packages/adapters/git.js +99 -23
  163. package/dist/src/packages/adapters/gitea.js +8 -27
  164. package/dist/src/packages/adapters/github.js +7 -25
  165. package/dist/src/packages/registry.js +4 -4
  166. package/dist/src/plugin/metadata-validator.js +1 -1
  167. package/dist/src/process/cli-command.d.mts +5 -0
  168. package/dist/src/process/cli-command.mjs +42 -0
  169. package/dist/src/providers/deploy-version.d.mts +1 -0
  170. package/dist/src/providers/deploy-version.mjs +17 -0
  171. package/dist/src/providers/provider-definitions.js +19 -16
  172. package/dist/src/providers/skill-ownership.d.mts +11 -2
  173. package/dist/src/providers/skill-ownership.mjs +35 -17
  174. package/dist/src/rules/activation.d.mts +11 -2
  175. package/dist/src/rules/activation.mjs +82 -6
  176. package/dist/src/rules/inventory.js +1 -1
  177. package/dist/src/rules/rule-frontmatter.schema.json +2 -0
  178. package/dist/src/schema/zod-validation.d.mts +6 -0
  179. package/dist/src/schema/zod-validation.mjs +50 -0
  180. package/dist/src/serve/executor-registry.js +297 -20
  181. package/dist/src/serve/executor-token.d.mts +1 -0
  182. package/dist/src/serve/executor-token.mjs +8 -0
  183. package/dist/src/serve/shared-host-scheduler.js +1102 -31
  184. package/dist/src/serve/worker-invocation.js +124 -15
  185. package/dist/src/sessions/adapters/claude.js +19 -18
  186. package/dist/src/sessions/adapters/codex.js +10 -9
  187. package/dist/src/sessions/adapters/copilot.js +14 -13
  188. package/dist/src/sessions/adapters/cursor.js +19 -18
  189. package/dist/src/sessions/adapters/deepseek-harness.js +9 -8
  190. package/dist/src/sessions/adapters/factory.js +11 -10
  191. package/dist/src/sessions/adapters/generic.js +32 -23
  192. package/dist/src/sessions/adapters/grokbot.js +35 -35
  193. package/dist/src/sessions/adapters/hermes.js +20 -19
  194. package/dist/src/sessions/adapters/muse.js +50 -49
  195. package/dist/src/sessions/adapters/omp.js +5 -4
  196. package/dist/src/sessions/adapters/openclaw.js +30 -29
  197. package/dist/src/sessions/adapters/opencode.js +26 -26
  198. package/dist/src/sessions/adapters/openhuman.js +24 -23
  199. package/dist/src/sessions/adapters/pi.js +5 -4
  200. package/dist/src/sessions/adapters/windsurf.js +8 -8
  201. package/dist/src/sessions/analytics.js +10 -9
  202. package/dist/src/sessions/candidates.js +7 -6
  203. package/dist/src/sessions/contracts.js +63 -53
  204. package/dist/src/sessions/output-registration.js +11 -10
  205. package/dist/src/sessions/workspace-discovery.js +3 -4
  206. package/dist/src/smiths/context-pipeline/aiwg-md.js +18 -1
  207. package/dist/src/smiths/context-pipeline/claude-hook.js +46 -10
  208. package/dist/src/smiths/context-pipeline/context-file-safety.js +330 -9
  209. package/dist/src/smiths/context-pipeline/finalization.js +3 -2
  210. package/dist/src/smiths/context-pipeline/generator.js +7 -19
  211. package/dist/src/smiths/context-pipeline/legacy-inject.js +3 -10
  212. package/dist/src/smiths/context-pipeline/managed-hook.js +5 -5
  213. package/dist/src/smiths/context-pipeline/parallelism-section.js +72 -5
  214. package/dist/src/smiths/context-pipeline/workspace-context.js +110 -22
  215. package/dist/src/tracker/project-inputs.js +2 -0
  216. package/dist/src/update/checker.d.mts +4 -0
  217. package/dist/src/update/checker.mjs +14 -0
  218. package/dist/src/update/notifier.mjs +2 -4
  219. package/dist/src/writing/exemplar-selection.js +6 -6
  220. package/dist/src/writing/voice-evaluation.js +24 -23
  221. package/dist/src/writing/writer-migration.js +14 -13
  222. package/dist/src/writing/writer-profile.js +19 -18
  223. package/dist/src/writing/writing-brief.js +18 -17
  224. package/dist/src/writing/writing-receipt.js +47 -46
  225. package/package.json +5 -2
  226. package/schemas/decision/DecisionReview.schema.json +78 -1
  227. package/schemas/mission-v1.schema.json +3 -0
  228. package/tools/agents/deploy-agents.mjs +67 -39
  229. package/tools/agents/providers/antigravity.mjs +6 -1
  230. package/tools/agents/providers/base.mjs +24 -14
  231. package/tools/agents/providers/claude.mjs +6 -3
  232. package/tools/agents/providers/codex.mjs +54 -57
  233. package/tools/agents/providers/context-write.mjs +28 -0
  234. package/tools/agents/providers/copilot.mjs +13 -4
  235. package/tools/agents/providers/cursor.mjs +1 -1
  236. package/tools/agents/providers/deepseek-harness.mjs +1 -1
  237. package/tools/agents/providers/factory.mjs +7 -1
  238. package/tools/agents/providers/grok-build.mjs +18 -2
  239. package/tools/agents/providers/grokbot.mjs +1 -1
  240. package/tools/agents/providers/hermes.mjs +12 -15
  241. package/tools/agents/providers/muse-hooks.mjs +21 -14
  242. package/tools/agents/providers/muse.mjs +7 -2
  243. package/tools/agents/providers/omp.mjs +31 -5
  244. package/tools/agents/providers/opencode.mjs +14 -5
  245. package/tools/agents/providers/openhuman.mjs +1 -1
  246. package/tools/agents/providers/pi.mjs +1 -1
  247. package/tools/agents/providers/provider-prepare-coordinator.mjs +29 -9
  248. package/tools/agents/providers/rule-delivery.mjs +289 -80
  249. package/tools/agents/providers/warp.mjs +4 -1
  250. package/tools/agents/providers/windsurf.mjs +13 -3
  251. package/tools/cli/workspace-status.mjs +5 -0
  252. package/tools/commands/deploy-prompts-codex.mjs +11 -8
  253. package/tools/ralph-external/lib/codex-adapter.mjs +6 -2
  254. package/tools/ralph-external/lib/consecutive-failure-circuit-breaker.mjs +26 -0
  255. package/tools/ralph-external/lib/launcher-recovery-evidence.mjs +7 -1
  256. package/tools/ralph-external/lib/muse-adapter.mjs +12 -2
  257. package/tools/ralph-external/lib/recovery-probe.mjs +14 -4
  258. package/tools/ralph-external/lib/worker-health.mjs +1 -1
  259. package/tools/ralph-external/lib/worker-isolation.mjs +65 -9
  260. package/tools/ralph-external/process-monitor.mjs +49 -8
  261. package/tools/ralph-external/session-launcher.mjs +405 -35
  262. package/tools/skills/deploy-skills-codex.mjs +11 -7
  263. package/vscode-extension/schemas/aiwg.config.v1.json +11 -1
@@ -8,6 +8,7 @@
8
8
 
9
9
  import { startServer, createServer } from './server.mjs';
10
10
  import { assertConfigDestination, assertConfigObject, assertProjectCredentials, isPathInsideDir, classifyMcpDestination, projectCredentialRemedy, writeConfigAtomic, writeConfigTransaction } from './config-file.mjs';
11
+ import { replaceServer } from './toml-editor.mjs';
11
12
  import fs from 'fs/promises';
12
13
  import path from 'path';
13
14
  import os from 'os';
@@ -335,16 +336,14 @@ enabled_tools = [
335
336
  if (error.code !== 'ENOENT') throw error;
336
337
  }
337
338
 
338
- // Check if AIWG MCP already configured
339
- if (existing.includes('[mcp_servers.aiwg]')) {
339
+ // Parse before deciding whether to write, including already configured files.
340
+ const edited = replaceServer(existing, 'aiwg', tomlContent.trim());
341
+ if (edited.alreadyPresent) {
340
342
  console.log('AIWG MCP already configured in ~/.codex/config.toml');
341
343
  return true;
342
344
  }
343
345
 
344
- // Append TOML config
345
- const updated = existing.trimEnd() + '\n' + tomlContent.trim() + '\n';
346
-
347
- await writeConfigAtomic(configPath, updated, { userScope: true });
346
+ await writeConfigAtomic(configPath, edited.text, { userScope: true });
348
347
  console.log(`MCP configuration appended to: ${configPath}`);
349
348
  console.log(`\nTo use AIWG MCP server with Codex:`);
350
349
  console.log(` 1. Restart Codex CLI`);
@@ -1125,7 +1124,7 @@ Usage:
1125
1124
  aiwg mcp profile import <file>
1126
1125
  aiwg mcp profile export <name> [--out <file>]
1127
1126
  aiwg mcp profile init-presets
1128
- aiwg mcp profile login <name> [--api-key-stdin | --api-key-file <path> | --api-key-env <ENV_VAR_NAME>]
1127
+ aiwg mcp profile login <name> [--api-key-interactive | --api-key-stdin | --api-key-file <path> | --api-key-store <name> | --api-key-env <ENV_VAR_NAME>]
1129
1128
 
1130
1129
  Profiles let you define named subsets of your registered MCP servers:
1131
1130
  aiwg mcp profile add dev --servers git-gitea,memory-fortemi --description "Dev work"
@@ -1163,10 +1162,10 @@ Logging in to a Codex profile's isolated runtime home (#3062):
1163
1162
  $OPENAI_KEY; visible to every other
1164
1163
  child process this shell starts
1165
1164
  In every case the key reaches codex over stdin only; it never appears on
1166
- argv, in logs, or in error messages. Exactly one of the three may be
1167
- given. Without a terminal and without any of the three, this errors
1168
- naming all three. See "Supplying credentials safely" in the docs:
1169
- docs/mcp/codex-profiles.md#supplying-credentials-safely
1165
+ argv, in logs, or in error messages. Use --api-key-store <name> for a named
1166
+ OS-store entry, or --api-key-interactive for hidden terminal entry. Give
1167
+ sources in preference order; a missing source without a terminal errors naming the options.
1168
+ See docs/cli/sensitive-values.md for exposure details and manager examples.
1170
1169
  `);
1171
1170
  }
1172
1171
 
@@ -1420,59 +1419,18 @@ async function handleProfileInitPresets() {
1420
1419
  }
1421
1420
  }
1422
1421
 
1423
- /**
1424
- * Read all of stdin and return it as one trimmed string (go/no-go review,
1425
- * blocker 1C's `--api-key-stdin`). Mirrors `src/cli/handlers/decision.ts`'s
1426
- * own private `readStdin()` + `.trim()` for `--token-stdin` — duplicated
1427
- * rather than imported for the same source/dist reason documented on
1428
- * `requireFlagValue` above (this file runs both compiled and directly from
1429
- * source). Input is expected to be a single line; a value containing
1430
- * additional lines is not specially rejected, matching that precedent.
1431
- */
1432
- async function readStdinAll() {
1433
- const chunks = [];
1434
- for await (const chunk of process.stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
1435
- return Buffer.concat(chunks).toString('utf8');
1436
- }
1437
-
1438
- /**
1439
- * Handle `aiwg mcp profile login <name> [--api-key-env <ENV_VAR_NAME> |
1440
- * --api-key-stdin | --api-key-file <path>]` (#3062, #3069, go/no-go review
1441
- * blocker 1C).
1442
- *
1443
- * Wires `loginInProfile()` (src/mcp/adapters/codex-runtime.ts), which
1444
- * already implements the full headless-safe behavior this command needs:
1445
- * with a terminal attached and none of the three given, it runs the
1446
- * interactive `codex login` OAuth flow. Exactly one credential source may
1447
- * be given; more than one is a usage error naming the flags given. In
1448
- * every case the key reaches `loginInProfile()` only as an in-memory value
1449
- * (or, for `--api-key-env`, a variable *name*) — never on argv, in logs, or
1450
- * in an error message — and travels to `codex` only on its stdin (see
1451
- * `spawnCodexLoginWithApiKey()`'s own doc comment). Without a terminal and
1452
- * without any of the three, `loginInProfile()` itself refuses, naming all
1453
- * three flags.
1454
- *
1455
- * `--api-key-env` reads the key from that named environment variable, as
1456
- * before. `--api-key-stdin` reads one line from this process's own stdin
1457
- * (refusing if stdin is a terminal, or if it is empty) — the least
1458
- * observable source, since the key then never exists in any environment or
1459
- * on any command line. `--api-key-file <path>` reads the key from a file
1460
- * that `readApiKeyFromFile()` requires to be a non-symlink, single-hard-
1461
- * link, regular file owned by the current user with no group/other
1462
- * permission bits on POSIX — refused otherwise, without ever printing the
1463
- * path's contents. `--api-key-env` remains the least preferred of the
1464
- * three (see "Supplying credentials safely" in the docs): the named
1465
- * variable is visible to every other child process this shell starts.
1466
- */
1422
+ /** Standard sensitive input; the Codex adapter still receives only an in-memory key. */
1467
1423
  async function handleProfileLogin(args) {
1468
1424
  let apiKeyEnv;
1469
1425
  let apiKeyFilePath;
1426
+ let apiKeyStore;
1470
1427
  try {
1471
1428
  // rejectEmpty: an explicitly-given-but-empty variable name/path can
1472
1429
  // never name a real environment variable or file (#3062 second review,
1473
1430
  // finding 4; go/no-go review blocker 1C).
1474
1431
  apiKeyEnv = requireFlagValue(args, { rejectEmpty: true }, '--api-key-env');
1475
1432
  apiKeyFilePath = requireFlagValue(args, { rejectEmpty: true }, '--api-key-file');
1433
+ apiKeyStore = requireFlagValue(args, { rejectEmpty: true }, '--api-key-store');
1476
1434
  } catch (err) {
1477
1435
  if (err instanceof MissingFlagValueError) {
1478
1436
  console.error(`${err.message}.`);
@@ -1482,16 +1440,6 @@ async function handleProfileLogin(args) {
1482
1440
  }
1483
1441
  const useStdin = args.includes('--api-key-stdin');
1484
1442
 
1485
- const sourcesGiven = [
1486
- apiKeyEnv !== undefined ? '--api-key-env' : null,
1487
- useStdin ? '--api-key-stdin' : null,
1488
- apiKeyFilePath !== undefined ? '--api-key-file' : null,
1489
- ].filter(Boolean);
1490
- if (sourcesGiven.length > 1) {
1491
- console.error(`Only one credential source may be given; got ${sourcesGiven.join(', ')}.`);
1492
- process.exit(2);
1493
- }
1494
-
1495
1443
  // Skip --api-key-env's/--api-key-file's own value when hunting for the
1496
1444
  // positional profile name, so e.g. `login --api-key-env FOO` (missing
1497
1445
  // name) never misreads FOO as the name (mirrors this file's own
@@ -1501,46 +1449,24 @@ async function handleProfileLogin(args) {
1501
1449
  for (let i = 0; i < args.length; i += 1) {
1502
1450
  const token = args[i];
1503
1451
  if (token === '--') break;
1504
- if (token === '--api-key-env' || token === '--api-key-file') { i += 1; continue; }
1452
+ if (token === '--api-key-env' || token === '--api-key-file' || token === '--api-key-store') { i += 1; continue; }
1505
1453
  if (!token.startsWith('--')) positional.push(token);
1506
1454
  }
1507
1455
  const name = positional[0];
1508
1456
  if (!name) {
1509
- console.error('Usage: aiwg mcp profile login <name> [--api-key-env <ENV_VAR_NAME> | --api-key-stdin | --api-key-file <path>]');
1457
+ console.error('Usage: aiwg mcp profile login <name> [--api-key-interactive | --api-key-stdin | --api-key-file <path> | --api-key-store <name> | --api-key-env <ENV_VAR_NAME>]');
1510
1458
  process.exit(1);
1511
1459
  }
1512
1460
 
1513
- let apiKey;
1514
- if (useStdin) {
1515
- if (process.stdin.isTTY) {
1516
- console.error(
1517
- '--api-key-stdin requires piped input, not a terminal; run ' +
1518
- `\`aiwg mcp profile login ${name}\` with no credential flag for the interactive login instead.`,
1519
- );
1520
- process.exit(2);
1521
- }
1522
- apiKey = (await readStdinAll()).trim();
1523
- if (!apiKey) {
1524
- console.error('--api-key-stdin received no key (stdin was empty).');
1525
- process.exit(2);
1526
- }
1527
- } else if (apiKeyFilePath !== undefined) {
1528
- const { readApiKeyFromFile } = await loadCodexRuntimeAdapter();
1529
- try {
1530
- apiKey = await readApiKeyFromFile(apiKeyFilePath);
1531
- } catch (err) {
1532
- console.error(err instanceof Error ? err.message : String(err));
1533
- process.exit(2);
1534
- }
1535
- if (!apiKey) {
1536
- console.error(`--api-key-file "${apiKeyFilePath}" is empty.`);
1537
- process.exit(2);
1538
- }
1539
- }
1540
-
1541
1461
  try {
1462
+ await loadCodexRuntimeAdapter();
1463
+ const { resolveSensitiveInput } = await import(pathToFileURL(path.join(findOwnPackageRoot(), 'dist', 'src', 'auth', 'sensitive-input.js')).href);
1464
+ const apiKey = await resolveSensitiveInput({
1465
+ prefix: 'api-key', stdin: useStdin, file: apiKeyFilePath, store: apiKeyStore,
1466
+ env: apiKeyEnv, interactive: args.includes('--api-key-interactive'),
1467
+ });
1542
1468
  const { loginInProfile } = await loadCodexRuntimeAdapter();
1543
- const loginOptions = apiKey !== undefined ? { apiKey } : apiKeyEnv ? { apiKeyEnv } : {};
1469
+ const loginOptions = { apiKey };
1544
1470
  await loginInProfile(name, loginOptions);
1545
1471
  console.log(`\nLogged in to profile "${name}".`);
1546
1472
  } catch (err) {
@@ -187,11 +187,15 @@ export async function writeConfigAtomic(file, content, { userScope = false, proj
187
187
  if (rejectSymlinkParents) await assertNoSymlinkParents(file, symlinkRoot);
188
188
  const info = await assertConfigDestination(file, projectRoot);
189
189
  const mode = restoreMode ?? (userScope ? 0o600 : info ? info.mode & 0o7777 : newFileMode ?? 0o666);
190
- await mkdir(dirname(file), { recursive: true });
190
+ // Restrict newly created parents for private settings, ownership and output files.
191
+ // Recursive mkdir leaves every existing directory's permissions unchanged.
192
+ await mkdir(dirname(file), { recursive: true, mode: (mode & 0o077) === 0 ? 0o700 : undefined });
191
193
  const temporary = `${file}.${randomUUID()}.tmp`;
192
194
  let handle;
195
+ let created = false;
193
196
  try {
194
197
  handle = await open(temporary, 'wx', mode);
198
+ created = true;
195
199
  await handle.writeFile(content, 'utf-8');
196
200
  if (userScope || info || restoreMode !== undefined || newFileMode !== undefined) await handle.chmod(mode);
197
201
  await handle.sync();
@@ -204,7 +208,7 @@ export async function writeConfigAtomic(file, content, { userScope = false, proj
204
208
  try {
205
209
  if (handle) await handle.close();
206
210
  } finally {
207
- await unlink(temporary).catch(() => {});
211
+ if (created) await unlink(temporary).catch(() => {});
208
212
  }
209
213
  }
210
214
  }
@@ -17,8 +17,8 @@
17
17
 
18
18
  import { lstat, mkdtemp, readFile, readlink, realpath, rmdir } from 'fs/promises';
19
19
  import { basename, dirname, isAbsolute, posix, relative, resolve, sep, win32 } from 'path';
20
- import { assertConfigDestination, assertNoSymlinkParents, writeConfigAtomic } from './config-file.mjs';
21
- import { resolveCredentialPolicy, stricterCredentialPolicy, warnCredentialPolicyRelaxation, warnDroppedOverlayPolicy } from './credentials.mjs';
20
+ import { assertConfigDestination, assertConfigObject, assertNoSymlinkParents, writeConfigAtomic } from './config-file.mjs';
21
+ import { credentialPolicyViolations, resolveCredentialPolicy, stricterCredentialPolicy, warnCredentialPolicyRelaxation, warnDroppedOverlayPolicy } from './credentials.mjs';
22
22
 
23
23
  /**
24
24
  * `platform` lets tests validate Windows path semantics (`C:\org`, UNC
@@ -62,9 +62,15 @@ export function resolveConfigLayers(configDirOverride, env = process.env, { plat
62
62
  return layers.length > 0 ? layers : null;
63
63
  }
64
64
 
65
- async function readLayer(path) {
65
+ async function readLayer(path, collection) {
66
66
  try {
67
- return JSON.parse(await readFile(path, 'utf-8'));
67
+ const parsed = JSON.parse(await readFile(path, 'utf-8'));
68
+ try {
69
+ assertConfigObject(parsed, collection);
70
+ } catch {
71
+ throw new Error(`Invalid MCP configuration layer ${path}: expected an object root and an object ${collection} map`);
72
+ }
73
+ return parsed;
68
74
  } catch (error) {
69
75
  if (error?.code === 'ENOENT') return null;
70
76
  if (error instanceof SyntaxError) throw new Error(`Invalid JSON in MCP configuration layer ${path}`);
@@ -115,7 +121,7 @@ export async function loadLayered(layers, filename, collection, defaults) {
115
121
  let lowerPolicySource;
116
122
  const writeLayer = layers.length - 1;
117
123
  for (const [index, dir] of layers.entries()) {
118
- const parsed = await readLayer(resolve(dir, filename));
124
+ const parsed = await readLayer(resolve(dir, filename), collection);
119
125
  if (!parsed) continue;
120
126
  const { [collection]: entries, ...rest } = parsed;
121
127
  if (index === writeLayer) {
@@ -298,16 +304,26 @@ export async function assertLayerWriteDestination(layers, filename) {
298
304
  /** Persist atomically, then refresh ownership only after a successful write. */
299
305
  export async function saveLayerData(file, data, collection, layers, layering, explicitPolicy = false) {
300
306
  if (layers) await assertLayerWriteDestination(layers, basename(file));
307
+ // Validate the current bytes even if the caller loaded and cached an older
308
+ // version, or called save(data) on a fresh standalone registry.
309
+ await readLayer(file, collection);
301
310
  const persisted = layering ? writeLayerData(data, collection, layering, explicitPolicy) : data;
302
311
  // The active identity layer file is always 0600: it is the personal write
303
312
  // layer of an active AIWG_CONFIG_LAYERS configuration and may carry secrets.
304
313
  // Without layering, `file` is whatever single directory the caller
305
314
  // configured (which may be a shared, group-readable organisation checkout
306
315
  // maintained directly, not a private identity directory): an existing file
307
- // keeps its mode, while a newly created file starts private at 0600.
316
+ // keeps its mode unless the persisted servers carry literal credentials,
317
+ // while a newly created file starts private at 0600.
318
+ const info = layers ? null : await assertConfigDestination(file);
319
+ const tighten = info && (info.mode & 0o077) !== 0 && collection === 'servers'
320
+ && Object.values(persisted[collection] || {}).some(server => credentialPolicyViolations(server, 'references').length > 0);
308
321
  await writeConfigAtomic(file, JSON.stringify(persisted, null, 2) + '\n', layers
309
322
  ? { userScope: true, rejectSymlinkParents: true }
310
- : { newFileMode: 0o600, rejectSymlinkParents: true });
323
+ : { newFileMode: 0o600, rejectSymlinkParents: true, ...(tighten ? { mode: 0o600 } : {}) });
324
+ if (tighten) {
325
+ console.warn(`Warning: tightened MCP registry ${file} from ${(info.mode & 0o7777).toString(8).padStart(4, '0')} to 0600 because it contains literal credentials.`);
326
+ }
311
327
  if (layering) {
312
328
  layering.own = new Set(Object.keys(persisted[collection] || {}));
313
329
  layering.ownFields = new Set(Object.keys(persisted).filter(field => field !== collection));
@@ -3,6 +3,7 @@ import { constants } from 'node:fs';
3
3
  import { access, lstat, readFile } from 'node:fs/promises';
4
4
  import { dirname, relative, resolve } from 'node:path';
5
5
  import { spawnSync } from 'node:child_process';
6
+ import { helperEnvironment } from '../process/cli-command.mjs';
6
7
  import { replaceServer } from './toml-editor.mjs';
7
8
  import { tomlString as string } from './toml-string.mjs';
8
9
 
@@ -144,7 +145,7 @@ export async function unmanageGrokBuildMcp(configPath, names, options = {}) {
144
145
  }
145
146
 
146
147
  function runJson(binary, args, cwd, env) {
147
- const run = spawnSync(binary, args, { cwd, env, encoding: 'utf8', timeout: 30_000, killSignal: 'SIGKILL' });
148
+ const run = spawnSync(binary, args, { cwd, env: helperEnvironment(env), encoding: 'utf8', timeout: 30_000, killSignal: 'SIGKILL' });
148
149
  if (run.error || run.status !== 0) return { ok: false, detail: run.error?.message || run.stderr || `exit ${run.status}` };
149
150
  try { return { ok: true, value: JSON.parse(run.stdout || '{}') }; }
150
151
  catch { return { ok: false, detail: 'Grok returned malformed JSON' }; }
@@ -4,6 +4,7 @@ import { spawnSync } from 'node:child_process';
4
4
  import { dirname, join } from 'node:path';
5
5
  import os from 'node:os';
6
6
  import { resolveMuseXdgSkillsDir } from '../providers/muse-paths.js';
7
+ import { helperEnvironment } from '../process/cli-command.mjs';
7
8
 
8
9
  // Mirrors tools/agents/providers/muse-hooks.mjs. src/ cannot import tools/:
9
10
  // tools/ is not part of the compiled dist/ tree.
@@ -47,7 +48,7 @@ function deepEqual(a, b) {
47
48
 
48
49
  function inspectMuseCli(env) {
49
50
  const result = spawnSync('muse', ['--version'], {
50
- env,
51
+ env: helperEnvironment(env),
51
52
  encoding: 'utf8',
52
53
  timeout: MUSE_VERSION_TIMEOUT_MS,
53
54
  killSignal: 'SIGKILL',
@@ -9,10 +9,10 @@
9
9
  *
10
10
  * @implements #889
11
11
  */
12
- import { resolveCredentialPolicy } from './credentials.mjs';
13
12
  import { isLowerLayerEntry, loadLayered, resolveProfileExtends, resolveConfigLayers, saveLayerData } from './config-layers.mjs';
14
- import { readFile, writeFile } from 'fs/promises';
15
- import { resolve } from 'path';
13
+ import { writeConfigAtomic } from './config-file.mjs';
14
+ import { readFile } from 'fs/promises';
15
+ import { parse, resolve } from 'path';
16
16
  import { resolveConfigDir } from '../config/user-config.js';
17
17
  // ─────────────────────────────────────────────
18
18
  // Constants
@@ -81,38 +81,21 @@ export class McpProfileRegistry {
81
81
  async load() {
82
82
  if (this.cache)
83
83
  return this.cache;
84
- if (this.layers) {
85
- const layered = await loadLayered(this.layers, PROFILES_FILENAME, 'profiles', DEFAULT_DATA);
86
- this.cache = layered.data;
87
- this.layering = layered.layering;
88
- return this.cache;
89
- }
90
- const filePath = this.getPath();
91
- try {
92
- const content = await readFile(filePath, 'utf-8');
93
- const parsed = JSON.parse(content);
94
- this.cache = {
95
- ...DEFAULT_DATA,
96
- ...parsed,
97
- profiles: parsed.profiles || {},
98
- };
99
- }
100
- catch {
101
- this.cache = { ...DEFAULT_DATA, profiles: {} };
102
- }
103
- try {
104
- resolveCredentialPolicy({ registryPolicy: this.cache.credentialPolicy, env: {} });
105
- }
106
- catch (error) {
107
- this.cache = null;
108
- throw new Error(`Invalid MCP credential policy in ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
109
- }
84
+ const loaded = await loadLayered(this.layers ?? [this.configDir], PROFILES_FILENAME, 'profiles', DEFAULT_DATA);
85
+ this.cache = loaded.data;
86
+ this.layering = this.layers ? loaded.layering : null;
110
87
  return this.cache;
111
88
  }
112
89
  async save(data = this.cache) {
113
90
  if (!data)
114
91
  return;
115
- await saveLayerData(this.getPath(), data, 'profiles', this.layers, this.layering);
92
+ // Load ownership before a fresh save so inherited fields stay in their layer.
93
+ let layering = this.layering;
94
+ if (this.layers && !layering) {
95
+ await this.load();
96
+ layering = this.layering;
97
+ }
98
+ await saveLayerData(this.getPath(), data, 'profiles', this.layers, layering);
116
99
  this.cache = data;
117
100
  if (this.layers) {
118
101
  this.clearCache();
@@ -311,7 +294,11 @@ export class McpProfileRegistry {
311
294
  const toExport = profileName
312
295
  ? { [profileName]: data.profiles[profileName] }
313
296
  : data.profiles;
314
- await writeFile(filePath, JSON.stringify({ profiles: toExport }, null, 2) + '\n', 'utf-8');
297
+ await writeConfigAtomic(filePath, JSON.stringify({ profiles: toExport }, null, 2) + '\n', {
298
+ userScope: true,
299
+ rejectSymlinkParents: true,
300
+ symlinkRoot: parse(resolve(filePath)).root,
301
+ });
315
302
  }
316
303
  async initPresets() {
317
304
  const data = structuredClone(await this.load());
@@ -10,10 +10,10 @@
10
10
  * @implements #889
11
11
  */
12
12
 
13
- import { resolveCredentialPolicy } from './credentials.mjs';
14
13
  import { isLowerLayerEntry, loadLayered, resolveProfileExtends, resolveConfigLayers, saveLayerData } from './config-layers.mjs';
15
- import { readFile, writeFile } from 'fs/promises';
16
- import { resolve } from 'path';
14
+ import { writeConfigAtomic } from './config-file.mjs';
15
+ import { readFile } from 'fs/promises';
16
+ import { parse, resolve } from 'path';
17
17
  import { homedir } from 'os';
18
18
  import { existsSync } from 'fs';
19
19
 
@@ -112,39 +112,21 @@ export class McpProfileRegistry {
112
112
  async load() {
113
113
  if (this.#cache) return this.#cache;
114
114
 
115
- if (this.#layers) {
116
- const layered = await loadLayered(this.#layers, PROFILES_FILENAME, 'profiles', DEFAULT_DATA);
117
- this.#cache = layered.data;
118
- this.#layering = layered.layering;
119
- return this.#cache;
120
- }
121
-
122
- const filePath = this.getPath();
123
- try {
124
- const content = await readFile(filePath, 'utf-8');
125
- const parsed = JSON.parse(content);
126
- this.#cache = {
127
- ...DEFAULT_DATA,
128
- ...parsed,
129
- profiles: parsed.profiles || {},
130
- };
131
- } catch {
132
- this.#cache = { ...DEFAULT_DATA, profiles: {} };
133
- }
134
-
135
- try {
136
- resolveCredentialPolicy({ registryPolicy: this.#cache.credentialPolicy, env: {} });
137
- } catch (error) {
138
- this.#cache = null;
139
- throw new Error(`Invalid MCP credential policy in ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
140
- }
141
-
115
+ const loaded = await loadLayered(this.#layers ?? [this.#configDir], PROFILES_FILENAME, 'profiles', DEFAULT_DATA);
116
+ this.#cache = loaded.data;
117
+ this.#layering = this.#layers ? loaded.layering : null;
142
118
  return this.#cache;
143
119
  }
144
120
 
145
121
  async save(data = this.#cache) {
146
122
  if (!data) return;
147
- await saveLayerData(this.getPath(), data, 'profiles', this.#layers, this.#layering);
123
+ // Load ownership before a fresh save so inherited fields stay in their layer.
124
+ let layering = this.#layering;
125
+ if (this.#layers && !layering) {
126
+ await this.load();
127
+ layering = this.#layering;
128
+ }
129
+ await saveLayerData(this.getPath(), data, 'profiles', this.#layers, layering);
148
130
  this.#cache = data;
149
131
  if (this.#layers) {
150
132
  this.clearCache();
@@ -402,7 +384,11 @@ export class McpProfileRegistry {
402
384
  throw new Error(`Profile "${profileName}" not found.`);
403
385
  }
404
386
 
405
- await writeFile(filePath, JSON.stringify({ profiles: toExport }, null, 2) + '\n', 'utf-8');
387
+ await writeConfigAtomic(filePath, JSON.stringify({ profiles: toExport }, null, 2) + '\n', {
388
+ userScope: true,
389
+ rejectSymlinkParents: true,
390
+ symlinkRoot: parse(resolve(filePath)).root,
391
+ });
406
392
  }
407
393
 
408
394
  /** Install preset profiles (does not overwrite existing) */
@@ -120,32 +120,9 @@ export class McpServerRegistry {
120
120
  async load() {
121
121
  if (this.cache)
122
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
- }
129
- const filePath = this.getPath();
130
- try {
131
- const content = await readFile(filePath, 'utf-8');
132
- const parsed = JSON.parse(content);
133
- this.cache = {
134
- ...DEFAULT_REGISTRY,
135
- ...parsed,
136
- servers: parsed.servers || {},
137
- };
138
- }
139
- catch {
140
- this.cache = { ...DEFAULT_REGISTRY, servers: {} };
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
- }
123
+ const loaded = await loadLayered(this.layers ?? [this.configDir], REGISTRY_FILENAME, 'servers', DEFAULT_REGISTRY);
124
+ this.cache = loaded.data;
125
+ this.layering = this.layers ? loaded.layering : null;
149
126
  return this.cache;
150
127
  }
151
128
  /** Save the registry to disk */
@@ -536,6 +513,9 @@ async function injectJson(registry, servers, configPath, provider, dryRun, resul
536
513
  return result;
537
514
  }
538
515
  async function injectToml(registry, servers, configPath, provider, dryRun, result, userScope, projectRoot, toolPlan = null) {
516
+ // A deliberately aliased Codex home is trusted; descendants below it are not.
517
+ const destinationRoot = projectRoot ?? (process.env.CODEX_HOME || resolve(process.env.HOME || process.env.USERPROFILE || homedir(), '.codex'));
518
+ await assertConfigDestination(configPath, destinationRoot);
539
519
  let existing = '';
540
520
  try {
541
521
  existing = await readFile(configPath, 'utf-8');
@@ -553,7 +533,7 @@ async function injectToml(registry, servers, configPath, provider, dryRun, resul
553
533
  result.serversInjected.push(server.name);
554
534
  }
555
535
  if (!dryRun) {
556
- await writeConfigAtomic(configPath, existing, { userScope, projectRoot });
536
+ await writeConfigAtomic(configPath, existing, { userScope, projectRoot: destinationRoot });
557
537
  for (const server of servers) {
558
538
  await registry.recordInjection(server.name, provider);
559
539
  }
@@ -115,33 +115,9 @@ export class McpServerRegistry {
115
115
  async load() {
116
116
  if (this.#cache) return this.#cache;
117
117
 
118
- if (this.#layers) {
119
- const layered = await loadLayered(this.#layers, REGISTRY_FILENAME, 'servers', DEFAULT_REGISTRY);
120
- this.#cache = layered.data;
121
- this.#layering = layered.layering;
122
- return this.#cache;
123
- }
124
-
125
- const filePath = this.getPath();
126
- try {
127
- const content = await readFile(filePath, 'utf-8');
128
- const parsed = JSON.parse(content);
129
- this.#cache = {
130
- ...DEFAULT_REGISTRY,
131
- ...parsed,
132
- servers: parsed.servers || {},
133
- };
134
- } catch {
135
- this.#cache = { ...DEFAULT_REGISTRY, servers: {} };
136
- }
137
-
138
- try {
139
- resolveCredentialPolicy({ registryPolicy: this.#cache.credentialPolicy, env: {} });
140
- } catch (error) {
141
- this.#cache = null;
142
- throw new Error(`Invalid MCP credential policy in ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
143
- }
144
-
118
+ const loaded = await loadLayered(this.#layers ?? [this.#configDir], REGISTRY_FILENAME, 'servers', DEFAULT_REGISTRY);
119
+ this.#cache = loaded.data;
120
+ this.#layering = this.#layers ? loaded.layering : null;
145
121
  return this.#cache;
146
122
  }
147
123
 
@@ -488,6 +464,9 @@ async function injectJson(registry, servers, configPath, provider, dryRun, resul
488
464
  }
489
465
 
490
466
  async function injectToml(registry, servers, configPath, provider, dryRun, result, userScope, projectRoot, toolPlan = null) {
467
+ // A deliberately aliased Codex home is trusted; descendants below it are not.
468
+ const destinationRoot = projectRoot ?? (process.env.CODEX_HOME || resolve(process.env.HOME || process.env.USERPROFILE || homedir(), '.codex'));
469
+ await assertConfigDestination(configPath, destinationRoot);
491
470
  let existing = '';
492
471
  try {
493
472
  existing = await readFile(configPath, 'utf-8');
@@ -504,7 +483,7 @@ async function injectToml(registry, servers, configPath, provider, dryRun, resul
504
483
  }
505
484
 
506
485
  if (!dryRun) {
507
- await writeConfigAtomic(configPath, existing, { userScope, projectRoot });
486
+ await writeConfigAtomic(configPath, existing, { userScope, projectRoot: destinationRoot });
508
487
 
509
488
  for (const server of servers) {
510
489
  await registry.recordInjection(server.name, provider);
@@ -34,6 +34,27 @@ const FRAMEWORKS = {
34
34
  'media-marketing-kit': path.join(AIWG_ROOT, 'agentic/code/frameworks/media-marketing-kit')
35
35
  };
36
36
 
37
+ /** Keep the published object contracts stable across native Zod converters. */
38
+ function retainToolJsonSchema(schema, seen = new Set()) {
39
+ if (!(schema instanceof z.ZodType) || seen.has(schema)) return;
40
+ seen.add(schema);
41
+ const definition = schema._zod.def;
42
+ if (schema instanceof z.ZodObject) {
43
+ const catchall = definition.catchall;
44
+ if ((!catchall && Object.keys(schema.shape).length > 0) || catchall instanceof z.ZodNever || catchall instanceof z.ZodUnknown) {
45
+ z.globalRegistry.add(schema, { ...z.globalRegistry.get(schema), additionalProperties: catchall instanceof z.ZodUnknown });
46
+ }
47
+ }
48
+ if (schema instanceof z.ZodRecord && definition.keyType instanceof z.ZodString && !definition.keyType._zod.def.checks?.length) {
49
+ z.globalRegistry.add(schema, { ...z.globalRegistry.get(schema), propertyNames: undefined });
50
+ }
51
+ for (const value of Object.values(definition)) {
52
+ if (value instanceof z.ZodType) retainToolJsonSchema(value, seen);
53
+ else if (Array.isArray(value)) for (const child of value) retainToolJsonSchema(child, seen);
54
+ else if (value && typeof value === 'object') for (const child of Object.values(value)) retainToolJsonSchema(child, seen);
55
+ }
56
+ }
57
+
37
58
  /**
38
59
  * Create and configure the AIWG MCP Server
39
60
  */
@@ -43,6 +64,18 @@ export async function createServer() {
43
64
  version: '1.0.0'
44
65
  });
45
66
 
67
+ const registerTool = server.registerTool.bind(server);
68
+ server.registerTool = (name, config, handler) => {
69
+ const schemas = {};
70
+ for (const key of ['inputSchema', 'outputSchema']) {
71
+ if (!config[key]) continue;
72
+ const schema = config[key] instanceof z.ZodType ? config[key] : z.object(config[key]);
73
+ retainToolJsonSchema(schema);
74
+ schemas[key] = schema;
75
+ }
76
+ return registerTool(name, { ...config, ...schemas }, handler);
77
+ };
78
+
46
79
  // ============================================
47
80
  // TOOLS
48
81
  // ============================================
@@ -248,7 +281,7 @@ export async function createServer() {
248
281
  description: 'Render AIWG template with provided variables',
249
282
  inputSchema: {
250
283
  template: z.string().describe('Template path (e.g., "sdlc/intake/project-intake-template.md")'),
251
- variables: z.record(z.string()).optional().describe('Template variable substitutions')
284
+ variables: z.record(z.string(), z.string()).optional().describe('Template variable substitutions')
252
285
  }
253
286
  }, async ({ template, variables = {} }) => {
254
287
  try {