@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
@@ -16,6 +16,8 @@ import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mc
16
16
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
17
17
  import { z } from 'zod';
18
18
  import fs from 'fs/promises';
19
+ import { constants } from 'node:fs';
20
+ import { randomUUID } from 'node:crypto';
19
21
  import path from 'path';
20
22
  import { fileURLToPath } from 'url';
21
23
  import { registerWriterProfileResources } from './tools/writer-profiles.mjs';
@@ -33,10 +35,321 @@ const FRAMEWORKS = {
33
35
  'media-marketing-kit': path.join(AIWG_ROOT, 'agentic/code/frameworks/media-marketing-kit')
34
36
  };
35
37
 
38
+ function assertArtifactContained(root, target) {
39
+ const relative = path.relative(root, target);
40
+ if (!relative || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
41
+ throw new Error('Artifact path must stay beneath the artifact root');
42
+ }
43
+ }
44
+
45
+ function rootedArtifactAccess() {
46
+ return process.platform === 'linux' && constants.O_NOFOLLOW && constants.O_DIRECTORY;
47
+ }
48
+
49
+ /** A root pinned as missing must be created by us, never adopted later. */
50
+ async function createArtifactRoot(store) {
51
+ let parent;
52
+ let directory;
53
+ let verified = false;
54
+ try {
55
+ let entry = store.configured;
56
+ if (rootedArtifactAccess()) {
57
+ parent = await fs.open(store.projectRoot, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
58
+ if (await fs.realpath(`/proc/self/fd/${parent.fd}`) !== store.projectRoot) {
59
+ throw new Error('Project root changed during artifact creation');
60
+ }
61
+ entry = `/proc/self/fd/${parent.fd}/.aiwg`;
62
+ }
63
+ // EEXIST is a refusal, including a symlink or directory installed since pinning.
64
+ await fs.mkdir(entry);
65
+ const created = await fs.lstat(entry);
66
+ if (created.isSymbolicLink() || !created.isDirectory()) throw new Error('Artifact root changed during creation');
67
+ // Windows cannot open directory handles through fs.open; its fallback
68
+ // uses static identity checks, as do the subsequent artifact operations.
69
+ if (process.platform !== 'win32') {
70
+ directory = await fs.open(entry, constants.O_RDONLY | (constants.O_DIRECTORY || 0) | (constants.O_NOFOLLOW || 0));
71
+ }
72
+ const opened = directory ? await directory.stat() : await fs.stat(entry);
73
+ const current = await fs.lstat(entry);
74
+ if (current.isSymbolicLink() || !opened.isDirectory()
75
+ || opened.dev !== created.dev || opened.ino !== created.ino
76
+ || opened.dev !== current.dev || opened.ino !== current.ino
77
+ || await fs.realpath(store.configured) !== store.root) {
78
+ throw new Error('Artifact root changed during creation');
79
+ }
80
+ store.identity = opened;
81
+ verified = true;
82
+ } finally {
83
+ const closed = await Promise.allSettled([...(directory ? [directory.close()] : []), ...(parent ? [parent.close()] : [])]);
84
+ const failure = closed.find(result => result.status === 'rejected');
85
+ if (verified && failure) throw failure.reason;
86
+ }
87
+ }
88
+
89
+ /** Pin an existing store once; only a missing local default may be bootstrapped. */
90
+ async function pinArtifactRoot(configured, projectRoot) {
91
+ try {
92
+ const root = await fs.realpath(configured);
93
+ const identity = await fs.stat(root);
94
+ const store = { configured, projectRoot, root, identity };
95
+ await verifyArtifactRoot(store);
96
+ return store;
97
+ } catch (error) {
98
+ if (error.code === 'ENOENT' && configured === path.join(projectRoot, '.aiwg')) {
99
+ const existing = await fs.lstat(configured).catch(missing => {
100
+ if (missing.code !== 'ENOENT') throw missing;
101
+ });
102
+ if (!existing) return { configured, projectRoot, root: configured, identity: null };
103
+ // A symlink (or other unexpected node) present where the default root
104
+ // should be created must not become the containment root later.
105
+ return {
106
+ configured,
107
+ projectRoot,
108
+ root: configured,
109
+ error: new Error('Default artifact root is not a creatable directory'),
110
+ };
111
+ }
112
+ // Keep discovery available, but never adopt a store that was unavailable
113
+ // at startup (including a dangling alias) on a later request.
114
+ return { configured, projectRoot, root: configured, error };
115
+ }
116
+ }
117
+
118
+ async function verifyArtifactRoot(store) {
119
+ if (store.error) throw store.error;
120
+ const canonical = await fs.realpath(store.configured);
121
+ const current = await fs.stat(store.configured);
122
+ if (canonical !== store.root || !current.isDirectory()
123
+ || current.dev !== store.identity.dev || current.ino !== store.identity.ino) {
124
+ throw new Error('Artifact root changed since it was pinned');
125
+ }
126
+ }
127
+
128
+ async function pinnedArtifactRoot(store, write) {
129
+ if (store.error) throw store.error;
130
+ if (!store.identity) {
131
+ if (!write) throw new Error('Default artifact root has not been created');
132
+ // Serialize first-write initialization so concurrent requests share one
133
+ // identity. Creation still rejects symlinks and uses a project descriptor.
134
+ store.initialization ||= createArtifactRoot(store);
135
+ await store.initialization;
136
+ }
137
+ await verifyArtifactRoot(store);
138
+ return store.root;
139
+ }
140
+
141
+ /** Resolve existing aliases before creating anything; dangling symlinks fail closed. */
142
+ async function resolveArtifactPath(store, artifactPath, { write = false } = {}) {
143
+ if (!artifactPath || path.isAbsolute(artifactPath) || path.win32.isAbsolute(artifactPath)
144
+ || /^[A-Za-z]:/.test(artifactPath)
145
+ || (process.platform === 'win32' && artifactPath.split(/[\\/]/).some(part => part !== '.'
146
+ && (/[<>:"|?*\x00-\x1f]/.test(part) || /[. ]$/.test(part)
147
+ || /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part))))
148
+ || artifactPath.split(/[\\/]/).includes('..')) {
149
+ throw new Error('Artifact path must be relative and contain no .. segments');
150
+ }
151
+ if (write && artifactPath.split(/[\\/]/).some(part => /^\.aiwg-location[. ]*(?::.*)?$/i.test(part))) {
152
+ throw new Error('Refusing to write an artifact location pointer');
153
+ }
154
+ const root = await pinnedArtifactRoot(store, write);
155
+ const target = path.resolve(root, artifactPath);
156
+ assertArtifactContained(root, target);
157
+ if (!rootedArtifactAccess()) {
158
+ let componentPath = root;
159
+ for (const component of path.relative(root, target).split(path.sep)) {
160
+ componentPath = path.join(componentPath, component);
161
+ const info = await fs.lstat(componentPath).catch(error => {
162
+ if (error.code !== 'ENOENT') throw error;
163
+ });
164
+ if (info?.isSymbolicLink()) throw new Error('Refusing artifact symlink component');
165
+ }
166
+ }
167
+ let ancestor = path.dirname(target);
168
+ const missing = [];
169
+ while (true) {
170
+ try {
171
+ // lstat distinguishes a missing directory from a dangling symlink.
172
+ await fs.lstat(ancestor);
173
+ break;
174
+ } catch (error) {
175
+ if (error.code !== 'ENOENT') throw error;
176
+ missing.unshift(path.basename(ancestor));
177
+ ancestor = path.dirname(ancestor);
178
+ }
179
+ }
180
+ const realAncestor = await fs.realpath(ancestor);
181
+ if (realAncestor !== root) assertArtifactContained(root, realAncestor);
182
+ const fullPath = path.resolve(realAncestor, ...missing, path.basename(target));
183
+ assertArtifactContained(root, fullPath);
184
+ let info;
185
+ try {
186
+ info = await fs.lstat(fullPath);
187
+ } catch (error) {
188
+ if (error.code !== 'ENOENT') throw error;
189
+ }
190
+ if (info) {
191
+ if (write && info.isSymbolicLink()) throw new Error('Refusing to write through an artifact symlink');
192
+ const realTarget = await fs.realpath(fullPath);
193
+ assertArtifactContained(root, realTarget);
194
+ if (!write) return { root, fullPath: realTarget };
195
+ }
196
+ return { root, fullPath };
197
+ }
198
+
199
+ /**
200
+ * Linux procfs descriptor paths anchor each lookup to an opened directory.
201
+ * Other platforms use static containment checks. Writes use a fresh inode;
202
+ * failed verification closes handles without unsafe pathname cleanup.
203
+ * Concurrent same-user relocation after verification is not a sandbox boundary.
204
+ */
205
+ async function openArtifact(store, artifactPath, { write = false } = {}) {
206
+ const { root, fullPath } = await resolveArtifactPath(store, artifactPath, { write });
207
+ if (!rootedArtifactAccess()) return openStaticArtifact(store, root, fullPath, { write });
208
+ const directories = [];
209
+ let file;
210
+ let transferred = false;
211
+ const descriptorPath = handle => `/proc/self/fd/${handle.fd}`;
212
+ async function verify(handle, { rootHandle = false, directory = false, entry } = {}) {
213
+ const actual = await fs.realpath(descriptorPath(handle));
214
+ if (rootHandle) {
215
+ if (actual !== root) throw new Error('Artifact root changed during access');
216
+ } else {
217
+ assertArtifactContained(root, actual);
218
+ }
219
+ const [opened, current] = await Promise.all([handle.stat(), fs.lstat(entry || actual)]);
220
+ if (rootHandle) {
221
+ await verifyArtifactRoot(store);
222
+ if (opened.dev !== store.identity.dev || opened.ino !== store.identity.ino) {
223
+ throw new Error('Artifact root changed since it was pinned');
224
+ }
225
+ }
226
+ if (current.isSymbolicLink() || opened.dev !== current.dev || opened.ino !== current.ino
227
+ || (directory ? !opened.isDirectory() : !opened.isFile())) {
228
+ throw new Error('Artifact changed during access or is not a regular file/directory');
229
+ }
230
+ if (!directory && (opened.nlink > 1 || current.nlink > 1)) {
231
+ throw new Error('Refusing multiply-linked artifact file');
232
+ }
233
+ return actual;
234
+ }
235
+ async function close() {
236
+ const closed = await Promise.allSettled([...(file ? [file.close()] : []), ...directories.map(handle => handle.close())]);
237
+ const failure = closed.find(result => result.status === 'rejected');
238
+ if (failure) throw failure.reason;
239
+ }
240
+ try {
241
+ let parent = await fs.open(root, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
242
+ directories.push(parent);
243
+ await verify(parent, { rootHandle: true, directory: true });
244
+ const components = path.relative(root, fullPath).split(path.sep);
245
+ const name = components.pop();
246
+ for (const component of components) {
247
+ const entry = path.join(descriptorPath(parent), component);
248
+ let child;
249
+ try {
250
+ child = await fs.open(entry, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
251
+ } catch (error) {
252
+ if (!write || error.code !== 'ENOENT') throw error;
253
+ await verify(parent, { rootHandle: directories.length === 1, directory: true });
254
+ try {
255
+ await fs.mkdir(entry);
256
+ } catch (mkdirError) {
257
+ if (mkdirError.code !== 'EEXIST') throw mkdirError;
258
+ }
259
+ child = await fs.open(entry, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
260
+ }
261
+ directories.push(child);
262
+ await verify(child, { directory: true });
263
+ parent = child;
264
+ }
265
+ const parentOptions = { rootHandle: directories.length === 1, directory: true };
266
+ await verify(parent, parentOptions);
267
+ const destination = path.join(descriptorPath(parent), name);
268
+ async function verifyDestination() {
269
+ const info = await fs.lstat(destination).catch(error => {
270
+ if (error.code !== 'ENOENT') throw error;
271
+ });
272
+ if (info && (info.isSymbolicLink() || !info.isFile())) {
273
+ throw new Error('Refusing to replace an artifact symlink or nonregular file');
274
+ }
275
+ return info;
276
+ }
277
+ const targetInfo = write ? await verifyDestination() : undefined;
278
+ const finalMode = targetInfo ? targetInfo.mode & 0o7777 : 0o666 & ~process.umask();
279
+ const entry = write ? path.join(descriptorPath(parent), `.aiwg-write-${randomUUID()}.tmp`) : destination;
280
+ file = await fs.open(entry,
281
+ (write ? constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL : constants.O_RDONLY)
282
+ | constants.O_NOFOLLOW | constants.O_NONBLOCK, 0o600);
283
+ await verify(file, { entry });
284
+ transferred = true;
285
+ return {
286
+ handle: file,
287
+ fullPath,
288
+ close,
289
+ async commit() {
290
+ await verify(directories[0], { rootHandle: true, directory: true });
291
+ await verify(file, { entry });
292
+ await verify(parent, parentOptions);
293
+ await verifyDestination();
294
+ await file.chmod(finalMode);
295
+ await fs.rename(entry, destination);
296
+ },
297
+ };
298
+ } finally {
299
+ if (!transferred) await Promise.allSettled([close()]);
300
+ }
301
+ }
302
+
303
+ /** Static checks for platforms without Linux descriptor paths. */
304
+ async function openStaticArtifact(store, root, fullPath, { write }) {
305
+ // Inspect the lexical path as well as the resolved path: contained aliases
306
+ // must not hide symlink components on this fallback.
307
+ async function check(target, { allowMissing = false } = {}) {
308
+ const relative = path.relative(root, target);
309
+ assertArtifactContained(root, target);
310
+ let current = root;
311
+ for (const component of relative.split(path.sep)) {
312
+ current = path.join(current, component);
313
+ const info = await fs.lstat(current).catch(error => {
314
+ if (!allowMissing || error.code !== 'ENOENT') throw error;
315
+ });
316
+ if (!info) continue;
317
+ if (info.isSymbolicLink()) throw new Error('Refusing artifact symlink component');
318
+ assertArtifactContained(root, await fs.realpath(current));
319
+ }
320
+ }
321
+ await check(fullPath, { allowMissing: write });
322
+ if (write) await fs.mkdir(path.dirname(fullPath), { recursive: true });
323
+ const target = await fs.lstat(fullPath).catch(error => {
324
+ if (!write || error.code !== 'ENOENT') throw error;
325
+ });
326
+ if (target && (!target.isFile() || target.isSymbolicLink())) throw new Error('Artifact must be a regular file');
327
+ if (!write && target.nlink > 1) throw new Error('Refusing multiply-linked artifact file');
328
+ const entry = write ? path.join(path.dirname(fullPath), `.aiwg-write-${randomUUID()}.tmp`) : fullPath;
329
+ const finalMode = target ? target.mode & 0o7777 : 0o666 & ~process.umask();
330
+ const handle = await fs.open(entry, write ? constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL : constants.O_RDONLY, 0o600);
331
+ try {
332
+ const opened = await handle.stat();
333
+ if (!opened.isFile() || opened.nlink > 1) throw new Error('Refusing nonregular or multiply-linked artifact file');
334
+ return {
335
+ handle, fullPath, close: () => handle.close(),
336
+ async commit() {
337
+ await verifyArtifactRoot(store);
338
+ await check(fullPath, { allowMissing: true });
339
+ await handle.chmod(finalMode);
340
+ await fs.rename(entry, fullPath);
341
+ },
342
+ };
343
+ } catch (error) {
344
+ await handle.close().catch(() => {});
345
+ throw error;
346
+ }
347
+ }
348
+
36
349
  /**
37
350
  * Create and configure the AIWG MCP Server
38
351
  */
39
- export function createServer() {
352
+ export async function createServer() {
40
353
  const server = new McpServer({
41
354
  name: 'aiwg',
42
355
  version: '1.0.0'
@@ -70,11 +383,72 @@ export function createServer() {
70
383
  // Opt-in subsystem toolsets (#1322-#1332)
71
384
  //
72
385
  // Enabled via AIWG_MCP_TOOLSETS env or `aiwg mcp serve --toolsets=`.
73
- // Known: flows, missions, memory, kb, research, activity-log, index, ralph, mc, ops, sandbox, all
386
+ // Known: flows, missions, memory, kb, research, activity-log, index, ralph, mc, ops, sandbox, decision, all
387
+ // `workers` (#2892 direct worker run/status/cancel) needs an EXPLICIT name;
388
+ // `all` deliberately does not reach it (src/mcp/tools/subsystems.mjs).
74
389
  // Default: none (core only — discovery + command-run)
75
390
  // ============================================
76
391
  const requested = parseToolsets(process.env.AIWG_MCP_TOOLSETS || '');
77
- registerOptInToolsets(server, requested);
392
+ // #2892 B1: the `workers` toolset's served project root is resolved ONCE,
393
+ // here, at registration time — never lazily from `process.cwd()` inside
394
+ // the tool handler on first request. A project root that cannot be found
395
+ // (not running inside an AIWG project) means `workers` is simply not
396
+ // registered; `registerWorkerInvokeToolset` refuses rather than falling
397
+ // back to an implicit cwd.
398
+ let projectRoot;
399
+ try {
400
+ projectRoot = await findProjectRoot();
401
+ } catch {
402
+ projectRoot = undefined;
403
+ }
404
+ await registerOptInToolsets(server, requested, { projectRoot });
405
+
406
+ // Resolve operator configuration once per trusted project, then pin it.
407
+ // Artifact tools default to the server-start project (the launch directory),
408
+ // not an ancestor discovered by walking up from it: a fresh launch directory
409
+ // inside another marked tree (for example a stray `.aiwg` in the temp dir)
410
+ // must pin its own missing default at startup, never adopt the ancestor's
411
+ // store. Callers select other projects explicitly via `project_dir`.
412
+ const artifactEnv = { ...process.env };
413
+ const trustedProjectRoot = await fs.realpath(process.cwd());
414
+ const trustedArtifactRoot = await resolveProjectAiwgDir(trustedProjectRoot, { env: artifactEnv });
415
+ const startupStore = await pinArtifactRoot(trustedArtifactRoot, trustedProjectRoot);
416
+ const trustedStore = startupStore.root;
417
+ const projects = new Map();
418
+ async function trustProject(requested, store) {
419
+ const canonical = await fs.realpath(requested);
420
+ const identity = await fs.stat(requested);
421
+ if (!identity.isDirectory()) throw new Error('project_dir must be a real directory');
422
+ if (canonical !== trustedProjectRoot) {
423
+ const inStore = path.relative(trustedStore, canonical);
424
+ if (canonical.split(path.sep).includes('.aiwg') || !inStore
425
+ || (inStore !== '..' && !inStore.startsWith(`..${path.sep}`) && !path.isAbsolute(inStore))) {
426
+ throw new Error('project_dir must not select a project inside an artifact store');
427
+ }
428
+ }
429
+ const configured = store ? undefined : await resolveProjectAiwgDir(canonical, { env: { ...process.env } });
430
+ return { canonical, identity, store: store || await pinArtifactRoot(configured, canonical) };
431
+ }
432
+ const startupProject = Promise.resolve(await trustProject(trustedProjectRoot, startupStore));
433
+ projects.set(trustedProjectRoot, startupProject);
434
+ const canonicalProjects = new Map([[trustedProjectRoot, startupProject]]);
435
+ async function artifactRoots(explicit) {
436
+ const requested = explicit ? path.resolve(trustedProjectRoot, explicit) : trustedProjectRoot;
437
+ // Key by the supplied project path, so a replacement cannot acquire fresh trust.
438
+ if (!projects.has(requested)) {
439
+ const canonical = await fs.realpath(requested);
440
+ if (!canonicalProjects.has(canonical)) canonicalProjects.set(canonical, trustProject(requested));
441
+ projects.set(requested, canonicalProjects.get(canonical));
442
+ }
443
+ const pinned = await projects.get(requested);
444
+ const canonical = await fs.realpath(requested);
445
+ const current = await fs.stat(requested);
446
+ if (canonical !== pinned.canonical || !current.isDirectory()
447
+ || current.dev !== pinned.identity.dev || current.ino !== pinned.identity.ino) {
448
+ throw new Error('Project root changed since it was pinned');
449
+ }
450
+ return pinned.store;
451
+ }
78
452
 
79
453
  // Tool: artifact-read
80
454
  server.registerTool('artifact-read', {
@@ -82,14 +456,21 @@ export function createServer() {
82
456
  description: 'Read artifact from .aiwg/ directory',
83
457
  inputSchema: {
84
458
  path: z.string().describe('Path relative to .aiwg/ (e.g., "requirements/UC-001.md")'),
85
- project_dir: z.string().optional().describe('Project directory path (defaults to auto-detected project root)')
459
+ project_dir: z.string().optional().describe('Real project directory outside artifact trees (defaults to server-start project; artifact store pinned on first use)')
86
460
  }
87
461
  }, async ({ path: artifactPath, project_dir }) => {
88
462
  try {
89
- // Auto-find project root if not specified
90
- const projectRoot = project_dir || await findProjectRoot();
91
- const fullPath = path.join(await resolveProjectAiwgDir(projectRoot), artifactPath);
92
- const content = await fs.readFile(fullPath, 'utf-8');
463
+ const store = await artifactRoots(project_dir);
464
+ const { handle, close } = await openArtifact(store, artifactPath);
465
+ let content;
466
+ let completed = false;
467
+ try {
468
+ content = await handle.readFile('utf-8');
469
+ completed = true;
470
+ } finally {
471
+ const [closed] = await Promise.allSettled([close()]);
472
+ if (completed && closed.status === 'rejected') throw closed.reason;
473
+ }
93
474
  return {
94
475
  content: [{
95
476
  type: 'text',
@@ -114,20 +495,22 @@ export function createServer() {
114
495
  inputSchema: {
115
496
  path: z.string().describe('Path relative to .aiwg/'),
116
497
  content: z.string().describe('Artifact content'),
117
- project_dir: z.string().optional().describe('Project directory path (defaults to auto-detected project root)')
498
+ project_dir: z.string().optional().describe('Real project directory outside artifact trees (defaults to server-start project; artifact store pinned on first use)')
118
499
  }
119
500
  }, async ({ path: artifactPath, content, project_dir }) => {
120
501
  try {
121
- // Auto-find project root if not specified
122
- const projectRoot = project_dir || await findProjectRoot();
123
- const fullPath = path.join(await resolveProjectAiwgDir(projectRoot), artifactPath);
124
- const dir = path.dirname(fullPath);
125
-
126
- // Ensure directory exists
127
- await fs.mkdir(dir, { recursive: true });
128
-
129
- // Write file
130
- await fs.writeFile(fullPath, content, 'utf-8');
502
+ const store = await artifactRoots(project_dir);
503
+ const { handle, fullPath, commit, close } = await openArtifact(store, artifactPath, { write: true });
504
+ let completed = false;
505
+ try {
506
+ await handle.writeFile(content, 'utf-8');
507
+ await commit();
508
+ completed = true;
509
+ } finally {
510
+ // Close every owned descriptor; preserve any write/verification error.
511
+ const [closed] = await Promise.allSettled([close()]);
512
+ if (completed && closed.status === 'rejected') throw closed.reason;
513
+ }
131
514
 
132
515
  return {
133
516
  content: [{
@@ -611,7 +994,7 @@ async function scanDirectory(dir, type) {
611
994
  * Start the MCP server with stdio transport
612
995
  */
613
996
  export async function startServer() {
614
- const server = createServer();
997
+ const server = await createServer();
615
998
  const transport = new StdioServerTransport();
616
999
 
617
1000
  console.error('[AIWG MCP] Starting server...');
@@ -0,0 +1,9 @@
1
+ /** Encode a complete TOML basic string using valid Unicode scalar values. */
2
+ export function tomlString(value) {
3
+ if (typeof value !== 'string' || [...value].some(char => {
4
+ const point = char.codePointAt(0);
5
+ return point >= 0xd800 && point <= 0xdfff;
6
+ })) throw new Error('TOML values must be strings containing valid Unicode scalar values');
7
+ // JSON escapes align with TOML basic strings except DEL must also be escaped.
8
+ return JSON.stringify(value).replace(/\u007f/g, '\\u007f');
9
+ }
@@ -0,0 +1,135 @@
1
+ import { parseTOML } from 'toml-eslint-parser';
2
+
3
+ /** Remove the root MCP subtree without interpreting strings as TOML structure. */
4
+ export function stripMcpServers(text) {
5
+ const fail = () => { throw new Error('Refusing to copy base Codex config: cannot safely classify TOML (malformed or unterminated structure)'); };
6
+ // Reuse the existing persistent-editor validator (no new dependency). The
7
+ // scanner below owns subtree removal; validation also catches ambiguous
8
+ // scalar syntax and duplicate definitions, beyond balanced delimiters.
9
+ try { parseTOML(text, { tomlVersion: '1.1.0' }); } catch { fail(); }
10
+ // A key path may use bare, literal, or escaped basic quoted components.
11
+ const keyPath = source => {
12
+ const parts = [];
13
+ let rest = source.trim();
14
+ while (rest) {
15
+ const match = /^(?:([A-Za-z0-9_-]+)|'([^'\r\n]*)'|"((?:[^"\\\r\n]|\\(?:[btnfr"\\]|u[\da-fA-F]{4}|U[\da-fA-F]{8}))*)")/.exec(rest);
16
+ if (!match) fail();
17
+ let key = match[1] ?? match[2];
18
+ if (key === undefined) {
19
+ key = match[3].replace(/\\(u[\da-fA-F]{4}|U[\da-fA-F]{8}|[btnfr"\\])/g, (_, escape) => {
20
+ if (escape[0] === 'u' || escape[0] === 'U') {
21
+ const code = Number.parseInt(escape.slice(1), 16);
22
+ if (code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff)) fail();
23
+ return String.fromCodePoint(code);
24
+ }
25
+ return { b: '\b', t: '\t', n: '\n', f: '\f', r: '\r', '"': '"', '\\': '\\' }[escape];
26
+ });
27
+ }
28
+ parts.push(key);
29
+ rest = rest.slice(match[0].length).trim();
30
+ if (!rest) break;
31
+ if (!rest.startsWith('.')) fail();
32
+ rest = rest.slice(1).trim();
33
+ if (!rest) fail();
34
+ }
35
+ if (!parts.length) fail();
36
+ return parts;
37
+ };
38
+
39
+ let table = [];
40
+ let quote = null;
41
+ let multiline = false;
42
+ const brackets = [];
43
+ let record = '';
44
+ let drop = false;
45
+ const kept = [];
46
+ for (const line of text.split(/(?<=\n)/)) {
47
+ let start = 0;
48
+ if (!record) {
49
+ const trimmed = line.trim();
50
+ if (!trimmed || trimmed.startsWith('#')) {
51
+ if (table[0] !== 'mcp_servers') kept.push(line);
52
+ continue;
53
+ }
54
+ if (trimmed.startsWith('[')) {
55
+ // Header strings are single-line key components; scan their quotes so
56
+ // a bracket or hash inside a quoted name cannot terminate the header.
57
+ let headerQuote = null;
58
+ let end = line.length;
59
+ for (let i = 0; i < line.length; i++) {
60
+ const char = line[i];
61
+ if (headerQuote) {
62
+ if (headerQuote === '"' && char === '\\') { i++; continue; }
63
+ if (char === headerQuote) headerQuote = null;
64
+ } else if (char === '"' || char === "'") headerQuote = char;
65
+ else if (char === '#') { end = i; break; }
66
+ }
67
+ if (headerQuote) fail();
68
+ const header = line.slice(0, end).trim();
69
+ const array = header.startsWith('[[');
70
+ const width = array ? 2 : 1;
71
+ if (!header.endsWith(array ? ']]' : ']')) fail();
72
+ table = keyPath(header.slice(width, -width));
73
+ if (table[0] !== 'mcp_servers') kept.push(line);
74
+ continue;
75
+ }
76
+ let keyQuote = null;
77
+ let equals = -1;
78
+ for (let i = 0; i < line.length; i++) {
79
+ const char = line[i];
80
+ if (keyQuote) {
81
+ if (keyQuote === '"' && char === '\\') { i++; continue; }
82
+ if (char === keyQuote) keyQuote = null;
83
+ } else if (char === '"' || char === "'") keyQuote = char;
84
+ else if (char === '=') { equals = i; break; }
85
+ else if (char === '#') break;
86
+ }
87
+ if (equals < 0 || keyQuote) fail();
88
+ const key = keyPath(line.slice(0, equals));
89
+ drop = table[0] === 'mcp_servers' || (table.length === 0 && key[0] === 'mcp_servers');
90
+ start = equals + 1;
91
+ if (!line.slice(start).split('#')[0].trim()) fail();
92
+ }
93
+ record += line;
94
+ for (let i = start; i < line.length; i++) {
95
+ const char = line[i];
96
+ if (quote) {
97
+ if (quote === '"' && char === '\\') {
98
+ // A backslash escapes the next character (including a multiline
99
+ // continuation); it cannot conceal a closing delimiter.
100
+ if (i + 1 >= line.length) fail();
101
+ i++;
102
+ continue;
103
+ }
104
+ if (char === quote) {
105
+ if (!multiline) quote = null;
106
+ else if (line.slice(i, i + 3) === quote.repeat(3)) {
107
+ let run = 3;
108
+ while (line[i + run] === quote) run++;
109
+ if (run > 5) fail();
110
+ i += run - 1;
111
+ quote = null;
112
+ multiline = false;
113
+ }
114
+ } else if (!multiline && (char === '\n' || char === '\r')) fail();
115
+ continue;
116
+ }
117
+ if (char === '#') break;
118
+ if (char === '"' || char === "'") {
119
+ quote = char;
120
+ multiline = line.slice(i, i + 3) === char.repeat(3);
121
+ if (multiline) i += 2;
122
+ } else if (char === '[' || char === '{') brackets.push(char);
123
+ else if (char === ']' || char === '}') {
124
+ if (brackets.pop() !== (char === ']' ? '[' : '{')) fail();
125
+ } else if (char === '=' && !brackets.includes('{')) fail();
126
+ }
127
+ if (quote && !multiline) fail();
128
+ if (!quote && brackets.length === 0) {
129
+ if (!drop) kept.push(record);
130
+ record = '';
131
+ }
132
+ }
133
+ if (quote || brackets.length || record) fail();
134
+ return kept.join('');
135
+ }