@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
@@ -0,0 +1,357 @@
1
+ /**
2
+ * MCP profile tool filters.
3
+ *
4
+ * A profile's providerOverrides hold toolDeny / toolAllow patterns of the form
5
+ * `<server>__<tool>`, where <tool> may contain `*`. The `*` provider key applies
6
+ * to every harness; a harness's own key adds to it.
7
+ *
8
+ * Each harness enforces tool filters differently, and some not at all:
9
+ *
10
+ * claude-code permissions.deny rules `mcp__<server>__<tool>` in
11
+ * .claude/settings.local.json (project) or ~/.claude/settings.json (user).
12
+ * toolAllow is refused: Claude has no restrict-only allowlist.
13
+ * https://code.claude.com/docs/en/permissions
14
+ * codex per-server disabled_tools / enabled_tools, exact names; enabled = false
15
+ * for a whole-server deny. https://learn.chatgpt.com/docs/extend/mcp
16
+ * opencode top-level tools map of `<server>_<tool>` globs; the last matching entry
17
+ * wins. https://opencode.ai/docs/mcp-servers
18
+ * factory per-server disabledTools; disabled = true for a whole-server deny.
19
+ * https://docs.factory.com/cli/configuration/mcp
20
+ * windsurf per-server disabledTools. https://docs.devin.ai/desktop/cascade/mcp
21
+ * antigravity per-server disabledTools; disabled = true for a whole-server deny.
22
+ * https://antigravity.google/docs/mcp
23
+ *
24
+ * Cursor, Warp, OMP and Grok Build document no file-level tool filter.
25
+ * Anything a harness cannot express is returned as a warning, never dropped silently.
26
+ */
27
+
28
+ import { readFile } from 'fs/promises';
29
+ import { createHash } from 'node:crypto';
30
+ import { assertConfigDestination, canonicalConfigPath, prepareConfigWrite, writeConfigTransaction } from './config-file.mjs';
31
+ import { resolve } from 'path';
32
+ import { homedir } from 'os';
33
+ import { normalizeRuntimeProviderId } from '../providers/provider-definitions.mjs';
34
+
35
+ const PER_SERVER_DISABLED_TOOLS = new Set(['factory', 'windsurf', 'antigravity']);
36
+ const SERVER_DISABLE_KEY = { factory: 'disabled', antigravity: 'disabled' };
37
+
38
+ function unique(values) {
39
+ return [...new Set(values)];
40
+ }
41
+
42
+ /** Merge the `*` overrides with the harness's own. Keys are normalized, so `claude` and `claude-code` both apply. */
43
+ export function resolveToolFilters(profile, provider) {
44
+ const target = normalizeRuntimeProviderId(provider) || provider;
45
+ const deny = [];
46
+ const allow = [];
47
+ for (const [key, override] of Object.entries(profile?.providerOverrides || {})) {
48
+ if (key !== '*' && (normalizeRuntimeProviderId(key) || key) !== target) continue;
49
+ deny.push(...(override?.toolDeny || []));
50
+ allow.push(...(override?.toolAllow || []));
51
+ }
52
+ return { deny: unique(deny), allow: unique(allow) };
53
+ }
54
+
55
+ export function hasToolFilters(filters) {
56
+ return Boolean(filters && (filters.deny.length > 0 || filters.allow.length > 0));
57
+ }
58
+
59
+ /** Split `<server>__<tool>`; returns null when there is no server part. */
60
+ export function parseToolPattern(pattern) {
61
+ const index = pattern.indexOf('__');
62
+ if (index <= 0 || index + 2 >= pattern.length) return null;
63
+ return { server: pattern.slice(0, index), tool: pattern.slice(index + 2) };
64
+ }
65
+
66
+ const isGlob = value => value.includes('*');
67
+
68
+ /**
69
+ * Group patterns by injected server. Patterns for servers outside the injected
70
+ * set do not apply; malformed patterns and glob server names are warnings.
71
+ */
72
+ function groupByServer(patterns, serverNames, kind, warnings) {
73
+ const groups = new Map();
74
+ for (const pattern of patterns) {
75
+ const parsed = parseToolPattern(pattern);
76
+ if (!parsed || isGlob(parsed.server)) {
77
+ warnings.push(`${kind} pattern "${pattern}" is not <server>__<tool> with a literal server name; not rendered.`);
78
+ continue;
79
+ }
80
+ if (!serverNames.includes(parsed.server)) continue;
81
+ if (!groups.has(parsed.server)) groups.set(parsed.server, []);
82
+ groups.get(parsed.server).push(parsed.tool);
83
+ }
84
+ return groups;
85
+ }
86
+
87
+ /** Claude Code and opencode name a server's tools after the server with these characters replaced. */
88
+ const sanitize = value => value.replace(/[^a-zA-Z0-9_-]/g, '_');
89
+
90
+ /**
91
+ * Compute what a harness should receive.
92
+ *
93
+ * Returns:
94
+ * serverFields per-server fields to merge into each entry (JSON harnesses)
95
+ * tomlLines per-server TOML lines to append to each [mcp_servers.<name>] table
96
+ * topLevel fields to merge into the harness config root (opencode `tools`)
97
+ * claudePermissions permission rules for Claude Code's settings file
98
+ * warnings filters the harness cannot express
99
+ */
100
+ export function planToolFilters(provider, serverNames, filters) {
101
+ const target = normalizeRuntimeProviderId(provider) || provider;
102
+ const plan = { serverFields: {}, tomlLines: {}, topLevel: {}, claudePermissions: null, warnings: [] };
103
+ if (target === 'claude-code' && filters?.allow.length) {
104
+ throw new Error(`claude-code: refusing toolAllow patterns ${filters.allow.map(pattern => JSON.stringify(pattern)).join(', ')}: `
105
+ + 'Claude Code has no restrict-only allowlist. permissions.allow pre-approves tools; deny rules cannot express all except, '
106
+ + 'and allow rules cannot carve exceptions out of deny rules. Use toolDeny instead.');
107
+ }
108
+ if (!hasToolFilters(filters)) return plan;
109
+ const deny = groupByServer(filters.deny, serverNames, 'toolDeny', plan.warnings);
110
+ const allow = groupByServer(filters.allow, serverNames, 'toolAllow', plan.warnings);
111
+ if (deny.size === 0 && allow.size === 0) return plan;
112
+
113
+ if (target === 'claude-code') {
114
+ const rule = (server, tool) => (tool === '*' ? `mcp__${sanitize(server)}` : `mcp__${sanitize(server)}__${tool}`);
115
+ const denyRules = [...deny].flatMap(([server, tools]) => tools.map(tool => rule(server, tool)));
116
+ plan.claudePermissions = { deny: unique(denyRules), allow: [] };
117
+ return plan;
118
+ }
119
+
120
+ if (target === 'opencode') {
121
+ const tools = {};
122
+ for (const server of allow.keys()) tools[`${sanitize(server)}_*`] = false;
123
+ for (const [server, list] of deny) {
124
+ for (const tool of list) {
125
+ const key = `${sanitize(server)}_${tool}`;
126
+ delete tools[key];
127
+ tools[key] = false;
128
+ }
129
+ }
130
+ // Specific allows follow denies under last-match-wins. An identical deny key wins.
131
+ for (const [server, list] of allow) {
132
+ for (const tool of list) {
133
+ if (deny.get(server)?.includes(tool)) continue;
134
+ const key = `${sanitize(server)}_${tool}`;
135
+ delete tools[key];
136
+ tools[key] = true;
137
+ }
138
+ }
139
+ plan.topLevel.tools = tools;
140
+ return plan;
141
+ }
142
+
143
+ if (target === 'codex') {
144
+ if (allow.size) {
145
+ const unrestricted = serverNames.filter(name => !allow.has(name) && !deny.get(name)?.includes('*'));
146
+ if (unrestricted.length) {
147
+ plan.warnings.push(`codex: toolAllow is per-server; servers without an allowlist remain unrestricted by toolAllow: ${unrestricted.join(', ')}. Add toolAllow patterns or toolDeny <server>__* for them.`);
148
+ }
149
+ }
150
+ for (const name of serverNames) {
151
+ const lines = [];
152
+ const denied = deny.get(name) || [];
153
+ const allowed = allow.get(name) || [];
154
+ if (denied.includes('*')) {
155
+ plan.tomlLines[name] = ['enabled = false'];
156
+ continue;
157
+ }
158
+ const exact = (list, kind) => list.filter(tool => {
159
+ if (!isGlob(tool)) return true;
160
+ plan.warnings.push(`codex: ${kind} "${name}__${tool}" is a glob; Codex matches tool names exactly, so it is not rendered.`);
161
+ return false;
162
+ });
163
+ const enabled = allowed.includes('*') ? [] : exact(allowed, 'toolAllow');
164
+ const disabled = exact(denied, 'toolDeny');
165
+ if (enabled.length > 0) lines.push(`enabled_tools = [${enabled.map(tool => JSON.stringify(tool)).join(', ')}]`);
166
+ if (disabled.length > 0) lines.push(`disabled_tools = [${disabled.map(tool => JSON.stringify(tool)).join(', ')}]`);
167
+ if (lines.length > 0) plan.tomlLines[name] = lines;
168
+ }
169
+ return plan;
170
+ }
171
+
172
+ if (PER_SERVER_DISABLED_TOOLS.has(target)) {
173
+ for (const [server, list] of deny) {
174
+ if (list.includes('*')) {
175
+ const key = SERVER_DISABLE_KEY[target];
176
+ if (key) plan.serverFields[server] = { [key]: true };
177
+ else plan.warnings.push(`${target}: toolDeny "${server}__*" disables a whole server, which ${target} has no documented file key for; not rendered.`);
178
+ continue;
179
+ }
180
+ const exact = list.filter(tool => {
181
+ if (!isGlob(tool)) return true;
182
+ plan.warnings.push(`${target}: toolDeny "${server}__${tool}" is a glob; disabledTools takes exact names, so it is not rendered.`);
183
+ return false;
184
+ });
185
+ if (exact.length > 0) plan.serverFields[server] = { disabledTools: exact };
186
+ }
187
+ for (const server of allow.keys()) {
188
+ plan.warnings.push(`${target}: toolAllow for "${server}" is not rendered; ${target} has no tool allowlist.`);
189
+ }
190
+ return plan;
191
+ }
192
+
193
+ plan.warnings.push(`${target}: tool filters are not rendered; ${target} documents no per-tool filter in its MCP config.`);
194
+ return plan;
195
+ }
196
+
197
+ /** Merge the per-server fields and top-level fields of a plan into a JSON harness config. */
198
+ export function applyJsonToolFilterPlan(config, serversKey, plan) {
199
+ const servers = config[serversKey] || {};
200
+ for (const [name, fields] of Object.entries(plan.serverFields)) {
201
+ if (servers[name]) {
202
+ const previous = servers[name];
203
+ if (Object.hasOwn(previous, 'disabledTools') && !Array.isArray(previous.disabledTools)) {
204
+ throw new Error(`Refusing tool filters for server "${name}": existing disabledTools must be an array`);
205
+ }
206
+ servers[name] = { ...previous, ...fields };
207
+ if (fields.disabledTools && Array.isArray(previous.disabledTools)) {
208
+ servers[name].disabledTools = unique([...previous.disabledTools, ...fields.disabledTools]);
209
+ }
210
+ }
211
+ }
212
+ if (plan.topLevel.tools) {
213
+ const tools = { ...(config.tools || {}) };
214
+ // opencode uses last-match-wins, including when an existing wildcard conflicts.
215
+ for (const key of Object.keys(plan.topLevel.tools)) delete tools[key];
216
+ config.tools = { ...tools, ...plan.topLevel.tools };
217
+ }
218
+ return config;
219
+ }
220
+
221
+ export function claudeSettingsPath(projectDir = '.', scope = 'project') {
222
+ if (scope === 'user') return resolve(process.env.HOME || process.env.USERPROFILE || homedir(), '.claude/settings.json');
223
+ return resolve(projectDir, '.claude/settings.local.json');
224
+ }
225
+
226
+ const isObject = value => value !== null && typeof value === 'object' && !Array.isArray(value);
227
+ const digest = value => createHash('sha256').update(value).digest('hex');
228
+
229
+ function validateSettings(settings, settingsPath) {
230
+ if (!isObject(settings)) throw new Error(`Claude Code settings ${settingsPath} must contain an object`);
231
+ if (Object.hasOwn(settings, 'permissions')) {
232
+ if (!isObject(settings.permissions)) {
233
+ throw new Error(`Claude Code settings ${settingsPath}: permissions must be an object`);
234
+ }
235
+ for (const key of ['allow', 'deny', 'ask', 'additionalDirectories']) {
236
+ if (Object.hasOwn(settings.permissions, key)
237
+ && (!Array.isArray(settings.permissions[key]) || settings.permissions[key].some(rule => typeof rule !== 'string'))) {
238
+ throw new Error(`Claude Code settings ${settingsPath}: permissions.${key} must be an array of strings`);
239
+ }
240
+ }
241
+ }
242
+ }
243
+
244
+ /** Validate destinations and settings before any MCP writes; keep ownership outside Claude's schema. */
245
+ export async function prepareClaudePermissions(settingsPath, permissions, {
246
+ userScope = false, projectRoot, managedDir, sidecar = false, mcpPath,
247
+ } = {}) {
248
+ if (permissions.allow?.length) {
249
+ throw new Error(`Claude Code: refusing permissions.allow ${permissions.allow.map(rule => JSON.stringify(rule)).join(', ')}: no restrict-only allowlist; use permissions.deny instead.`);
250
+ }
251
+ settingsPath = resolve(settingsPath);
252
+ if (mcpPath && settingsPath === resolve(mcpPath)) {
253
+ throw new Error(`Refusing Claude Code settings sidecar ${settingsPath}: collides with MCP config`);
254
+ }
255
+ const info = await assertConfigDestination(settingsPath, projectRoot);
256
+ if (mcpPath && info) {
257
+ const mcpInfo = await assertConfigDestination(mcpPath, projectRoot);
258
+ if (mcpInfo && info.dev === mcpInfo.dev && info.ino === mcpInfo.ino) {
259
+ throw new Error(`Refusing Claude Code settings sidecar ${settingsPath}: collides with MCP config`);
260
+ }
261
+ }
262
+ let content;
263
+ let existing = {};
264
+ if (info) {
265
+ content = await readFile(settingsPath, 'utf-8');
266
+ try {
267
+ existing = JSON.parse(content);
268
+ } catch (error) {
269
+ if (error instanceof SyntaxError) {
270
+ throw new Error(`Refusing to overwrite malformed Claude Code settings ${settingsPath}: invalid JSON`);
271
+ }
272
+ throw error;
273
+ }
274
+ }
275
+ validateSettings(existing, settingsPath);
276
+
277
+ const canonicalPath = await canonicalConfigPath(settingsPath);
278
+ const recordPath = managedDir ? resolve(managedDir, 'claude-tool-permissions', `${digest(canonicalPath)}.json`) : null;
279
+ let record = null;
280
+ if (recordPath && await assertConfigDestination(recordPath, managedDir)) {
281
+ try {
282
+ record = JSON.parse(await readFile(recordPath, 'utf-8'));
283
+ if (!isObject(record) || record.settingsPath !== canonicalPath || !Array.isArray(record.denyAdded)
284
+ || record.denyAdded.some(rule => typeof rule !== 'string')
285
+ || (record.denyDigest !== undefined && (typeof record.denyDigest !== 'string' || !/^[a-f0-9]{64}$/.test(record.denyDigest)))
286
+ || (record.denyPreserved !== undefined && (!Array.isArray(record.denyPreserved)
287
+ || record.denyPreserved.some(rule => typeof rule !== 'string' || !record.denyAdded.includes(rule))))) {
288
+ throw new Error('unsupported ownership record shape');
289
+ }
290
+ } catch (error) {
291
+ throw new Error(`Invalid Claude permission ownership record ${recordPath}: ${error.message}. Delete ${recordPath} to reset ownership; existing deny rules will be treated as user-owned.`);
292
+ }
293
+ }
294
+ if (sidecar && info && (!record?.sidecar || record.contentDigest !== digest(content))) {
295
+ throw new Error(`Refusing implicit Claude Code settings sidecar ${settingsPath}: existing file was not created by AIWG or was changed. Choose another --out path.`);
296
+ }
297
+ if (sidecar && !recordPath) throw new Error('Claude settings sidecars require an ownership record directory');
298
+
299
+ const desired = unique(permissions.deny);
300
+ const current = existing.permissions || {};
301
+ const deny = [...(current.deny || [])];
302
+ const owned = [];
303
+ const preserved = [];
304
+ const warnings = [];
305
+ const unchangedDeny = record?.denyDigest === digest(JSON.stringify(deny));
306
+ for (const rule of record?.denyAdded || []) {
307
+ const index = deny.indexOf(rule);
308
+ if (index < 0) continue;
309
+ if (!unchangedDeny || record.denyPreserved?.includes(rule)) {
310
+ owned.push(rule);
311
+ preserved.push(rule);
312
+ } else if (desired.includes(rule)) owned.push(rule);
313
+ else deny.splice(index, 1); // Remove only our occurrence; preserve user-added duplicates.
314
+ }
315
+ for (const rule of desired) {
316
+ if (deny.includes(rule)) continue; // A pre-existing user rule never becomes AIWG-owned.
317
+ deny.push(rule);
318
+ owned.push(rule);
319
+ }
320
+ const retained = preserved.filter(rule => !desired.includes(rule));
321
+ if (retained.length) {
322
+ warnings.push(`Claude Code settings ${settingsPath}: preserving previously tracked deny rules because permissions.deny changed or ownership is uncertain: ${unique(retained).join(', ')}. Remove these rules manually from permissions.deny in ${settingsPath} if no longer wanted.`);
323
+ }
324
+ const active = Boolean(record || desired.length || sidecar);
325
+ const merged = active ? { ...existing, permissions: { ...current, deny } } : existing;
326
+ const nextContent = JSON.stringify(merged, null, 2) + '\n';
327
+ const writes = active ? [{
328
+ file: settingsPath, content: nextContent,
329
+ options: { userScope: userScope || sidecar, projectRoot, newFileMode: 0o600 },
330
+ }] : [];
331
+ if (active && recordPath) {
332
+ writes.push({
333
+ file: recordPath,
334
+ content: JSON.stringify({
335
+ settingsPath: canonicalPath, denyAdded: owned, denyPreserved: unique(preserved),
336
+ denyDigest: digest(JSON.stringify(deny)), sidecar,
337
+ ...(sidecar ? { contentDigest: digest(nextContent) } : {}),
338
+ }, null, 2) + '\n',
339
+ options: { userScope: true, projectRoot: managedDir },
340
+ });
341
+ }
342
+ for (const { file, options } of writes) await prepareConfigWrite(file, options);
343
+ return {
344
+ merged, active, warnings, writes,
345
+ async commit() { await writeConfigTransaction(writes); },
346
+ };
347
+ }
348
+
349
+ /** Merge deny rules after validation, optionally tracking ownership for profile switching. */
350
+ export async function mergeClaudePermissions(settingsPath, permissions, { dryRun = false, ...options } = {}) {
351
+ const prepared = await prepareClaudePermissions(settingsPath, permissions, options);
352
+ if (!dryRun) {
353
+ await prepared.commit();
354
+ for (const warning of prepared.warnings) console.warn(`WARNING ${warning}`);
355
+ }
356
+ return prepared.merged;
357
+ }
@@ -10,7 +10,7 @@
10
10
 
