@aiwg/cli 2026.10.6 → 2026.10.7
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/decision.js +3 -2
- 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/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/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/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 +17 -90
- package/dist/src/mcp/grok-build-config.mjs +2 -1
- package/dist/src/mcp/muse-native-config.mjs +2 -1
- 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/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/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/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
|
@@ -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
|
@@ -1124,7 +1124,7 @@ Usage:
|
|
|
1124
1124
|
aiwg mcp profile import <file>
|
|
1125
1125
|
aiwg mcp profile export <name> [--out <file>]
|
|
1126
1126
|
aiwg mcp profile init-presets
|
|
1127
|
-
aiwg mcp profile login <name> [--api-key-stdin | --api-key-file <path> | --api-key-env <ENV_VAR_NAME>]
|
|
1127
|
+
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
1128
|
|
|
1129
1129
|
Profiles let you define named subsets of your registered MCP servers:
|
|
1130
1130
|
aiwg mcp profile add dev --servers git-gitea,memory-fortemi --description "Dev work"
|
|
@@ -1162,10 +1162,10 @@ Logging in to a Codex profile's isolated runtime home (#3062):
|
|
|
1162
1162
|
$OPENAI_KEY; visible to every other
|
|
1163
1163
|
child process this shell starts
|
|
1164
1164
|
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/
|
|
1165
|
+
argv, in logs, or in error messages. Use --api-key-store <name> for a named
|
|
1166
|
+
OS-store entry, or --api-key-interactive for hidden terminal entry. Give
|
|
1167
|
+
sources in preference order; a missing source without a terminal errors naming the options.
|
|
1168
|
+
See docs/cli/sensitive-values.md for exposure details and manager examples.
|
|
1169
1169
|
`);
|
|
1170
1170
|
}
|
|
1171
1171
|
|
|
@@ -1419,59 +1419,18 @@ async function handleProfileInitPresets() {
|
|
|
1419
1419
|
}
|
|
1420
1420
|
}
|
|
1421
1421
|
|
|
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
|
-
*/
|
|
1422
|
+
/** Standard sensitive input; the Codex adapter still receives only an in-memory key. */
|
|
1466
1423
|
async function handleProfileLogin(args) {
|
|
1467
1424
|
let apiKeyEnv;
|
|
1468
1425
|
let apiKeyFilePath;
|
|
1426
|
+
let apiKeyStore;
|
|
1469
1427
|
try {
|
|
1470
1428
|
// rejectEmpty: an explicitly-given-but-empty variable name/path can
|
|
1471
1429
|
// never name a real environment variable or file (#3062 second review,
|
|
1472
1430
|
// finding 4; go/no-go review blocker 1C).
|
|
1473
1431
|
apiKeyEnv = requireFlagValue(args, { rejectEmpty: true }, '--api-key-env');
|
|
1474
1432
|
apiKeyFilePath = requireFlagValue(args, { rejectEmpty: true }, '--api-key-file');
|
|
1433
|
+
apiKeyStore = requireFlagValue(args, { rejectEmpty: true }, '--api-key-store');
|
|
1475
1434
|
} catch (err) {
|
|
1476
1435
|
if (err instanceof MissingFlagValueError) {
|
|
1477
1436
|
console.error(`${err.message}.`);
|
|
@@ -1481,16 +1440,6 @@ async function handleProfileLogin(args) {
|
|
|
1481
1440
|
}
|
|
1482
1441
|
const useStdin = args.includes('--api-key-stdin');
|
|
1483
1442
|
|
|
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
1443
|
// Skip --api-key-env's/--api-key-file's own value when hunting for the
|
|
1495
1444
|
// positional profile name, so e.g. `login --api-key-env FOO` (missing
|
|
1496
1445
|
// name) never misreads FOO as the name (mirrors this file's own
|
|
@@ -1500,46 +1449,24 @@ async function handleProfileLogin(args) {
|
|
|
1500
1449
|
for (let i = 0; i < args.length; i += 1) {
|
|
1501
1450
|
const token = args[i];
|
|
1502
1451
|
if (token === '--') break;
|
|
1503
|
-
if (token === '--api-key-env' || token === '--api-key-file') { i += 1; continue; }
|
|
1452
|
+
if (token === '--api-key-env' || token === '--api-key-file' || token === '--api-key-store') { i += 1; continue; }
|
|
1504
1453
|
if (!token.startsWith('--')) positional.push(token);
|
|
1505
1454
|
}
|
|
1506
1455
|
const name = positional[0];
|
|
1507
1456
|
if (!name) {
|
|
1508
|
-
console.error('Usage: aiwg mcp profile login <name> [--api-key-
|
|
1457
|
+
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
1458
|
process.exit(1);
|
|
1510
1459
|
}
|
|
1511
1460
|
|
|
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
1461
|
try {
|
|
1462
|
+
await loadCodexRuntimeAdapter();
|
|
1463
|
+
const { resolveSensitiveInput } = await import(pathToFileURL(path.join(findOwnPackageRoot(), 'dist', 'src', 'auth', 'sensitive-input.js')).href);
|
|
1464
|
+
const apiKey = await resolveSensitiveInput({
|
|
1465
|
+
prefix: 'api-key', stdin: useStdin, file: apiKeyFilePath, store: apiKeyStore,
|
|
1466
|
+
env: apiKeyEnv, interactive: args.includes('--api-key-interactive'),
|
|
1467
|
+
});
|
|
1541
1468
|
const { loginInProfile } = await loadCodexRuntimeAdapter();
|
|
1542
|
-
const loginOptions =
|
|
1469
|
+
const loginOptions = { apiKey };
|
|
1543
1470
|
await loginInProfile(name, loginOptions);
|
|
1544
1471
|
console.log(`\nLogged in to profile "${name}".`);
|
|
1545
1472
|
} catch (err) {
|
|
@@ -3,6 +3,7 @@ import { constants } from 'node:fs';
|
|
|
3
3
|
import { access, lstat, readFile } from 'node:fs/promises';
|
|
4
4
|
import { dirname, relative, resolve } from 'node:path';
|
|
5
5
|
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import { helperEnvironment } from '../process/cli-command.mjs';
|
|
6
7
|
import { replaceServer } from './toml-editor.mjs';
|
|
7
8
|
import { tomlString as string } from './toml-string.mjs';
|
|
8
9
|
|
|
@@ -144,7 +145,7 @@ export async function unmanageGrokBuildMcp(configPath, names, options = {}) {
|
|
|
144
145
|
}
|
|
145
146
|
|
|
146
147
|
function runJson(binary, args, cwd, env) {
|
|
147
|
-
const run = spawnSync(binary, args, { cwd, env, encoding: 'utf8', timeout: 30_000, killSignal: 'SIGKILL' });
|
|
148
|
+
const run = spawnSync(binary, args, { cwd, env: helperEnvironment(env), encoding: 'utf8', timeout: 30_000, killSignal: 'SIGKILL' });
|
|
148
149
|
if (run.error || run.status !== 0) return { ok: false, detail: run.error?.message || run.stderr || `exit ${run.status}` };
|
|
149
150
|
try { return { ok: true, value: JSON.parse(run.stdout || '{}') }; }
|
|
150
151
|
catch { return { ok: false, detail: 'Grok returned malformed JSON' }; }
|
|
@@ -4,6 +4,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
import { resolveMuseXdgSkillsDir } from '../providers/muse-paths.js';
|
|
7
|
+
import { helperEnvironment } from '../process/cli-command.mjs';
|
|
7
8
|
|
|
8
9
|
// Mirrors tools/agents/providers/muse-hooks.mjs. src/ cannot import tools/:
|
|
9
10
|
// tools/ is not part of the compiled dist/ tree.
|
|
@@ -47,7 +48,7 @@ function deepEqual(a, b) {
|
|
|
47
48
|
|
|
48
49
|
function inspectMuseCli(env) {
|
|
49
50
|
const result = spawnSync('muse', ['--version'], {
|
|
50
|
-
env,
|
|
51
|
+
env: helperEnvironment(env),
|
|
51
52
|
encoding: 'utf8',
|
|
52
53
|
timeout: MUSE_VERSION_TIMEOUT_MS,
|
|
53
54
|
killSignal: 'SIGKILL',
|
|
@@ -2707,6 +2707,9 @@ export async function registerWorkerInvokeToolset(server, options = {}) {
|
|
|
2707
2707
|
}
|
|
2708
2708
|
|
|
2709
2709
|
async function status(input) {
|
|
2710
|
+
const { projectRecoveryStatus, mergeRecoveryStatuses } = await importImpl('mission-protocol/worker-status.js');
|
|
2711
|
+
const recoveryStatus = breaker => projectRecoveryStatus(typeof breaker?.recoveryStatus === 'function'
|
|
2712
|
+
? { ...breaker.recoveryStatus(), observedAt: now().toISOString() } : null, 'live');
|
|
2710
2713
|
if (!input.invocation_id) {
|
|
2711
2714
|
// #2915 round 8 (D4): ordinary status, read-only, no id — lists every
|
|
2712
2715
|
// invocation this server currently tracks as a `dispatched` reservation
|
|
@@ -2725,11 +2728,19 @@ export async function registerWorkerInvokeToolset(server, options = {}) {
|
|
|
2725
2728
|
.filter(scope => scope.state === 'operator-intervention')
|
|
2726
2729
|
.map(scope => ({ scopeId: scope.scopeKey, reason: scope.operatorReason,
|
|
2727
2730
|
resetInstruction: { operation: 'reset_recovery_scope', scope_id: scope.scopeKey } })));
|
|
2728
|
-
|
|
2731
|
+
const recovery = mergeRecoveryStatuses([...breakers].map((breaker, index) => {
|
|
2732
|
+
const status = recoveryStatus(breaker);
|
|
2733
|
+
if (breakers.size > 1) {
|
|
2734
|
+
for (const scope of status.scopes) scope.owner = `owner-${index + 1}`;
|
|
2735
|
+
for (const transition of status.transitions) transition.owner = `owner-${index + 1}`;
|
|
2736
|
+
}
|
|
2737
|
+
return status;
|
|
2738
|
+
}));
|
|
2739
|
+
return { unresolvedReservations, interventionScopes, recovery };
|
|
2729
2740
|
}
|
|
2730
2741
|
const entry = registry.get(input.invocation_id);
|
|
2731
2742
|
if (!entry) throw new Error('Unknown invocation_id');
|
|
2732
|
-
return publicStatus(entry, healthHook);
|
|
2743
|
+
return { ...publicStatus(entry, healthHook), recovery: recoveryStatus(entry.recoveryBreaker ?? registry.recoveryOwner) };
|
|
2733
2744
|
}
|
|
2734
2745
|
|
|
2735
2746
|
/**
|
|
@@ -10,6 +10,7 @@ import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
|
10
10
|
import { homedir } from 'node:os';
|
|
11
11
|
import path from 'node:path';
|
|
12
12
|
import { boundedExecFileSync } from '../process/bounded-child-process.mjs';
|
|
13
|
+
import { helperEnvironment } from '../process/cli-command.mjs';
|
|
13
14
|
import { projectAiwgPath, resolveProjectAiwgDir } from '../config/project-artifacts.js';
|
|
14
15
|
export function projectMemoryHome() {
|
|
15
16
|
const override = process.env.AIWG_PROJECT_MEMORY_HOME;
|
|
@@ -248,7 +249,7 @@ function sanitizeProjectId(id) {
|
|
|
248
249
|
}
|
|
249
250
|
function discoverGitRemotes(cwd) {
|
|
250
251
|
try {
|
|
251
|
-
const stdout = boundedExecFileSync('git', ['remote', '-v'], 'quick', { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
252
|
+
const stdout = boundedExecFileSync('git', ['remote', '-v'], 'quick', { cwd, env: helperEnvironment(), encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
252
253
|
const remotes = stdout
|
|
253
254
|
.split('\n')
|
|
254
255
|
.map((line) => line.trim().split(/\s+/)[1])
|
|
@@ -1,2 +1,92 @@
|
|
|
1
|
-
|
|
1
|
+
const recoveryStates = new Set(['closed', 'open', 'half-open', 'canary-ready', 'canary', 'operator-intervention']);
|
|
2
|
+
const recoveryScopes = new Set(['invocation', 'model', 'installation', 'quota', 'provider', 'host']);
|
|
3
|
+
const recoveryReasons = new Set(['NOT_FOUND', 'SPAWN_REFUSED', 'AUTH', 'RATE_LIMIT', 'NETWORK',
|
|
4
|
+
'RESOURCE', 'SANDBOX', 'MODEL', 'UNKNOWN']);
|
|
5
|
+
const objectRecord = (value) => value !== null
|
|
6
|
+
&& typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
7
|
+
const finiteClock = (value) => typeof value === 'number' && Number.isFinite(value)
|
|
8
|
+
&& Math.abs(value) <= Number.MAX_SAFE_INTEGER ? value : null;
|
|
9
|
+
/** Read-only projection of the existing breaker owner. Only fixed reason codes
|
|
10
|
+
* and scope identities survive; worker captures, actions and diagnostics do not.
|
|
11
|
+
* Clock values are converted with the owner's persisted wall-clock anchor,
|
|
12
|
+
* never with the reader's clock (which may belong to another process/boot). */
|
|
13
|
+
export function projectRecoveryStatus(value, source = 'recorded') {
|
|
14
|
+
const record = objectRecord(value);
|
|
15
|
+
const observedAtMs = finiteClock(record?.observedAtMs);
|
|
16
|
+
const wall = typeof record?.observedAt === 'string' ? Date.parse(record.observedAt) : NaN;
|
|
17
|
+
const observedAt = Number.isFinite(wall) ? new Date(wall).toISOString() : null;
|
|
18
|
+
const at = (clock) => {
|
|
19
|
+
if (clock === null || observedAtMs === null || observedAt === null)
|
|
20
|
+
return null;
|
|
21
|
+
const mapped = wall + clock - observedAtMs;
|
|
22
|
+
return Number.isFinite(mapped) && Math.abs(mapped) <= 8.64e15 ? new Date(mapped).toISOString() : null;
|
|
23
|
+
};
|
|
24
|
+
const identity = (item) => {
|
|
25
|
+
if (!recoveryScopes.has(item.scope) || typeof item.scopeKey !== 'string'
|
|
26
|
+
|| !/^[\p{L}\p{N}_.:/@+~%\-]{1,256}$/u.test(item.scopeKey)
|
|
27
|
+
|| !item.scopeKey.startsWith(`${String(item.scope)}:`))
|
|
28
|
+
return null;
|
|
29
|
+
return { scopeKey: item.scopeKey, scope: item.scope };
|
|
30
|
+
};
|
|
31
|
+
const reasons = (value) => Array.isArray(value)
|
|
32
|
+
? [...new Set(value.slice(0, 32).filter(reason => recoveryReasons.has(reason)))] : [];
|
|
33
|
+
const rawScopes = Array.isArray(record?.scopes) ? record.scopes : [];
|
|
34
|
+
const rawTransitions = Array.isArray(record?.transitions) ? record.transitions : [];
|
|
35
|
+
const scopes = [];
|
|
36
|
+
for (const raw of rawScopes.slice(0, 32)) {
|
|
37
|
+
const item = objectRecord(raw), id = item && identity(item);
|
|
38
|
+
if (!item || !id || !recoveryStates.has(item.state))
|
|
39
|
+
continue;
|
|
40
|
+
const nextEligibleAtMs = finiteClock(item.nextEligibleAtMs);
|
|
41
|
+
scopes.push({ ...id, state: item.state, reasons: reasons(item.reasons),
|
|
42
|
+
nextEligibleAtMs, nextEligibleAt: at(nextEligibleAtMs), canaryInFlight: item.canaryInFlight === true });
|
|
43
|
+
}
|
|
44
|
+
const transitions = [];
|
|
45
|
+
for (const raw of rawTransitions.slice(-32)) {
|
|
46
|
+
const item = objectRecord(raw), id = item && identity(item);
|
|
47
|
+
const clock = finiteClock(item?.atMs);
|
|
48
|
+
if (!item || !id || clock === null || !recoveryStates.has(item.from) || !recoveryStates.has(item.to))
|
|
49
|
+
continue;
|
|
50
|
+
const nextEligibleAtMs = finiteClock(item.nextEligibleAtMs);
|
|
51
|
+
transitions.push({ ...id, from: item.from, to: item.to,
|
|
52
|
+
reasons: reasons(item.reasons), atMs: clock, at: at(clock), nextEligibleAtMs, nextEligibleAt: at(nextEligibleAtMs) });
|
|
53
|
+
}
|
|
54
|
+
const omittedScopeCount = typeof record?.omittedScopeCount === 'number'
|
|
55
|
+
&& Number.isSafeInteger(record.omittedScopeCount) && record.omittedScopeCount >= 0 ? record.omittedScopeCount : 0;
|
|
56
|
+
return { source: record ? source : 'unavailable', observedAt, observedAtMs, scopes, transitions,
|
|
57
|
+
omittedScopeCount: omittedScopeCount + Math.max(0, rawScopes.length - 32),
|
|
58
|
+
truncated: record?.truncated === true || omittedScopeCount > 0 || rawScopes.length > 32 || rawTransitions.length > 32 };
|
|
59
|
+
}
|
|
60
|
+
/** Merge independently owned observations without letting one owner's close
|
|
61
|
+
* hide another owner's active scope. Callers first select the latest record
|
|
62
|
+
* for each owner; owner labels are supplied from validated launcher identity. */
|
|
63
|
+
export function mergeRecoveryStatuses(statuses) {
|
|
64
|
+
const available = statuses.filter(status => status.source !== 'unavailable');
|
|
65
|
+
const latest = available.reduce((last, status) => !last
|
|
66
|
+
|| Date.parse(status.observedAt ?? '') >= Date.parse(last.observedAt ?? '') ? status : last, null);
|
|
67
|
+
if (!latest)
|
|
68
|
+
return projectRecoveryStatus(null);
|
|
69
|
+
const allScopes = available.flatMap(status => status.scopes);
|
|
70
|
+
const scopes = allScopes.filter(scope => scope.state !== 'closed').concat(allScopes.filter(scope => scope.state === 'closed'));
|
|
71
|
+
const transitions = available.flatMap(status => status.transitions).sort((a, b) => (a.at ? Date.parse(a.at) : a.atMs) - (b.at ? Date.parse(b.at) : b.atMs));
|
|
72
|
+
const omittedScopeCount = available.reduce((count, status) => count + status.omittedScopeCount, 0) + Math.max(0, scopes.length - 32);
|
|
73
|
+
return { source: latest.source, observedAt: latest.observedAt, observedAtMs: latest.observedAtMs,
|
|
74
|
+
scopes: scopes.slice(0, 32), transitions: transitions.slice(-32), omittedScopeCount,
|
|
75
|
+
truncated: available.some(status => status.truncated) || omittedScopeCount > 0 || transitions.length > 32 };
|
|
76
|
+
}
|
|
77
|
+
/** Mechanical correction records carry fixed codes and invocation identities. */
|
|
78
|
+
export function projectStartupCorrection(value) {
|
|
79
|
+
const record = objectRecord(value);
|
|
80
|
+
const safeId = (value) => typeof value === 'string'
|
|
81
|
+
&& /^[\p{L}\p{N}_.:/@+~%\-]{1,256}$/u.test(value);
|
|
82
|
+
if (!record || record.action !== 'refresh-executable-binding' || record.reasonCode !== 'NOT_FOUND'
|
|
83
|
+
|| !['applied', 'stopped'].includes(String(record.state)) || !safeId(record.previousInvocationId))
|
|
84
|
+
return null;
|
|
85
|
+
const stopCodes = new Set(['ATTEMPT_LIMIT', 'CANCELLED', 'DEADLINE', 'UNCHANGED_EXECUTABLE', 'QUALIFICATION_REFUSED',
|
|
86
|
+
'BINDING_CHANGED', 'RETRY_REFUSED', 'RETRY_FAILED']);
|
|
87
|
+
return { action: 'refresh-executable-binding', reasonCode: 'NOT_FOUND', state: record.state,
|
|
88
|
+
previousInvocationId: record.previousInvocationId,
|
|
89
|
+
...(typeof record.stopCode === 'string' && stopCodes.has(record.stopCode) ? { stopCode: record.stopCode } : {}),
|
|
90
|
+
...(safeId(record.retryInvocationId) ? { retryInvocationId: record.retryInvocationId } : {}) };
|
|
91
|
+
}
|
|
2
92
|
//# sourceMappingURL=worker-status.js.map
|
|
@@ -11,7 +11,7 @@ export function loadRecordedWorkerInstallations(input: {
|
|
|
11
11
|
storeDigest?: string;
|
|
12
12
|
/** Request validation after store presence, before recorded policy/adapter checks. */
|
|
13
13
|
precheckPolicy?: () => void;
|
|
14
|
-
}): Promise<{ registry: ProviderModelCapabilityRegistry; installations: Record<string, WorkerResolutionBinding>; drivers: Record<string, WorkerInvocationDriver>; observations: Record<string, import('./model-discovery.js').ProviderModelDiscovery> }>;
|
|
14
|
+
}): Promise<{ registry: ProviderModelCapabilityRegistry; installations: Record<string, WorkerResolutionBinding>; drivers: Record<string, WorkerInvocationDriver>; observations: Record<string, import('./model-discovery.js').ProviderModelDiscovery>; qualificationFailures?: Record<string, string> }>;
|
|
15
15
|
|
|
16
16
|
export function hasProvider(name: string): boolean;
|
|
17
17
|
|