@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
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** Validate bounded artifact declarations without touching the worker filesystem. */
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const safeId = /^[a-z0-9][a-z0-9-]{0,51}$/;
|
|
5
|
+
const safeName = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
|
|
6
|
+
const allowed = new Set(['name', 'content', 'location', 'prefix', 'relativePath']);
|
|
7
|
+
|
|
8
|
+
function normalizedRelative(value) {
|
|
9
|
+
return typeof value === 'string' && value.length > 0 && value.length <= 4096
|
|
10
|
+
&& !/[\\\x00-\x1f\x7f]/.test(value) && !path.posix.isAbsolute(value)
|
|
11
|
+
&& !value.split('/').some(part => !part || part === '.' || part === '..')
|
|
12
|
+
&& path.posix.normalize(value) === value;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** The installed module owns the prefix; its loader rechecks the whole declaration. */
|
|
16
|
+
export function validateWorkerArtifacts(invocation) {
|
|
17
|
+
const artifacts = invocation.artifacts ?? [];
|
|
18
|
+
if (!Array.isArray(artifacts) || artifacts.length > 16) throw new Error('Worker artifact count exceeds bound');
|
|
19
|
+
const names = new Set();
|
|
20
|
+
let total = 0, workspaceCount = 0;
|
|
21
|
+
for (const artifact of artifacts) {
|
|
22
|
+
if (!artifact || typeof artifact.name !== 'string' || !safeName.test(artifact.name)
|
|
23
|
+
|| /[^A-Za-z0-9_-]/.test(artifact.name) || names.has(artifact.name)
|
|
24
|
+
|| typeof artifact.content !== 'string' || Object.keys(artifact).some(key => !allowed.has(key))) {
|
|
25
|
+
throw new Error('Invalid worker artifact declaration');
|
|
26
|
+
}
|
|
27
|
+
names.add(artifact.name);
|
|
28
|
+
const size = Buffer.byteLength(artifact.content);
|
|
29
|
+
total += size;
|
|
30
|
+
if (size > 1048576 || total > 4194304) throw new Error('Worker artifact bytes exceed bound');
|
|
31
|
+
if (artifact.location === 'workspace-relative') {
|
|
32
|
+
if (++workspaceCount > 1) throw new Error('Only one workspace-relative worker artifact is allowed');
|
|
33
|
+
const { prefix, relativePath } = artifact;
|
|
34
|
+
const id = invocation.invocationId;
|
|
35
|
+
if (typeof id !== 'string' || !safeId.test(id) || /[^a-z0-9-]/.test(id)) throw new Error('Invalid worker artifact invocation id');
|
|
36
|
+
if (!normalizedRelative(prefix) || prefix.length > 256 || !prefix.endsWith('-')
|
|
37
|
+
|| !normalizedRelative(relativePath) || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(path.posix.basename(relativePath))
|
|
38
|
+
|| path.posix.dirname(relativePath) !== `${prefix}${id}`) {
|
|
39
|
+
throw new Error('Worker artifact path must be normalized and bound to its prefix and invocation id');
|
|
40
|
+
}
|
|
41
|
+
if (typeof invocation.cwd !== 'string' || !path.posix.isAbsolute(invocation.cwd)
|
|
42
|
+
|| /[\\\x00-\x1f\x7f]/.test(invocation.cwd) || path.posix.normalize(invocation.cwd) !== invocation.cwd) {
|
|
43
|
+
throw new Error('Worker artifact requires a normalized absolute cwd');
|
|
44
|
+
}
|
|
45
|
+
const target = path.posix.resolve(invocation.cwd, relativePath);
|
|
46
|
+
if (!target.startsWith(`${invocation.cwd === '/' ? '' : invocation.cwd}/`)) throw new Error('Worker artifact escapes cwd');
|
|
47
|
+
if (['briefPath', 'stdoutPath', 'stderrPath', 'finalPath'].some(key => invocation[key] && path.resolve(invocation[key]) === target)) {
|
|
48
|
+
throw new Error('Worker artifact and input/output paths must be distinct');
|
|
49
|
+
}
|
|
50
|
+
} else if (artifact.location !== undefined && artifact.location !== 'run-directory'
|
|
51
|
+
|| artifact.prefix !== undefined || artifact.relativePath !== undefined) {
|
|
52
|
+
throw new Error('Invalid worker artifact declaration');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return artifacts;
|
|
56
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Parses direct worker requests; configured role authority is validated by the resolver.
|
|
2
|
+
import { DEFAULT_WORKER_SELECTION, WorkerPolicyError } from '../models/provider-policy.js';
|
|
3
|
+
/** Only fixed argument guidance is safe to expose in public request diagnostics. */
|
|
4
|
+
export class WorkerRequestArgumentError extends WorkerPolicyError {
|
|
5
|
+
pointer;
|
|
6
|
+
constructor(key) {
|
|
7
|
+
super('OG_CONFIG_REFERENCE', key === 'task'
|
|
8
|
+
? `Missing required --task. Valid task classes: ${Object.keys(DEFAULT_WORKER_SELECTION.taskClasses).join(', ')}, plus classes configured in defaults.workerSelection.taskClasses.`
|
|
9
|
+
: `--${key.replace('_', '-')} must be an absolute path; relative paths are not resolved against cwd or --workdir.`);
|
|
10
|
+
this.pointer = `/request/${key}`;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** Reject unsupported syntax before discovery or any path reads. */
|
|
14
|
+
export function parseWorkerCommandArgs(args) {
|
|
15
|
+
const names = { role: 'role', task: 'task', group: 'group', brief: 'brief',
|
|
16
|
+
workdir: 'workdir', 'final-output': 'final_output', transport: 'transport', format: 'format' };
|
|
17
|
+
const result = {};
|
|
18
|
+
let json = false;
|
|
19
|
+
let premiumAuthorized = false;
|
|
20
|
+
for (let i = 0; i < args.length; i++) {
|
|
21
|
+
const token = args[i];
|
|
22
|
+
if (token === '--authorize-premium') {
|
|
23
|
+
if (premiumAuthorized)
|
|
24
|
+
throw new Error('Duplicate --authorize-premium');
|
|
25
|
+
premiumAuthorized = true;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (token === '--json') {
|
|
29
|
+
if (json)
|
|
30
|
+
throw new Error('Duplicate --json');
|
|
31
|
+
json = true;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const match = /^--([^=]+)(?:=([\s\S]*))?$/.exec(token);
|
|
35
|
+
if (!match || !Object.hasOwn(names, match[1]))
|
|
36
|
+
throw new Error(`Unsupported models resolve argument: ${token}`);
|
|
37
|
+
const key = names[match[1]];
|
|
38
|
+
if (key in result)
|
|
39
|
+
throw new Error(`Duplicate --${match[1]}`);
|
|
40
|
+
const value = match[2] ?? args[++i];
|
|
41
|
+
if (!value || value.startsWith('--') && match[2] === undefined || value.includes('\0')) {
|
|
42
|
+
if (key === 'task' && !value?.includes('\0'))
|
|
43
|
+
throw new WorkerRequestArgumentError('task');
|
|
44
|
+
throw new Error(`--${match[1]} requires a value`);
|
|
45
|
+
}
|
|
46
|
+
result[key] = value;
|
|
47
|
+
}
|
|
48
|
+
if (json && result.format && result.format !== 'json')
|
|
49
|
+
throw new Error('--json conflicts with --format');
|
|
50
|
+
if (json)
|
|
51
|
+
result.format = 'json';
|
|
52
|
+
for (const key of ['role', 'task', 'group', 'brief', 'workdir', 'final_output']) {
|
|
53
|
+
if (key === 'task' && !result[key])
|
|
54
|
+
throw new WorkerRequestArgumentError('task');
|
|
55
|
+
if (!result[key])
|
|
56
|
+
throw new Error(`Missing ${key}`);
|
|
57
|
+
}
|
|
58
|
+
if (!/^[a-z][a-z0-9-]{0,62}$/.test(result.role))
|
|
59
|
+
throw new Error('Unsupported role');
|
|
60
|
+
if (!/^[a-z][a-z0-9-]{0,62}$/.test(result.group))
|
|
61
|
+
throw new Error('Invalid group');
|
|
62
|
+
if (result.transport && !['shell', 'mcp'].includes(result.transport))
|
|
63
|
+
throw new Error('Unsupported transport');
|
|
64
|
+
if (result.format && !['shell', 'json'].includes(result.format))
|
|
65
|
+
throw new Error('Unsupported renderer; use POSIX shell or JSON');
|
|
66
|
+
return { ...result, ...(premiumAuthorized ? { authority: { premiumAuthorized: true, maxAutoTier: 3 } } : {}) };
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=worker-command-request.js.map
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/** Inspect worker paths and read bounded briefs without following symlinks. */
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { constants } from 'node:fs';
|
|
4
|
+
import { lstat, realpath, open } from 'node:fs/promises';
|
|
5
|
+
import { workerConfigDigest } from '../config/aiwg-config.js';
|
|
6
|
+
/** Include the Codex transport capture in the existing output ownership checks. */
|
|
7
|
+
export function workerOutputPaths(invocation) {
|
|
8
|
+
const paths = { stdout: invocation.stdoutPath, stderr: invocation.stderrPath, final: invocation.finalPath };
|
|
9
|
+
const lastMessage = `${invocation.finalPath}.last-message`;
|
|
10
|
+
if (invocation.transport === 'shell' && invocation.selected.cli === 'codex' && invocation.argv?.includes(lastMessage)) {
|
|
11
|
+
paths.lastMessage = lastMessage;
|
|
12
|
+
}
|
|
13
|
+
return paths;
|
|
14
|
+
}
|
|
15
|
+
export async function inspectWorkerPath(filename, kind) {
|
|
16
|
+
if (!path.isAbsolute(filename) || filename.includes('\0'))
|
|
17
|
+
throw new Error('Absolute NUL-free paths are required');
|
|
18
|
+
if (filename.split('/').includes('..'))
|
|
19
|
+
throw new Error('Worker path traversal is not allowed');
|
|
20
|
+
const normalized = path.resolve(filename);
|
|
21
|
+
let current = normalized;
|
|
22
|
+
while (true) {
|
|
23
|
+
try {
|
|
24
|
+
const canonical = await realpath(current);
|
|
25
|
+
if (canonical !== current)
|
|
26
|
+
throw new Error(`Symlink path is not allowed: ${filename}`);
|
|
27
|
+
if (current !== normalized)
|
|
28
|
+
return false;
|
|
29
|
+
const stat = await lstat(normalized);
|
|
30
|
+
if (kind === 'directory' ? !stat.isDirectory() : !stat.isFile() || stat.nlink !== 1) {
|
|
31
|
+
throw new Error(`Expected an unaliased ${kind}: ${filename}`);
|
|
32
|
+
}
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (error.code !== 'ENOENT')
|
|
37
|
+
throw error;
|
|
38
|
+
const parent = path.dirname(current);
|
|
39
|
+
if (parent === current)
|
|
40
|
+
throw error;
|
|
41
|
+
current = parent;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const inspectPath = inspectWorkerPath;
|
|
46
|
+
export async function readBrief(filename) {
|
|
47
|
+
if (!await inspectPath(filename, 'file'))
|
|
48
|
+
return null;
|
|
49
|
+
const handle = await open(filename, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
50
|
+
try {
|
|
51
|
+
const stat = await handle.stat();
|
|
52
|
+
if (!stat.isFile() || stat.nlink !== 1 || stat.size > 262144)
|
|
53
|
+
throw new Error('Brief must be a bounded regular file (256 KiB)');
|
|
54
|
+
const buffer = Buffer.alloc(262145);
|
|
55
|
+
let length = 0;
|
|
56
|
+
while (length < buffer.length) {
|
|
57
|
+
const { bytesRead } = await handle.read(buffer, length, buffer.length - length, null);
|
|
58
|
+
if (!bytesRead)
|
|
59
|
+
break;
|
|
60
|
+
length += bytesRead;
|
|
61
|
+
}
|
|
62
|
+
if (length > 262144)
|
|
63
|
+
throw new Error('Brief exceeds 256 KiB');
|
|
64
|
+
return buffer.subarray(0, length);
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
await handle.close();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** Canonical containment shared by launch metadata and saved-plan authority. */
|
|
71
|
+
const beneath = (root, filename) => {
|
|
72
|
+
const r = path.relative(root, filename);
|
|
73
|
+
return r === '' || !r.startsWith('..') && !path.isAbsolute(r);
|
|
74
|
+
};
|
|
75
|
+
async function resolveRealish(target) {
|
|
76
|
+
const parts = target.split('/').filter(Boolean);
|
|
77
|
+
for (let i = parts.length; i >= 0; i--) {
|
|
78
|
+
const candidate = i === 0 ? '/' : `/${parts.slice(0, i).join('/')}`;
|
|
79
|
+
try {
|
|
80
|
+
const real = await realpath(candidate);
|
|
81
|
+
const remainder = parts.slice(i).join('/');
|
|
82
|
+
return remainder ? path.join(real, remainder) : real;
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
if (error.code !== 'ENOENT')
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return target;
|
|
90
|
+
}
|
|
91
|
+
export async function realBeneath(root, filename) {
|
|
92
|
+
const [realRoot, realFilename] = await Promise.all([resolveRealish(root), resolveRealish(filename)]);
|
|
93
|
+
return beneath(realRoot, realFilename);
|
|
94
|
+
}
|
|
95
|
+
/** The persisted conductor plan, never worker-supplied JSON, owns the minted ID. */
|
|
96
|
+
export async function assertConductorWorkerPlan(saved, filename, drivers) {
|
|
97
|
+
if (!filename)
|
|
98
|
+
throw new Error('Conductor-owned saved plan is required to verify invocation identity');
|
|
99
|
+
const invocation = saved.invocation;
|
|
100
|
+
if (!invocation?.cwd)
|
|
101
|
+
throw new Error('Conductor plan has no worker cwd');
|
|
102
|
+
const roots = [invocation.cwd, ...(drivers[invocation.selected.cli]?.qualification.installation?.workerWritableRoots ?? [])];
|
|
103
|
+
for (const root of roots) {
|
|
104
|
+
if (await realBeneath(root, filename))
|
|
105
|
+
throw new Error('Invocation plan must be outside worker-writable roots');
|
|
106
|
+
}
|
|
107
|
+
if (!await inspectWorkerPath(filename, 'file'))
|
|
108
|
+
throw new Error('Conductor-owned saved plan is missing');
|
|
109
|
+
// Pin every ancestor, just as launch input reads do, and refuse aliased files.
|
|
110
|
+
let directory = await open('/', constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
111
|
+
try {
|
|
112
|
+
const components = filename.split('/').filter(Boolean), leaf = components.pop();
|
|
113
|
+
for (const component of components) {
|
|
114
|
+
const next = await open(`/proc/self/fd/${directory.fd}/${component}`, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
115
|
+
await directory.close();
|
|
116
|
+
directory = next;
|
|
117
|
+
}
|
|
118
|
+
const handle = await open(`/proc/self/fd/${directory.fd}/${leaf}`, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
119
|
+
try {
|
|
120
|
+
const stat = await handle.stat();
|
|
121
|
+
if (!stat.isFile() || stat.nlink !== 1 || stat.uid !== process.getuid() || stat.size > 1048576) {
|
|
122
|
+
throw new Error('Conductor-owned saved plan must be bounded, unaliased and owned');
|
|
123
|
+
}
|
|
124
|
+
const bytes = Buffer.alloc(1048577);
|
|
125
|
+
let length = 0;
|
|
126
|
+
while (length < bytes.length) {
|
|
127
|
+
const { bytesRead } = await handle.read(bytes, length, bytes.length - length, length);
|
|
128
|
+
if (!bytesRead)
|
|
129
|
+
break;
|
|
130
|
+
length += bytesRead;
|
|
131
|
+
}
|
|
132
|
+
if (length > 1048576 || workerConfigDigest(JSON.parse(bytes.subarray(0, length).toString('utf8'))) !== workerConfigDigest(saved)) {
|
|
133
|
+
throw new Error('Invocation identity differs from conductor-owned saved plan');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
await handle.close();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
finally {
|
|
141
|
+
await directory.close();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=worker-invocation-paths.js.map
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/** Validate and render a prepared invocation against trusted installation controls. */
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { validateWorkerArtifacts } from './worker-artifacts.mjs';
|
|
6
|
+
import { assertValidWorkerPayload } from '../mission-protocol/codecs.js';
|
|
7
|
+
import { qualificationDigest } from '../models/qualification-digest.mjs';
|
|
8
|
+
import { qualifyWorkerInvocationProfile, assertOperatorWorkerNativeArgv, assertWorkerSelectionAllowed, assertWorkerLaunchMode, requiresWorkerExternalBoundary, assertWorkerHealthQualification, WorkerPolicyError, DEFAULT_WORKER_SELECTION } from '../models/provider-policy.js';
|
|
9
|
+
/**
|
|
10
|
+
* The exact digest formula an MCP worker binding's `inputSchemaDigest`/
|
|
11
|
+
* `outputSchemaDigest` are pinned against (used below and reused verbatim by
|
|
12
|
+
* the #2892 MCP worker-invoke bridge's own live `tools/list` schema-drift
|
|
13
|
+
* check — see `src/mcp/tools/worker-invoke.mjs` M3). Deliberately the raw
|
|
14
|
+
* `JSON.stringify` of the schema object, NOT `workerConfigDigest`'s
|
|
15
|
+
* canonicalized (sorted-key) form — a schema's own declared key order is
|
|
16
|
+
* part of what gets pinned at qualification time.
|
|
17
|
+
*/
|
|
18
|
+
export function mcpToolSchemaDigest(schema) {
|
|
19
|
+
return `sha256:${createHash('sha256').update(JSON.stringify(schema)).digest('hex')}`;
|
|
20
|
+
}
|
|
21
|
+
export async function prepareWorkerInvocation(invocation, qualification) {
|
|
22
|
+
const result = structuredClone(invocation);
|
|
23
|
+
validateWorkerArtifacts(result);
|
|
24
|
+
// shellCommand is derived syntax, never authority supplied by a brief.
|
|
25
|
+
if (result.transport === 'shell')
|
|
26
|
+
result.shellCommand = '';
|
|
27
|
+
// Validate the established invocation shape first. Health is optional and
|
|
28
|
+
// is checked after policy, installation and transport qualification below.
|
|
29
|
+
const contract = { ...result };
|
|
30
|
+
delete contract.health;
|
|
31
|
+
assertValidWorkerPayload('workerResolvedInvocation', contract);
|
|
32
|
+
if ((qualification.platform ?? process.platform) !== 'linux') {
|
|
33
|
+
throw new Error('Unsupported worker invocation platform; Linux is required');
|
|
34
|
+
}
|
|
35
|
+
if (!Number.isFinite(qualification.now) || Date.parse(result.expiresAt) <= qualification.now) {
|
|
36
|
+
throw new Error('Worker invocation is expired or has no validation clock');
|
|
37
|
+
}
|
|
38
|
+
if (qualification.adapter.id !== qualification.adapterId) {
|
|
39
|
+
throw new Error('Worker invocation adapter identity mismatch');
|
|
40
|
+
}
|
|
41
|
+
assertWorkerLaunchMode(qualification.adapter, qualification.permissions);
|
|
42
|
+
if (result.isolation !== undefined && result.isolation !== (qualification.permissions?.launchMode ?? 'systemd-unit')) {
|
|
43
|
+
throw new WorkerPolicyError('OG_PIN_MISMATCH', 'Worker launch mode differs from the execution policy');
|
|
44
|
+
}
|
|
45
|
+
for (const [key, value] of Object.entries(qualification.expectedDigests ?? {})) {
|
|
46
|
+
if (result[key] !== value)
|
|
47
|
+
throw new WorkerPolicyError('OG_PIN_MISMATCH', `Worker invocation ${key} changed`);
|
|
48
|
+
}
|
|
49
|
+
const verified = Date.parse(qualification.adapter.qualification.verifiedAt ?? '');
|
|
50
|
+
const policyDays = qualification.policy?.freshness?.policyDays ?? DEFAULT_WORKER_SELECTION.freshness.policyDays;
|
|
51
|
+
if (!Number.isFinite(verified) || verified > qualification.now || qualification.now - verified >= policyDays * 86400000) {
|
|
52
|
+
throw new Error('Worker installation qualification is stale or invalid');
|
|
53
|
+
}
|
|
54
|
+
assertWorkerSelectionAllowed(result.selected, qualification);
|
|
55
|
+
const profile = qualifyWorkerInvocationProfile(qualification.adapter, {
|
|
56
|
+
profile: result.selected.profileRef, transport: result.transport,
|
|
57
|
+
cliVersion: qualification.cliVersion,
|
|
58
|
+
requiredControls: qualification.requiredControls, policy: qualification.policy,
|
|
59
|
+
permissions: qualification.permissions, effort: result.selected.effort.value, boundary: qualification.boundary,
|
|
60
|
+
mcp: qualification.mcp,
|
|
61
|
+
});
|
|
62
|
+
if (requiresWorkerExternalBoundary(qualification.adapter, { ...qualification,
|
|
63
|
+
profile: result.selected.profileRef, transport: result.transport, effort: result.selected.effort.value })) {
|
|
64
|
+
const installed = qualification.installation;
|
|
65
|
+
const evidence = profile.installationQualification;
|
|
66
|
+
if (!installed || !evidence || ['scopeDigest', 'fixtureDigest', 'verifiedAt', 'evidenceRef']
|
|
67
|
+
.some(key => installed.evidence?.[key] !== evidence[key])
|
|
68
|
+
|| !qualification.boundary || qualificationDigest(qualification.boundary) !== qualificationDigest(installed.boundary)) {
|
|
69
|
+
throw new WorkerPolicyError('OG_ADAPTER_UNQUALIFIED', 'Worker installation evidence and qualified boundary must match');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (profile.installationQualification) {
|
|
73
|
+
const installedAt = Date.parse(profile.installationQualification.verifiedAt);
|
|
74
|
+
if (!Number.isFinite(installedAt) || installedAt > qualification.now
|
|
75
|
+
|| qualification.now - installedAt >= policyDays * 86400000) {
|
|
76
|
+
throw new Error('Worker installation qualification is stale or invalid');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const scopeDigest = profile.installationQualification?.scopeDigest ?? qualification.adapter.qualification.fixtureDigest;
|
|
80
|
+
if (result.selected.qualificationDigest !== scopeDigest)
|
|
81
|
+
throw new WorkerPolicyError('OG_PIN_MISMATCH', 'Worker qualification digest changed');
|
|
82
|
+
const absolute = (value) => typeof value === 'string' && path.posix.isAbsolute(value) && !value.includes('\0');
|
|
83
|
+
for (const key of ['cwd', 'briefPath', 'stdoutPath', 'stderrPath', 'finalPath']) {
|
|
84
|
+
if (!absolute(result[key]))
|
|
85
|
+
throw new Error(`Worker invocation requires absolute ${key}`);
|
|
86
|
+
}
|
|
87
|
+
const briefPath = path.posix.normalize(result.briefPath);
|
|
88
|
+
if ([result.stdoutPath, result.stderrPath, result.finalPath].some(value => path.posix.normalize(value) === briefPath)
|
|
89
|
+
|| path.posix.normalize(result.stdoutPath) === path.posix.normalize(result.stderrPath)) {
|
|
90
|
+
throw new Error('Worker invocation input/output paths must be distinct');
|
|
91
|
+
}
|
|
92
|
+
if (result.transport === 'shell') {
|
|
93
|
+
assertOperatorWorkerNativeArgv(qualification.adapter, result.argv, result.selected, qualification.permissions);
|
|
94
|
+
if (!absolute(result.argv[0]) || result.argv[0].includes('=') || result.argv.some(value => value.includes('\0'))) {
|
|
95
|
+
throw new WorkerPolicyError('OG_CONFIG_REFERENCE', 'Worker invocation requires an absolute binary without equals and NUL-free argv');
|
|
96
|
+
}
|
|
97
|
+
if (result.envNames.some(name => !profile.envAllowlist.includes(name))) {
|
|
98
|
+
throw new Error('Worker invocation environment is outside the qualified allowlist');
|
|
99
|
+
}
|
|
100
|
+
const quote = (value) => `'${value.replaceAll("'", "'\"'\"'")}'`;
|
|
101
|
+
// Only validated environment names appear as shell syntax; their values
|
|
102
|
+
// remain in the conductor environment and are never printed in the plan.
|
|
103
|
+
const environment = result.envNames.map(name => `${name}="\${${name}-}"`).join(' ');
|
|
104
|
+
// The frozen 'closed' value means empty stdin (immediate EOF). 'brief-json'
|
|
105
|
+
// pipes a conductor-built params document framed from the brief file
|
|
106
|
+
// (worker-stdin-frame.mjs), so brief text never enters any argv.
|
|
107
|
+
const worker = `env -i${environment ? ` ${environment}` : ''} ${result.argv.map(quote).join(' ')}`;
|
|
108
|
+
const outputs = `> ${quote(result.stdoutPath)} 2> ${quote(result.stderrPath)}`;
|
|
109
|
+
if (result.stdin === 'brief-json') {
|
|
110
|
+
const framer = fileURLToPath(new URL('./worker-stdin-frame.mjs', import.meta.url));
|
|
111
|
+
const frame = [process.execPath, framer, result.briefPath, result.selected.model, result.cwd].map(quote).join(' ');
|
|
112
|
+
result.shellCommand = `cd ${quote(result.cwd)} && env -i ${frame} | ${worker} ${outputs}`;
|
|
113
|
+
}
|
|
114
|
+
else if (result.stdin === 'adapter-frame') {
|
|
115
|
+
const binding = qualification.stdinFramerBinding;
|
|
116
|
+
if (!binding || !absolute(binding.storePath) || !/^sha256:[a-f0-9]{64}$/.test(binding.storeDigest)) {
|
|
117
|
+
throw new Error('Adapter stdin framer requires a trusted installation binding');
|
|
118
|
+
}
|
|
119
|
+
const framer = fileURLToPath(new URL('./worker-stdin-frame.mjs', import.meta.url));
|
|
120
|
+
const frame = [process.execPath, framer, '--adapter-frame', result.briefPath, result.cwd,
|
|
121
|
+
result.selected.cli, qualification.adapterId, result.selected.qualificationDigest, result.briefDigest,
|
|
122
|
+
binding.storePath, binding.storeDigest, JSON.stringify(qualification.policy ?? null), result.selected.model].map(quote).join(' ');
|
|
123
|
+
result.shellCommand = `cd ${quote(result.cwd)} && env -i${environment ? ` ${environment}` : ''} ${frame} | ${worker} ${outputs}`;
|
|
124
|
+
}
|
|
125
|
+
else if (result.stdin === 'closed' || result.stdin === 'brief-text') {
|
|
126
|
+
const stdin = result.stdin === 'closed' ? '< /dev/null' : `< ${quote(result.briefPath)}`;
|
|
127
|
+
result.shellCommand = `cd ${quote(result.cwd)} && ${worker} ${stdin} ${outputs}`;
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
throw new Error(`Unsupported worker stdin mode: ${result.stdin}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
const binding = result.mcp;
|
|
135
|
+
for (const key of ['serverRef', 'tool', 'inputSchemaDigest', 'outputSchemaDigest', 'sessionIsolation']) {
|
|
136
|
+
if (binding[key] !== profile.mcp[key])
|
|
137
|
+
throw new WorkerPolicyError('OG_PIN_MISMATCH', `MCP worker binding changed: ${key}`);
|
|
138
|
+
}
|
|
139
|
+
if (binding.timeoutSeconds > result.limits.wallSeconds) {
|
|
140
|
+
throw new Error('MCP worker timeout exceeds the invocation deadline');
|
|
141
|
+
}
|
|
142
|
+
for (const key of ['input', 'output']) {
|
|
143
|
+
const schema = qualification.mcpSchemas?.[key];
|
|
144
|
+
if (!schema || mcpToolSchemaDigest(schema) !== binding[`${key}SchemaDigest`]) {
|
|
145
|
+
throw new WorkerPolicyError('OG_PIN_MISMATCH', `MCP worker ${key} schema digest changed`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const { default: Ajv2020 } = await import('ajv/dist/2020.js');
|
|
149
|
+
const { default: addFormats } = await import('ajv-formats');
|
|
150
|
+
const validate = addFormats(new Ajv2020({ strict: true, allErrors: true })).compile(qualification.mcpSchemas.input);
|
|
151
|
+
if (!validate(binding.arguments))
|
|
152
|
+
throw new Error('MCP worker arguments violate the pinned input schema');
|
|
153
|
+
}
|
|
154
|
+
assertValidWorkerPayload('workerResolvedInvocation', result);
|
|
155
|
+
if (result.health !== undefined) {
|
|
156
|
+
if (result.health.profileRef !== result.selected.profileRef
|
|
157
|
+
|| result.health.hardTimeoutSeconds > Math.min(result.limits.wallSeconds, result.mcp?.timeoutSeconds ?? Infinity)) {
|
|
158
|
+
throw new Error('Worker health profile or wall ceiling mismatch');
|
|
159
|
+
}
|
|
160
|
+
if (['native', 'external'].includes(result.health.stepEnforcement)
|
|
161
|
+
&& profile.controls.stepCap?.support !== result.health.stepEnforcement)
|
|
162
|
+
throw new Error('Unqualified worker step enforcement');
|
|
163
|
+
}
|
|
164
|
+
assertWorkerHealthQualification(profile, { profileRef: result.selected.profileRef, cliVersion: qualification.cliVersion,
|
|
165
|
+
accountScope: qualification.installation?.accountScope ?? '', inferenceProvider: result.selected.inferenceProvider,
|
|
166
|
+
model: result.selected.model, effort: result.selected.effort.value });
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=worker-invocation-preparation.js.map
|