@aiwg/cli 2026.10.6 → 2026.10.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/aiwg.mjs +4 -2
- package/dist/src/a2a/agent-card.js +11 -0
- package/dist/src/api/index.d.ts +1 -0
- package/dist/src/api/index.js +1 -0
- package/dist/src/artifacts/fortemi-shard-contract/2.2.0/contract.json +103 -0
- package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-config.schema.json +212 -0
- package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-set.schema.json +240 -0
- package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-manifest.schema.json +451 -0
- package/dist/src/artifacts/fortemi-shard-export.js +284 -0
- package/dist/src/artifacts/index-preparation.js +2 -0
- package/dist/src/auth/client.js +5 -1
- package/dist/src/auth/credential-store.js +5 -1
- package/dist/src/auth/sensitive-input.js +151 -0
- package/dist/src/channel/manager.mjs +3 -0
- package/dist/src/cli/handlers/agentcard.js +47 -25
- package/dist/src/cli/handlers/auth.js +22 -2
- package/dist/src/cli/handlers/cost-report.js +7 -1
- package/dist/src/cli/handlers/decision.js +36 -10
- package/dist/src/cli/handlers/effect.js +4 -84
- package/dist/src/cli/handlers/guided-setup.js +1 -1
- package/dist/src/cli/handlers/init.js +1 -1
- package/dist/src/cli/handlers/install.js +6 -0
- package/dist/src/cli/handlers/local-executor.js +33 -6
- package/dist/src/cli/handlers/marketplace.js +15 -4
- package/dist/src/cli/handlers/setup.js +24 -20
- package/dist/src/cli/handlers/use.js +57 -15
- package/dist/src/cli/prompt-utils.js +73 -0
- package/dist/src/config/gitignore.js +4 -0
- package/dist/src/config/workspace.js +2 -1
- package/dist/src/cost/fleet-report.js +20 -22
- package/dist/src/decision/adapters/jev.js +20 -31
- package/dist/src/decision/ask.js +6 -1
- package/dist/src/decision/benchmark.js +19 -6
- package/dist/src/decision/driver.js +2 -2
- package/dist/src/decision/jev-credentials.js +18 -5
- package/dist/src/decision/ledger.js +230 -34
- package/dist/src/decision/qualification/gate-evidence.js +6 -4
- package/dist/src/decision/qualification/load-staged.js +47 -10
- package/dist/src/decision/qualification/runner.js +7 -3
- package/dist/src/discovery/interview.js +10 -5
- package/dist/src/discovery/plan-apply.js +26 -30
- package/dist/src/discovery/plan-validation.js +2 -2
- package/dist/src/effects/host.js +91 -0
- package/dist/src/extensions/commands/definitions.js +2 -2
- package/dist/src/extensions/project-local-gitignore.js +2 -0
- package/dist/src/issues/cli.js +16 -5
- package/dist/src/issues/live.js +44 -12
- package/dist/src/marketplace/exchange.js +18 -6
- package/dist/src/marketplace/provenance.js +3 -2
- package/dist/src/mcp/adapters/codex-runtime.js +46 -76
- package/dist/src/mcp/cli.mjs +122 -109
- package/dist/src/mcp/credentials.d.mts +4 -0
- package/dist/src/mcp/credentials.mjs +44 -3
- package/dist/src/mcp/grok-build-config.mjs +4 -1
- package/dist/src/mcp/muse-native-config.mjs +2 -1
- package/dist/src/mcp/omp-config.mjs +2 -0
- package/dist/src/mcp/registry.js +19 -15
- package/dist/src/mcp/registry.mjs +2 -18
- package/dist/src/mcp/tools/agentic-sandbox.mjs +46 -28
- package/dist/src/mcp/tools/worker-invoke.mjs +13 -2
- package/dist/src/memory/project-registry.js +2 -1
- package/dist/src/mission-protocol/worker-status.js +91 -1
- package/dist/src/models/grok-build-models.js +20 -0
- package/dist/src/models/worker-installation.d.mts +1 -1
- package/dist/src/models/worker-installation.mjs +65 -4
- package/dist/src/packages/adapters/git.js +99 -23
- package/dist/src/packages/adapters/gitea.js +8 -27
- package/dist/src/packages/adapters/github.js +7 -25
- package/dist/src/packages/registry.js +4 -4
- package/dist/src/process/cli-command.d.mts +5 -0
- package/dist/src/process/cli-command.mjs +42 -0
- package/dist/src/providers/provider-definitions.js +2 -2
- package/dist/src/serve/shared-host-scheduler.js +4 -1
- package/dist/src/serve/worker-invocation.js +10 -0
- package/dist/src/smiths/context-pipeline/claude-hook.js +9 -9
- package/dist/src/smiths/context-pipeline/context-file-safety.js +330 -9
- package/dist/src/smiths/context-pipeline/finalization.js +3 -2
- package/dist/src/smiths/context-pipeline/generator.js +7 -19
- package/dist/src/smiths/context-pipeline/legacy-inject.js +3 -10
- package/dist/src/smiths/context-pipeline/managed-hook.js +5 -5
- package/dist/src/smiths/context-pipeline/workspace-context.js +4 -12
- package/dist/src/storage/backends/fortemi.js +107 -49
- package/dist/src/tracker/project-inputs.js +2 -0
- package/dist/src/update/notifier.mjs +2 -4
- package/package.json +1 -1
- package/tools/agents/providers/base.mjs +5 -1
- package/tools/agents/providers/claude.mjs +5 -2
- package/tools/agents/providers/codex.mjs +4 -1
- package/tools/agents/providers/context-write.mjs +28 -0
- package/tools/agents/providers/copilot.mjs +5 -2
- package/tools/agents/providers/hermes.mjs +5 -2
- package/tools/agents/providers/omp.mjs +3 -1
- package/tools/agents/providers/rule-delivery.mjs +29 -12
- package/tools/agents/providers/warp.mjs +4 -1
- package/tools/agents/providers/windsurf.mjs +6 -3
- package/tools/ralph-external/lib/consecutive-failure-circuit-breaker.mjs +26 -0
- package/tools/ralph-external/lib/launcher-recovery-evidence.mjs +7 -1
- package/tools/ralph-external/lib/recovery-probe.mjs +14 -4
- package/tools/ralph-external/process-monitor.mjs +42 -4
- package/tools/ralph-external/session-launcher.mjs +125 -3
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* @implements #2009
|
|
9
9
|
*/
|
|
10
10
|
import { execFile } from 'node:child_process';
|
|
11
|
+
import { helperEnvironment } from '../process/cli-command.mjs';
|
|
11
12
|
import { createHash, createPrivateKey, createPublicKey, sign as cryptoSign, verify as cryptoVerify, } from 'node:crypto';
|
|
12
13
|
import { lstat, readFile, readdir } from 'node:fs/promises';
|
|
13
14
|
import path from 'node:path';
|
|
@@ -157,7 +158,7 @@ async function git(cwd, args) {
|
|
|
157
158
|
cwd,
|
|
158
159
|
encoding: 'utf8',
|
|
159
160
|
timeout: 120_000,
|
|
160
|
-
env: { ...
|
|
161
|
+
env: { ...helperEnvironment(), GIT_TERMINAL_PROMPT: '0' },
|
|
161
162
|
});
|
|
162
163
|
return stdout.trim();
|
|
163
164
|
}
|
|
@@ -166,7 +167,7 @@ async function gitBytes(cwd, args) {
|
|
|
166
167
|
cwd,
|
|
167
168
|
encoding: 'buffer',
|
|
168
169
|
timeout: 120_000,
|
|
169
|
-
env: { ...
|
|
170
|
+
env: { ...helperEnvironment(), GIT_TERMINAL_PROMPT: '0' },
|
|
170
171
|
});
|
|
171
172
|
return stdout;
|
|
172
173
|
}
|
|
@@ -14,11 +14,13 @@
|
|
|
14
14
|
*
|
|
15
15
|
* @implements #892
|
|
16
16
|
*/
|
|
17
|
+
import { readPrivateValueFile } from '../../auth/sensitive-input.js';
|
|
17
18
|
import { signalChild } from '../../process/signal-child.mjs';
|
|
18
|
-
import {
|
|
19
|
+
import { helperEnvironment, HELPER_ENV_ALLOWLIST, DESKTOP_HELPER_ENV_ALLOWLIST } from '../../process/cli-command.mjs';
|
|
20
|
+
import { readFile, mkdir, symlink, access, readdir, chmod } from 'fs/promises';
|
|
19
21
|
import { join } from 'path';
|
|
20
22
|
import { homedir } from 'os';
|
|
21
|
-
import { existsSync
|
|
23
|
+
import { existsSync } from 'fs';
|
|
22
24
|
import { spawn, spawnSync } from 'child_process';
|
|
23
25
|
import { boundedSpawnSync, resolveSyncProcessTimeoutMs } from '../../process/bounded-child-process.mjs';
|
|
24
26
|
import { isInteractive } from '../../cli/env.js';
|
|
@@ -228,73 +230,9 @@ function envWithoutSecretValue(base, envVarName, secretValue) {
|
|
|
228
230
|
}
|
|
229
231
|
return result;
|
|
230
232
|
}
|
|
231
|
-
/**
|
|
232
|
-
* Read an API key from a private file (#3062 go/no-go review, blocker 1C):
|
|
233
|
-
* `--api-key-file <path>` reads the key from disk instead of an
|
|
234
|
-
* environment variable, so a file that does not meet the same bar the
|
|
235
|
-
* decision ledger's own file writer enforces
|
|
236
|
-
* (`src/decision/ledger.ts`'s `openLedgerFileForAppend`) is refused rather
|
|
237
|
-
* than silently trusted. This mirrors that check for a read instead of a
|
|
238
|
-
* write — there is no already-exported helper at this layer to reuse (the
|
|
239
|
-
* ledger's checks are private to that module and tied to its own
|
|
240
|
-
* create-if-missing write path), so this re-implements the same checks for
|
|
241
|
-
* a read: never follow a symlink; the open file descriptor must resolve to
|
|
242
|
-
* a regular file with exactly one hard link; and, on POSIX, must be owned
|
|
243
|
-
* by the current user with no group/other permission bits. The file's
|
|
244
|
-
* contents are returned trimmed; no error message here ever includes them.
|
|
245
|
-
*/
|
|
233
|
+
/** Backward-compatible MCP export; the validation is shared by all input commands. */
|
|
246
234
|
async function readApiKeyFromFile(filePath) {
|
|
247
|
-
|
|
248
|
-
const pre = await lstat(filePath);
|
|
249
|
-
if (pre.isSymbolicLink()) {
|
|
250
|
-
throw new Error(`--api-key-file: "${filePath}" is a symlink; refusing to follow it.`);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
catch (error) {
|
|
254
|
-
if (error.code === 'ENOENT') {
|
|
255
|
-
throw new Error(`--api-key-file: "${filePath}" does not exist.`);
|
|
256
|
-
}
|
|
257
|
-
if (error instanceof Error && error.message.startsWith('--api-key-file:'))
|
|
258
|
-
throw error;
|
|
259
|
-
throw new Error(`--api-key-file: cannot stat "${filePath}": ${error.message}`);
|
|
260
|
-
}
|
|
261
|
-
const hasNoFollow = typeof constants.O_NOFOLLOW === 'number';
|
|
262
|
-
const flags = constants.O_RDONLY | (hasNoFollow ? constants.O_NOFOLLOW : 0);
|
|
263
|
-
let handle;
|
|
264
|
-
try {
|
|
265
|
-
handle = await open(filePath, flags);
|
|
266
|
-
}
|
|
267
|
-
catch (error) {
|
|
268
|
-
const code = error.code;
|
|
269
|
-
if (code === 'ELOOP')
|
|
270
|
-
throw new Error(`--api-key-file: "${filePath}" is a symlink; refusing to follow it.`);
|
|
271
|
-
if (code === 'ENOENT')
|
|
272
|
-
throw new Error(`--api-key-file: "${filePath}" does not exist.`);
|
|
273
|
-
throw new Error(`--api-key-file: cannot open "${filePath}": ${error.message}`);
|
|
274
|
-
}
|
|
275
|
-
try {
|
|
276
|
-
const stat = await handle.stat();
|
|
277
|
-
if (!stat.isFile()) {
|
|
278
|
-
throw new Error(`--api-key-file: "${filePath}" is not a regular file.`);
|
|
279
|
-
}
|
|
280
|
-
if (stat.nlink !== 1) {
|
|
281
|
-
throw new Error(`--api-key-file: "${filePath}" has more than one hard link; refusing to read it.`);
|
|
282
|
-
}
|
|
283
|
-
if (process.platform !== 'win32') {
|
|
284
|
-
const uid = typeof process.getuid === 'function' ? process.getuid() : null;
|
|
285
|
-
if (uid !== null && stat.uid !== uid) {
|
|
286
|
-
throw new Error(`--api-key-file: "${filePath}" is not owned by the current user.`);
|
|
287
|
-
}
|
|
288
|
-
if ((stat.mode & 0o077) !== 0) {
|
|
289
|
-
throw new Error(`--api-key-file: "${filePath}" must not be readable or writable by group or other ` +
|
|
290
|
-
'(chmod 600 it).');
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
return (await handle.readFile({ encoding: 'utf-8' })).trim();
|
|
294
|
-
}
|
|
295
|
-
finally {
|
|
296
|
-
await handle.close();
|
|
297
|
-
}
|
|
235
|
+
return readPrivateValueFile(filePath, '--api-key-file');
|
|
298
236
|
}
|
|
299
237
|
/**
|
|
300
238
|
* Async, credential-safe launch of `codex login --with-api-key` (#3062
|
|
@@ -320,17 +258,37 @@ function spawnCodexLoginWithApiKey(apiKey, env) {
|
|
|
320
258
|
return new Promise((resolve) => {
|
|
321
259
|
const child = spawn('codex', ['login', '--with-api-key'], {
|
|
322
260
|
env,
|
|
323
|
-
stdio: ['pipe', '
|
|
261
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
324
262
|
});
|
|
325
263
|
let settled = false;
|
|
326
264
|
let timer;
|
|
265
|
+
const stdout = [];
|
|
266
|
+
const stderr = [];
|
|
267
|
+
let outputBytes = 0;
|
|
268
|
+
// Buffer through close so a credential split between chunks cannot escape
|
|
269
|
+
// redaction. The combined cap bounds both memory and forwarded diagnostics.
|
|
270
|
+
const collect = (target, chunk) => {
|
|
271
|
+
if (settled)
|
|
272
|
+
return;
|
|
273
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
274
|
+
outputBytes += buffer.length;
|
|
275
|
+
if (outputBytes > 1024 * 1024) {
|
|
276
|
+
settle({ status: null, signal: 'SIGKILL', error: new Error('codex login output exceeded the 1 MiB limit.') });
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
target.push(buffer);
|
|
280
|
+
};
|
|
281
|
+
const onStdout = (chunk) => collect(stdout, chunk);
|
|
282
|
+
const onStderr = (chunk) => collect(stderr, chunk);
|
|
283
|
+
child.stdout?.on('data', onStdout);
|
|
284
|
+
child.stderr?.on('data', onStderr);
|
|
327
285
|
// #3062 go/no-go review, blocker 2: stdin error, write completion,
|
|
328
|
-
// child 'error', child '
|
|
286
|
+
// child 'error', child 'close', and the deadline all funnel through this
|
|
329
287
|
// one function, which settles exactly once, always clears the timer and
|
|
330
288
|
// removes every listener this call added, and kills the child if it is
|
|
331
289
|
// still alive on a failing settlement (a running process on the
|
|
332
290
|
// timeout path; a process that may still be running on an unexpected
|
|
333
|
-
// stdin-write error). The '
|
|
291
|
+
// stdin-write error). The 'close' path never reaches the kill — by the
|
|
334
292
|
// time that event fires the child has already exited.
|
|
335
293
|
const settle = (result) => {
|
|
336
294
|
if (settled)
|
|
@@ -340,13 +298,23 @@ function spawnCodexLoginWithApiKey(apiKey, env) {
|
|
|
340
298
|
clearTimeout(timer);
|
|
341
299
|
child.stdin?.removeListener('error', onStdinError);
|
|
342
300
|
child.removeListener('error', onChildError);
|
|
343
|
-
child.removeListener('
|
|
301
|
+
child.removeListener('close', onClose);
|
|
302
|
+
child.stdout?.removeListener('data', onStdout);
|
|
303
|
+
child.stderr?.removeListener('data', onStderr);
|
|
304
|
+
child.stdout?.destroy();
|
|
305
|
+
child.stderr?.destroy();
|
|
344
306
|
if (result.error && child.exitCode === null && child.signalCode === null) {
|
|
345
307
|
try {
|
|
346
308
|
signalChild(child, 'SIGKILL');
|
|
347
309
|
}
|
|
348
310
|
catch { /* already gone */ }
|
|
349
311
|
}
|
|
312
|
+
// On a failed/oversized stream, suppress buffered fragments entirely:
|
|
313
|
+
// the missing remainder might contain the rest of a credential.
|
|
314
|
+
if (!result.error) {
|
|
315
|
+
process.stdout.write(Buffer.concat(stdout).toString('utf8').split(apiKey).join('[redacted]'));
|
|
316
|
+
process.stderr.write(Buffer.concat(stderr).toString('utf8').split(apiKey).join('[redacted]'));
|
|
317
|
+
}
|
|
350
318
|
resolve(result);
|
|
351
319
|
};
|
|
352
320
|
timer = setTimeout(() => {
|
|
@@ -358,15 +326,15 @@ function spawnCodexLoginWithApiKey(apiKey, env) {
|
|
|
358
326
|
}, boundMs);
|
|
359
327
|
const onChildError = (err) => settle({ status: null, signal: null, error: err });
|
|
360
328
|
child.on('error', onChildError);
|
|
361
|
-
const
|
|
362
|
-
child.on('
|
|
329
|
+
const onClose = (code, signal) => settle({ status: code, signal });
|
|
330
|
+
child.on('close', onClose);
|
|
363
331
|
// A child that exits (or closes its read end) before reading stdin
|
|
364
332
|
// raises EPIPE/ECONNRESET on the *stdin stream*, not on the child
|
|
365
333
|
// object — `child.on('error', ...)` above never sees it, so without
|
|
366
334
|
// this listener it is an unhandled 'error' event, which Node turns into
|
|
367
335
|
// an uncaught exception. Installed before the write/end below, per the
|
|
368
336
|
// review. EPIPE/ECONNRESET specifically mean "the child already went
|
|
369
|
-
// away"; the already-firing (or about-to-fire) '
|
|
337
|
+
// away"; the already-firing (or about-to-fire) 'close' event carries the
|
|
370
338
|
// real, non-secret result, so this swallows exactly those two codes and
|
|
371
339
|
// lets settle() happen there instead. Anything else is a genuine,
|
|
372
340
|
// unexpected stdin failure and is itself the settling error.
|
|
@@ -413,7 +381,9 @@ export async function loginInProfile(profile, options = {}) {
|
|
|
413
381
|
await ensureRuntimeHome(profile);
|
|
414
382
|
const rtHome = runtimeHomePath(profile);
|
|
415
383
|
const env = {
|
|
416
|
-
...process.env,
|
|
384
|
+
...helperEnvironment(process.env, [
|
|
385
|
+
...HELPER_ENV_ALLOWLIST, ...DESKTOP_HELPER_ENV_ALLOWLIST, 'NODE_EXTRA_CA_CERTS',
|
|
386
|
+
]),
|
|
417
387
|
HOME: rtHome,
|
|
418
388
|
CODEX_HOME: rtHome,
|
|
419
389
|
AIWG_CONFIG: resolveUserConfigDir(),
|
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)) {
|
|
@@ -1124,7 +1210,7 @@ Usage:
|
|
|
1124
1210
|
aiwg mcp profile import <file>
|
|
1125
1211
|
aiwg mcp profile export <name> [--out <file>]
|
|
1126
1212
|
aiwg mcp profile init-presets
|
|
1127
|
-
aiwg mcp profile login <name> [--api-key-stdin | --api-key-file <path> | --api-key-env <ENV_VAR_NAME>]
|
|
1213
|
+
aiwg mcp profile login <name> [--api-key-interactive | --api-key-stdin | --api-key-file <path> | --api-key-store <name> | --api-key-env <ENV_VAR_NAME>]
|
|
1128
1214
|
|
|
1129
1215
|
Profiles let you define named subsets of your registered MCP servers:
|
|
1130
1216
|
aiwg mcp profile add dev --servers git-gitea,memory-fortemi --description "Dev work"
|
|
@@ -1162,10 +1248,10 @@ Logging in to a Codex profile's isolated runtime home (#3062):
|
|
|
1162
1248
|
$OPENAI_KEY; visible to every other
|
|
1163
1249
|
child process this shell starts
|
|
1164
1250
|
In every case the key reaches codex over stdin only; it never appears on
|
|
1165
|
-
argv, in logs, or in error messages.
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
docs/
|
|
1251
|
+
argv, in logs, or in error messages. Use --api-key-store <name> for a named
|
|
1252
|
+
OS-store entry, or --api-key-interactive for hidden terminal entry. Give
|
|
1253
|
+
sources in preference order; a missing source without a terminal errors naming the options.
|
|
1254
|
+
See docs/cli/sensitive-values.md for exposure details and manager examples.
|
|
1169
1255
|
`);
|
|
1170
1256
|
}
|
|
1171
1257
|
|
|
@@ -1419,59 +1505,18 @@ async function handleProfileInitPresets() {
|
|
|
1419
1505
|
}
|
|
1420
1506
|
}
|
|
1421
1507
|
|
|
1422
|
-
/**
|
|
1423
|
-
* Read all of stdin and return it as one trimmed string (go/no-go review,
|
|
1424
|
-
* blocker 1C's `--api-key-stdin`). Mirrors `src/cli/handlers/decision.ts`'s
|
|
1425
|
-
* own private `readStdin()` + `.trim()` for `--token-stdin` — duplicated
|
|
1426
|
-
* rather than imported for the same source/dist reason documented on
|
|
1427
|
-
* `requireFlagValue` above (this file runs both compiled and directly from
|
|
1428
|
-
* source). Input is expected to be a single line; a value containing
|
|
1429
|
-
* additional lines is not specially rejected, matching that precedent.
|
|
1430
|
-
*/
|
|
1431
|
-
async function readStdinAll() {
|
|
1432
|
-
const chunks = [];
|
|
1433
|
-
for await (const chunk of process.stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
|
|
1434
|
-
return Buffer.concat(chunks).toString('utf8');
|
|
1435
|
-
}
|
|
1436
|
-
|
|
1437
|
-
/**
|
|
1438
|
-
* Handle `aiwg mcp profile login <name> [--api-key-env <ENV_VAR_NAME> |
|
|
1439
|
-
* --api-key-stdin | --api-key-file <path>]` (#3062, #3069, go/no-go review
|
|
1440
|
-
* blocker 1C).
|
|
1441
|
-
*
|
|
1442
|
-
* Wires `loginInProfile()` (src/mcp/adapters/codex-runtime.ts), which
|
|
1443
|
-
* already implements the full headless-safe behavior this command needs:
|
|
1444
|
-
* with a terminal attached and none of the three given, it runs the
|
|
1445
|
-
* interactive `codex login` OAuth flow. Exactly one credential source may
|
|
1446
|
-
* be given; more than one is a usage error naming the flags given. In
|
|
1447
|
-
* every case the key reaches `loginInProfile()` only as an in-memory value
|
|
1448
|
-
* (or, for `--api-key-env`, a variable *name*) — never on argv, in logs, or
|
|
1449
|
-
* in an error message — and travels to `codex` only on its stdin (see
|
|
1450
|
-
* `spawnCodexLoginWithApiKey()`'s own doc comment). Without a terminal and
|
|
1451
|
-
* without any of the three, `loginInProfile()` itself refuses, naming all
|
|
1452
|
-
* three flags.
|
|
1453
|
-
*
|
|
1454
|
-
* `--api-key-env` reads the key from that named environment variable, as
|
|
1455
|
-
* before. `--api-key-stdin` reads one line from this process's own stdin
|
|
1456
|
-
* (refusing if stdin is a terminal, or if it is empty) — the least
|
|
1457
|
-
* observable source, since the key then never exists in any environment or
|
|
1458
|
-
* on any command line. `--api-key-file <path>` reads the key from a file
|
|
1459
|
-
* that `readApiKeyFromFile()` requires to be a non-symlink, single-hard-
|
|
1460
|
-
* link, regular file owned by the current user with no group/other
|
|
1461
|
-
* permission bits on POSIX — refused otherwise, without ever printing the
|
|
1462
|
-
* path's contents. `--api-key-env` remains the least preferred of the
|
|
1463
|
-
* three (see "Supplying credentials safely" in the docs): the named
|
|
1464
|
-
* variable is visible to every other child process this shell starts.
|
|
1465
|
-
*/
|
|
1508
|
+
/** Standard sensitive input; the Codex adapter still receives only an in-memory key. */
|
|
1466
1509
|
async function handleProfileLogin(args) {
|
|
1467
1510
|
let apiKeyEnv;
|
|
1468
1511
|
let apiKeyFilePath;
|
|
1512
|
+
let apiKeyStore;
|
|
1469
1513
|
try {
|
|
1470
1514
|
// rejectEmpty: an explicitly-given-but-empty variable name/path can
|
|
1471
1515
|
// never name a real environment variable or file (#3062 second review,
|
|
1472
1516
|
// finding 4; go/no-go review blocker 1C).
|
|
1473
1517
|
apiKeyEnv = requireFlagValue(args, { rejectEmpty: true }, '--api-key-env');
|
|
1474
1518
|
apiKeyFilePath = requireFlagValue(args, { rejectEmpty: true }, '--api-key-file');
|
|
1519
|
+
apiKeyStore = requireFlagValue(args, { rejectEmpty: true }, '--api-key-store');
|
|
1475
1520
|
} catch (err) {
|
|
1476
1521
|
if (err instanceof MissingFlagValueError) {
|
|
1477
1522
|
console.error(`${err.message}.`);
|
|
@@ -1481,16 +1526,6 @@ async function handleProfileLogin(args) {
|
|
|
1481
1526
|
}
|
|
1482
1527
|
const useStdin = args.includes('--api-key-stdin');
|
|
1483
1528
|
|
|
1484
|
-
const sourcesGiven = [
|
|
1485
|
-
apiKeyEnv !== undefined ? '--api-key-env' : null,
|
|
1486
|
-
useStdin ? '--api-key-stdin' : null,
|
|
1487
|
-
apiKeyFilePath !== undefined ? '--api-key-file' : null,
|
|
1488
|
-
].filter(Boolean);
|
|
1489
|
-
if (sourcesGiven.length > 1) {
|
|
1490
|
-
console.error(`Only one credential source may be given; got ${sourcesGiven.join(', ')}.`);
|
|
1491
|
-
process.exit(2);
|
|
1492
|
-
}
|
|
1493
|
-
|
|
1494
1529
|
// Skip --api-key-env's/--api-key-file's own value when hunting for the
|
|
1495
1530
|
// positional profile name, so e.g. `login --api-key-env FOO` (missing
|
|
1496
1531
|
// name) never misreads FOO as the name (mirrors this file's own
|
|
@@ -1500,46 +1535,24 @@ async function handleProfileLogin(args) {
|
|
|
1500
1535
|
for (let i = 0; i < args.length; i += 1) {
|
|
1501
1536
|
const token = args[i];
|
|
1502
1537
|
if (token === '--') break;
|
|
1503
|
-
if (token === '--api-key-env' || token === '--api-key-file') { i += 1; continue; }
|
|
1538
|
+
if (token === '--api-key-env' || token === '--api-key-file' || token === '--api-key-store') { i += 1; continue; }
|
|
1504
1539
|
if (!token.startsWith('--')) positional.push(token);
|
|
1505
1540
|
}
|
|
1506
1541
|
const name = positional[0];
|
|
1507
1542
|
if (!name) {
|
|
1508
|
-
console.error('Usage: aiwg mcp profile login <name> [--api-key-
|
|
1543
|
+
console.error('Usage: aiwg mcp profile login <name> [--api-key-interactive | --api-key-stdin | --api-key-file <path> | --api-key-store <name> | --api-key-env <ENV_VAR_NAME>]');
|
|
1509
1544
|
process.exit(1);
|
|
1510
1545
|
}
|
|
1511
1546
|
|
|
1512
|
-
let apiKey;
|
|
1513
|
-
if (useStdin) {
|
|
1514
|
-
if (process.stdin.isTTY) {
|
|
1515
|
-
console.error(
|
|
1516
|
-
'--api-key-stdin requires piped input, not a terminal; run ' +
|
|
1517
|
-
`\`aiwg mcp profile login ${name}\` with no credential flag for the interactive login instead.`,
|
|
1518
|
-
);
|
|
1519
|
-
process.exit(2);
|
|
1520
|
-
}
|
|
1521
|
-
apiKey = (await readStdinAll()).trim();
|
|
1522
|
-
if (!apiKey) {
|
|
1523
|
-
console.error('--api-key-stdin received no key (stdin was empty).');
|
|
1524
|
-
process.exit(2);
|
|
1525
|
-
}
|
|
1526
|
-
} else if (apiKeyFilePath !== undefined) {
|
|
1527
|
-
const { readApiKeyFromFile } = await loadCodexRuntimeAdapter();
|
|
1528
|
-
try {
|
|
1529
|
-
apiKey = await readApiKeyFromFile(apiKeyFilePath);
|
|
1530
|
-
} catch (err) {
|
|
1531
|
-
console.error(err instanceof Error ? err.message : String(err));
|
|
1532
|
-
process.exit(2);
|
|
1533
|
-
}
|
|
1534
|
-
if (!apiKey) {
|
|
1535
|
-
console.error(`--api-key-file "${apiKeyFilePath}" is empty.`);
|
|
1536
|
-
process.exit(2);
|
|
1537
|
-
}
|
|
1538
|
-
}
|
|
1539
|
-
|
|
1540
1547
|
try {
|
|
1548
|
+
await loadCodexRuntimeAdapter();
|
|
1549
|
+
const { resolveSensitiveInput } = await import(pathToFileURL(path.join(findOwnPackageRoot(), 'dist', 'src', 'auth', 'sensitive-input.js')).href);
|
|
1550
|
+
const apiKey = await resolveSensitiveInput({
|
|
1551
|
+
prefix: 'api-key', stdin: useStdin, file: apiKeyFilePath, store: apiKeyStore,
|
|
1552
|
+
env: apiKeyEnv, interactive: args.includes('--api-key-interactive'),
|
|
1553
|
+
});
|
|
1541
1554
|
const { loginInProfile } = await loadCodexRuntimeAdapter();
|
|
1542
|
-
const loginOptions =
|
|
1555
|
+
const loginOptions = { apiKey };
|
|
1543
1556
|
await loginInProfile(name, loginOptions);
|
|
1544
1557
|
console.log(`\nLogged in to profile "${name}".`);
|
|
1545
1558
|
} catch (err) {
|
|
@@ -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
|
}
|