@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.
Files changed (100) hide show
  1. package/bin/aiwg.mjs +4 -2
  2. package/dist/src/a2a/agent-card.js +11 -0
  3. package/dist/src/api/index.d.ts +1 -0
  4. package/dist/src/api/index.js +1 -0
  5. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/contract.json +103 -0
  6. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-config.schema.json +212 -0
  7. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-set.schema.json +240 -0
  8. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-manifest.schema.json +451 -0
  9. package/dist/src/artifacts/fortemi-shard-export.js +284 -0
  10. package/dist/src/artifacts/index-preparation.js +2 -0
  11. package/dist/src/auth/client.js +5 -1
  12. package/dist/src/auth/credential-store.js +5 -1
  13. package/dist/src/auth/sensitive-input.js +151 -0
  14. package/dist/src/channel/manager.mjs +3 -0
  15. package/dist/src/cli/handlers/agentcard.js +47 -25
  16. package/dist/src/cli/handlers/auth.js +22 -2
  17. package/dist/src/cli/handlers/cost-report.js +7 -1
  18. package/dist/src/cli/handlers/decision.js +36 -10
  19. package/dist/src/cli/handlers/effect.js +4 -84
  20. package/dist/src/cli/handlers/guided-setup.js +1 -1
  21. package/dist/src/cli/handlers/init.js +1 -1
  22. package/dist/src/cli/handlers/install.js +6 -0
  23. package/dist/src/cli/handlers/local-executor.js +33 -6
  24. package/dist/src/cli/handlers/marketplace.js +15 -4
  25. package/dist/src/cli/handlers/setup.js +24 -20
  26. package/dist/src/cli/handlers/use.js +57 -15
  27. package/dist/src/cli/prompt-utils.js +73 -0
  28. package/dist/src/config/gitignore.js +4 -0
  29. package/dist/src/config/workspace.js +2 -1
  30. package/dist/src/cost/fleet-report.js +20 -22
  31. package/dist/src/decision/adapters/jev.js +20 -31
  32. package/dist/src/decision/ask.js +6 -1
  33. package/dist/src/decision/benchmark.js +19 -6
  34. package/dist/src/decision/driver.js +2 -2
  35. package/dist/src/decision/jev-credentials.js +18 -5
  36. package/dist/src/decision/ledger.js +230 -34
  37. package/dist/src/decision/qualification/gate-evidence.js +6 -4
  38. package/dist/src/decision/qualification/load-staged.js +47 -10
  39. package/dist/src/decision/qualification/runner.js +7 -3
  40. package/dist/src/discovery/interview.js +10 -5
  41. package/dist/src/discovery/plan-apply.js +26 -30
  42. package/dist/src/discovery/plan-validation.js +2 -2
  43. package/dist/src/effects/host.js +91 -0
  44. package/dist/src/extensions/commands/definitions.js +2 -2
  45. package/dist/src/extensions/project-local-gitignore.js +2 -0
  46. package/dist/src/issues/cli.js +16 -5
  47. package/dist/src/issues/live.js +44 -12
  48. package/dist/src/marketplace/exchange.js +18 -6
  49. package/dist/src/marketplace/provenance.js +3 -2
  50. package/dist/src/mcp/adapters/codex-runtime.js +46 -76
  51. package/dist/src/mcp/cli.mjs +122 -109
  52. package/dist/src/mcp/credentials.d.mts +4 -0
  53. package/dist/src/mcp/credentials.mjs +44 -3
  54. package/dist/src/mcp/grok-build-config.mjs +4 -1
  55. package/dist/src/mcp/muse-native-config.mjs +2 -1
  56. package/dist/src/mcp/omp-config.mjs +2 -0
  57. package/dist/src/mcp/registry.js +19 -15
  58. package/dist/src/mcp/registry.mjs +2 -18
  59. package/dist/src/mcp/tools/agentic-sandbox.mjs +46 -28
  60. package/dist/src/mcp/tools/worker-invoke.mjs +13 -2
  61. package/dist/src/memory/project-registry.js +2 -1
  62. package/dist/src/mission-protocol/worker-status.js +91 -1
  63. package/dist/src/models/grok-build-models.js +20 -0
  64. package/dist/src/models/worker-installation.d.mts +1 -1
  65. package/dist/src/models/worker-installation.mjs +65 -4
  66. package/dist/src/packages/adapters/git.js +99 -23
  67. package/dist/src/packages/adapters/gitea.js +8 -27
  68. package/dist/src/packages/adapters/github.js +7 -25
  69. package/dist/src/packages/registry.js +4 -4
  70. package/dist/src/process/cli-command.d.mts +5 -0
  71. package/dist/src/process/cli-command.mjs +42 -0
  72. package/dist/src/providers/provider-definitions.js +2 -2
  73. package/dist/src/serve/shared-host-scheduler.js +4 -1
  74. package/dist/src/serve/worker-invocation.js +10 -0
  75. package/dist/src/smiths/context-pipeline/claude-hook.js +9 -9
  76. package/dist/src/smiths/context-pipeline/context-file-safety.js +330 -9
  77. package/dist/src/smiths/context-pipeline/finalization.js +3 -2
  78. package/dist/src/smiths/context-pipeline/generator.js +7 -19
  79. package/dist/src/smiths/context-pipeline/legacy-inject.js +3 -10
  80. package/dist/src/smiths/context-pipeline/managed-hook.js +5 -5
  81. package/dist/src/smiths/context-pipeline/workspace-context.js +4 -12
  82. package/dist/src/storage/backends/fortemi.js +107 -49
  83. package/dist/src/tracker/project-inputs.js +2 -0
  84. package/dist/src/update/notifier.mjs +2 -4
  85. package/package.json +1 -1
  86. package/tools/agents/providers/base.mjs +5 -1
  87. package/tools/agents/providers/claude.mjs +5 -2
  88. package/tools/agents/providers/codex.mjs +4 -1
  89. package/tools/agents/providers/context-write.mjs +28 -0
  90. package/tools/agents/providers/copilot.mjs +5 -2
  91. package/tools/agents/providers/hermes.mjs +5 -2
  92. package/tools/agents/providers/omp.mjs +3 -1
  93. package/tools/agents/providers/rule-delivery.mjs +29 -12
  94. package/tools/agents/providers/warp.mjs +4 -1
  95. package/tools/agents/providers/windsurf.mjs +6 -3
  96. package/tools/ralph-external/lib/consecutive-failure-circuit-breaker.mjs +26 -0
  97. package/tools/ralph-external/lib/launcher-recovery-evidence.mjs +7 -1
  98. package/tools/ralph-external/lib/recovery-probe.mjs +14 -4
  99. package/tools/ralph-external/process-monitor.mjs +42 -4
  100. 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];
