@aiwg/cli 2026.10.8 → 2026.10.10
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/model-capabilities.v1.json +197 -3
- package/bin/aiwg.mjs +8 -0
- package/dist/src/artifacts/cli.js +9 -5
- package/dist/src/auth/config.js +3 -8
- package/dist/src/auth/resource-credentials.js +35 -2
- package/dist/src/cli/handlers/diagnose.js +13 -0
- package/dist/src/cli/handlers/mc-admission.js +39 -0
- package/dist/src/cli/handlers/mc.js +2 -31
- package/dist/src/cli/handlers/models-help.js +52 -0
- package/dist/src/cli/handlers/models.js +11 -81
- package/dist/src/cli/handlers/resource-versions.js +8 -5
- package/dist/src/cli/handlers/script-runner.js +13 -1
- package/dist/src/cli/handlers/use-addon-profile.js +72 -0
- package/dist/src/cli/handlers/use.js +209 -233
- package/dist/src/cli/ui.js +1 -0
- package/dist/src/config/aiwg-config.js +38 -184
- package/dist/src/config/cli-help.js +54 -0
- package/dist/src/config/cli.js +5 -69
- package/dist/src/config/config-normalization.js +10 -0
- package/dist/src/config/worker-activation-gate.js +26 -0
- package/dist/src/config/worker-adoption-cli.js +102 -0
- package/dist/src/config/worker-adoption.js +368 -0
- package/dist/src/config/worker-config-digest.js +14 -0
- package/dist/src/config/worker-group-templates.js +150 -0
- package/dist/src/config/worker-guidance-warning.js +28 -0
- package/dist/src/config/worker-owner-lock.js +152 -0
- package/dist/src/config/worker-parallelism.js +154 -0
- package/dist/src/config/worker-write.js +75 -0
- package/dist/src/decision/telemetry/exporter.js +2 -1
- package/dist/src/decision/telemetry/index.js +1 -0
- package/dist/src/decision/telemetry/mapping.js +43 -0
- package/dist/src/decision/telemetry/otlp-http.js +5 -2
- package/dist/src/decision/telemetry/redaction.js +6 -1
- package/dist/src/decision/telemetry/types.js +3 -0
- package/dist/src/decision/telemetry/worker.js +69 -0
- package/dist/src/lint/rule-activation.js +15 -7
- package/dist/src/lint/runner.js +2 -2
- package/dist/src/mcp/server.mjs +2 -11
- package/dist/src/mcp/stdio-lifecycle.mjs +27 -0
- package/dist/src/mcp/tools/worker-failure-diagnostics.mjs +9 -0
- package/dist/src/mcp/tools/worker-invoke.mjs +6 -6
- package/dist/src/mission-protocol/codecs.js +8 -38
- package/dist/src/mission-protocol/worker-contract-diagnostics.js +62 -0
- package/dist/src/mission-protocol/worker-invocation-types.js +2 -0
- package/dist/src/models/conductor-data-policy.d.mts +8 -0
- package/dist/src/models/conductor-data-policy.mjs +190 -0
- package/dist/src/models/config-loader.js +19 -3
- package/dist/src/models/config-writer.js +27 -0
- package/dist/src/models/model-capabilities.v1.json +197 -3
- package/dist/src/models/provider-policy.js +7 -155
- package/dist/src/models/resolver.js +2 -293
- package/dist/src/models/worker-artifact-binding.mjs +12 -0
- package/dist/src/models/worker-direct-readiness-policy.js +155 -0
- package/dist/src/models/worker-direct-readiness.js +9 -218
- package/dist/src/models/worker-health-readiness.js +105 -26
- package/dist/src/models/worker-installation.mjs +12 -14
- package/dist/src/models/worker-member-references.js +64 -0
- package/dist/src/models/worker-plan.js +317 -0
- package/dist/src/models/worker-selection-policy.js +217 -0
- package/dist/src/models/wrapper-deployment.js +1 -1
- package/dist/src/resources/web-release.js +76 -65
- package/dist/src/serve/shared-host-admission-checks.js +116 -0
- package/dist/src/serve/shared-host-scheduler.js +25 -107
- package/dist/src/serve/shared-host-starvation.js +73 -0
- package/dist/src/serve/worker-artifacts.d.mts +4 -0
- package/dist/src/serve/worker-artifacts.mjs +56 -0
- package/dist/src/serve/worker-command-request.js +68 -0
- package/dist/src/serve/worker-invocation-paths.js +144 -0
- package/dist/src/serve/worker-invocation-preparation.js +169 -0
- package/dist/src/serve/worker-invocation.js +24 -291
- package/dist/src/serve/worker-stdin-frame.d.mts +2 -2
- package/dist/src/serve/worker-stdin-frame.mjs +6 -6
- package/dist/src/smiths/context-pipeline/parallelism-section.js +19 -7
- package/dist/src/storage/backends/fortemi.js +33 -29
- package/dist/src/storage/backends/logseq.js +16 -3
- package/dist/src/storage/backends/postgres.js +74 -33
- package/dist/src/storage/backends/postgrest.js +37 -13
- package/dist/src/storage/config.js +6 -1
- package/dist/src/uhp/config.js +4 -0
- package/package.json +9 -1
- package/schemas/mission-v1.schema.json +20 -19
- package/schemas/models/model-config-compatibility.v1.schema.json +16 -0
- package/tools/agents/providers/claude-post-deploy.mjs +35 -0
- package/tools/agents/providers/claude.mjs +3 -30
- package/tools/agents/providers/codex-post-deploy.mjs +13 -0
- package/tools/agents/providers/codex.mjs +2 -8
- package/tools/agents/providers/conductor-data-guard.mjs +61 -0
- package/tools/agents/providers/rule-delivery.mjs +3 -16
- package/tools/agents/providers/rule-presentation.mjs +38 -0
- package/tools/plugin/package-plugins.mjs +1 -1
- package/tools/ralph-external/lib/claude-adapter.mjs +2 -2
- package/tools/ralph-external/lib/codex-adapter.mjs +3 -3
- package/tools/ralph-external/lib/managed-worker-attempt.mjs +86 -0
- package/tools/ralph-external/lib/provider-adapter.mjs +11 -260
- package/tools/ralph-external/lib/session-attempt.mjs +85 -0
- package/tools/ralph-external/lib/strict-worker-json.mjs +190 -0
- package/tools/ralph-external/lib/worker-artifacts.mjs +209 -0
- package/tools/ralph-external/lib/worker-final.mjs +143 -0
- package/tools/ralph-external/lib/worker-inputs.mjs +55 -0
- package/tools/ralph-external/lib/worker-isolation.mjs +2 -44
- package/tools/ralph-external/lib/worker-launch-cli.mjs +47 -0
- package/tools/ralph-external/lib/worker-native-sandbox.mjs +83 -0
- package/tools/ralph-external/session-launcher.mjs +73 -217
- package/vscode-extension/schemas/aiwg.config.v1.json +161 -2
|
@@ -1,14 +1,17 @@
|
|
|
1
|
+
import { WorkerRequestArgumentError } from './worker-command-request.js';
|
|
2
|
+
export { parseWorkerCommandArgs } from './worker-command-request.js';
|
|
1
3
|
/** One nonlaunching typed renderer shared by models resolve and Ralph. */
|
|
2
4
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { inspectWorkerPath as inspectPath, workerOutputPaths, readBrief, assertConductorWorkerPlan } from './worker-invocation-paths.js';
|
|
6
|
+
export { inspectWorkerPath } from './worker-invocation-paths.js';
|
|
7
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
7
8
|
import { resolveWorkerPlan } from '../models/resolver.js';
|
|
8
9
|
import { workerConfigDigest, validateWorkerHealthConfig } from '../config/aiwg-config.js';
|
|
9
10
|
import { assertValidWorkerPayload } from '../mission-protocol/codecs.js';
|
|
10
11
|
import { qualificationDigest } from '../models/qualification-digest.mjs';
|
|
11
|
-
import {
|
|
12
|
+
import { prepareWorkerInvocation } from './worker-invocation-preparation.js';
|
|
13
|
+
export { prepareWorkerInvocation, mcpToolSchemaDigest } from './worker-invocation-preparation.js';
|
|
14
|
+
import { isOperatorQualifiedWorkerAdapter, assertOperatorWorkerNativeArgv, assertWorkerLaunchMode, WorkerRoleEffortError, requiredWorkerControls, isCanonicalWorkerModelKey, WorkerPolicyError, DEFAULT_WORKER_SELECTION } from '../models/provider-policy.js';
|
|
12
15
|
/** Frozen per-attempt windows; parent/transport ceilings include time already spent. */
|
|
13
16
|
export function resolveWorkerEffectiveHealth(config, input) {
|
|
14
17
|
const roleDefaults = { efficiency: 1, coding: 1, reasoning: 2 };
|
|
@@ -70,165 +73,6 @@ export function resolveWorkerEffectiveHealth(config, input) {
|
|
|
70
73
|
assertValidWorkerPayload('workerEffectiveHealth', health);
|
|
71
74
|
return health;
|
|
72
75
|
}
|
|
73
|
-
/**
|
|
74
|
-
* The exact digest formula an MCP worker binding's `inputSchemaDigest`/
|
|
75
|
-
* `outputSchemaDigest` are pinned against (used below and reused verbatim by
|
|
76
|
-
* the #2892 MCP worker-invoke bridge's own live `tools/list` schema-drift
|
|
77
|
-
* check — see `src/mcp/tools/worker-invoke.mjs` M3). Deliberately the raw
|
|
78
|
-
* `JSON.stringify` of the schema object, NOT `workerConfigDigest`'s
|
|
79
|
-
* canonicalized (sorted-key) form — a schema's own declared key order is
|
|
80
|
-
* part of what gets pinned at qualification time.
|
|
81
|
-
*/
|
|
82
|
-
export function mcpToolSchemaDigest(schema) {
|
|
83
|
-
return `sha256:${createHash('sha256').update(JSON.stringify(schema)).digest('hex')}`;
|
|
84
|
-
}
|
|
85
|
-
export async function prepareWorkerInvocation(invocation, qualification) {
|
|
86
|
-
const result = structuredClone(invocation);
|
|
87
|
-
// shellCommand is derived syntax, never authority supplied by a brief.
|
|
88
|
-
if (result.transport === 'shell')
|
|
89
|
-
result.shellCommand = '';
|
|
90
|
-
// Validate the established invocation shape first. Health is optional and
|
|
91
|
-
// is checked after policy, installation and transport qualification below.
|
|
92
|
-
const contract = { ...result };
|
|
93
|
-
delete contract.health;
|
|
94
|
-
assertValidWorkerPayload('workerResolvedInvocation', contract);
|
|
95
|
-
if ((qualification.platform ?? process.platform) !== 'linux') {
|
|
96
|
-
throw new Error('Unsupported worker invocation platform; Linux is required');
|
|
97
|
-
}
|
|
98
|
-
if (!Number.isFinite(qualification.now) || Date.parse(result.expiresAt) <= qualification.now) {
|
|
99
|
-
throw new Error('Worker invocation is expired or has no validation clock');
|
|
100
|
-
}
|
|
101
|
-
if (qualification.adapter.id !== qualification.adapterId) {
|
|
102
|
-
throw new Error('Worker invocation adapter identity mismatch');
|
|
103
|
-
}
|
|
104
|
-
assertWorkerLaunchMode(qualification.adapter, qualification.permissions);
|
|
105
|
-
if (result.isolation !== undefined && result.isolation !== (qualification.permissions?.launchMode ?? 'systemd-unit')) {
|
|
106
|
-
throw new WorkerPolicyError('OG_PIN_MISMATCH', 'Worker launch mode differs from the execution policy');
|
|
107
|
-
}
|
|
108
|
-
for (const [key, value] of Object.entries(qualification.expectedDigests ?? {})) {
|
|
109
|
-
if (result[key] !== value)
|
|
110
|
-
throw new WorkerPolicyError('OG_PIN_MISMATCH', `Worker invocation ${key} changed`);
|
|
111
|
-
}
|
|
112
|
-
const verified = Date.parse(qualification.adapter.qualification.verifiedAt ?? '');
|
|
113
|
-
const policyDays = qualification.policy?.freshness?.policyDays ?? DEFAULT_WORKER_SELECTION.freshness.policyDays;
|
|
114
|
-
if (!Number.isFinite(verified) || verified > qualification.now || qualification.now - verified >= policyDays * 86400000) {
|
|
115
|
-
throw new Error('Worker installation qualification is stale or invalid');
|
|
116
|
-
}
|
|
117
|
-
assertWorkerSelectionAllowed(result.selected, qualification);
|
|
118
|
-
const profile = qualifyWorkerInvocationProfile(qualification.adapter, {
|
|
119
|
-
profile: result.selected.profileRef, transport: result.transport,
|
|
120
|
-
cliVersion: qualification.cliVersion,
|
|
121
|
-
requiredControls: qualification.requiredControls, policy: qualification.policy,
|
|
122
|
-
permissions: qualification.permissions, effort: result.selected.effort.value, boundary: qualification.boundary,
|
|
123
|
-
mcp: qualification.mcp,
|
|
124
|
-
});
|
|
125
|
-
if (requiresWorkerExternalBoundary(qualification.adapter, { ...qualification,
|
|
126
|
-
profile: result.selected.profileRef, transport: result.transport, effort: result.selected.effort.value })) {
|
|
127
|
-
const installed = qualification.installation;
|
|
128
|
-
const evidence = profile.installationQualification;
|
|
129
|
-
if (!installed || !evidence || ['scopeDigest', 'fixtureDigest', 'verifiedAt', 'evidenceRef']
|
|
130
|
-
.some(key => installed.evidence?.[key] !== evidence[key])
|
|
131
|
-
|| !qualification.boundary || qualificationDigest(qualification.boundary) !== qualificationDigest(installed.boundary)) {
|
|
132
|
-
throw new WorkerPolicyError('OG_ADAPTER_UNQUALIFIED', 'Worker installation evidence and qualified boundary must match');
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
if (profile.installationQualification) {
|
|
136
|
-
const installedAt = Date.parse(profile.installationQualification.verifiedAt);
|
|
137
|
-
if (!Number.isFinite(installedAt) || installedAt > qualification.now
|
|
138
|
-
|| qualification.now - installedAt >= policyDays * 86400000) {
|
|
139
|
-
throw new Error('Worker installation qualification is stale or invalid');
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
const scopeDigest = profile.installationQualification?.scopeDigest ?? qualification.adapter.qualification.fixtureDigest;
|
|
143
|
-
if (result.selected.qualificationDigest !== scopeDigest)
|
|
144
|
-
throw new WorkerPolicyError('OG_PIN_MISMATCH', 'Worker qualification digest changed');
|
|
145
|
-
const absolute = (value) => typeof value === 'string' && path.posix.isAbsolute(value) && !value.includes('\0');
|
|
146
|
-
for (const key of ['cwd', 'briefPath', 'stdoutPath', 'stderrPath', 'finalPath']) {
|
|
147
|
-
if (!absolute(result[key]))
|
|
148
|
-
throw new Error(`Worker invocation requires absolute ${key}`);
|
|
149
|
-
}
|
|
150
|
-
const briefPath = path.posix.normalize(result.briefPath);
|
|
151
|
-
if ([result.stdoutPath, result.stderrPath, result.finalPath].some(value => path.posix.normalize(value) === briefPath)
|
|
152
|
-
|| path.posix.normalize(result.stdoutPath) === path.posix.normalize(result.stderrPath)) {
|
|
153
|
-
throw new Error('Worker invocation input/output paths must be distinct');
|
|
154
|
-
}
|
|
155
|
-
if (result.transport === 'shell') {
|
|
156
|
-
assertOperatorWorkerNativeArgv(qualification.adapter, result.argv, result.selected, qualification.permissions);
|
|
157
|
-
if (!absolute(result.argv[0]) || result.argv[0].includes('=') || result.argv.some(value => value.includes('\0'))) {
|
|
158
|
-
throw new WorkerPolicyError('OG_CONFIG_REFERENCE', 'Worker invocation requires an absolute binary without equals and NUL-free argv');
|
|
159
|
-
}
|
|
160
|
-
if (result.envNames.some(name => !profile.envAllowlist.includes(name))) {
|
|
161
|
-
throw new Error('Worker invocation environment is outside the qualified allowlist');
|
|
162
|
-
}
|
|
163
|
-
const quote = (value) => `'${value.replaceAll("'", "'\"'\"'")}'`;
|
|
164
|
-
// Only validated environment names appear as shell syntax; their values
|
|
165
|
-
// remain in the conductor environment and are never printed in the plan.
|
|
166
|
-
const environment = result.envNames.map(name => `${name}="\${${name}-}"`).join(' ');
|
|
167
|
-
// The frozen 'closed' value means empty stdin (immediate EOF). 'brief-json'
|
|
168
|
-
// pipes a conductor-built params document framed from the brief file
|
|
169
|
-
// (worker-stdin-frame.mjs), so brief text never enters any argv.
|
|
170
|
-
const worker = `env -i${environment ? ` ${environment}` : ''} ${result.argv.map(quote).join(' ')}`;
|
|
171
|
-
const outputs = `> ${quote(result.stdoutPath)} 2> ${quote(result.stderrPath)}`;
|
|
172
|
-
if (result.stdin === 'brief-json') {
|
|
173
|
-
const framer = fileURLToPath(new URL('./worker-stdin-frame.mjs', import.meta.url));
|
|
174
|
-
const frame = [process.execPath, framer, result.briefPath, result.selected.model, result.cwd].map(quote).join(' ');
|
|
175
|
-
result.shellCommand = `cd ${quote(result.cwd)} && env -i ${frame} | ${worker} ${outputs}`;
|
|
176
|
-
}
|
|
177
|
-
else if (result.stdin === 'adapter-frame') {
|
|
178
|
-
const binding = qualification.stdinFramerBinding;
|
|
179
|
-
if (!binding || !absolute(binding.storePath) || !/^sha256:[a-f0-9]{64}$/.test(binding.storeDigest)) {
|
|
180
|
-
throw new Error('Adapter stdin framer requires a trusted installation binding');
|
|
181
|
-
}
|
|
182
|
-
const framer = fileURLToPath(new URL('./worker-stdin-frame.mjs', import.meta.url));
|
|
183
|
-
const frame = [process.execPath, framer, '--adapter-frame', result.briefPath, result.cwd,
|
|
184
|
-
result.selected.cli, qualification.adapterId, result.selected.qualificationDigest, result.briefDigest,
|
|
185
|
-
binding.storePath, binding.storeDigest, JSON.stringify(qualification.policy ?? null)].map(quote).join(' ');
|
|
186
|
-
result.shellCommand = `cd ${quote(result.cwd)} && env -i${environment ? ` ${environment}` : ''} ${frame} | ${worker} ${outputs}`;
|
|
187
|
-
}
|
|
188
|
-
else if (result.stdin === 'closed' || result.stdin === 'brief-text') {
|
|
189
|
-
const stdin = result.stdin === 'closed' ? '< /dev/null' : `< ${quote(result.briefPath)}`;
|
|
190
|
-
result.shellCommand = `cd ${quote(result.cwd)} && ${worker} ${stdin} ${outputs}`;
|
|
191
|
-
}
|
|
192
|
-
else {
|
|
193
|
-
throw new Error(`Unsupported worker stdin mode: ${result.stdin}`);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
else {
|
|
197
|
-
const binding = result.mcp;
|
|
198
|
-
for (const key of ['serverRef', 'tool', 'inputSchemaDigest', 'outputSchemaDigest', 'sessionIsolation']) {
|
|
199
|
-
if (binding[key] !== profile.mcp[key])
|
|
200
|
-
throw new WorkerPolicyError('OG_PIN_MISMATCH', `MCP worker binding changed: ${key}`);
|
|
201
|
-
}
|
|
202
|
-
if (binding.timeoutSeconds > result.limits.wallSeconds) {
|
|
203
|
-
throw new Error('MCP worker timeout exceeds the invocation deadline');
|
|
204
|
-
}
|
|
205
|
-
for (const key of ['input', 'output']) {
|
|
206
|
-
const schema = qualification.mcpSchemas?.[key];
|
|
207
|
-
if (!schema || mcpToolSchemaDigest(schema) !== binding[`${key}SchemaDigest`]) {
|
|
208
|
-
throw new WorkerPolicyError('OG_PIN_MISMATCH', `MCP worker ${key} schema digest changed`);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
const { default: Ajv2020 } = await import('ajv/dist/2020.js');
|
|
212
|
-
const { default: addFormats } = await import('ajv-formats');
|
|
213
|
-
const validate = addFormats(new Ajv2020({ strict: true, allErrors: true })).compile(qualification.mcpSchemas.input);
|
|
214
|
-
if (!validate(binding.arguments))
|
|
215
|
-
throw new Error('MCP worker arguments violate the pinned input schema');
|
|
216
|
-
}
|
|
217
|
-
assertValidWorkerPayload('workerResolvedInvocation', result);
|
|
218
|
-
if (result.health !== undefined) {
|
|
219
|
-
if (result.health.profileRef !== result.selected.profileRef
|
|
220
|
-
|| result.health.hardTimeoutSeconds > Math.min(result.limits.wallSeconds, result.mcp?.timeoutSeconds ?? Infinity)) {
|
|
221
|
-
throw new Error('Worker health profile or wall ceiling mismatch');
|
|
222
|
-
}
|
|
223
|
-
if (['native', 'external'].includes(result.health.stepEnforcement)
|
|
224
|
-
&& profile.controls.stepCap?.support !== result.health.stepEnforcement)
|
|
225
|
-
throw new Error('Unqualified worker step enforcement');
|
|
226
|
-
}
|
|
227
|
-
assertWorkerHealthQualification(profile, { profileRef: result.selected.profileRef, cliVersion: qualification.cliVersion,
|
|
228
|
-
accountScope: qualification.installation?.accountScope ?? '', inferenceProvider: result.selected.inferenceProvider,
|
|
229
|
-
model: result.selected.model, effort: result.selected.effort.value });
|
|
230
|
-
return result;
|
|
231
|
-
}
|
|
232
76
|
const invocationChecks = {
|
|
233
77
|
invocation_build_failed: { pointer: '/invocation/buildInvocation',
|
|
234
78
|
message: 'Worker adapter could not build the invocation; check the selected model, effort, permissions and request paths.' },
|
|
@@ -269,127 +113,7 @@ export async function buildWorkerInvocationTransport(driver, input) {
|
|
|
269
113
|
}
|
|
270
114
|
return transport;
|
|
271
115
|
}
|
|
272
|
-
/** Only fixed argument guidance is safe to expose in public request diagnostics. */
|
|
273
|
-
class WorkerRequestArgumentError extends WorkerPolicyError {
|
|
274
|
-
pointer;
|
|
275
|
-
constructor(key) {
|
|
276
|
-
super('OG_CONFIG_REFERENCE', key === 'task'
|
|
277
|
-
? `Missing required --task. Valid task classes: ${Object.keys(DEFAULT_WORKER_SELECTION.taskClasses).join(', ')}, plus classes configured in defaults.workerSelection.taskClasses.`
|
|
278
|
-
: `--${key.replace('_', '-')} must be an absolute path; relative paths are not resolved against cwd or --workdir.`);
|
|
279
|
-
this.pointer = `/request/${key}`;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
/** Reject unsupported syntax before discovery or any path reads. */
|
|
283
|
-
export function parseWorkerCommandArgs(args) {
|
|
284
|
-
const names = { role: 'role', task: 'task', group: 'group', brief: 'brief',
|
|
285
|
-
workdir: 'workdir', 'final-output': 'final_output', transport: 'transport', format: 'format' };
|
|
286
|
-
const result = {};
|
|
287
|
-
let json = false;
|
|
288
|
-
let premiumAuthorized = false;
|
|
289
|
-
for (let i = 0; i < args.length; i++) {
|
|
290
|
-
const token = args[i];
|
|
291
|
-
if (token === '--authorize-premium') {
|
|
292
|
-
if (premiumAuthorized)
|
|
293
|
-
throw new Error('Duplicate --authorize-premium');
|
|
294
|
-
premiumAuthorized = true;
|
|
295
|
-
continue;
|
|
296
|
-
}
|
|
297
|
-
if (token === '--json') {
|
|
298
|
-
if (json)
|
|
299
|
-
throw new Error('Duplicate --json');
|
|
300
|
-
json = true;
|
|
301
|
-
continue;
|
|
302
|
-
}
|
|
303
|
-
const match = /^--([^=]+)(?:=([\s\S]*))?$/.exec(token);
|
|
304
|
-
if (!match || !Object.hasOwn(names, match[1]))
|
|
305
|
-
throw new Error(`Unsupported models resolve argument: ${token}`);
|
|
306
|
-
const key = names[match[1]];
|
|
307
|
-
if (key in result)
|
|
308
|
-
throw new Error(`Duplicate --${match[1]}`);
|
|
309
|
-
const value = match[2] ?? args[++i];
|
|
310
|
-
if (!value || value.startsWith('--') && match[2] === undefined || value.includes('\0')) {
|
|
311
|
-
if (key === 'task' && !value?.includes('\0'))
|
|
312
|
-
throw new WorkerRequestArgumentError('task');
|
|
313
|
-
throw new Error(`--${match[1]} requires a value`);
|
|
314
|
-
}
|
|
315
|
-
result[key] = value;
|
|
316
|
-
}
|
|
317
|
-
if (json && result.format && result.format !== 'json')
|
|
318
|
-
throw new Error('--json conflicts with --format');
|
|
319
|
-
if (json)
|
|
320
|
-
result.format = 'json';
|
|
321
|
-
for (const key of ['role', 'task', 'group', 'brief', 'workdir', 'final_output']) {
|
|
322
|
-
if (key === 'task' && !result[key])
|
|
323
|
-
throw new WorkerRequestArgumentError('task');
|
|
324
|
-
if (!result[key])
|
|
325
|
-
throw new Error(`Missing ${key}`);
|
|
326
|
-
}
|
|
327
|
-
if (!['efficiency', 'coding', 'reasoning'].includes(result.role))
|
|
328
|
-
throw new Error('Unsupported role');
|
|
329
|
-
if (!/^[a-z][a-z0-9-]{0,62}$/.test(result.group))
|
|
330
|
-
throw new Error('Invalid group');
|
|
331
|
-
if (result.transport && !['shell', 'mcp'].includes(result.transport))
|
|
332
|
-
throw new Error('Unsupported transport');
|
|
333
|
-
if (result.format && !['shell', 'json'].includes(result.format))
|
|
334
|
-
throw new Error('Unsupported renderer; use POSIX shell or JSON');
|
|
335
|
-
return { ...result, ...(premiumAuthorized ? { authority: { premiumAuthorized: true, maxAutoTier: 3 } } : {}) };
|
|
336
|
-
}
|
|
337
116
|
const bytesDigest = (bytes) => `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
|
|
338
|
-
export async function inspectWorkerPath(filename, kind) {
|
|
339
|
-
if (!path.isAbsolute(filename) || filename.includes('\0'))
|
|
340
|
-
throw new Error('Absolute NUL-free paths are required');
|
|
341
|
-
if (filename.split('/').includes('..'))
|
|
342
|
-
throw new Error('Worker path traversal is not allowed');
|
|
343
|
-
const normalized = path.resolve(filename);
|
|
344
|
-
let current = normalized;
|
|
345
|
-
while (true) {
|
|
346
|
-
try {
|
|
347
|
-
const canonical = await realpath(current);
|
|
348
|
-
if (canonical !== current)
|
|
349
|
-
throw new Error(`Symlink path is not allowed: ${filename}`);
|
|
350
|
-
if (current !== normalized)
|
|
351
|
-
return false;
|
|
352
|
-
const stat = await lstat(normalized);
|
|
353
|
-
if (kind === 'directory' ? !stat.isDirectory() : !stat.isFile() || stat.nlink !== 1) {
|
|
354
|
-
throw new Error(`Expected an unaliased ${kind}: ${filename}`);
|
|
355
|
-
}
|
|
356
|
-
return true;
|
|
357
|
-
}
|
|
358
|
-
catch (error) {
|
|
359
|
-
if (error.code !== 'ENOENT')
|
|
360
|
-
throw error;
|
|
361
|
-
const parent = path.dirname(current);
|
|
362
|
-
if (parent === current)
|
|
363
|
-
throw error;
|
|
364
|
-
current = parent;
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
const inspectPath = inspectWorkerPath;
|
|
369
|
-
async function readBrief(filename) {
|
|
370
|
-
if (!await inspectPath(filename, 'file'))
|
|
371
|
-
return null;
|
|
372
|
-
const handle = await open(filename, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
373
|
-
try {
|
|
374
|
-
const stat = await handle.stat();
|
|
375
|
-
if (!stat.isFile() || stat.nlink !== 1 || stat.size > 262144)
|
|
376
|
-
throw new Error('Brief must be a bounded regular file (256 KiB)');
|
|
377
|
-
const buffer = Buffer.alloc(262145);
|
|
378
|
-
let length = 0;
|
|
379
|
-
while (length < buffer.length) {
|
|
380
|
-
const { bytesRead } = await handle.read(buffer, length, buffer.length - length, null);
|
|
381
|
-
if (!bytesRead)
|
|
382
|
-
break;
|
|
383
|
-
length += bytesRead;
|
|
384
|
-
}
|
|
385
|
-
if (length > 262144)
|
|
386
|
-
throw new Error('Brief exceeds 256 KiB');
|
|
387
|
-
return buffer.subarray(0, length);
|
|
388
|
-
}
|
|
389
|
-
finally {
|
|
390
|
-
await handle.close();
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
117
|
/** Structured root-check failure; only these explicit path details are public. */
|
|
394
118
|
export class WorkerPathScopeError extends WorkerPolicyError {
|
|
395
119
|
pointer;
|
|
@@ -399,7 +123,7 @@ export class WorkerPathScopeError extends WorkerPolicyError {
|
|
|
399
123
|
}
|
|
400
124
|
}
|
|
401
125
|
/** Re-resolve saved JSON through the same trusted owners; saved argv is never authority. */
|
|
402
|
-
export async function revalidateWorkerInvocationPlan(saved, snapshot, drivers, now = new Date().toISOString()) {
|
|
126
|
+
export async function revalidateWorkerInvocationPlan(saved, snapshot, drivers, now = new Date().toISOString(), invocationPath) {
|
|
403
127
|
const managed = saved.mode === 'managed' && snapshot.config.parallelism?.worker_mode === 'managed';
|
|
404
128
|
if (!saved.request || (saved.mode !== 'direct' && !managed) || saved.mode !== (snapshot.config.parallelism?.worker_mode ?? 'direct') || saved.status !== 'eligible'
|
|
405
129
|
|| (managed && saved.invocation?.mode !== 'managed')
|
|
@@ -407,7 +131,9 @@ export async function revalidateWorkerInvocationPlan(saved, snapshot, drivers, n
|
|
|
407
131
|
throw new Error('A fresh launchable models resolve JSON plan is required');
|
|
408
132
|
}
|
|
409
133
|
assertValidWorkerPayload('workerResolvedInvocation', saved.invocation);
|
|
410
|
-
|
|
134
|
+
if (saved.invocation.invocationId)
|
|
135
|
+
await assertConductorWorkerPlan(saved, invocationPath, drivers);
|
|
136
|
+
const current = await resolveWorkerInvocation(snapshot, saved.request, drivers, now, { digests: saved.digests, briefDigest: saved.invocation.briefDigest ?? null, invocationId: saved.invocation.invocationId });
|
|
411
137
|
// Expiry is calculated from the resolution clock. All execution bindings,
|
|
412
138
|
// controls, thresholds and paths must remain byte-for-byte digest equivalent.
|
|
413
139
|
if (!current.invocation || current.status !== 'eligible'
|
|
@@ -449,7 +175,7 @@ export async function resolveWorkerInvocation(snapshot, request, drivers, now =
|
|
|
449
175
|
if (managedIntent && workerConfigDigest(brief.intent) !== workerConfigDigest(managedIntent)) {
|
|
450
176
|
throw new Error('Managed brief differs from parent task authority');
|
|
451
177
|
}
|
|
452
|
-
if (brief.intent.role !== request.role || brief.intent.taskClass !== request.task || brief.intent.group !== request.group
|
|
178
|
+
if (brief.intent.role !== (snapshot.models.defaults.workerSelection?.roleGuidance?.[request.role]?.baseRole ?? request.role) || brief.intent.taskClass !== request.task || brief.intent.group !== request.group
|
|
453
179
|
|| brief.workspace.path !== request.workdir || brief.output.resultPath !== request.final_output) {
|
|
454
180
|
throw new WorkerPolicyError('OG_PIN_MISMATCH', 'Brief role/task/group/workspace/output differs from request');
|
|
455
181
|
}
|
|
@@ -545,14 +271,20 @@ export async function resolveWorkerInvocation(snapshot, request, drivers, now =
|
|
|
545
271
|
requiredControls: healthConfig?.requireStepCap ? ['stepCap'] : [] });
|
|
546
272
|
pointer = '/parallelism/health';
|
|
547
273
|
failureCode = 'OG_ADAPTER_UNQUALIFIED';
|
|
548
|
-
const healthInput = { role: request.role, effort: tuple.effort,
|
|
274
|
+
const healthInput = { role: plan.policy?.roleGuidance?.[request.role]?.baseRole ?? request.role, effort: tuple.effort,
|
|
549
275
|
modelKey: tuple.modelKey, profileRef: tuple.profile, wallSeconds: limits.wallSeconds,
|
|
550
276
|
transportSeconds: adapter.invocation.kind === 'http' ? adapter.invocation.timeoutSeconds : undefined,
|
|
551
277
|
stepEnforcement: controls.includes('stepCap') ? profile.controls.stepCap.support : 'unavailable' };
|
|
552
278
|
let effectiveHealth = resolveWorkerEffectiveHealth(healthConfig, healthInput);
|
|
553
|
-
const
|
|
279
|
+
const invocationId = expected?.invocationId ?? randomUUID();
|
|
280
|
+
const transport = await buildWorkerInvocationTransport(driver, { invocationId, selected: structuredClone(selected), permissions: structuredClone(tuple.permissions),
|
|
554
281
|
cwd: request.workdir, briefPath: request.brief, brief: bytes?.toString('utf8') ?? '', finalPath, limits: structuredClone(limits),
|
|
555
|
-
health: structuredClone(effectiveHealth) });
|
|
282
|
+
health: structuredClone(effectiveHealth), role: request.role });
|
|
283
|
+
for (const output of Object.values(workerOutputPaths({ ...transport, selected, transport: tuple.transport, finalPath, stdoutPath, stderrPath }))) {
|
|
284
|
+
await inspectPath(output, 'output');
|
|
285
|
+
if (path.resolve(request.brief) === output)
|
|
286
|
+
throw new Error('Brief and output paths must be distinct');
|
|
287
|
+
}
|
|
556
288
|
pointer = '/invocation/buildInvocation';
|
|
557
289
|
if (transport.stdin === 'adapter-frame' && typeof driver.buildWorkerStdinFrame !== 'function')
|
|
558
290
|
throw new Error('Installed adapter stdin framer is required');
|
|
@@ -564,7 +296,8 @@ export async function resolveWorkerInvocation(snapshot, request, drivers, now =
|
|
|
564
296
|
isolation: tuple.permissions.launchMode ?? 'systemd-unit',
|
|
565
297
|
policyDigest, catalogDigest: plan.digests.catalogDigest, profileDigest: plan.digests.profileDigest,
|
|
566
298
|
expiresAt, cwd: request.workdir, briefPath: request.brief, stdoutPath, stderrPath, finalPath,
|
|
567
|
-
...(transport.artifacts ? { artifacts: transport.artifacts
|
|
299
|
+
...(transport.artifacts ? { artifacts: transport.artifacts,
|
|
300
|
+
...(transport.artifacts.some(artifact => artifact.location === 'workspace-relative') ? { invocationId } : {}) } : {}),
|
|
568
301
|
argv: transport.argv, stdin: transport.stdin, envNames: transport.envNames, mcp: transport.mcp,
|
|
569
302
|
...(tuple.transport === 'shell' ? { shellCommand: '' } : {}),
|
|
570
303
|
readiness: bytes !== null && workspaceExists && outputParentsExist ? 'launchable' : 'preparatory', briefDigest,
|
|
@@ -3,8 +3,8 @@ export const WORKER_STDIN_FRAME_MAX_BRIEF_BYTES: number;
|
|
|
3
3
|
export const WORKER_STDIN_FRAME_MAX_BYTES: number;
|
|
4
4
|
export function recordedWorkerStdinBytes(input: {
|
|
5
5
|
filename: string; workspace: string; cli: string; adapterId: string; scopeDigest: string; briefDigest: string;
|
|
6
|
-
storePath: string; storeDigest: string; policy?: import('../models/types.js').WorkerSelectionPolicy;
|
|
6
|
+
storePath: string; storeDigest: string; model?: string; policy?: import('../models/types.js').WorkerSelectionPolicy;
|
|
7
7
|
}): Promise<Buffer | null>;
|
|
8
8
|
export function workerStdinDocument(input: { brief: string | Buffer; model: string; cwd: string }): string;
|
|
9
9
|
export function workerStdinBytes(invocation: { stdin: 'closed' | 'brief-text' | 'brief-json' | 'adapter-frame'; cwd?: string;
|
|
10
|
-
selected?: { model?: string } }, brief: string | Buffer, adapter?: { buildWorkerStdinFrame?(brief: string | Buffer): string | Uint8Array }): Buffer | null;
|
|
10
|
+
selected?: { model?: string } }, brief: string | Buffer, adapter?: { buildWorkerStdinFrame?(brief: string | Buffer, invocation?: { selected?: { model?: string } }): string | Uint8Array }): Buffer | null;
|
|
@@ -39,7 +39,7 @@ export function workerStdinBytes(invocation, brief, adapter) {
|
|
|
39
39
|
if (invocation.stdin === 'adapter-frame') {
|
|
40
40
|
if (typeof adapter?.buildWorkerStdinFrame !== 'function') throw new Error('Installed adapter stdin framer is required');
|
|
41
41
|
if (Buffer.byteLength(brief) > WORKER_STDIN_FRAME_MAX_BRIEF_BYTES) throw new Error('Brief exceeds the framing bound');
|
|
42
|
-
const frame = adapter.buildWorkerStdinFrame(brief);
|
|
42
|
+
const frame = adapter.buildWorkerStdinFrame(brief, invocation);
|
|
43
43
|
if (typeof frame !== 'string' && !(frame instanceof Uint8Array)) throw new Error('Adapter stdin frame must be bytes or text');
|
|
44
44
|
if (Buffer.byteLength(frame) === 0) throw new Error('Adapter stdin frame must not be empty');
|
|
45
45
|
if (Buffer.byteLength(frame) > WORKER_STDIN_FRAME_MAX_BYTES) throw new Error('Adapter stdin frame exceeds byte bound');
|
|
@@ -67,7 +67,7 @@ async function readBounded(filename) {
|
|
|
67
67
|
|
|
68
68
|
/** Bound host authority only; workspace is an execution scope, never a config source. */
|
|
69
69
|
export async function recordedWorkerStdinBytes({ filename, workspace, cli, adapterId, scopeDigest, briefDigest,
|
|
70
|
-
storePath, storeDigest, policy }) {
|
|
70
|
+
storePath, storeDigest, policy, model }) {
|
|
71
71
|
if (!filename?.startsWith('/') || !storePath?.startsWith('/')
|
|
72
72
|
|| !/^sha256:[a-f0-9]{64}$/.test(storeDigest ?? '')) throw new Error('Invalid adapter stdin framing request');
|
|
73
73
|
const { loadRecordedWorkerInstallations } = await import('../models/worker-installation.mjs');
|
|
@@ -80,7 +80,7 @@ export async function recordedWorkerStdinBytes({ filename, workspace, cli, adapt
|
|
|
80
80
|
|| driver.qualification.installation?.evidence?.scopeDigest !== scopeDigest) throw new Error('Adapter stdin binding changed');
|
|
81
81
|
const brief = await readBounded(filename);
|
|
82
82
|
if (`sha256:${createHash('sha256').update(brief).digest('hex')}` !== briefDigest) throw new Error('Adapter stdin brief changed');
|
|
83
|
-
return workerStdinBytes({ stdin: 'adapter-frame' }, brief, driver);
|
|
83
|
+
return workerStdinBytes({ stdin: 'adapter-frame', selected: { model } }, brief, driver);
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
// Pipeline stage: `worker-stdin-frame.mjs BRIEF_PATH MODEL CWD`. Only the
|
|
@@ -89,13 +89,13 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
|
|
|
89
89
|
const [briefPath, model, cwd] = process.argv.slice(2);
|
|
90
90
|
try {
|
|
91
91
|
if (briefPath === '--adapter-frame') {
|
|
92
|
-
const [filename, workspace, cli, adapterId, scopeDigest, briefDigest, storePath, storeDigest, policyJson] = process.argv.slice(3);
|
|
93
|
-
if (process.argv.length !==
|
|
92
|
+
const [filename, workspace, cli, adapterId, scopeDigest, briefDigest, storePath, storeDigest, policyJson, selectedModel] = process.argv.slice(3);
|
|
93
|
+
if (process.argv.length !== 13) throw new Error('Invalid adapter stdin framing request');
|
|
94
94
|
// Reuse the recorded-installation loader; no module path or code supplied
|
|
95
95
|
// by a brief is ever imported. The conductor binds store bytes and policy;
|
|
96
96
|
// never resolve configuration from the worker-writable workspace.
|
|
97
97
|
process.stdout.write(await recordedWorkerStdinBytes({ filename, workspace, cli, adapterId, scopeDigest, briefDigest,
|
|
98
|
-
storePath, storeDigest, policy: JSON.parse(policyJson) ?? undefined }));
|
|
98
|
+
storePath, storeDigest, policy: JSON.parse(policyJson) ?? undefined, model: selectedModel }));
|
|
99
99
|
} else {
|
|
100
100
|
if (process.argv.length !== 5 || !briefPath?.startsWith('/')) throw new Error('usage: worker-stdin-frame.mjs BRIEF_PATH MODEL CWD');
|
|
101
101
|
process.stdout.write(workerStdinDocument({ brief: await readBounded(briefPath), model, cwd }));
|
|
@@ -87,7 +87,7 @@ export const MAX_SHOWN_GROUPS = 8;
|
|
|
87
87
|
*/
|
|
88
88
|
function excludedMembers(parallelism, group) {
|
|
89
89
|
const excluded = [];
|
|
90
|
-
for (const member of group.members) {
|
|
90
|
+
for (const member of (group.members ?? [])) {
|
|
91
91
|
if (member.enabled === false)
|
|
92
92
|
excluded.push(`${member.id} (member disabled)`);
|
|
93
93
|
else if (parallelism?.clis?.[member.cli]?.enabled === false)
|
|
@@ -97,15 +97,19 @@ function excludedMembers(parallelism, group) {
|
|
|
97
97
|
}
|
|
98
98
|
function groupSummaries(entries, parallelism) {
|
|
99
99
|
return entries.map(([id, group]) => {
|
|
100
|
-
const roles = [...new Set(group.members.flatMap(m => m.roles ?? []))].join('/');
|
|
101
|
-
const clis = [...new Set(group.members.map(m => m.cli))].join('/');
|
|
102
|
-
const parts = [`${oneLine(id, 64)} (members: ${group.members.length}`];
|
|
100
|
+
const roles = [...new Set((group.members ?? []).flatMap(m => m.roles ?? []))].join('/');
|
|
101
|
+
const clis = [...new Set((group.members ?? []).map(m => m.cli))].join('/');
|
|
102
|
+
const parts = [`${oneLine(id, 64)} (members: ${(group.members ?? []).length}`];
|
|
103
103
|
if (clis)
|
|
104
104
|
parts.push(`clis: ${oneLine(clis, 96)}`);
|
|
105
105
|
if (roles)
|
|
106
106
|
parts.push(`roles: ${oneLine(roles, 96)}`);
|
|
107
107
|
if (group.maxParallel !== undefined)
|
|
108
108
|
parts.push(`maxParallel: ${group.maxParallel}`);
|
|
109
|
+
if (group.efforts)
|
|
110
|
+
parts.push(`efforts: ${oneLine(group.efforts.join('/'), 96)}`);
|
|
111
|
+
if (group.dataPolicy)
|
|
112
|
+
parts.push(`dataPolicy: ${oneLine(JSON.stringify(group.dataPolicy), 240)}`);
|
|
109
113
|
if (group.requiredRoles?.length)
|
|
110
114
|
parts.push(`requiredRoles: ${group.requiredRoles.join('/')}`);
|
|
111
115
|
parts.push('configured');
|
|
@@ -147,13 +151,13 @@ async function observationSummaries(entries, deps) {
|
|
|
147
151
|
`Cached model observations read at ${readAt.toISOString()}; catalog fetchedAt: ${stamp(catalog?.discovery?.fetchedAt)}. Ages are measured at this read and grow afterward. This cache may differ from the resolver's current installation observations; no eligibility read is performed by context generation. Missing or ambiguous observations have unknown age.`,
|
|
148
152
|
'',
|
|
149
153
|
...entries.map(([id, group]) => {
|
|
150
|
-
const observations = group.members.flatMap(member => {
|
|
154
|
+
const observations = (group.members ?? []).flatMap(member => {
|
|
151
155
|
// Mirror the resolver's exact, unambiguous id/API-id lookup per installation.
|
|
152
156
|
const rows = catalog?.discovery?.instances?.[member.cli]?.models
|
|
153
157
|
.filter(row => row.id === member.model || row.apiModelId === member.model) ?? [];
|
|
154
158
|
return rows.length === 1 && rows[0].worker ? [rows[0].worker] : [];
|
|
155
159
|
});
|
|
156
|
-
return `- ${oneLine(id, 64)} cached observations: ${observations.length}/${group.members.length} members; observedAt: ${range(observations.map(row => row.observedAt))}; verifiedAt: ${range(observations.map(row => row.verifiedAt))}.`;
|
|
160
|
+
return `- ${oneLine(id, 64)} cached observations: ${observations.length}/${(group.members ?? []).length} members; observedAt: ${range(observations.map(row => row.observedAt))}; verifiedAt: ${range(observations.map(row => row.verifiedAt))}.`;
|
|
157
161
|
}),
|
|
158
162
|
'',
|
|
159
163
|
];
|
|
@@ -313,7 +317,7 @@ export async function buildParallelismSection(projectPath, deps = {}) {
|
|
|
313
317
|
const hard = healthValue(health, 'hardTimeoutSeconds', 1800);
|
|
314
318
|
lines.push('### Orchestration groups (group dispatch enabled)');
|
|
315
319
|
lines.push('');
|
|
316
|
-
const entries = Object.entries(
|
|
320
|
+
const entries = Object.entries(resolved.groups ?? {});
|
|
317
321
|
if (entries.length > 0) {
|
|
318
322
|
// Slice before summarizing so at most MAX_SHOWN_GROUPS summaries are built.
|
|
319
323
|
const shown = groupSummaries(entries.slice(0, MAX_SHOWN_GROUPS), cfg.parallelism).join(' | ');
|
|
@@ -328,6 +332,14 @@ export async function buildParallelismSection(projectPath, deps = {}) {
|
|
|
328
332
|
lines.push('');
|
|
329
333
|
lines.push(...await observationSummaries(entries.slice(0, MAX_SHOWN_GROUPS), deps));
|
|
330
334
|
const models = await new ConfigLoader(projectPath, { aiwgHome: resolveAiwgHome() }).load(cfg.parallelism);
|
|
335
|
+
for (const [pointer, origins] of Object.entries(models.workerPolicyOrigins ?? {})) {
|
|
336
|
+
if (!pointer.startsWith('/parallelism/groups/') || !pointer.endsWith('/template'))
|
|
337
|
+
continue;
|
|
338
|
+
lines.push(`Template expansion ${oneLine(pointer, 160)}: ${origins.map(origin => `${oneLine(origin.source, 240)}#${oneLine(origin.pointer, 160)} (${origin.digest})`).join(' → ')}. Contributions are retained in workerPolicyOrigins; inherited ceilings remain binding.`);
|
|
339
|
+
}
|
|
340
|
+
const aliases = Object.entries(models.defaults.workerSelection?.roleGuidance ?? {}).filter(([, guidance]) => guidance.baseRole);
|
|
341
|
+
for (const [role, guidance] of aliases)
|
|
342
|
+
lines.push(`Project role ${role} aliases ${guidance.baseRole}; ordered groups: ${guidance.groups.join(', ')}. Resolver eligibility and base-role qualification remain required.`);
|
|
331
343
|
const freshness = models.defaults.workerSelection.freshness;
|
|
332
344
|
lines.push(`Resolver freshness limits from merged model policy: observation warning after ${freshness.warnHours} hours; observation blocked after ${freshness.blockHours} hours; verification blocked after ${freshness.policyDays} days. Future-dated evidence is blocked. Re-resolve before launch; cached ages do not establish eligibility.`);
|
|
333
345
|
lines.push('');
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
*/
|
|
39
39
|
import { createHash } from 'node:crypto';
|
|
40
40
|
import { debuglog } from 'node:util';
|
|
41
|
+
import { hasSecretLookingLiterals } from '../../mcp/credentials.mjs';
|
|
41
42
|
const FORTEMI_QUERY_LIMIT = 50;
|
|
42
43
|
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
43
44
|
function schemaProperties(tool) {
|
|
@@ -82,7 +83,7 @@ export function resolveMcpRequestHeaders(server, environment = process.env) {
|
|
|
82
83
|
const headers = { ...(server.headers ?? {}) };
|
|
83
84
|
for (const [header, envName] of Object.entries(server.headerEnv ?? {})) {
|
|
84
85
|
if (!ENV_NAME.test(envName)) {
|
|
85
|
-
throw new Error(
|
|
86
|
+
throw new Error('storage(fortemi): invalid environment variable reference; use ENV_NAME, file:ABSOLUTE_PATH or store:NAME.');
|
|
86
87
|
}
|
|
87
88
|
const value = environment[envName];
|
|
88
89
|
if (!value) {
|
|
@@ -382,20 +383,31 @@ export const createDefaultMcpClient = async (serverName, registryOverride, envir
|
|
|
382
383
|
}
|
|
383
384
|
// Lazy imports keep unit tests that inject a stub isolated from transports.
|
|
384
385
|
const { Client } = await import('@modelcontextprotocol/sdk/client/index.js');
|
|
386
|
+
const hasCredentials = [server.env, server.headers, server.envFrom, server.headerEnv]
|
|
387
|
+
.some(map => Object.keys(map ?? {}).length > 0);
|
|
388
|
+
if (hasCredentials && server.type === 'stdio' && debuglog('child_process').enabled) {
|
|
389
|
+
throw new Error('MCP stdio credentials require NODE_DEBUG child_process logging to be disabled.');
|
|
390
|
+
}
|
|
391
|
+
// Reuse the registry resolver for file/store references. Preserve legacy env
|
|
392
|
+
// bytes and Authorization's existing Bearer prefix at the transport boundary.
|
|
385
393
|
const hasNewReferences = [...Object.values(server.headerEnv ?? {}), ...Object.values(server.envFrom ?? {})]
|
|
386
394
|
.some(reference => /^(?:file|store):/.test(reference));
|
|
387
|
-
// Keep legacy header/environment behavior unchanged. New locators are resolved
|
|
388
|
-
// only at connection time and never fed back to the registry or config writers.
|
|
389
|
-
if (hasNewReferences && server.type === 'stdio' && debuglog('child_process').enabled) {
|
|
390
|
-
throw new Error('MCP file/store stdio credentials require NODE_DEBUG child_process logging to be disabled.');
|
|
391
|
-
}
|
|
392
395
|
const resolved = hasNewReferences
|
|
393
396
|
? await resolveMcpCredentialMaps(server, { ...sensitiveInput, environment })
|
|
394
|
-
: undefined;
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
397
|
+
: { env: server.env ? { ...server.env } : undefined, headers: resolveMcpRequestHeaders(server, environment) };
|
|
398
|
+
if (!hasNewReferences) {
|
|
399
|
+
for (const [key, name] of Object.entries(server.envFrom ?? {})) {
|
|
400
|
+
if (!ENV_NAME.test(name) || !environment[name]) {
|
|
401
|
+
throw new Error('storage(fortemi): envFrom requires a set ENV_NAME, file:ABSOLUTE_PATH or store:NAME.');
|
|
402
|
+
}
|
|
403
|
+
resolved.env = { ...resolved.env, [key]: environment[name] };
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
const secrets = [
|
|
407
|
+
...Object.entries(resolved.env ?? {}).filter(([key, value]) => key in (server.envFrom ?? {}) || hasSecretLookingLiterals({ name: server.name, env: { [key]: value } })).map(([, value]) => value),
|
|
408
|
+
...Object.values(resolved.headers ?? {}),
|
|
409
|
+
...Object.values(resolved.headers ?? {}).map(value => value.replace(/^Bearer /i, '')),
|
|
410
|
+
].filter(Boolean).sort((a, b) => b.length - a.length);
|
|
399
411
|
const redact = (text) => secrets.reduce((text, secret) => text.split(secret).join('[REDACTED]'), text);
|
|
400
412
|
const protect = async (operation) => {
|
|
401
413
|
try {
|
|
@@ -421,8 +433,8 @@ export const createDefaultMcpClient = async (serverName, registryOverride, envir
|
|
|
421
433
|
transport = new StdioClientTransport({
|
|
422
434
|
command: server.command ?? '',
|
|
423
435
|
args: server.args ?? [],
|
|
424
|
-
env: resolved
|
|
425
|
-
...(
|
|
436
|
+
env: resolved.env,
|
|
437
|
+
...(hasCredentials ? { stderr: 'pipe' } : {}),
|
|
426
438
|
});
|
|
427
439
|
}
|
|
428
440
|
else {
|
|
@@ -430,8 +442,8 @@ export const createDefaultMcpClient = async (serverName, registryOverride, envir
|
|
|
430
442
|
throw new Error(`storage(fortemi): MCP server "${serverName}" has no URL`);
|
|
431
443
|
}
|
|
432
444
|
const url = validateRemoteMcpUrl(server.url);
|
|
433
|
-
const headers = resolved
|
|
434
|
-
if (
|
|
445
|
+
const headers = resolved.headers ?? {};
|
|
446
|
+
if (hasNewReferences) {
|
|
435
447
|
for (const header of Object.keys(server.headerEnv ?? {})) {
|
|
436
448
|
if (header.toLowerCase() === 'authorization')
|
|
437
449
|
headers[header] = `Bearer ${headers[header]}`;
|
|
@@ -462,36 +474,28 @@ export const createDefaultMcpClient = async (serverName, registryOverride, envir
|
|
|
462
474
|
}
|
|
463
475
|
}
|
|
464
476
|
const client = new Client({ name: 'aiwg-storage-fortemi-adapter', version: '1.0.0' }, { capabilities: {} });
|
|
465
|
-
if (
|
|
477
|
+
if (hasCredentials && server.type === 'stdio') {
|
|
466
478
|
// The recipient's stderr can contain credentials. Drain without forwarding.
|
|
467
479
|
transport.stderr?.on('data', () => { });
|
|
468
480
|
}
|
|
469
|
-
|
|
470
|
-
await protect(() => client.connect(transport));
|
|
471
|
-
else
|
|
472
|
-
await client.connect(transport);
|
|
481
|
+
await protect(() => client.connect(transport));
|
|
473
482
|
return {
|
|
474
483
|
async callTool(name, args) {
|
|
475
484
|
const call = async () => unwrapMcpToolResult(await client.callTool({ name, arguments: args }));
|
|
476
|
-
return
|
|
485
|
+
return protectResult(await protect(call));
|
|
477
486
|
},
|
|
478
487
|
async listTools() {
|
|
479
|
-
return
|
|
488
|
+
return protectResult(await protect(() => client.listTools()));
|
|
480
489
|
},
|
|
481
490
|
serverVersion() {
|
|
482
|
-
return
|
|
491
|
+
return protectResult(client.getServerVersion());
|
|
483
492
|
},
|
|
484
493
|
async close() {
|
|
485
|
-
|
|
486
|
-
await protect(() => client.close());
|
|
487
|
-
else
|
|
488
|
-
await client.close();
|
|
494
|
+
await protect(() => client.close());
|
|
489
495
|
},
|
|
490
496
|
};
|
|
491
497
|
}
|
|
492
498
|
catch (error) {
|
|
493
|
-
if (!hasNewReferences)
|
|
494
|
-
throw error;
|
|
495
499
|
throw new Error(redact(error instanceof Error ? error.message : 'MCP transport failed'));
|
|
496
500
|
}
|
|
497
501
|
};
|