@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.
- package/dist/src/artifacts/cli.js +9 -5
- package/dist/src/auth/config.js +3 -8
- package/dist/src/auth/resource-credentials.js +35 -2
- package/dist/src/cli/handlers/cost-report.js +7 -1
- package/dist/src/cli/handlers/decision.js +33 -8
- package/dist/src/cli/handlers/diagnose.js +13 -0
- package/dist/src/cli/handlers/local-executor.js +33 -6
- package/dist/src/cli/handlers/resource-versions.js +8 -5
- package/dist/src/cli/handlers/script-runner.js +13 -1
- package/dist/src/cli/handlers/use.js +18 -3
- package/dist/src/cost/fleet-report.js +20 -22
- 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/mcp/cli.mjs +105 -19
- 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 +2 -0
- 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/models/grok-build-models.js +20 -0
- package/dist/src/providers/provider-definitions.js +2 -2
- package/dist/src/resources/web-release.js +76 -65
- package/dist/src/serve/shared-host-scheduler.js +4 -1
- package/dist/src/storage/backends/fortemi.js +112 -50
- package/dist/src/storage/backends/logseq.js +16 -3
- package/dist/src/storage/backends/postgres.js +74 -33
- package/dist/src/storage/backends/postgrest.js +37 -13
- package/dist/src/storage/config.js +6 -1
- package/dist/src/uhp/config.js +4 -0
- package/package.json +1 -1
package/dist/src/mcp/cli.mjs
CHANGED
|
@@ -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,...>
|
|
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=
|
|
183
|
-
|
|
184
|
-
--env-from <K=
|
|
185
|
-
|
|
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
|
|
722
|
-
const
|
|
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
|
-
|
|
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:
|
|
743
|
-
headerEnv
|
|
744
|
-
envFrom
|
|
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',
|
|
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
|
|
794
|
-
const
|
|
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 =
|
|
803
|
-
if (
|
|
804
|
-
if (
|
|
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`
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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];
|
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
|
}
|
|
@@ -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: '
|
|
690
|
+
revision: '2bdd1d6a6369de0e8c68132ea4539e9abd9e14a8',
|
|
691
691
|
runtime: 'Grok Build CLI',
|
|
692
|
-
lastVerified: '2026-
|
|
692
|
+
lastVerified: '2026-10-26',
|
|
693
693
|
},
|
|
694
694
|
surfaces: {
|
|
695
695
|
primary: 'grok-build',
|