@@ -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
- const ENV_REFERENCE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
36
- function validateCredentialReferences(def) {
37
- for (const [header, envName] of Object.entries(def.headerEnv ?? {})) {
38
- if (!header.trim())
39
- throw new Error('MCP header-env header name must not be empty');
40
- if (!ENV_REFERENCE_NAME.test(envName)) {
41
- throw new Error(`Invalid MCP header environment variable reference "${envName}"`);
42
- }
43
- }
44
- for (const [key, envName] of Object.entries(def.envFrom ?? {})) {
45
- if (!ENV_REFERENCE_NAME.test(key))
46
- throw new Error(`Invalid MCP env-from variable name "${key}"`);
47
- if (!ENV_REFERENCE_NAME.test(envName)) {
48
- throw new Error(`Invalid MCP env-from environment variable reference "${envName}"`);
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-backed server configuration, never tool arguments.
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 { readFile, stat } from 'node:fs/promises';
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 managementBearer(env = process.env) {
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
- if (!tokenFile) throw new Error('AIWG_SANDBOX_MANAGEMENT_TOKEN_FILE is required for the sandbox toolset');
42
- const metadata = await stat(tokenFile);
43
- if (!metadata.isFile()) throw new Error('sandbox management token path is not a regular file');
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 token = String(await readFile(tokenFile, 'utf8')).trim();
48
- if (!token || /[\r\n]/.test(token)) throw new Error('sandbox management token file must contain one non-empty bearer token');
49
- return token;
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 response = await this.fetch(`${baseUrl}${path}`, {
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
- assertNoWorkerBinding(parsed, 'mcp');
164
- return typedResult(parsed, response.status, validate);
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
- return mcpError(`${error.code ?? 'sandbox_malformed_response'}: ${error.message}`);
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
- return { unresolvedReservations, interventionScopes };
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
- export {};
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
- rejections.push({ registration, reason: reason === 'module-registration' && error?.code === 'WORKER_ADAPTER_CONFLICT'
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
  }