@aiwg/cli 2026.10.6 → 2026.10.8
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/bin/aiwg.mjs +4 -2
- package/dist/src/a2a/agent-card.js +11 -0
- package/dist/src/api/index.d.ts +1 -0
- package/dist/src/api/index.js +1 -0
- package/dist/src/artifacts/fortemi-shard-contract/2.2.0/contract.json +103 -0
- package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-config.schema.json +212 -0
- package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-set.schema.json +240 -0
- package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-manifest.schema.json +451 -0
- package/dist/src/artifacts/fortemi-shard-export.js +284 -0
- package/dist/src/artifacts/index-preparation.js +2 -0
- package/dist/src/auth/client.js +5 -1
- package/dist/src/auth/credential-store.js +5 -1
- package/dist/src/auth/sensitive-input.js +151 -0
- package/dist/src/channel/manager.mjs +3 -0
- package/dist/src/cli/handlers/agentcard.js +47 -25
- package/dist/src/cli/handlers/auth.js +22 -2
- package/dist/src/cli/handlers/cost-report.js +7 -1
- package/dist/src/cli/handlers/decision.js +36 -10
- package/dist/src/cli/handlers/effect.js +4 -84
- package/dist/src/cli/handlers/guided-setup.js +1 -1
- package/dist/src/cli/handlers/init.js +1 -1
- package/dist/src/cli/handlers/install.js +6 -0
- package/dist/src/cli/handlers/local-executor.js +33 -6
- package/dist/src/cli/handlers/marketplace.js +15 -4
- package/dist/src/cli/handlers/setup.js +24 -20
- package/dist/src/cli/handlers/use.js +57 -15
- package/dist/src/cli/prompt-utils.js +73 -0
- package/dist/src/config/gitignore.js +4 -0
- package/dist/src/config/workspace.js +2 -1
- package/dist/src/cost/fleet-report.js +20 -22
- package/dist/src/decision/adapters/jev.js +20 -31
- package/dist/src/decision/ask.js +6 -1
- package/dist/src/decision/benchmark.js +19 -6
- package/dist/src/decision/driver.js +2 -2
- package/dist/src/decision/jev-credentials.js +18 -5
- package/dist/src/decision/ledger.js +230 -34
- package/dist/src/decision/qualification/gate-evidence.js +6 -4
- package/dist/src/decision/qualification/load-staged.js +47 -10
- package/dist/src/decision/qualification/runner.js +7 -3
- package/dist/src/discovery/interview.js +10 -5
- package/dist/src/discovery/plan-apply.js +26 -30
- package/dist/src/discovery/plan-validation.js +2 -2
- package/dist/src/effects/host.js +91 -0
- package/dist/src/extensions/commands/definitions.js +2 -2
- package/dist/src/extensions/project-local-gitignore.js +2 -0
- package/dist/src/issues/cli.js +16 -5
- package/dist/src/issues/live.js +44 -12
- package/dist/src/marketplace/exchange.js +18 -6
- package/dist/src/marketplace/provenance.js +3 -2
- package/dist/src/mcp/adapters/codex-runtime.js +46 -76
- package/dist/src/mcp/cli.mjs +122 -109
- package/dist/src/mcp/credentials.d.mts +4 -0
- package/dist/src/mcp/credentials.mjs +44 -3
- package/dist/src/mcp/grok-build-config.mjs +4 -1
- package/dist/src/mcp/muse-native-config.mjs +2 -1
- package/dist/src/mcp/omp-config.mjs +2 -0
- package/dist/src/mcp/registry.js +19 -15
- package/dist/src/mcp/registry.mjs +2 -18
- package/dist/src/mcp/tools/agentic-sandbox.mjs +46 -28
- package/dist/src/mcp/tools/worker-invoke.mjs +13 -2
- package/dist/src/memory/project-registry.js +2 -1
- package/dist/src/mission-protocol/worker-status.js +91 -1
- package/dist/src/models/grok-build-models.js +20 -0
- package/dist/src/models/worker-installation.d.mts +1 -1
- package/dist/src/models/worker-installation.mjs +65 -4
- package/dist/src/packages/adapters/git.js +99 -23
- package/dist/src/packages/adapters/gitea.js +8 -27
- package/dist/src/packages/adapters/github.js +7 -25
- package/dist/src/packages/registry.js +4 -4
- package/dist/src/process/cli-command.d.mts +5 -0
- package/dist/src/process/cli-command.mjs +42 -0
- package/dist/src/providers/provider-definitions.js +2 -2
- package/dist/src/serve/shared-host-scheduler.js +4 -1
- package/dist/src/serve/worker-invocation.js +10 -0
- package/dist/src/smiths/context-pipeline/claude-hook.js +9 -9
- package/dist/src/smiths/context-pipeline/context-file-safety.js +330 -9
- package/dist/src/smiths/context-pipeline/finalization.js +3 -2
- package/dist/src/smiths/context-pipeline/generator.js +7 -19
- package/dist/src/smiths/context-pipeline/legacy-inject.js +3 -10
- package/dist/src/smiths/context-pipeline/managed-hook.js +5 -5
- package/dist/src/smiths/context-pipeline/workspace-context.js +4 -12
- package/dist/src/storage/backends/fortemi.js +107 -49
- package/dist/src/tracker/project-inputs.js +2 -0
- package/dist/src/update/notifier.mjs +2 -4
- package/package.json +1 -1
- package/tools/agents/providers/base.mjs +5 -1
- package/tools/agents/providers/claude.mjs +5 -2
- package/tools/agents/providers/codex.mjs +4 -1
- package/tools/agents/providers/context-write.mjs +28 -0
- package/tools/agents/providers/copilot.mjs +5 -2
- package/tools/agents/providers/hermes.mjs +5 -2
- package/tools/agents/providers/omp.mjs +3 -1
- package/tools/agents/providers/rule-delivery.mjs +29 -12
- package/tools/agents/providers/warp.mjs +4 -1
- package/tools/agents/providers/windsurf.mjs +6 -3
- package/tools/ralph-external/lib/consecutive-failure-circuit-breaker.mjs +26 -0
- package/tools/ralph-external/lib/launcher-recovery-evidence.mjs +7 -1
- package/tools/ralph-external/lib/recovery-probe.mjs +14 -4
- package/tools/ralph-external/process-monitor.mjs +42 -4
- package/tools/ralph-external/session-launcher.mjs +125 -3
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { assertEnvironmentCredentialReferences } from './credentials.mjs';
|
|
1
2
|
import { writeConfigAtomic } from './config-file.mjs';
|
|
2
3
|
import { constants } from 'node:fs';
|
|
3
4
|
import { access, lstat, readFile } from 'node:fs/promises';
|
|
4
5
|
import { dirname, relative, resolve } from 'node:path';
|
|
5
6
|
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { helperEnvironment } from '../process/cli-command.mjs';
|
|
6
8
|
import { replaceServer } from './toml-editor.mjs';
|
|
7
9
|
import { tomlString as string } from './toml-string.mjs';
|
|
8
10
|
|
|
@@ -19,6 +21,7 @@ function inline(values = {}) {
|
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
export function renderGrokMcpServer(server) {
|
|
24
|
+
assertEnvironmentCredentialReferences(server, 'grok-build');
|
|
22
25
|
if (!server?.name) throw new Error('Grok MCP server name is required');
|
|
23
26
|
const lines = [`[mcp_servers.${key(server.name)}]`];
|
|
24
27
|
if (server.type === 'stdio') {
|
|
@@ -144,7 +147,7 @@ export async function unmanageGrokBuildMcp(configPath, names, options = {}) {
|
|
|
144
147
|
}
|
|
145
148
|
|
|
146
149
|
function runJson(binary, args, cwd, env) {
|
|
147
|
-
const run = spawnSync(binary, args, { cwd, env, encoding: 'utf8', timeout: 30_000, killSignal: 'SIGKILL' });
|
|
150
|
+
const run = spawnSync(binary, args, { cwd, env: helperEnvironment(env), encoding: 'utf8', timeout: 30_000, killSignal: 'SIGKILL' });
|
|
148
151
|
if (run.error || run.status !== 0) return { ok: false, detail: run.error?.message || run.stderr || `exit ${run.status}` };
|
|
149
152
|
try { return { ok: true, value: JSON.parse(run.stdout || '{}') }; }
|
|
150
153
|
catch { return { ok: false, detail: 'Grok returned malformed JSON' }; }
|
|
@@ -4,6 +4,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
import { resolveMuseXdgSkillsDir } from '../providers/muse-paths.js';
|
|
7
|
+
import { helperEnvironment } from '../process/cli-command.mjs';
|
|
7
8
|
|
|
8
9
|
// Mirrors tools/agents/providers/muse-hooks.mjs. src/ cannot import tools/:
|
|
9
10
|
// tools/ is not part of the compiled dist/ tree.
|
|
@@ -47,7 +48,7 @@ function deepEqual(a, b) {
|
|
|
47
48
|
|
|
48
49
|
function inspectMuseCli(env) {
|
|
49
50
|
const result = spawnSync('muse', ['--version'], {
|
|
50
|
-
env,
|
|
51
|
+
env: helperEnvironment(env),
|
|
51
52
|
encoding: 'utf8',
|
|
52
53
|
timeout: MUSE_VERSION_TIMEOUT_MS,
|
|
53
54
|
killSignal: 'SIGKILL',
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertEnvironmentCredentialReferences } from './credentials.mjs';
|
|
1
2
|
import { assertConfigDestination, assertNoSymlinkParents, writeConfigAtomic } from './config-file.mjs';
|
|
2
3
|
import { readFile, mkdir, lstat, unlink, open } from 'node:fs/promises';
|
|
3
4
|
import { dirname, parse, resolve } from 'node:path';
|
|
@@ -50,6 +51,7 @@ async function atomic(file, data, options) {
|
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
export function ompServerConfig(server) {
|
|
54
|
+
assertEnvironmentCredentialReferences(server, 'omp');
|
|
53
55
|
const config = {};
|
|
54
56
|
for (const field of ['command', 'args', 'env', 'cwd', 'url', 'headers', 'enabled', 'timeout', 'requestIdFormat', 'auth', 'oauth', 'type']) {
|
|
55
57
|
if (server[field] !== undefined) config[field] = server[field];
|
package/dist/src/mcp/registry.js
CHANGED
|
@@ -7,7 +7,7 @@ import { manageGrokBuildMcp } from './grok-build-config.mjs';
|
|
|
7
7
|
import { replaceServer } from './toml-editor.mjs';
|
|
8
8
|
import { resolveOmpPaths } from '../providers/omp-paths.mjs';
|
|
9
9
|
import { applyJsonToolFilterPlan, claudeSettingsPath, prepareClaudePermissions, planToolFilters } from './tool-filters.mjs';
|
|
10
|
-
import { assertCredentialPolicy, resolveCredentialPolicy, validateEnvReferenceName } from './credentials.mjs';
|
|
10
|
+
import { assertCredentialPolicy, resolveCredentialPolicy, validateEnvReferenceName, credentialReferenceInput, validateCredentialReferences, assertEnvironmentCredentialReferences } from './credentials.mjs';
|
|
11
11
|
/**
|
|
12
12
|
* MCP Server Registry
|
|
13
13
|
*
|
|
@@ -23,6 +23,7 @@ import { qualifyWorkerInvocationProfile, requiredWorkerControls } from '../model
|
|
|
23
23
|
import { dirname, isAbsolute, resolve } from 'path';
|
|
24
24
|
import { homedir } from 'os';
|
|
25
25
|
import { resolveConfigDir } from '../config/user-config.js';
|
|
26
|
+
import { resolveSensitiveInput } from '../auth/sensitive-input.js';
|
|
26
27
|
// ============================================
|
|
27
28
|
// Registry
|
|
28
29
|
// ============================================
|
|
@@ -32,22 +33,24 @@ const DEFAULT_REGISTRY = {
|
|
|
32
33
|
kind: 'McpServerRegistry',
|
|
33
34
|
servers: {},
|
|
34
35
|
};
|
|
35
|
-
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
36
|
+
/** Resolve references only for AIWG's in-memory transport, never for a config writer. */
|
|
37
|
+
export async function resolveMcpCredentialMaps(server, dependencies = {}) {
|
|
38
|
+
const result = { env: server.env ? { ...server.env } : undefined, headers: server.headers ? { ...server.headers } : undefined };
|
|
39
|
+
for (const [field, target, flag] of [['envFrom', 'env', '--env-from'], ['headerEnv', 'headers', '--header-env']]) {
|
|
40
|
+
for (const [key, reference] of Object.entries(server[field] ?? {})) {
|
|
41
|
+
const input = credentialReferenceInput(reference, flag);
|
|
42
|
+
// Explicit locators prevent terminal prompts and implicit stdin consumption.
|
|
43
|
+
try {
|
|
44
|
+
const value = await resolveSensitiveInput({ ...input, prefix: 'value' }, dependencies);
|
|
45
|
+
result[target] = { ...result[target], [key]: value };
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
const message = error instanceof Error ? error.message : 'MCP credential input failed';
|
|
49
|
+
throw new Error(message.replace(/--value-(file|store|env)/g, (_, source) => `${flag} KEY=${source === 'env' ? 'ENV_NAME' : source === 'file' ? 'file:ABSOLUTE_PATH' : 'store:NAME'}`));
|
|
50
|
+
}
|
|
49
51
|
}
|
|
50
52
|
}
|
|
53
|
+
return result;
|
|
51
54
|
}
|
|
52
55
|
export function workerServerDigest(server) {
|
|
53
56
|
const { workerQualification, description, injectedProviders, addedAt, updatedAt, ...execution } = server;
|
|
@@ -300,6 +303,7 @@ function tomlInlineTable(values) {
|
|
|
300
303
|
return `{ ${Object.entries(values).map(([name, value]) => `${tomlKey(name)} = ${tomlString(value)}`).join(', ')} }`;
|
|
301
304
|
}
|
|
302
305
|
export function buildServerToml(server) {
|
|
306
|
+
assertEnvironmentCredentialReferences(server, 'codex');
|
|
303
307
|
const lines = [];
|
|
304
308
|
lines.push(`[mcp_servers.${tomlKey(server.name)}]`);
|
|
305
309
|
if (server.type === 'stdio') {
|
|
@@ -8,7 +8,7 @@ import { manageGrokBuildMcp } from './grok-build-config.mjs';
|
|
|
8
8
|
import { replaceServer } from './toml-editor.mjs';
|
|
9
9
|
import { resolveOmpPaths } from '../providers/omp-paths.mjs';
|
|
10
10
|
import { applyJsonToolFilterPlan, claudeSettingsPath, prepareClaudePermissions, planToolFilters } from './tool-filters.mjs';
|
|
11
|
-
import { assertCredentialPolicy, resolveCredentialPolicy, validateEnvReferenceName } from './credentials.mjs';
|
|
11
|
+
import { assertCredentialPolicy, resolveCredentialPolicy, validateEnvReferenceName, validateCredentialReferences, assertEnvironmentCredentialReferences } from './credentials.mjs';
|
|
12
12
|
/**
|
|
13
13
|
* MCP Server Registry (Runtime ESM)
|
|
14
14
|
*
|
|
@@ -72,23 +72,6 @@ const DEFAULT_REGISTRY = {
|
|
|
72
72
|
servers: {},
|
|
73
73
|
};
|
|
74
74
|
|
|
75
|
-
const ENV_REFERENCE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
76
|
-
|
|
77
|
-
function validateCredentialReferences(def) {
|
|
78
|
-
for (const [header, envName] of Object.entries(def.headerEnv || {})) {
|
|
79
|
-
if (!header.trim()) throw new Error('MCP header-env header name must not be empty');
|
|
80
|
-
if (!ENV_REFERENCE_NAME.test(envName)) {
|
|
81
|
-
throw new Error(`Invalid MCP header environment variable reference "${envName}"`);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
for (const [key, envName] of Object.entries(def.envFrom || {})) {
|
|
85
|
-
if (!ENV_REFERENCE_NAME.test(key)) throw new Error(`Invalid MCP env-from variable name "${key}"`);
|
|
86
|
-
if (!ENV_REFERENCE_NAME.test(envName)) {
|
|
87
|
-
throw new Error(`Invalid MCP env-from environment variable reference "${envName}"`);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
75
|
// The typed worker dispatch boundary owns qualification. Legacy runtime edits
|
|
93
76
|
// invalidate its binding without importing the TypeScript policy owner.
|
|
94
77
|
export function workerServerDigest(server) {
|
|
@@ -280,6 +263,7 @@ function tomlInlineTable(values) {
|
|
|
280
263
|
}
|
|
281
264
|
|
|
282
265
|
export function buildServerToml(server) {
|
|
266
|
+
assertEnvironmentCredentialReferences(server, 'codex');
|
|
283
267
|
const lines = [];
|
|
284
268
|
lines.push(`[mcp_servers.${tomlKey(server.name)}]`);
|
|
285
269
|
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Governed Agentic Sandbox fleet and activity MCP surface.
|
|
3
3
|
*
|
|
4
|
-
* Credentials are file-
|
|
4
|
+
* Credentials are private-file or native-store configuration, never tool arguments.
|
|
5
5
|
* Every tool in this module uses the management bearer domain; executor-plane
|
|
6
6
|
* credentials are deliberately out of scope and cannot be substituted.
|
|
7
7
|
*
|
|
8
8
|
* @implements #2015
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import { access } from 'node:fs/promises';
|
|
12
12
|
import { z } from 'zod';
|
|
13
13
|
import { integer, dateTime } from '../../schema/zod-validation.mjs';
|
|
14
14
|
import { assertNoWorkerBinding, mcpError, mcpJson } from '../helpers.mjs';
|
|
@@ -36,17 +36,27 @@ function configuredBaseUrl(env = process.env) {
|
|
|
36
36
|
return url.toString().replace(/\/$/, '');
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
async function
|
|
39
|
+
async function sensitiveInputModule() {
|
|
40
|
+
// Source MCP modules and the published dist tree share the compiled owner.
|
|
41
|
+
for (const location of [new URL('../../auth/sensitive-input.js', import.meta.url), new URL('../../../dist/src/auth/sensitive-input.js', import.meta.url)]) {
|
|
42
|
+
try { await access(location); } catch { continue; }
|
|
43
|
+
return import(location.href);
|
|
44
|
+
}
|
|
45
|
+
throw new Error('Sandbox credentials require the compiled AIWG auth module, which this installation is missing. Run `aiwg doctor` to repair the installation.');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function managementBearer(env = process.env, readStore) {
|
|
40
49
|
const tokenFile = String(env.AIWG_SANDBOX_MANAGEMENT_TOKEN_FILE ?? '').trim();
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (process.platform !== 'win32' && (metadata.mode & 0o077) !== 0) {
|
|
45
|
-
throw new Error('sandbox management token file must not be accessible by group or other users');
|
|
50
|
+
const tokenStore = String(env.AIWG_SANDBOX_MANAGEMENT_TOKEN_STORE ?? '').trim();
|
|
51
|
+
if (!tokenFile && !tokenStore) {
|
|
52
|
+
throw new Error('Sandbox management bearer requires AIWG_SANDBOX_MANAGEMENT_TOKEN_FILE or AIWG_SANDBOX_MANAGEMENT_TOKEN_STORE.');
|
|
46
53
|
}
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
54
|
+
const { readPrivateValueFile, resolveSensitiveInput } = await sensitiveInputModule();
|
|
55
|
+
return resolveSensitiveInput(tokenFile ? { prefix: 'token', file: tokenFile } : { prefix: 'token', store: tokenStore }, {
|
|
56
|
+
isTTY: false,
|
|
57
|
+
readFile: (pathname) => readPrivateValueFile(pathname, 'AIWG_SANDBOX_MANAGEMENT_TOKEN_FILE'),
|
|
58
|
+
...(readStore ? { readStore } : {}),
|
|
59
|
+
});
|
|
50
60
|
}
|
|
51
61
|
|
|
52
62
|
function containsRestricted(value) {
|
|
@@ -138,32 +148,40 @@ function typedResult(body, status, validate) {
|
|
|
138
148
|
}
|
|
139
149
|
|
|
140
150
|
export class AgenticSandboxMcpClient {
|
|
141
|
-
constructor({ env = process.env, fetch = globalThis.fetch.bind(globalThis) } = {}) {
|
|
151
|
+
constructor({ env = process.env, fetch = globalThis.fetch.bind(globalThis), readStore } = {}) {
|
|
142
152
|
this.env = env;
|
|
143
153
|
this.fetch = fetch;
|
|
154
|
+
this.readStore = readStore;
|
|
144
155
|
}
|
|
145
156
|
|
|
146
157
|
async request(path, { method = 'GET', body, headers = {}, validate = (value) => safeResponse(value) } = {}) {
|
|
147
158
|
assertNoWorkerBinding(body, 'mcp');
|
|
148
159
|
const baseUrl = configuredBaseUrl(this.env);
|
|
149
|
-
const token = await managementBearer(this.env);
|
|
150
|
-
const
|
|
151
|
-
method,
|
|
152
|
-
headers: { accept: 'application/json', authorization: `Bearer ${token}`, ...(body === undefined ? {} : { 'content-type': 'application/json' }), ...headers },
|
|
153
|
-
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
154
|
-
});
|
|
155
|
-
let parsed;
|
|
156
|
-
try {
|
|
157
|
-
parsed = await response.json();
|
|
158
|
-
} catch {
|
|
159
|
-
if (response.status === 404 || response.status === 405) parsed = {};
|
|
160
|
-
else return mcpError(`sandbox returned non-JSON HTTP ${response.status}`);
|
|
161
|
-
}
|
|
160
|
+
const token = await managementBearer(this.env, this.readStore);
|
|
161
|
+
const redact = (text) => String(text).split(token).join('[REDACTED]');
|
|
162
162
|
try {
|
|
163
|
-
|
|
164
|
-
|
|
163
|
+
const response = await this.fetch(`${baseUrl}${path}`, {
|
|
164
|
+
method,
|
|
165
|
+
headers: { accept: 'application/json', authorization: `Bearer ${token}`, ...(body === undefined ? {} : { 'content-type': 'application/json' }), ...headers },
|
|
166
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
167
|
+
});
|
|
168
|
+
let parsed;
|
|
169
|
+
try {
|
|
170
|
+
const value = await response.json();
|
|
171
|
+
// Upstream errors and metadata can reflect the authorization header.
|
|
172
|
+
parsed = JSON.parse(JSON.stringify(value).split(JSON.stringify(token).slice(1, -1)).join('[REDACTED]'));
|
|
173
|
+
} catch {
|
|
174
|
+
if (response.status === 404 || response.status === 405) parsed = {};
|
|
175
|
+
else return mcpError(`sandbox returned non-JSON HTTP ${response.status}`);
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
assertNoWorkerBinding(parsed, 'mcp');
|
|
179
|
+
return typedResult(parsed, response.status, validate);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
return mcpError(`${error.code ?? 'sandbox_malformed_response'}: ${error.message}`);
|
|
182
|
+
}
|
|
165
183
|
} catch (error) {
|
|
166
|
-
|
|
184
|
+
throw new Error(redact(error instanceof Error ? error.message : 'Sandbox management request failed'));
|
|
167
185
|
}
|
|
168
186
|
}
|
|
169
187
|
}
|
|
@@ -2707,6 +2707,9 @@ export async function registerWorkerInvokeToolset(server, options = {}) {
|
|
|
2707
2707
|
}
|
|
2708
2708
|
|
|
2709
2709
|
async function status(input) {
|
|
2710
|
+
const { projectRecoveryStatus, mergeRecoveryStatuses } = await importImpl('mission-protocol/worker-status.js');
|
|
2711
|
+
const recoveryStatus = breaker => projectRecoveryStatus(typeof breaker?.recoveryStatus === 'function'
|
|
2712
|
+
? { ...breaker.recoveryStatus(), observedAt: now().toISOString() } : null, 'live');
|
|
2710
2713
|
if (!input.invocation_id) {
|
|
2711
2714
|
// #2915 round 8 (D4): ordinary status, read-only, no id — lists every
|
|
2712
2715
|
// invocation this server currently tracks as a `dispatched` reservation
|
|
@@ -2725,11 +2728,19 @@ export async function registerWorkerInvokeToolset(server, options = {}) {
|
|
|
2725
2728
|
.filter(scope => scope.state === 'operator-intervention')
|
|
2726
2729
|
.map(scope => ({ scopeId: scope.scopeKey, reason: scope.operatorReason,
|
|
2727
2730
|
resetInstruction: { operation: 'reset_recovery_scope', scope_id: scope.scopeKey } })));
|
|
2728
|
-
|
|
2731
|
+
const recovery = mergeRecoveryStatuses([...breakers].map((breaker, index) => {
|
|
2732
|
+
const status = recoveryStatus(breaker);
|
|
2733
|
+
if (breakers.size > 1) {
|
|
2734
|
+
for (const scope of status.scopes) scope.owner = `owner-${index + 1}`;
|
|
2735
|
+
for (const transition of status.transitions) transition.owner = `owner-${index + 1}`;
|
|
2736
|
+
}
|
|
2737
|
+
return status;
|
|
2738
|
+
}));
|
|
2739
|
+
return { unresolvedReservations, interventionScopes, recovery };
|
|
2729
2740
|
}
|
|
2730
2741
|
const entry = registry.get(input.invocation_id);
|
|
2731
2742
|
if (!entry) throw new Error('Unknown invocation_id');
|
|
2732
|
-
return publicStatus(entry, healthHook);
|
|
2743
|
+
return { ...publicStatus(entry, healthHook), recovery: recoveryStatus(entry.recoveryBreaker ?? registry.recoveryOwner) };
|
|
2733
2744
|
}
|
|
2734
2745
|
|
|
2735
2746
|
/**
|
|
@@ -10,6 +10,7 @@ import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
|
10
10
|
import { homedir } from 'node:os';
|
|
11
11
|
import path from 'node:path';
|
|
12
12
|
import { boundedExecFileSync } from '../process/bounded-child-process.mjs';
|
|
13
|
+
import { helperEnvironment } from '../process/cli-command.mjs';
|
|
13
14
|
import { projectAiwgPath, resolveProjectAiwgDir } from '../config/project-artifacts.js';
|
|
14
15
|
export function projectMemoryHome() {
|
|
15
16
|
const override = process.env.AIWG_PROJECT_MEMORY_HOME;
|
|
@@ -248,7 +249,7 @@ function sanitizeProjectId(id) {
|
|
|
248
249
|
}
|
|
249
250
|
function discoverGitRemotes(cwd) {
|
|
250
251
|
try {
|
|
251
|
-
const stdout = boundedExecFileSync('git', ['remote', '-v'], 'quick', { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
252
|
+
const stdout = boundedExecFileSync('git', ['remote', '-v'], 'quick', { cwd, env: helperEnvironment(), encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
252
253
|
const remotes = stdout
|
|
253
254
|
.split('\n')
|
|
254
255
|
.map((line) => line.trim().split(/\s+/)[1])
|
|
@@ -1,2 +1,92 @@
|
|
|
1
|
-
|
|
1
|
+
const recoveryStates = new Set(['closed', 'open', 'half-open', 'canary-ready', 'canary', 'operator-intervention']);
|
|
2
|
+
const recoveryScopes = new Set(['invocation', 'model', 'installation', 'quota', 'provider', 'host']);
|
|
3
|
+
const recoveryReasons = new Set(['NOT_FOUND', 'SPAWN_REFUSED', 'AUTH', 'RATE_LIMIT', 'NETWORK',
|
|
4
|
+
'RESOURCE', 'SANDBOX', 'MODEL', 'UNKNOWN']);
|
|
5
|
+
const objectRecord = (value) => value !== null
|
|
6
|
+
&& typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
7
|
+
const finiteClock = (value) => typeof value === 'number' && Number.isFinite(value)
|
|
8
|
+
&& Math.abs(value) <= Number.MAX_SAFE_INTEGER ? value : null;
|
|
9
|
+
/** Read-only projection of the existing breaker owner. Only fixed reason codes
|
|
10
|
+
* and scope identities survive; worker captures, actions and diagnostics do not.
|
|
11
|
+
* Clock values are converted with the owner's persisted wall-clock anchor,
|
|
12
|
+
* never with the reader's clock (which may belong to another process/boot). */
|
|
13
|
+
export function projectRecoveryStatus(value, source = 'recorded') {
|
|
14
|
+
const record = objectRecord(value);
|
|
15
|
+
const observedAtMs = finiteClock(record?.observedAtMs);
|
|
16
|
+
const wall = typeof record?.observedAt === 'string' ? Date.parse(record.observedAt) : NaN;
|
|
17
|
+
const observedAt = Number.isFinite(wall) ? new Date(wall).toISOString() : null;
|
|
18
|
+
const at = (clock) => {
|
|
19
|
+
if (clock === null || observedAtMs === null || observedAt === null)
|
|
20
|
+
return null;
|
|
21
|
+
const mapped = wall + clock - observedAtMs;
|
|
22
|
+
return Number.isFinite(mapped) && Math.abs(mapped) <= 8.64e15 ? new Date(mapped).toISOString() : null;
|
|
23
|
+
};
|
|
24
|
+
const identity = (item) => {
|
|
25
|
+
if (!recoveryScopes.has(item.scope) || typeof item.scopeKey !== 'string'
|
|
26
|
+
|| !/^[\p{L}\p{N}_.:/@+~%\-]{1,256}$/u.test(item.scopeKey)
|
|
27
|
+
|| !item.scopeKey.startsWith(`${String(item.scope)}:`))
|
|
28
|
+
return null;
|
|
29
|
+
return { scopeKey: item.scopeKey, scope: item.scope };
|
|
30
|
+
};
|
|
31
|
+
const reasons = (value) => Array.isArray(value)
|
|
32
|
+
? [...new Set(value.slice(0, 32).filter(reason => recoveryReasons.has(reason)))] : [];
|
|
33
|
+
const rawScopes = Array.isArray(record?.scopes) ? record.scopes : [];
|
|
34
|
+
const rawTransitions = Array.isArray(record?.transitions) ? record.transitions : [];
|
|
35
|
+
const scopes = [];
|
|
36
|
+
for (const raw of rawScopes.slice(0, 32)) {
|
|
37
|
+
const item = objectRecord(raw), id = item && identity(item);
|
|
38
|
+
if (!item || !id || !recoveryStates.has(item.state))
|
|
39
|
+
continue;
|
|
40
|
+
const nextEligibleAtMs = finiteClock(item.nextEligibleAtMs);
|
|
41
|
+
scopes.push({ ...id, state: item.state, reasons: reasons(item.reasons),
|
|
42
|
+
nextEligibleAtMs, nextEligibleAt: at(nextEligibleAtMs), canaryInFlight: item.canaryInFlight === true });
|
|
43
|
+
}
|
|
44
|
+
const transitions = [];
|
|
45
|
+
for (const raw of rawTransitions.slice(-32)) {
|
|
46
|
+
const item = objectRecord(raw), id = item && identity(item);
|
|
47
|
+
const clock = finiteClock(item?.atMs);
|
|
48
|
+
if (!item || !id || clock === null || !recoveryStates.has(item.from) || !recoveryStates.has(item.to))
|
|
49
|
+
continue;
|
|
50
|
+
const nextEligibleAtMs = finiteClock(item.nextEligibleAtMs);
|
|
51
|
+
transitions.push({ ...id, from: item.from, to: item.to,
|
|
52
|
+
reasons: reasons(item.reasons), atMs: clock, at: at(clock), nextEligibleAtMs, nextEligibleAt: at(nextEligibleAtMs) });
|
|
53
|
+
}
|
|
54
|
+
const omittedScopeCount = typeof record?.omittedScopeCount === 'number'
|
|
55
|
+
&& Number.isSafeInteger(record.omittedScopeCount) && record.omittedScopeCount >= 0 ? record.omittedScopeCount : 0;
|
|
56
|
+
return { source: record ? source : 'unavailable', observedAt, observedAtMs, scopes, transitions,
|
|
57
|
+
omittedScopeCount: omittedScopeCount + Math.max(0, rawScopes.length - 32),
|
|
58
|
+
truncated: record?.truncated === true || omittedScopeCount > 0 || rawScopes.length > 32 || rawTransitions.length > 32 };
|
|
59
|
+
}
|
|
60
|
+
/** Merge independently owned observations without letting one owner's close
|
|
61
|
+
* hide another owner's active scope. Callers first select the latest record
|
|
62
|
+
* for each owner; owner labels are supplied from validated launcher identity. */
|
|
63
|
+
export function mergeRecoveryStatuses(statuses) {
|
|
64
|
+
const available = statuses.filter(status => status.source !== 'unavailable');
|
|
65
|
+
const latest = available.reduce((last, status) => !last
|
|
66
|
+
|| Date.parse(status.observedAt ?? '') >= Date.parse(last.observedAt ?? '') ? status : last, null);
|
|
67
|
+
if (!latest)
|
|
68
|
+
return projectRecoveryStatus(null);
|
|
69
|
+
const allScopes = available.flatMap(status => status.scopes);
|
|
70
|
+
const scopes = allScopes.filter(scope => scope.state !== 'closed').concat(allScopes.filter(scope => scope.state === 'closed'));
|
|
71
|
+
const transitions = available.flatMap(status => status.transitions).sort((a, b) => (a.at ? Date.parse(a.at) : a.atMs) - (b.at ? Date.parse(b.at) : b.atMs));
|
|
72
|
+
const omittedScopeCount = available.reduce((count, status) => count + status.omittedScopeCount, 0) + Math.max(0, scopes.length - 32);
|
|
73
|
+
return { source: latest.source, observedAt: latest.observedAt, observedAtMs: latest.observedAtMs,
|
|
74
|
+
scopes: scopes.slice(0, 32), transitions: transitions.slice(-32), omittedScopeCount,
|
|
75
|
+
truncated: available.some(status => status.truncated) || omittedScopeCount > 0 || transitions.length > 32 };
|
|
76
|
+
}
|
|
77
|
+
/** Mechanical correction records carry fixed codes and invocation identities. */
|
|
78
|
+
export function projectStartupCorrection(value) {
|
|
79
|
+
const record = objectRecord(value);
|
|
80
|
+
const safeId = (value) => typeof value === 'string'
|
|
81
|
+
&& /^[\p{L}\p{N}_.:/@+~%\-]{1,256}$/u.test(value);
|
|
82
|
+
if (!record || record.action !== 'refresh-executable-binding' || record.reasonCode !== 'NOT_FOUND'
|
|
83
|
+
|| !['applied', 'stopped'].includes(String(record.state)) || !safeId(record.previousInvocationId))
|
|
84
|
+
return null;
|
|
85
|
+
const stopCodes = new Set(['ATTEMPT_LIMIT', 'CANCELLED', 'DEADLINE', 'UNCHANGED_EXECUTABLE', 'QUALIFICATION_REFUSED',
|
|
86
|
+
'BINDING_CHANGED', 'RETRY_REFUSED', 'RETRY_FAILED']);
|
|
87
|
+
return { action: 'refresh-executable-binding', reasonCode: 'NOT_FOUND', state: record.state,
|
|
88
|
+
previousInvocationId: record.previousInvocationId,
|
|
89
|
+
...(typeof record.stopCode === 'string' && stopCodes.has(record.stopCode) ? { stopCode: record.stopCode } : {}),
|
|
90
|
+
...(safeId(record.retryInvocationId) ? { retryInvocationId: record.retryInvocationId } : {}) };
|
|
91
|
+
}
|
|
2
92
|
//# sourceMappingURL=worker-status.js.map
|
|
@@ -83,6 +83,13 @@ export function parseGrokBuildModelConfig(text, scope) {
|
|
|
83
83
|
parsed.projectModelFields = true;
|
|
84
84
|
continue;
|
|
85
85
|
}
|
|
86
|
+
// Presence only: never retain the command or endpoint in discovery output.
|
|
87
|
+
if ((table === 'auth' || table === 'grok_com_config') && key === 'auth_provider_command') {
|
|
88
|
+
parsed.hasExternalAuthProvider = Boolean(unquote(value)?.trim());
|
|
89
|
+
}
|
|
90
|
+
if (table === 'endpoints' && ['models_base_url', 'models_list_url', 'models_endpoint'].includes(key)) {
|
|
91
|
+
parsed.hasCustomModelsEndpoint ||= Boolean(unquote(value)?.trim());
|
|
92
|
+
}
|
|
86
93
|
if (table === 'models') {
|
|
87
94
|
if (key === 'default')
|
|
88
95
|
parsed.defaultModel = unquote(value);
|
|
@@ -299,6 +306,19 @@ export async function discoverGrokBuildModels(options = {}) {
|
|
|
299
306
|
const discovered = inspectModels(report, validationDiagnostics);
|
|
300
307
|
const parsedByLayer = await Promise.all(layers.map(layer => layer.role === 'env_overlay'
|
|
301
308
|
&& layer.path.includes('(inline)') ? parseEnvOverlay(env) : readLayer(layer)));
|
|
309
|
+
// Reviewed upstream 2bdd1d6a: external auth plus a custom models endpoint
|
|
310
|
+
// discards [model.*] and ordinary allowed_models. Inspect has no effective
|
|
311
|
+
// catalog, so config reconstruction cannot establish selectable aliases.
|
|
312
|
+
// Conservatively withhold discovery when any active layer names this setup;
|
|
313
|
+
// do not execute the auth provider or fetch its endpoint to fill the gap.
|
|
314
|
+
const externalAuth = Boolean(env.GROK_AUTH_PROVIDER_COMMAND?.trim())
|
|
315
|
+
|| parsedByLayer.some(layer => layer?.hasExternalAuthProvider);
|
|
316
|
+
const customEndpoint = Boolean(env.GROK_MODELS_BASE_URL?.trim() || env.GROK_MODELS_LIST_URL?.trim())
|
|
317
|
+
|| parsedByLayer.some(layer => layer?.hasCustomModelsEndpoint);
|
|
318
|
+
if (externalAuth && customEndpoint) {
|
|
319
|
+
return { ...base, ...(runtimeVersion ? { runtimeVersion } : {}), errorKind: 'invalid-output',
|
|
320
|
+
error: 'Grok Build external auth with a custom models endpoint requires its runtime catalog; config model aliases and ordinary allowed_models are ignored upstream. Automatic discovery is deferred for this configuration.' };
|
|
321
|
+
}
|
|
302
322
|
let requirementsDefault;
|
|
303
323
|
let requirementsAllowed;
|
|
304
324
|
for (let i = 0; i < layers.length; i++) {
|
|
@@ -11,7 +11,7 @@ export function loadRecordedWorkerInstallations(input: {
|
|
|
11
11
|
storeDigest?: string;
|
|
12
12
|
/** Request validation after store presence, before recorded policy/adapter checks. */
|
|
13
13
|
precheckPolicy?: () => void;
|
|
14
|
-
}): Promise<{ registry: ProviderModelCapabilityRegistry; installations: Record<string, WorkerResolutionBinding>; drivers: Record<string, WorkerInvocationDriver>; observations: Record<string, import('./model-discovery.js').ProviderModelDiscovery> }>;
|
|
14
|
+
}): Promise<{ registry: ProviderModelCapabilityRegistry; installations: Record<string, WorkerResolutionBinding>; drivers: Record<string, WorkerInvocationDriver>; observations: Record<string, import('./model-discovery.js').ProviderModelDiscovery>; qualificationFailures?: Record<string, string> }>;
|
|
15
15
|
|
|
16
16
|
export function hasProvider(name: string): boolean;
|
|
17
17
|
|
|
@@ -824,6 +824,40 @@ export async function prepareWorkerInstallationStore({ workerWritableRoots, appl
|
|
|
824
824
|
return assertTrustedStore(installationStorePath(), workerWritableRoots, true, apply);
|
|
825
825
|
}
|
|
826
826
|
|
|
827
|
+
// Use the existing delivery ledger's host-selected keyring, never public keys
|
|
828
|
+
// supplied by a registration. PAE separates this authority from effect records.
|
|
829
|
+
const qualificationPayloadType = 'application/vnd.aiwg.worker-installation-qualification+json';
|
|
830
|
+
const requalifyMessage = 'Re-run aiwg doctor --probe-workers --qualify. Existing unsigned approvals require deliberate one-time requalification.';
|
|
831
|
+
const keyRecoveryMessage = 'Worker qualification signing trust is unavailable. Check effects.keyProvider in aiwg.config, unlock or restore the existing effect-ledger key, and run aiwg effect keys init if its keyring is not initialized; then re-run aiwg doctor --probe-workers --qualify.';
|
|
832
|
+
|
|
833
|
+
async function qualificationTrust(cwd, scope) {
|
|
834
|
+
const { effectSession, openLedger } = await import('../effects/host.js');
|
|
835
|
+
const { openEffectLedger, readLedgerKeyring } = await import('../effects/ledger.js');
|
|
836
|
+
const ledger = scope ? openEffectLedger({ projectDir: cwd, scope, writer: 'cli' }) : openLedger(await effectSession(cwd));
|
|
837
|
+
const keyring = await readLedgerKeyring(ledger);
|
|
838
|
+
if (!keyring) throw new Error(keyRecoveryMessage);
|
|
839
|
+
return { ledger, keyring };
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
async function qualificationPayload(record, signedAt, projectDir, scope) {
|
|
843
|
+
const { canonicalJson } = await import('../security/artifact-trust.js');
|
|
844
|
+
const { signature: _signature, ...body } = record;
|
|
845
|
+
return Buffer.from(canonicalJson({ record: JSON.parse(JSON.stringify(body)), signedAt, projectDir, scope }), 'utf8');
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
async function qualificationSignatureFailure(record, trust) {
|
|
849
|
+
if (!record.signature) return 'signature-missing';
|
|
850
|
+
const { keyid, sig, signedAt, projectDir, scope } = record.signature;
|
|
851
|
+
if (typeof keyid !== 'string' || typeof sig !== 'string' || typeof signedAt !== 'string'
|
|
852
|
+
|| typeof projectDir !== 'string' || !path.isAbsolute(projectDir) || !scope) return 'signature-invalid';
|
|
853
|
+
let keyring;
|
|
854
|
+
try { ({ keyring } = await trust(projectDir, scope)); } catch { return 'signature-trust-unavailable'; }
|
|
855
|
+
const { checkSignature } = await import('../effects/keyring.js');
|
|
856
|
+
const result = checkSignature(keyring, keyid, sig, qualificationPayloadType,
|
|
857
|
+
await qualificationPayload(record, signedAt, projectDir, scope), signedAt);
|
|
858
|
+
return result === 'ok' ? null : result === 'signature-invalid' ? result : `signature-${result}`;
|
|
859
|
+
}
|
|
860
|
+
|
|
827
861
|
async function readInstallationStore(filename, expectedDigest) {
|
|
828
862
|
try {
|
|
829
863
|
const bytes = await readInstalledFile(filename);
|
|
@@ -864,6 +898,7 @@ export async function inspectRecordedWorkerQualifications({ cwd, policy, paralle
|
|
|
864
898
|
|| (configured.executable && await realpath(configured.executable) !== await realpath(installation.executable))) continue;
|
|
865
899
|
const proof = adapter.invocationProfiles?.[binding.profile]?.installationQualification;
|
|
866
900
|
if (!proof || ['scopeDigest', 'fixtureDigest', 'verifiedAt', 'evidenceRef'].some(key => proof[key] !== installation.evidence?.[key])) continue;
|
|
901
|
+
if (await qualificationSignatureFailure(record, () => qualificationTrust(cwd))) continue;
|
|
867
902
|
await assertWorkerInstallationEvidence({ ...installation, adapter, now: Date.now(), policy }, proof.scopeDigest);
|
|
868
903
|
results.push({ cli, provider: adapter.provider });
|
|
869
904
|
} catch { /* Malformed, expired or unbound evidence is not qualification. */ }
|
|
@@ -879,6 +914,15 @@ export async function recordWorkerInstallation(instance, options) {
|
|
|
879
914
|
const installation = structuredClone(options.installation);
|
|
880
915
|
const result = await qualifyWorkerInstallation(instance, { ...installation, now: Date.now() });
|
|
881
916
|
if (result.status !== 'ENABLED') return result;
|
|
917
|
+
let signingKey, keyring, signingProjectDir;
|
|
918
|
+
try {
|
|
919
|
+
signingProjectDir = await realpath(options.projectDir ?? installation.cwd);
|
|
920
|
+
const trust = await qualificationTrust(signingProjectDir);
|
|
921
|
+
keyring = trust.keyring;
|
|
922
|
+
signingKey = await trust.ledger.signingKey();
|
|
923
|
+
const { activeKey } = await import('../effects/keyring.js');
|
|
924
|
+
if (activeKey(keyring).keyid !== signingKey.keyid) throw new Error('inactive key');
|
|
925
|
+
} catch { return { status: 'DISABLED', reason: keyRecoveryMessage }; }
|
|
882
926
|
const adapter = structuredClone(installation.adapter);
|
|
883
927
|
adapter.invocationProfiles[installation.profile].installationQualification = {
|
|
884
928
|
scopeDigest: result.scopeDigest, fixtureDigest: result.fixtureDigest,
|
|
@@ -913,6 +957,14 @@ export async function recordWorkerInstallation(instance, options) {
|
|
|
913
957
|
installation: { ...installation, environmentNames, versionProbe: { rawBanner: result.rawVersionBanner, version: result.cliVersion } },
|
|
914
958
|
...(options.mcpSchemas ? { mcpSchemas: structuredClone(options.mcpSchemas) } : {}),
|
|
915
959
|
...(options.mcpProfile ? { mcpProfile: structuredClone(options.mcpProfile) } : {}) };
|
|
960
|
+
const signedAt = new Date().toISOString();
|
|
961
|
+
try {
|
|
962
|
+
const payload = await qualificationPayload(record, signedAt, signingProjectDir, keyring.scope);
|
|
963
|
+
const sig = signingKey.signPae(qualificationPayloadType, payload);
|
|
964
|
+
const { checkSignature } = await import('../effects/keyring.js');
|
|
965
|
+
if (checkSignature(keyring, signingKey.keyid, sig, qualificationPayloadType, payload, signedAt) !== 'ok') throw new Error('invalid signature');
|
|
966
|
+
record.signature = { keyid: signingKey.keyid, sig, signedAt, projectDir: signingProjectDir, scope: keyring.scope };
|
|
967
|
+
} catch { return { status: 'DISABLED', reason: keyRecoveryMessage }; }
|
|
916
968
|
const retained = [];
|
|
917
969
|
for (const entry of stored.registrations) {
|
|
918
970
|
const workspace = typeof entry.installation?.cwd === 'string'
|
|
@@ -944,7 +996,8 @@ export async function loadRecordedWorkerInstallations({ registry, cwd, policy, p
|
|
|
944
996
|
// Underlying errors can contain probe output, environment values or paths.
|
|
945
997
|
const rejections = [];
|
|
946
998
|
const fail = () => new WorkerPolicyError('OG_ADAPTER_UNQUALIFIED',
|
|
947
|
-
`Trusted installation qualification is required (${[...new Set(rejections.map(row => row.reason))].join(', ')})
|
|
999
|
+
`Trusted installation qualification is required (${[...new Set(rejections.map(row => row.reason))].join(', ')})${rejections.some(row => row.reason.startsWith('signature-'))
|
|
1000
|
+
? `. ${rejections.some(row => row.reason === 'signature-trust-unavailable') ? keyRecoveryMessage : requalifyMessage}` : ''}`,
|
|
948
1001
|
{ rejections });
|
|
949
1002
|
let reason = 'store-binding';
|
|
950
1003
|
let filename;
|
|
@@ -968,6 +1021,7 @@ export async function loadRecordedWorkerInstallations({ registry, cwd, policy, p
|
|
|
968
1021
|
const installations = {};
|
|
969
1022
|
const drivers = {};
|
|
970
1023
|
const observations = {};
|
|
1024
|
+
const qualificationFailures = Object.create(null);
|
|
971
1025
|
const resolvedRegistry = structuredClone(registry);
|
|
972
1026
|
const requestedWorkspace = await realpath(cwd);
|
|
973
1027
|
const workspaces = await Promise.all(stored.registrations.map(async record => {
|
|
@@ -990,6 +1044,11 @@ export async function loadRecordedWorkerInstallations({ registry, cwd, policy, p
|
|
|
990
1044
|
if (!/^[a-z][a-z0-9-]{0,62}$/.test(cli)) throw fail();
|
|
991
1045
|
if (counts.get(JSON.stringify([cli, workspaces[registration]])) !== 1) throw fail();
|
|
992
1046
|
if (selected.get(cli) !== registration) continue;
|
|
1047
|
+
// A conductor-pinned store digest also binds the original config source.
|
|
1048
|
+
// Framing from a worker cwd must never read that cwd's configuration.
|
|
1049
|
+
reason = await qualificationSignatureFailure(record, (projectDir, scope) => storePath === undefined
|
|
1050
|
+
? qualificationTrust(cwd) : qualificationTrust(projectDir, scope));
|
|
1051
|
+
if (reason) throw fail();
|
|
993
1052
|
const installation = { ...record.installation, adapter, now: Date.now(), environment: {} };
|
|
994
1053
|
reason = 'environment-name';
|
|
995
1054
|
if (record.installation.environment && Object.keys(record.installation.environment).length) throw fail();
|
|
@@ -1106,13 +1165,15 @@ export async function loadRecordedWorkerInstallations({ registry, cwd, policy, p
|
|
|
1106
1165
|
Object.assign(resolvedRegistry, candidateRegistry);
|
|
1107
1166
|
} catch (error) {
|
|
1108
1167
|
// One rejected installation cannot grant authority or hide qualified peers.
|
|
1109
|
-
|
|
1110
|
-
? 'adapter-conflict' : reason
|
|
1168
|
+
const failureReason = reason === 'module-registration' && error?.code === 'WORKER_ADAPTER_CONFLICT'
|
|
1169
|
+
? 'adapter-conflict' : reason;
|
|
1170
|
+
rejections.push({ registration, reason: failureReason });
|
|
1171
|
+
if (typeof record?.cli === 'string' && /^[a-z][a-z0-9-]{0,62}$/.test(record.cli)) qualificationFailures[record.cli] = failureReason;
|
|
1111
1172
|
}
|
|
1112
1173
|
}
|
|
1113
1174
|
if (!Object.keys(installations).length) {
|
|
1114
1175
|
if (!rejections.length) rejections.push({ registration: null, reason: 'no-records' });
|
|
1115
1176
|
throw fail();
|
|
1116
1177
|
}
|
|
1117
|
-
return { registry: resolvedRegistry, installations, drivers, observations };
|
|
1178
|
+
return { registry: resolvedRegistry, installations, drivers, observations, qualificationFailures };
|
|
1118
1179
|
}
|