11
11
  import { readFile, stat } from 'node:fs/promises';
12
12
  import { z } from 'zod';
13
- import { mcpError, mcpJson } from '../helpers.mjs';
13
+ import { assertNoWorkerBinding, mcpError, mcpJson } from '../helpers.mjs';
14
14
 
15
15
  const FLEET_VERSION = 'agentic-orchestration/v1';
16
16
  const ACTIVITY_VERSION = 'activity.event/v1';
@@ -143,6 +143,7 @@ export class AgenticSandboxMcpClient {
143
143
  }
144
144
 
145
145
  async request(path, { method = 'GET', body, headers = {}, validate = (value) => safeResponse(value) } = {}) {
146
+ assertNoWorkerBinding(body, 'mcp');
146
147
  const baseUrl = configuredBaseUrl(this.env);
147
148
  const token = await managementBearer(this.env);
148
149
  const response = await this.fetch(`${baseUrl}${path}`, {
@@ -158,6 +159,7 @@ export class AgenticSandboxMcpClient {
158
159
  else return mcpError(`sandbox returned non-JSON HTTP ${response.status}`);
159
160
  }
160
161
  try {
162
+ assertNoWorkerBinding(parsed, 'mcp');
161
163
  return typedResult(parsed, response.status, validate);
162
164
  } catch (error) {
163
165
  return mcpError(`${error.code ?? 'sandbox_malformed_response'}: ${error.message}`);
@@ -82,6 +82,21 @@ const SYNTHETIC_SETUP = z.object({
82
82
  pattern: z.object({ id: z.string(), version: z.string() }).passthrough(),
83
83
  files: z.record(z.string(), z.unknown()),
84
84
  }).passthrough();
85
+ const ASK = z.object({
86
+ schema: z.literal('aiwg-decision-ask/v1'),
87
+ status: z.enum(['answered', 'fallback']),
88
+ answer: z.union([z.string(), z.boolean(), z.number()]).nullable(),
89
+ confidence: z.number().min(0).max(1).nullable(),
90
+ probability: z.number().min(0).max(1).optional(),
91
+ fallback: z.enum(['llm']).nullable(),
92
+ reason: z.string(),
93
+ model: z.string().nullable(),
94
+ usage: z.object({
95
+ inputTokens: z.number().int().nonnegative().nullable(),
96
+ outputTokens: z.number().int().nonnegative().nullable(),
97
+ }).nullable(),
98
+ latencyMs: z.number().nonnegative(),
99
+ }).passthrough();
85
100
 
86
101
  let driverPromise;
87
102
 
@@ -176,6 +191,49 @@ export function registerDecisionToolset(server) {
176
191
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
177
192
  }, async ({ profile, opt_in }) => (await driver()).evaluateMcpProfile(profile, opt_in, options()), EVALUATE);
178
193
 
194
+ registerJsonTool(server, 'decision-ask', {
195
+ title: 'Ask bounded Jev decision',
196
+ description: 'Ask a bounded yes/no, choice, or small ordinal decision. Falls back to the caller on missing setup or low confidence.',
197
+ inputSchema: {
198
+ question: z.string().min(1),
199
+ yes_no: z.boolean().default(false),
200
+ choices: z.array(z.string()).min(2).max(255).optional().describe('Choice ids, each optionally written as id=description so Jev knows what the option means'),
201
+ scale: z.object({ low: z.number().int(), high: z.number().int() }).optional(),
202
+ context: z.string().optional(),
203
+ threshold: z.number().min(0).max(1).default(0.8),
204
+ timeout_ms: z.number().int().positive().default(15000),
205
+ caller: z.string().optional().describe('Optional decision-point attribution tag, recorded in the decision ledger (#2946)'),
206
+ },
207
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
208
+ }, async ({ question, yes_no, choices, scale, context, threshold, timeout_ms, caller }) => {
209
+ const modeCount = (yes_no ? 1 : 0) + (choices ? 1 : 0) + (scale ? 1 : 0);
210
+ if (modeCount !== 1) throw new Error('Select exactly one of yes_no, choices, or scale');
211
+ const driverModule = await driver();
212
+ const opts = options();
213
+ const input = {
214
+ question,
215
+ mode: yes_no ? { kind: 'yes-no' } : choices ? { kind: 'choices', choices } : { kind: 'scale', low: scale.low, high: scale.high },
216
+ ...(context === undefined ? {} : { context }),
217
+ threshold,
218
+ timeoutMs: timeout_ms,
219
+ };
220
+ const result = await driverModule.askDecision(input, opts);
221
+ // Every decision-ask call records through the same ledger function the CLI
222
+ // `ask` handler uses, tagged with this tool's transport (#2946). Attribution
223
+ // resolution and the write both happen inside recordDecisionAsk's own
224
+ // bounded, never-throwing step (review r1 findings 3 and 4): a provider-
225
+ // detection failure, a slow/failed write, or an invalid caller tag can
226
+ // never replace `result` with an error or hold up this tool call.
227
+ const sanitizedCaller = typeof caller === 'string' && driverModule.ATTRIBUTION_FIELD_PATTERN.test(caller) ? caller : null;
228
+ await driverModule.recordDecisionAsk(input, result, {
229
+ transport: 'mcp',
230
+ projectRoot: opts.cwd,
231
+ env: opts.env,
232
+ caller: sanitizedCaller,
233
+ });
234
+ return result;
235
+ }, ASK);
236
+
179
237
  registerJsonTool(server, 'decision-setup-synthetic-classification', {
180
238
  title: 'Prepare synthetic classification artifacts',
181
239
  description: 'Return reviewable pinned synthetic classification artifacts and a dispatcher request.',
@@ -215,6 +215,7 @@ export function registerFlowToolset(server) {
215
215
  }
216
216
 
217
217
  export function registerMissionToolset(server) {
218
+ registerWorkerResolveTool(server);
218
219
  server.registerTool('mission-guide', {
219
220
  title: 'Show AIWG Mission Primitive',
220
221
  description: 'Fetch the aiwg-mission kernel skill. This is the AIWG-owned Mission primitive; it is distinct from the lower-level mc-* Mission Control toolset.',
@@ -318,3 +319,40 @@ export function registerMissionToolset(server) {
318
319
  }
319
320
  });
320
321
  }
322
+
323
+ /** Frozen B-OG-33/35 read-only surface; the CLI and MCP share the typed renderer. */
324
+ export function registerWorkerResolveTool(server) {
325
+ server.registerTool('worker-resolve', {
326
+ title: 'Resolve Worker Invocation',
327
+ description: 'Resolve a nonlaunching qualified worker plan without Mission or MC state.',
328
+ inputSchema: z.object({
329
+ role: z.enum(['efficiency', 'coding', 'reasoning']),
330
+ task: z.string().min(1),
331
+ group: z.string().min(1).regex(/^[a-z][a-z0-9-]{0,62}$/),
332
+ brief: z.string().min(1),
333
+ workdir: z.string().min(1),
334
+ final_output: z.string().min(1),
335
+ transport: z.enum(['shell', 'mcp']).default('shell'),
336
+ format: z.enum(['shell', 'json']).optional(),
337
+ project_dir: z.string().min(1).optional(),
338
+ }).strict(),
339
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
340
+ }, async input => {
341
+ try {
342
+ const args = ['models', 'resolve'];
343
+ for (const [key, value] of Object.entries(input)) {
344
+ if (key === 'project_dir' || key === 'format') continue;
345
+ args.push(`--${key === 'final_output' ? 'final-output' : key}=${value}`);
346
+ }
347
+ const format = input.format ?? 'json';
348
+ args.push(`--format=${format}`);
349
+ const { stdout, stderr, code } = await runAiwgCli(args, { cwd: input.project_dir, timeoutMs: 30000 });
350
+ if (format === 'shell') {
351
+ return { ...mcpJson(stdout || stderr), ...(code === 0 ? {} : { isError: true }) };
352
+ }
353
+ let plan;
354
+ try { plan = JSON.parse(stdout); } catch { return mcpError(`worker-resolve failed (exit ${code}): ${stderr}`); }
355
+ return { ...mcpJson(plan), ...(code === 0 ? {} : { isError: true }) };
356
+ } catch (error) { return mcpError(`worker-resolve: ${error.message}`); }
357
+ });
358
+ }
@@ -17,6 +17,7 @@
17
17
  import { z } from 'zod';
18
18
  import { runAiwgCli, mcpError, mcpJson } from '../helpers.mjs';
19
19
  import { registerFlowToolset, registerMissionToolset } from './orchestration.mjs';
20
+ import { registerWorkerInvokeToolset, WORKERS_TOOLSET_NAME } from './worker-invoke.mjs';
20
21
  import { registerAgenticSandboxToolset } from './agentic-sandbox.mjs';
21
22
  import { registerDecisionToolset } from './decision.mjs';
22
23
 
@@ -572,6 +573,9 @@ function registerOpsToolset(server) {
572
573
  const TOOLSET_REGISTRY = {
573
574
  flows: registerFlowToolset,
574
575
  missions: registerMissionToolset,
576
+ // #2892: bounded direct worker run/status/cancel. Explicit opt-in name
577
+ // ONLY — see WILDCARD_EXCLUDED_TOOLSETS below for why `all` cannot reach it.
578
+ [WORKERS_TOOLSET_NAME]: registerWorkerInvokeToolset,
575
579
  memory: (server) => {
576
580
  registerStorageSubsystem(server, STORAGE_SUBSYSTEMS.find(s => s.name === 'memory'));
577
581
  registerStorageSubsystem(server, STORAGE_SUBSYSTEMS.find(s => s.name === 'reflections'));
@@ -592,6 +596,14 @@ const TOOLSET_REGISTRY = {
592
596
  decision: registerDecisionToolset,
593
597
  };
594
598
 
599
+ /**
600
+ * Toolsets that a bare `all` must NOT silently enable. Enabling these
601
+ * requires naming them explicitly in the AIWG_MCP_TOOLSETS list (#2892
602
+ * defect 4: a worker-invoke bridge must not become reachable just because
603
+ * an operator wanted "every read-only discovery toolset" via `all`).
604
+ */
605
+ const WILDCARD_EXCLUDED_TOOLSETS = new Set([WORKERS_TOOLSET_NAME]);
606
+
595
607
  /**
596
608
  * Parse AIWG_MCP_TOOLSETS env or `--toolsets` CLI value into a Set.
597
609
  *
@@ -601,18 +613,19 @@ const TOOLSET_REGISTRY = {
601
613
  * Examples:
602
614
  * AIWG_MCP_TOOLSETS=flows,missions,ralph
603
615
  * AIWG_MCP_TOOLSETS=core,memory (core is implicit; harmless)
604
- * AIWG_MCP_TOOLSETS=all (all known toolsets)
616
+ * AIWG_MCP_TOOLSETS=all (all known toolsets EXCEPT `workers`,
617
+ * which needs an explicit opt-in)
618
+ * AIWG_MCP_TOOLSETS=all,workers (explicit opt-in on top of `all`)
605
619
  */
606
620
  export function parseToolsets(value) {
607
621
  if (!value || typeof value !== 'string') return new Set();
608
622
  const items = value.split(',').map((s) => s.trim().toLowerCase()).filter(Boolean);
609
- if (items.includes('all')) {
610
- return new Set(Object.keys(TOOLSET_REGISTRY));
611
- }
612
- const out = new Set();
613
623
  const known = new Set(Object.keys(TOOLSET_REGISTRY));
624
+ const out = items.includes('all')
625
+ ? new Set([...known].filter((name) => !WILDCARD_EXCLUDED_TOOLSETS.has(name)))
626
+ : new Set();
614
627
  for (const item of items) {
615
- if (item === 'core') continue; // always on
628
+ if (item === 'core' || item === 'all') continue; // 'core' always on; 'all' handled above
616
629
  if (!known.has(item)) {
617
630
  console.error(`[AIWG MCP] Unknown toolset: "${item}" — known: core, ${[...known].join(', ')}, all`);
618
631
  continue;
@@ -624,16 +637,34 @@ export function parseToolsets(value) {
624
637
 
625
638
  /**
626
639
  * Register all opt-in toolsets requested via env/flag.
640
+ *
641
+ * `options` is passed through to every registration function; only `workers`
642
+ * (#2892 B1) consumes it (`projectRoot`, pinned at registration — the served
643
+ * root must be resolved by the caller, e.g. `server.mjs`'s `findProjectRoot()`,
644
+ * never defaulted to `process.cwd()` here). Every other registration
645
+ * function in `TOOLSET_REGISTRY` is synchronous and ignores the extra arg.
646
+ *
647
+ * This function is `async` (returns a `Promise`) because `workers`'
648
+ * registration does real async work (realpath the root, resolve the default
649
+ * concurrency cap) before it calls `server.registerTool`. Calling it WITHOUT
650
+ * `await` is still safe for every other toolset: each registration function
651
+ * is invoked synchronously inside the loop below, so a purely-synchronous
652
+ * toolset's `server.registerTool` calls complete before this function's
653
+ * first (and only) `await` is ever reached — only `workers`'s own Promise is
654
+ * deferred into `pending`, and only an `await`ing caller needs to wait on it.
627
655
  */
628
- export function registerOptInToolsets(server, toolsets) {
656
+ export async function registerOptInToolsets(server, toolsets, options = {}) {
629
657
  const enabled = [];
658
+ const pending = [];
630
659
  for (const name of toolsets) {
631
660
  const fn = TOOLSET_REGISTRY[name];
632
661
  if (fn) {
633
- fn(server);
662
+ const result = fn(server, options);
663
+ if (result && typeof result.then === 'function') pending.push(result);
634
664
  enabled.push(name);
635
665
  }
636
666
  }
667
+ if (pending.length > 0) await Promise.all(pending);
637
668
  if (enabled.length > 0) {
638
669
  console.error(`[AIWG MCP] Opt-in toolsets enabled: ${enabled.join(', ')}`);
639
670
  } else {