@aiwg/cli 2026.10.7 → 2026.10.9

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 (33) hide show
  1. package/dist/src/artifacts/cli.js +9 -5
  2. package/dist/src/auth/config.js +3 -8
  3. package/dist/src/auth/resource-credentials.js +35 -2
  4. package/dist/src/cli/handlers/cost-report.js +7 -1
  5. package/dist/src/cli/handlers/decision.js +33 -8
  6. package/dist/src/cli/handlers/diagnose.js +13 -0
  7. package/dist/src/cli/handlers/local-executor.js +33 -6
  8. package/dist/src/cli/handlers/resource-versions.js +8 -5
  9. package/dist/src/cli/handlers/script-runner.js +13 -1
  10. package/dist/src/cli/handlers/use.js +18 -3
  11. package/dist/src/cost/fleet-report.js +20 -22
  12. package/dist/src/decision/driver.js +2 -2
  13. package/dist/src/decision/jev-credentials.js +18 -5
  14. package/dist/src/decision/ledger.js +230 -34
  15. package/dist/src/mcp/cli.mjs +105 -19
  16. package/dist/src/mcp/credentials.d.mts +4 -0
  17. package/dist/src/mcp/credentials.mjs +44 -3
  18. package/dist/src/mcp/grok-build-config.mjs +2 -0
  19. package/dist/src/mcp/omp-config.mjs +2 -0
  20. package/dist/src/mcp/registry.js +19 -15
  21. package/dist/src/mcp/registry.mjs +2 -18
  22. package/dist/src/mcp/tools/agentic-sandbox.mjs +46 -28
  23. package/dist/src/models/grok-build-models.js +20 -0
  24. package/dist/src/providers/provider-definitions.js +2 -2
  25. package/dist/src/resources/web-release.js +76 -65
  26. package/dist/src/serve/shared-host-scheduler.js +4 -1
  27. package/dist/src/storage/backends/fortemi.js +112 -50
  28. package/dist/src/storage/backends/logseq.js +16 -3
  29. package/dist/src/storage/backends/postgres.js +74 -33
  30. package/dist/src/storage/backends/postgrest.js +37 -13
  31. package/dist/src/storage/config.js +6 -1
  32. package/dist/src/uhp/config.js +4 -0
  33. package/package.json +1 -1
@@ -29,7 +29,7 @@ import {
29
29
  prepareClaudePermissions,
30
30
  resolveToolFilters,
31
31
  } from './tool-filters.mjs';
32
- import { assertCredentialPolicy, CREDENTIAL_POLICIES, installPolicyCheckedServer, redactCredentialArgs, redactCredentialText, redactCredentialRecord, resolveCredentialPolicy } from './credentials.mjs';
32
+ import { assertCredentialPolicy, CREDENTIAL_POLICIES, credentialReferenceInput, validateCredentialReferences, hasSecretLookingLiterals, installPolicyCheckedServer, redactCredentialArgs, redactCredentialText, redactCredentialRecord, resolveCredentialPolicy } from './credentials.mjs';
33
33
  import { getMcpInjectionDefinition } from '../providers/provider-definitions.mjs';
34
34
  import { manageOmpMcp } from './omp-config.mjs';
35
35
  import { claudeDesktopConfigPath, resolveClaudeInstallTarget, legacyClaudeMigration } from './claude-install.mjs';
@@ -176,13 +176,15 @@ Server Options (for add/update):
176
176
  --url <url> Server URL (for http/sse types)
177
177
  --type <type> Server type: http (default), stdio, sse
178
178
  --command <cmd> Command to run (for stdio type)
179
- --args <a1,a2,...> Command arguments (comma-separated, for stdio)
179
+ --args <a1,a2,...> Literal command arguments (visible on argv; prefer env/header references)
180
180
  --env <K=V,...> Environment variables (comma-separated K=V pairs)
181
181
  --headers <K=V,...> HTTP headers (comma-separated K=V pairs)
182
- --header-env <K=ENV,...>
183
- Resolve HTTP header values from environment variables
184
- --env-from <K=ENV,...>
185
- Resolve stdio server variables from environment variables
182
+ --header-env <K=SOURCE,...>
183
+ HTTP header references: ENV_NAME, file:ABSOLUTE_PATH, store:NAME
184
+ --env-from <K=SOURCE,...>
185
+ Stdio variable references (same sources; repeatable)
186
+ File/store require the AIWG runtime client; native configs use ENV_NAME
187
+ stdin:STORE_NAME / interactive:STORE_NAME save one value to the OS store
186
188
  --description <text> Optional description
