@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.
- package/agentic/code/providers/capability-matrix.yaml +270 -0
- package/agentic/code/providers/model-capabilities.v1.json +3091 -24
- package/bin/aiwg.mjs +86 -21
- package/dist/src/a2a/codecs.js +9 -0
- package/dist/src/artifacts/corpus-tools/vision-extract.js +105 -15
- package/dist/src/artifacts/repair.js +9 -4
- package/dist/src/channel/manager.mjs +59 -10
- package/dist/src/cli/child-process-result.js +40 -0
- package/dist/src/cli/env.js +38 -16
- package/dist/src/cli/flag-parsing.js +236 -0
- package/dist/src/cli/handlers/decision.js +297 -2
- package/dist/src/cli/handlers/diagnose.js +9 -2
- package/dist/src/cli/handlers/feedback.js +14 -5
- package/dist/src/cli/handlers/init.js +245 -30
- package/dist/src/cli/handlers/mc.js +23 -0
- package/dist/src/cli/handlers/models.js +69 -6
- package/dist/src/cli/handlers/ralph.js +3 -0
- package/dist/src/cli/handlers/refresh.js +16 -0
- package/dist/src/cli/handlers/regenerate.js +14 -0
- package/dist/src/cli/handlers/runtime-info.js +24 -9
- package/dist/src/cli/handlers/script-runner.js +16 -6
- package/dist/src/cli/handlers/session.js +396 -62
- package/dist/src/cli/handlers/sessions.js +146 -40
- package/dist/src/cli/handlers/setup-manifest.js +439 -39
- package/dist/src/cli/handlers/setup.js +139 -39
- package/dist/src/cli/handlers/subcommands.js +3 -1
- package/dist/src/cli/handlers/team.js +18 -8
- package/dist/src/cli/handlers/use.js +13 -0
- package/dist/src/cli/handlers/utilities.js +106 -23
- package/dist/src/cli/prompt-utils.js +72 -0
- package/dist/src/cli/provider-resolution.js +11 -0
- package/dist/src/config/aiwg-config.js +268 -4
- package/dist/src/config/cli.js +146 -19
- package/dist/src/config/dispatch-thresholds-runtime.mjs +38 -0
- package/dist/src/config/project-artifacts-health.mjs +14 -3
- package/dist/src/config/user-config.js +36 -10
- package/dist/src/decision/adapters/jev.js +80 -28
- package/dist/src/decision/ask.js +352 -0
- package/dist/src/decision/driver.js +4 -0
- package/dist/src/decision/entry.js +38 -6
- package/dist/src/decision/heldout/collector.js +10 -1
- package/dist/src/decision/index.js +3 -0
- package/dist/src/decision/interop.js +60 -8
- package/dist/src/decision/jev-credentials.js +121 -0
- package/dist/src/decision/ledger.js +839 -0
- package/dist/src/decision/validate.js +6 -3
- package/dist/src/effects/dispatch-verifier.js +252 -0
- package/dist/src/effects/index.js +1 -0
- package/dist/src/features/installer.js +2 -2
- package/dist/src/flow/graph-metadata.js +5 -0
- package/dist/src/gates/providers/loader.js +3 -1
- package/dist/src/installation/manager-command.mjs +1 -1
- package/dist/src/installation/manager.mjs +17 -2
- package/dist/src/lint/runner.js +2 -2
- package/dist/src/mcp/adapters/codex-runtime.js +360 -56
- package/dist/src/mcp/cli.mjs +587 -77
- package/dist/src/mcp/config-file.mjs +279 -0
- package/dist/src/mcp/config-layers.mjs +348 -0
- package/dist/src/mcp/credentials.mjs +484 -0
- package/dist/src/mcp/grok-build-config.mjs +16 -26
- package/dist/src/mcp/helpers.mjs +106 -2
- package/dist/src/mcp/omp-config.mjs +50 -29
- package/dist/src/mcp/profiles.js +97 -23
- package/dist/src/mcp/profiles.mjs +99 -27
- package/dist/src/mcp/registry.js +264 -57
- package/dist/src/mcp/registry.mjs +198 -45
- package/dist/src/mcp/server.mjs +403 -20
- package/dist/src/mcp/toml-string.mjs +9 -0
- package/dist/src/mcp/toml-strip-mcp.mjs +135 -0
- package/dist/src/mcp/tool-filters.mjs +357 -0
- package/dist/src/mcp/tools/agentic-sandbox.mjs +3 -1
- package/dist/src/mcp/tools/decision.mjs +58 -0
- package/dist/src/mcp/tools/orchestration.mjs +38 -0
- package/dist/src/mcp/tools/subsystems.mjs +39 -8
- package/dist/src/mcp/tools/worker-invoke.mjs +1539 -0
- package/dist/src/mcp/tools/worker-shell-control.mjs +258 -0
- package/dist/src/mcp/tools/worker-stdin-runtime.mjs +79 -0
- package/dist/src/memory/project-registry.js +2 -2
- package/dist/src/mission-protocol/codecs.js +639 -1
- package/dist/src/mission-protocol/types.js +110 -0
- package/dist/src/mission-protocol/worker-status.js +2 -0
- package/dist/src/models/config-loader.js +70 -13
- package/dist/src/models/grok-build-models.js +93 -41
- package/dist/src/models/model-capabilities.v1.json +3091 -24
- package/dist/src/models/model-discovery.js +862 -63
- package/dist/src/models/provider-policy.js +625 -11
- package/dist/src/models/qualification-digest.mjs +17 -0
- package/dist/src/models/resolver.js +259 -0
- package/dist/src/models/worker-direct-readiness.js +529 -0
- package/dist/src/models/worker-dispatch-offer.js +169 -0
- package/dist/src/models/worker-health-readiness.js +131 -0
- package/dist/src/models/worker-installation.mjs +879 -0
- package/dist/src/models/wrapper-route.js +18 -0
- package/dist/src/ops/registry.js +9 -14
- package/dist/src/plugins/standalone-packager.js +4 -3
- package/dist/src/process/bounded-child-process.mjs +387 -0
- package/dist/src/process/cli-command.mjs +75 -0
- package/dist/src/providers/capability-matrix.js +8 -0
- package/dist/src/providers/capability-matrix.yaml +270 -0
- package/dist/src/providers/grok-build-headless.js +43 -21
- package/dist/src/providers/provider-definitions.js +9 -1
- package/dist/src/providers/provider-definitions.mjs +9 -2
- package/dist/src/serve/executor-registry.js +16 -24
- package/dist/src/serve/fleet-mission-conductor.js +9 -0
- package/dist/src/serve/mission-conductor.js +9 -0
- package/dist/src/serve/shared-host-scheduler.js +2 -0
- package/dist/src/serve/worker-invocation.js +538 -0
- package/dist/src/serve/worker-stdin-frame.mjs +107 -0
- package/dist/src/sessions/adapter-registry.js +45 -0
- package/dist/src/sessions/adapters/generic.js +37 -3
- package/dist/src/sessions/adapters/grokbot-locator.js +111 -0
- package/dist/src/sessions/adapters/grokbot.js +423 -24
- package/dist/src/sessions/adapters/muse-workspace-scan.js +108 -0
- package/dist/src/sessions/adapters/muse.js +68 -29
- package/dist/src/sessions/adapters/pi.js +1 -0
- package/dist/src/sessions/batch-contracts.js +13 -1
- package/dist/src/sessions/batch-import.js +23 -42
- package/dist/src/sessions/candidates.js +47 -11
- package/dist/src/sessions/contracts.js +35 -0
- package/dist/src/sessions/coverage-preflight.js +134 -0
- package/dist/src/sessions/discovery.js +3 -0
- package/dist/src/sessions/grok-build-cli.js +67 -5
- package/dist/src/sessions/importer.js +24 -8
- package/dist/src/sessions/index.js +2 -0
- package/dist/src/sessions/repository.js +54 -13
- package/dist/src/sessions/workspace-discovery.js +99 -37
- package/dist/src/skills/adapters/clawhub.js +10 -8
- package/dist/src/skills/adapters/openclaw.js +10 -7
- package/dist/src/smiths/context-pipeline/parallelism-section.js +192 -3
- package/dist/src/smiths/context-pipeline/workspace-context.js +116 -9
- package/dist/src/smiths/hook-bridge/codex-translator.js +6 -5
- package/dist/src/smiths/mcpsmith/example.js +1 -1
- package/dist/src/smiths/toolsmith/runtime-discovery.mjs +10 -14
- package/dist/src/storage/backends/logseq.js +68 -7
- package/dist/src/storage/fortemi-qualification-receipt.js +2 -2
- package/dist/src/testing/generators/test-code-generator.js +57 -30
- package/dist/src/uhp/client.js +11 -1
- package/dist/src/uhp/mission.js +5 -0
- package/dist/src/update/checker.mjs +5 -5
- package/dist/src/update/notifier.mjs +54 -2
- package/dist/src/writing/fidelity.js +18 -1
- package/package.json +18 -1
- package/schemas/mission-v1.schema.json +2429 -0
- package/schemas/models/model-catalog.v1.schema.json +301 -0
- package/schemas/models/model-config-compatibility.v1.schema.json +281 -0
- package/schemas/models/model-policy.v1.schema.json +14 -0
- package/schemas/models/provider-model-capabilities.v1.schema.json +1066 -0
- package/tools/_resolve-impl.mjs +39 -0
- package/tools/agents/deploy-agents.mjs +15 -2
- package/tools/agents/providers/base.mjs +63 -43
- package/tools/agents/providers/grokbot.mjs +11 -12
- package/tools/agents/providers/hermes.mjs +6 -3
- package/tools/agents/providers/omp.mjs +5 -2
- package/tools/agents/providers/openclaw-translator.mjs +19 -5
- package/tools/providers/deepseek-harness-transport.mjs +358 -0
- package/tools/providers/omp-transport.mjs +192 -0
- package/tools/ralph-external/lib/claude-adapter.mjs +389 -0
- package/tools/ralph-external/lib/codex-adapter.mjs +432 -0
- package/tools/ralph-external/lib/deepseek-harness-adapter.mjs +397 -0
- package/tools/ralph-external/lib/factory-adapter.mjs +503 -0
- package/tools/ralph-external/lib/muse-adapter.mjs +364 -0
- package/tools/ralph-external/lib/omp-adapter.mjs +315 -0
- package/tools/ralph-external/lib/opencode-adapter.mjs +575 -0
- package/tools/ralph-external/lib/openhuman-adapter.mjs +75 -0
- package/tools/ralph-external/lib/pi-adapter.mjs +333 -0
- package/tools/ralph-external/lib/provider-adapter.mjs +690 -0
- package/tools/ralph-external/lib/worker-health.mjs +439 -0
- 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 {
|