@aiwg/cli 2026.10.6 → 2026.10.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/bin/aiwg.mjs +4 -2
  2. package/dist/src/a2a/agent-card.js +11 -0
  3. package/dist/src/api/index.d.ts +1 -0
  4. package/dist/src/api/index.js +1 -0
  5. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/contract.json +103 -0
  6. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-config.schema.json +212 -0
  7. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-set.schema.json +240 -0
  8. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-manifest.schema.json +451 -0
  9. package/dist/src/artifacts/fortemi-shard-export.js +284 -0
  10. package/dist/src/artifacts/index-preparation.js +2 -0
  11. package/dist/src/auth/client.js +5 -1
  12. package/dist/src/auth/credential-store.js +5 -1
  13. package/dist/src/auth/sensitive-input.js +151 -0
  14. package/dist/src/channel/manager.mjs +3 -0
  15. package/dist/src/cli/handlers/agentcard.js +47 -25
  16. package/dist/src/cli/handlers/auth.js +22 -2
  17. package/dist/src/cli/handlers/cost-report.js +7 -1
  18. package/dist/src/cli/handlers/decision.js +36 -10
  19. package/dist/src/cli/handlers/effect.js +4 -84
  20. package/dist/src/cli/handlers/guided-setup.js +1 -1
  21. package/dist/src/cli/handlers/init.js +1 -1
  22. package/dist/src/cli/handlers/install.js +6 -0
  23. package/dist/src/cli/handlers/local-executor.js +33 -6
  24. package/dist/src/cli/handlers/marketplace.js +15 -4
  25. package/dist/src/cli/handlers/setup.js +24 -20
  26. package/dist/src/cli/handlers/use.js +57 -15
  27. package/dist/src/cli/prompt-utils.js +73 -0
  28. package/dist/src/config/gitignore.js +4 -0
  29. package/dist/src/config/workspace.js +2 -1
  30. package/dist/src/cost/fleet-report.js +20 -22
  31. package/dist/src/decision/adapters/jev.js +20 -31
  32. package/dist/src/decision/ask.js +6 -1
  33. package/dist/src/decision/benchmark.js +19 -6
  34. package/dist/src/decision/driver.js +2 -2
  35. package/dist/src/decision/jev-credentials.js +18 -5
  36. package/dist/src/decision/ledger.js +230 -34
  37. package/dist/src/decision/qualification/gate-evidence.js +6 -4
  38. package/dist/src/decision/qualification/load-staged.js +47 -10
  39. package/dist/src/decision/qualification/runner.js +7 -3
  40. package/dist/src/discovery/interview.js +10 -5
  41. package/dist/src/discovery/plan-apply.js +26 -30
  42. package/dist/src/discovery/plan-validation.js +2 -2
  43. package/dist/src/effects/host.js +91 -0
  44. package/dist/src/extensions/commands/definitions.js +2 -2
  45. package/dist/src/extensions/project-local-gitignore.js +2 -0
  46. package/dist/src/issues/cli.js +16 -5
  47. package/dist/src/issues/live.js +44 -12
  48. package/dist/src/marketplace/exchange.js +18 -6
  49. package/dist/src/marketplace/provenance.js +3 -2
  50. package/dist/src/mcp/adapters/codex-runtime.js +46 -76
  51. package/dist/src/mcp/cli.mjs +122 -109
  52. package/dist/src/mcp/credentials.d.mts +4 -0
  53. package/dist/src/mcp/credentials.mjs +44 -3
  54. package/dist/src/mcp/grok-build-config.mjs +4 -1
  55. package/dist/src/mcp/muse-native-config.mjs +2 -1
  56. package/dist/src/mcp/omp-config.mjs +2 -0
  57. package/dist/src/mcp/registry.js +19 -15
  58. package/dist/src/mcp/registry.mjs +2 -18
  59. package/dist/src/mcp/tools/agentic-sandbox.mjs +46 -28
  60. package/dist/src/mcp/tools/worker-invoke.mjs +13 -2
  61. package/dist/src/memory/project-registry.js +2 -1
  62. package/dist/src/mission-protocol/worker-status.js +91 -1
  63. package/dist/src/models/grok-build-models.js +20 -0
  64. package/dist/src/models/worker-installation.d.mts +1 -1
  65. package/dist/src/models/worker-installation.mjs +65 -4
  66. package/dist/src/packages/adapters/git.js +99 -23
  67. package/dist/src/packages/adapters/gitea.js +8 -27
  68. package/dist/src/packages/adapters/github.js +7 -25
  69. package/dist/src/packages/registry.js +4 -4
  70. package/dist/src/process/cli-command.d.mts +5 -0
  71. package/dist/src/process/cli-command.mjs +42 -0
  72. package/dist/src/providers/provider-definitions.js +2 -2
  73. package/dist/src/serve/shared-host-scheduler.js +4 -1
  74. package/dist/src/serve/worker-invocation.js +10 -0
  75. package/dist/src/smiths/context-pipeline/claude-hook.js +9 -9
  76. package/dist/src/smiths/context-pipeline/context-file-safety.js +330 -9
  77. package/dist/src/smiths/context-pipeline/finalization.js +3 -2
  78. package/dist/src/smiths/context-pipeline/generator.js +7 -19
  79. package/dist/src/smiths/context-pipeline/legacy-inject.js +3 -10
  80. package/dist/src/smiths/context-pipeline/managed-hook.js +5 -5
  81. package/dist/src/smiths/context-pipeline/workspace-context.js +4 -12
  82. package/dist/src/storage/backends/fortemi.js +107 -49
  83. package/dist/src/tracker/project-inputs.js +2 -0
  84. package/dist/src/update/notifier.mjs +2 -4
  85. package/package.json +1 -1
  86. package/tools/agents/providers/base.mjs +5 -1
  87. package/tools/agents/providers/claude.mjs +5 -2
  88. package/tools/agents/providers/codex.mjs +4 -1
  89. package/tools/agents/providers/context-write.mjs +28 -0
  90. package/tools/agents/providers/copilot.mjs +5 -2
  91. package/tools/agents/providers/hermes.mjs +5 -2
  92. package/tools/agents/providers/omp.mjs +3 -1
  93. package/tools/agents/providers/rule-delivery.mjs +29 -12
  94. package/tools/agents/providers/warp.mjs +4 -1
  95. package/tools/agents/providers/windsurf.mjs +6 -3
  96. package/tools/ralph-external/lib/consecutive-failure-circuit-breaker.mjs +26 -0
  97. package/tools/ralph-external/lib/launcher-recovery-evidence.mjs +7 -1
  98. package/tools/ralph-external/lib/recovery-probe.mjs +14 -4
  99. package/tools/ralph-external/process-monitor.mjs +42 -4
  100. package/tools/ralph-external/session-launcher.mjs +125 -3
@@ -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: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
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: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
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 { readFile, mkdir, symlink, access, readdir, chmod, lstat, open } from 'fs/promises';
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, constants } from 'fs';
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
- try {
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', 'inherit', 'inherit'],
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 'exit', and the deadline all funnel through this
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 'exit' path never reaches the kill — by 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('exit', onExit);
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 onExit = (code, signal) => settle({ status: code, signal });
362
- child.on('exit', onExit);
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) 'exit' event carries the
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(),
@@ -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)) {
@@ -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. Exactly one of the three may be
1166
- given. Without a terminal and without any of the three, this errors
1167
- naming all three. See "Supplying credentials safely" in the docs:
1168
- docs/mcp/codex-profiles.md#supplying-credentials-safely
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-env <ENV_VAR_NAME> | --api-key-stdin | --api-key-file <path>]');
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 = apiKey !== undefined ? { apiKey } : apiKeyEnv ? { apiKeyEnv } : {};
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` (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
  }