187
189
 
188
190
  Inject Options:
@@ -701,6 +703,83 @@ function parseKVPairs(str) {
701
703
  return Object.keys(result).length > 0 ? result : undefined;
702
704
  }
703
705
 
706
+ function parseHeaderPairs(str) {
707
+ if (!str) return undefined;
708
+ const entries = str.split(',').flatMap(pair => {
709
+ const delimiter = pair.search(/[=:]/);
710
+ return delimiter < 0 ? [] : [[pair.slice(0, delimiter).trim(), pair.slice(delimiter + 1).trim()]];
711
+ });
712
+ return entries.length ? Object.fromEntries(entries) : undefined;
713
+ }
714
+
715
+ /** Validate every occurrence, retaining the existing comma-separated spelling. */
716
+ function parseCredentialReferences(args, flag) {
717
+ let result;
718
+ for (let index = 0; index < args.length; index++) {
719
+ const token = args[index];
720
+ if (token === '--') break;
721
+ if (token !== flag && !token.startsWith(`${flag}=`)) continue;
722
+ const value = token === flag ? args[++index] : token.slice(flag.length + 1);
723
+ if (!value || value.startsWith('--')) throw new Error(`${flag} requires KEY=ENV_NAME, KEY=file:ABSOLUTE_PATH, or KEY=store:NAME.`);
724
+ for (const pair of value.split(',')) {
725
+ const equals = pair.indexOf('=');
726
+ const key = pair.slice(0, equals).trim();
727
+ if (equals < 1 || !key) throw new Error(`${flag} requires KEY=ENV_NAME, KEY=file:ABSOLUTE_PATH, or KEY=store:NAME.`);
728
+ const reference = pair.slice(equals + 1).trim();
729
+ credentialReferenceInput(reference.replace(/^(?:stdin|interactive):/, 'store:'), flag);
730
+ result = { ...result, [key]: reference };
731
+ }
732
+ }
733
+ return result;
734
+ }
735
+
736
+ /** Transient input is saved by the existing raw-value store; registry receives only its name. */
737
+ export async function persistCredentialInputs(server, dependencies) {
738
+ const inputs = [];
739
+ for (const field of ['envFrom', 'headerEnv']) {
740
+ for (const [key, reference] of Object.entries(server[field] || {})) {
741
+ const match = /^(stdin|interactive):(.+)$/.exec(reference);
742
+ if (match) {
743
+ credentialReferenceInput(`store:${match[2]}`, field === 'envFrom' ? '--env-from' : '--header-env');
744
+ inputs.push({ field, key, source: match[1], name: match[2] });
745
+ }
746
+ }
747
+ }
748
+ if (inputs.filter(input => input.source === 'stdin').length > 1) {
749
+ throw new Error('Only one --env-from/--header-env value may use stdin:STORE_NAME per invocation; use file:PATH or store:NAME for additional values.');
750
+ }
751
+ const persistent = Object.fromEntries(['envFrom', 'headerEnv'].map(field => [field,
752
+ Object.fromEntries(Object.entries(server[field] || {}).map(([key, reference]) => [key, reference.replace(/^(?:stdin|interactive):/, 'store:')]))]));
753
+ validateCredentialReferences(persistent);
754
+ if (!inputs.length) return;
755
+ const { resolveSensitiveInput, namedSensitiveStore } = dependencies
756
+ ?? await import(pathToFileURL(path.join(findOwnPackageRoot(), 'dist', 'src', 'auth', 'sensitive-input.js')).href);
757
+ // Read/validate all requested inputs before changing the store.
758
+ const pending = [];
759
+ for (const input of inputs) {
760
+ try {
761
+ const value = await resolveSensitiveInput({ prefix: 'value', [input.source]: true });
762
+ pending.push({ ...input, value });
763
+ } catch (error) {
764
+ const flag = input.field === 'envFrom' ? '--env-from' : '--header-env';
765
+ const message = error instanceof Error ? error.message : 'MCP credential input failed';
766
+ throw new Error(message.replace(/--value-(interactive|stdin|file|store|env)(?: <(?:path|name)>)?/g, (_, source) =>
767
+ `${flag} KEY=${['interactive', 'stdin'].includes(source) ? `${source}:STORE_NAME` : source === 'env' ? 'ENV_NAME' : source === 'file' ? 'file:ABSOLUTE_PATH' : 'store:NAME'}`));
768
+ }
769
+ }
770
+ for (const { field, key, name, value } of pending) {
771
+ try { await namedSensitiveStore(name).saveSecret(value); }
772
+ catch { throw new Error('MCP credential input could not be saved to the native store; use --env-from/--header-env with file:PATH or ENV_NAME instead.'); }
773
+ server[field][key] = `store:${name}`;
774
+ }
775
+ }
776
+
777
+ function warnLiteralCredentials(server) {
778
+ if (hasSecretLookingLiterals(server)) {
779
+ console.error('Warning: secret-bearing --env/--headers literals are deprecated; argv and saved config expose their values. Use --env-from/--header-env references. See docs/cli/sensitive-values.md.');
780
+ }
781
+ }
782
+
704
783
  /**
705
784
  * Handle `aiwg mcp add <name> [opts]`
706
785
  */
