@aiwg/cli 2026.10.9 → 2026.10.11
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 +275 -17
- package/bin/aiwg.mjs +8 -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/sessions.js +198 -15
- package/dist/src/cli/handlers/use-addon-profile.js +72 -0
- package/dist/src/cli/handlers/use.js +203 -234
- 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-context.js +337 -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 +114 -16
- 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 +275 -17
- package/dist/src/models/provider-policy.js +14 -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/policy/authorization.js +29 -1
- package/dist/src/process/aiwg-probe-command.d.mts +15 -0
- package/dist/src/process/aiwg-probe-command.mjs +37 -0
- 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/sessions/adapters/grokbot.js +7 -0
- package/dist/src/sessions/batch-import.js +34 -6
- package/dist/src/sessions/contracts.js +19 -0
- package/dist/src/sessions/coverage-preflight.js +7 -1
- package/dist/src/sessions/fortemi-export-mapping.js +33 -3
- package/dist/src/sessions/host-catalog.js +157 -0
- package/dist/src/sessions/image-blobs.js +141 -0
- package/dist/src/sessions/importer.js +23 -3
- package/dist/src/sessions/index.js +2 -0
- package/dist/src/sessions/refresh.js +25 -2
- package/dist/src/sessions/repository.js +164 -22
- package/dist/src/sessions/workspace-discovery.js +5 -2
- package/dist/src/smiths/context-pipeline/parallelism-section.js +19 -7
- package/package.json +15 -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/muse-adapter.mjs +10 -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-context.mjs +237 -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-invocation-ancestry.mjs +132 -0
- package/tools/ralph-external/lib/worker-invocation-processes.mjs +123 -0
- package/tools/ralph-external/lib/worker-isolation-status.mjs +48 -0
- package/tools/ralph-external/lib/worker-isolation.mjs +19 -73
- package/tools/ralph-external/lib/worker-launch-cli.mjs +49 -0
- package/tools/ralph-external/lib/worker-native-sandbox.mjs +83 -0
- package/tools/ralph-external/lib/worker-overflow.mjs +48 -13
- package/tools/ralph-external/lib/worker-retry-evidence.mjs +103 -0
- package/tools/ralph-external/lib/worker-termination.mjs +82 -0
- package/tools/ralph-external/session-launcher.mjs +124 -313
- package/vscode-extension/schemas/aiwg.config.v1.json +161 -2
|
@@ -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
|
|
@@ -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 }));
|
|
@@ -152,6 +152,13 @@ export class GrokbotSessionAdapter {
|
|
|
152
152
|
* rename-swap), not appended to. See the class-level doc comment.
|
|
153
153
|
*/
|
|
154
154
|
sourceContinuity = 'snapshot';
|
|
155
|
+
/**
|
|
156
|
+
* Account scope (#3232): conversations carry no workspace identity, so
|
|
157
|
+
* every workspace authorizing the same `--grokbot-root` would otherwise
|
|
158
|
+
* import the same conversations. The importer routes account-scoped
|
|
159
|
+
* sources to the host catalog once; workspace catalogs keep a reference.
|
|
160
|
+
*/
|
|
161
|
+
scopeClass = 'account';
|
|
155
162
|
interchange;
|
|
156
163
|
constructor(limits, maxFiles = 10_000) {
|
|
157
164
|
this.limits = limits;
|