@@ -718,8 +797,8 @@ async function handleAdd(args) {
718
797
  const argsStr = parseFlag(args, '--args');
719
798
  const envStr = parseFlag(args, '--env');
720
799
  const headersStr = parseFlag(args, '--headers');
721
- const headerEnvStr = parseFlag(args, '--header-env');
722
- const envFromStr = parseFlag(args, '--env-from');
800
+ const headerEnv = parseCredentialReferences(args, '--header-env');
801
+ const envFrom = parseCredentialReferences(args, '--env-from');
723
802
  const description = parseFlag(args, '--description');
724
803
 
725
804
  if (type === 'stdio' && !command) {
@@ -732,24 +811,28 @@ async function handleAdd(args) {
732
811
  }
733
812
 
734
813
  const registry = new McpServerRegistry();
735
- await registry.add({
814
+ const definition = {
736
815
  name,
737
816
  type,
738
817
  url,
739
818
  command,
740
819
  args: argsStr ? argsStr.split(',') : undefined,
741
820
  env: parseKVPairs(envStr),
742
- headers: parseKVPairs(headersStr),
743
- headerEnv: parseKVPairs(headerEnvStr),
744
- envFrom: parseKVPairs(envFromStr),
821
+ headers: parseHeaderPairs(headersStr),
822
+ headerEnv,
823
+ envFrom,
745
824
  description,
746
- });
825
+ };
826
+ warnLiteralCredentials(definition);
827
+ if ([...Object.values(definition.envFrom || {}), ...Object.values(definition.headerEnv || {})].some(ref => /^(?:stdin|interactive):/.test(ref)) && await registry.get(name)) throw new Error(`MCP server "${name}" already exists`);
828
+ await persistCredentialInputs(definition);
829
+ await registry.add(definition);
747
830
 
748
831
  console.log(`Added MCP server: ${name}`);
749
832
  if (url) console.log(` URL: ${redactUrlUserinfo(url)}`);
750
833
  if (command) console.log(` Command: ${redactCredentialText(command)}`);
751
834
  console.log(` Type: ${type}`);
752
- for (const [key, values] of [['env', parseKVPairs(envStr)], ['headers', parseKVPairs(headersStr)]]) {
835
+ for (const [key, values] of [['env', definition.env], ['headers', definition.headers]]) {
753
836
  if (values) console.log(` ${key}: ${Object.keys(values).join(', ')}`);
754
837
  }
755
838
  console.log(`\nUse "aiwg mcp inject --provider <name>" to inject into a provider config.`);
@@ -790,8 +873,8 @@ async function handleUpdate(args) {
790
873
  const argsStr = parseFlag(args, '--args');
791
874
  const envStr = parseFlag(args, '--env');
792
875
  const headersStr = parseFlag(args, '--headers');
793
- const headerEnvStr = parseFlag(args, '--header-env');
794
- const envFromStr = parseFlag(args, '--env-from');
876
+ const headerEnv = parseCredentialReferences(args, '--header-env');
877
+ const envFrom = parseCredentialReferences(args, '--env-from');
795
878
  const description = parseFlag(args, '--description');
796
879
 
797
880
  if (url !== undefined) updates.url = url;
@@ -799,9 +882,9 @@ async function handleUpdate(args) {
799
882
  if (command !== undefined) updates.command = command;
800
883
  if (argsStr !== undefined) updates.args = argsStr.split(',');
801
884
  if (envStr !== undefined) updates.env = parseKVPairs(envStr);
802
- if (headersStr !== undefined) updates.headers = parseKVPairs(headersStr);
803
- if (headerEnvStr !== undefined) updates.headerEnv = parseKVPairs(headerEnvStr);
804
- if (envFromStr !== undefined) updates.envFrom = parseKVPairs(envFromStr);
885
+ if (headersStr !== undefined) updates.headers = parseHeaderPairs(headersStr);
886
+ if (headerEnv !== undefined) updates.headerEnv = headerEnv;
887
+ if (envFrom !== undefined) updates.envFrom = envFrom;
805
888
  if (description !== undefined) updates.description = description;
806
889
 
807
890
  if (Object.keys(updates).length === 0) {
@@ -810,6 +893,9 @@ async function handleUpdate(args) {
810
893
  }
811
894
 
812
895
  const registry = new McpServerRegistry();
896
+ warnLiteralCredentials(updates);
897
+ if ([...Object.values(updates.envFrom || {}), ...Object.values(updates.headerEnv || {})].some(ref => /^(?:stdin|interactive):/.test(ref)) && !(await registry.get(name))) throw new Error(`MCP server "${name}" not found`);
898
+ await persistCredentialInputs(updates);
813
899
  await registry.update(name, updates);
814
900
  console.log(`Updated MCP server: ${name}`);
815
901
  for (const [key, value] of Object.entries(updates)) {
@@ -21,6 +21,10 @@ export function isCredentialReferenceValue(value: unknown): boolean;
21
21
  export function credentialLikeArgs(args?: string[], options?: { includeReferences?: boolean }): string[];
22
22
  export function redactCredentialArgs(args?: string[]): string[] | undefined;
23
23
  export function validateEnvReferenceName(name: string): void;
24
+ export function credentialReferenceInput(reference: string, flag?: string): { env?: string; file?: string; store?: string };
25
+ export function validateCredentialReferences(server: Pick<CredentialBearingServer, 'headerEnv' | 'envFrom'>): void;
26
+ export function assertEnvironmentCredentialReferences(server: CredentialBearingServer, provider: string): void;
27
+ export function hasSecretLookingLiterals(server: CredentialBearingServer): boolean;
24
28
  export function renderCredentialMaps(
25
29
  server: CredentialBearingServer,
26
30
  provider: string,
@@ -3,9 +3,9 @@
3
3
  *
4
4
  * A registry entry carries credentials in two ways:
5
5
  * - literal values in `env` / `headers` (and OMP `auth` / `oauth` secrets)
6
- * - references: `headerEnv` (header -> variable) and `envFrom`
7
- * (server variable -> variable), which name a variable the harness
8
- * resolves when it starts the server. Only the variable name is written.
6
+ * - references: `headerEnv` and `envFrom` name a variable the harness
7
+ * resolves, or a file:/store: locator resolved only by AIWG's runtime
8
+ * client. Only the locator is written.
9
9
  *
10
10
  * Each harness spells a reference differently; ENV_REFERENCE_SYNTAX holds the
11
11
  * documented form. `null` means the harness documents no interpolation in its
@@ -48,6 +48,39 @@ export function validateEnvReferenceName(name) {
48
48
  }
49
49
  }
50
50
 
51
+ /** Durable locators only: the CLI saves ephemeral stdin/terminal input to the named store. */
52
+ export function credentialReferenceInput(reference, flag = '--env-from/--header-env') {
53
+ if (typeof reference === 'string') {
54
+ if (ENV_REFERENCE_NAME.test(reference)) return { env: reference };
55
+ if (/^store:[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/.test(reference)) return { store: reference.slice(6) };
56
+ // Absolute paths keep resolution independent of the consuming process's cwd.
57
+ if (/^file:(?:\/|[A-Za-z]:[\\/])[^\u0000-\u001f\u007f]+$/.test(reference)) return { file: reference.slice(5) };
58
+ }
59
+ throw new Error(`${flag} requires KEY=ENV_NAME, KEY=file:ABSOLUTE_PATH, or KEY=store:NAME.`);
60
+ }
61
+
62
+ export function validateCredentialReferences(server) {
63
+ for (const [field, flag] of [['headerEnv', '--header-env'], ['envFrom', '--env-from']]) {
64
+ for (const [key, reference] of Object.entries(server[field] || {})) {
65
+ if (field === 'headerEnv' ? !key.trim() : !ENV_REFERENCE_NAME.test(key)) {
66
+ throw new Error(`${flag} requires a valid ${field === 'headerEnv' ? 'header' : 'variable'} name.`);
67
+ }
68
+ credentialReferenceInput(reference, flag);
69
+ }
70
+ }
71
+ }
72
+
73
+ /** Native provider configs cannot call AIWG's private-file/native-store resolver. */
74
+ export function assertEnvironmentCredentialReferences(server, provider) {
75
+ for (const [field, flag] of [['envFrom', '--env-from'], ['headerEnv', '--header-env']]) {
76
+ for (const reference of Object.values(server[field] || {})) {
77
+ if (!credentialReferenceInput(reference, flag).env) {
78
+ throw new Error(`${provider} cannot resolve MCP file/store references. Use ${flag} KEY=ENV_NAME for provider configs; file/store references require the AIWG runtime client.`);
79
+ }
80
+ }
81
+ }
82
+ }
83
+
51
84
  function hasEntries(value) {
52
85
  return value !== null && typeof value === 'object' && Object.keys(value).length > 0;
53
86
  }
@@ -62,6 +95,7 @@ function hasReferences(server) {
62
95
  * harness has no documented syntax for them.
63
96
  */
64
97
  export function renderCredentialMaps(server, provider) {
98
+ assertEnvironmentCredentialReferences(server, provider);
65
99
  const env = server.env ? { ...server.env } : undefined;
66
100
  const headers = server.headers ? { ...server.headers } : undefined;
67
101
  if (!hasReferences(server)) return { env, headers };
@@ -116,6 +150,13 @@ function credentialName(key) {
116
150
  || CREDENTIAL_QUERY_NAME.test(part.replace(/[_\s]+/g, '-'))));
117
151
  }
118
152
 
153
+ /** Reuse policy scanner/name rules for the compatibility CLI warning. Never returns values. */
154
+ export function hasSecretLookingLiterals(server) {
155
+ return ['env', 'headers'].some(field => Object.entries(server[field] || {}).some(([key, value]) =>
156
+ typeof value === 'string' && literalValue(value, false)
157
+ && (credentialName(key) || /^(?:cookie|set-cookie)$/i.test(key) || scanCredentialText(value).some(hit => hit.credential))));
158
+ }
159
+
119
160
  function literalValue(value, includeReferences) {
120
161
  return value !== '' && (includeReferences || !isCredentialReferenceValue(value));
121
162
  }
@@ -1,3 +1,4 @@
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';
@@ -20,6 +21,7 @@ function inline(values = {}) {
20
21
  }
21
22
 
22
23
  export function renderGrokMcpServer(server) {
24
+ assertEnvironmentCredentialReferences(server, 'grok-build');
23
25
  if (!server?.name) throw new Error('Grok MCP server name is required');
24
26
  const lines = [`[mcp_servers.${key(server.name)}]`];
25
27
  if (server.type === 'stdio') {
@@ -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
  }
@@ -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++) {
@@ -687,9 +687,9 @@ const BUILT_IN_SEEDS = [
687
687
  source: 'https://github.com/xai-org/grok-build',
688
688
  version: '1.0.38',
689
689
  // Reviewed public tree (docs + inspect schema), not an all-zero placeholder.
690
- revision: '4247f661689354b831191f11eeeac8424993fe3d',
690
+ revision: '2bdd1d6a6369de0e8c68132ea4539e9abd9e14a8',
691
691
  runtime: 'Grok Build CLI',
692
- lastVerified: '2026-09-20',
692
+ lastVerified: '2026-10-26',
693
693
  },
694
694
  surfaces: {
695
695
  primary: 'grok-build',