@aiwg/cli 2026.10.7 → 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/dist/src/cli/handlers/cost-report.js +7 -1
- package/dist/src/cli/handlers/decision.js +33 -8
- package/dist/src/cli/handlers/local-executor.js +33 -6
- package/dist/src/cost/fleet-report.js +20 -22
- package/dist/src/decision/driver.js +2 -2
- package/dist/src/decision/jev-credentials.js +18 -5
- package/dist/src/decision/ledger.js +230 -34
- package/dist/src/mcp/cli.mjs +105 -19
- package/dist/src/mcp/credentials.d.mts +4 -0
- package/dist/src/mcp/credentials.mjs +44 -3
- package/dist/src/mcp/grok-build-config.mjs +2 -0
- package/dist/src/mcp/omp-config.mjs +2 -0
- package/dist/src/mcp/registry.js +19 -15
- package/dist/src/mcp/registry.mjs +2 -18
- package/dist/src/mcp/tools/agentic-sandbox.mjs +46 -28
- package/dist/src/models/grok-build-models.js +20 -0
- package/dist/src/providers/provider-definitions.js +2 -2
- package/dist/src/serve/shared-host-scheduler.js +4 -1
- package/dist/src/storage/backends/fortemi.js +107 -49
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** OpenRouter fleet cost-report CLI handler. @issue #1187 */
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { sensitiveInputOptions } from '../../auth/sensitive-input.js';
|
|
4
5
|
import { FleetConfigMissingError, formatFleetSpendReport, generateFleetSpendReport, } from '../../cost/fleet-report.js';
|
|
5
6
|
function option(args, name) {
|
|
6
7
|
const index = args.indexOf(name);
|
|
@@ -9,10 +10,11 @@ function option(args, name) {
|
|
|
9
10
|
}
|
|
10
11
|
function usage() {
|
|
11
12
|
return [
|
|
12
|
-
'Usage: aiwg cost-report (--fleet | --key <key_ref>) [--source openrouter] [--config <fleet.yaml>] [--json]',
|
|
13
|
+
'Usage: aiwg cost-report (--fleet | --key <key_ref> [--key-store <name>]) [--source openrouter] [--config <fleet.yaml>] [--json]',
|
|
13
14
|
'',
|
|
14
15
|
'Reads bot-to-key references from ~/.config/aiwg/fleet.yaml and credentials from',
|
|
15
16
|
'~/.config/aiwg/keys/<key_ref> or AIWG_OPENROUTER_KEY_<KEY_REF>.',
|
|
17
|
+
'Use --key-store <name> for one key, or per-entry key_store references in fleet YAML.',
|
|
16
18
|
'',
|
|
17
19
|
'AIWG observes and correlates spend; OpenRouter enforces all key limits and caps.',
|
|
18
20
|
].join('\n');
|
|
@@ -40,6 +42,9 @@ export const costReportHandler = {
|
|
|
40
42
|
? path.resolve(ctx.cwd, configuredPath)
|
|
41
43
|
: path.join(os.homedir(), '.config', 'aiwg', 'fleet.yaml');
|
|
42
44
|
try {
|
|
45
|
+
const keyStore = sensitiveInputOptions(ctx.args, 'key').store;
|
|
46
|
+
if (keyStore !== undefined && !keyRef)
|
|
47
|
+
return { exitCode: 2, message: '--key-store requires --key <key_ref>; use per-entry key_store references for --fleet.' };
|
|
43
48
|
const capValue = option(ctx.args, '--monthly-cap');
|
|
44
49
|
const cap = capValue === undefined ? 0 : Number(capValue);
|
|
45
50
|
if (!Number.isFinite(cap) || cap < 0)
|
|
@@ -53,6 +58,7 @@ export const costReportHandler = {
|
|
|
53
58
|
bot: option(ctx.args, '--bot') ?? keyRef,
|
|
54
59
|
machine: option(ctx.args, '--machine') ?? os.hostname(),
|
|
55
60
|
key_ref: keyRef,
|
|
61
|
+
...(keyStore !== undefined ? { key_store: keyStore } : {}),
|
|
56
62
|
monthly_cap: cap,
|
|
57
63
|
}],
|
|
58
64
|
} : {}),
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { resolveSensitiveInput, sensitiveInputOptions } from '../../auth/sensitive-input.js';
|
|
2
2
|
import { decisionPricing } from '../../decision/pricing.js';
|
|
3
|
-
import { ATTRIBUTION_FIELD_PATTERN, DecisionAskUsageError, decisionCapabilities, DecisionLedgerSinceError, decisionLedgerHistory, decisionLedgerStats, evaluateRequestPath, listPatterns, livePlan, MAX_ATTRIBUTION_FIELD_LENGTH, materializeSyntheticClassificationSetup, parseDecisionLedgerSince, readDecisionContextFile, runDecisionOffloadCheckpoint, runOfflinePattern, setupJev, showPattern, syntheticClassificationSetup, validateDecisionInput, } from '../../decision/driver.js';
|
|
3
|
+
import { ATTRIBUTION_FIELD_PATTERN, DecisionAskUsageError, decisionCapabilities, DecisionLedgerSinceError, decisionLedgerHistory, decisionLedgerStats, rotateDecisionLedger, parseDecisionLedgerRetention, evaluateRequestPath, listPatterns, livePlan, MAX_ATTRIBUTION_FIELD_LENGTH, materializeSyntheticClassificationSetup, parseDecisionLedgerSince, readDecisionContextFile, runDecisionOffloadCheckpoint, runOfflinePattern, setupJev, showPattern, syntheticClassificationSetup, validateDecisionInput, } from '../../decision/driver.js';
|
|
4
4
|
function jsonResult(value, exitCode = 0) {
|
|
5
5
|
return { exitCode, message: JSON.stringify(value, null, 2), rawOutput: true };
|
|
6
6
|
}
|
|
7
7
|
function usage() {
|
|
8
8
|
return [
|
|
9
|
-
'Usage: aiwg decision <capabilities|status|patterns|validate|evaluate|setup|history|stats|pricing> [options]',
|
|
9
|
+
'Usage: aiwg decision <capabilities|status|patterns|validate|evaluate|setup|history|stats|rotate|pricing> [options]',
|
|
10
10
|
' aiwg decision ask --question "<q>" (--yes-no | --choices a,b,c | --scale 1-5) [--context <text> | --context-file <path> | --context-stdin] [--threshold 0.8] [--timeout-ms 15000] [--passes N] [--escalation-passes N] [--provider <id>] [--session <id>] [--caller <tag>] [--record-text] [--json]',
|
|
11
11
|
'',
|
|
12
12
|
'Commands:',
|
|
@@ -20,9 +20,10 @@ function usage() {
|
|
|
20
20
|
' setup synthetic-classification [--output-dir <dir>]',
|
|
21
21
|
' setup jev [--token-interactive | --token-stdin | --token-file <path> | --token-store <name> | --token-env <name>] [--region <r>] [--endpoint <url>] [--verify] [--remove]',
|
|
22
22
|
' ask --question "<q>" (--yes-no | --choices a,b,c | --scale 1-5) [--provider <id>] [--session <id>] [--caller <tag>] [--record-text]',
|
|
23
|
-
' history [--since <date>] [--provider <id>] [--session <id>] [--kind yes-no|choices|scale] [--status answered|fallback] [--caller <tag>|<prefix>* | --no-caller] [--json]',
|
|
23
|
+
' history [--since <date>] [--provider <id>] [--session <id>] [--kind yes-no|choices|scale] [--status answered|fallback] [--caller <tag>|<prefix>* | --no-caller] [--include-archives] [--json]',
|
|
24
24
|
' pricing [--refresh --model jev-X.Y.Z] [--dry-run] [--offline | --allow-snapshot] [--json]',
|
|
25
|
-
' stats [--since <date>] [--provider <id>] [--session <id>] [--kind yes-no|choices|scale] [--status answered|fallback] [--caller <tag>|<prefix>* | --no-caller] [--group-by provider|session|kind|caller|passes|escalation] [--json]',
|
|
25
|
+
' stats [--since <date>] [--provider <id>] [--session <id>] [--kind yes-no|choices|scale] [--status answered|fallback] [--caller <tag>|<prefix>* | --no-caller] [--group-by provider|session|kind|caller|passes|escalation] [--include-archives] [--json]',
|
|
26
|
+
' rotate [--keep-last <Nd|N> | --all] [--to <path>] [--json]',
|
|
26
27
|
'',
|
|
27
28
|
'Notes:',
|
|
28
29
|
' Every COMPLETED `ask` call (CLI and MCP) appends one record to the decision',
|
|
@@ -42,9 +43,13 @@ function usage() {
|
|
|
42
43
|
` --session and --caller must match ${ATTRIBUTION_FIELD_PATTERN.source} (<= ${MAX_ATTRIBUTION_FIELD_LENGTH} characters).`,
|
|
43
44
|
' `history`/`stats` accept `--caller <tag>` (exact), `--caller \'<prefix>*\'` (prefix; the only wildcard), or `--no-caller` (records with no tag; not combinable with `--caller`).',
|
|
44
45
|
' `history`/`stats` reject unknown flags and positional arguments with exit 2.',
|
|
45
|
-
'
|
|
46
|
-
'
|
|
47
|
-
'
|
|
46
|
+
' `rotate` keeps the last 90 days by default; --keep-last accepts days or a',
|
|
47
|
+
' positive count. Rotating everything requires --all. Malformed lines stay',
|
|
48
|
+
' inline unless --all is used. Archives are private .jsonl files beneath',
|
|
49
|
+
' archive/decision/ in the resolved artifact root; --to must stay there.',
|
|
50
|
+
' `history`/`stats` optionally read archives with --include-archives, offline.',
|
|
51
|
+
' Appenders, rotation and readers share one cross-process lock.',
|
|
52
|
+
' The ledger trusts only the local filesystem under the project',
|
|
48
53
|
' artifact root, owned by the same user running aiwg — it is not a defense',
|
|
49
54
|
' against a hostile process already running as that user.',
|
|
50
55
|
].join('\n');
|
|
@@ -207,7 +212,7 @@ function parseGroupBy(value) {
|
|
|
207
212
|
return value;
|
|
208
213
|
}
|
|
209
214
|
const HISTORY_VALUE_FLAGS = ['--since', '--provider', '--session', '--kind', '--status', '--caller'];
|
|
210
|
-
const HISTORY_BOOL_FLAGS = ['--json', '--no-caller'];
|
|
215
|
+
const HISTORY_BOOL_FLAGS = ['--json', '--no-caller', '--include-archives'];
|
|
211
216
|
const STATS_VALUE_FLAGS = [...HISTORY_VALUE_FLAGS, '--group-by'];
|
|
212
217
|
const STATS_BOOL_FLAGS = HISTORY_BOOL_FLAGS;
|
|
213
218
|
/**
|
|
@@ -269,6 +274,7 @@ function parseLedgerFilters(args) {
|
|
|
269
274
|
kind: parseKindFilter(takeOption(args, '--kind')),
|
|
270
275
|
status: parseStatusFilter(takeOption(args, '--status')),
|
|
271
276
|
...parseCallerFilter(args),
|
|
277
|
+
...(args.includes('--include-archives') ? { includeArchives: true } : {}),
|
|
272
278
|
};
|
|
273
279
|
}
|
|
274
280
|
/** One header line stating the caller filter, shown only when one is set. */
|
|
@@ -323,6 +329,23 @@ async function decisionStats(ctx) {
|
|
|
323
329
|
]);
|
|
324
330
|
return { exitCode: 0, message: [...header, ...lines].join('\n'), rawOutput: true };
|
|
325
331
|
}
|
|
332
|
+
async function decisionRotate(ctx) {
|
|
333
|
+
const args = ctx.args.slice(1);
|
|
334
|
+
assertLedgerArgs(args, ['--keep-last', '--to'], ['--all', '--json']);
|
|
335
|
+
const keepLast = args.includes('--keep-last') ? takeRequiredOption(args, '--keep-last') : undefined;
|
|
336
|
+
const all = args.includes('--all');
|
|
337
|
+
try {
|
|
338
|
+
parseDecisionLedgerRetention(keepLast, all);
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
throw new DecisionAskUsageError('invalid-retention', error instanceof Error ? error.message : String(error));
|
|
342
|
+
}
|
|
343
|
+
const result = await rotateDecisionLedger({ projectRoot: ctx.cwd, keepLast, all,
|
|
344
|
+
to: args.includes('--to') ? takeRequiredOption(args, '--to') : undefined });
|
|
345
|
+
if (args.includes('--json'))
|
|
346
|
+
return jsonResult(result);
|
|
347
|
+
return { exitCode: 0, rawOutput: true, message: `Archived ${result.archived} line(s)${result.archivePath ? ` to ${result.archivePath}` : ''}; retained ${result.retained} line(s) inline (${result.malformed} malformed).` };
|
|
348
|
+
}
|
|
326
349
|
async function executeDecision(ctx) {
|
|
327
350
|
const [command, subcommand, ...rest] = ctx.args;
|
|
328
351
|
const options = { cwd: ctx.cwd, frameworkRoot: ctx.frameworkRoot, env: process.env };
|
|
@@ -335,6 +358,8 @@ async function executeDecision(ctx) {
|
|
|
335
358
|
return await decisionAsk(ctx);
|
|
336
359
|
if (command === 'history')
|
|
337
360
|
return await decisionHistory(ctx);
|
|
361
|
+
if (command === 'rotate')
|
|
362
|
+
return await decisionRotate(ctx);
|
|
338
363
|
if (command === 'pricing') {
|
|
339
364
|
const args = ctx.args.slice(1);
|
|
340
365
|
assertLedgerArgs(args, ['--model'], ['--refresh', '--dry-run', '--offline', '--allow-snapshot', '--json']);
|
|
@@ -21,9 +21,22 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
|
21
21
|
import { createHash, randomUUID } from 'crypto';
|
|
22
22
|
import { pathToFileURL } from 'node:url';
|
|
23
23
|
import { projectAiwgPath } from '../../config/project-artifacts.js';
|
|
24
|
+
import { resolveSensitiveInput, sensitiveInputOptions } from '../../auth/sensitive-input.js';
|
|
24
25
|
const DEFAULT_PORT = 8200;
|
|
25
26
|
const DEFAULT_BIND = '127.0.0.1';
|
|
26
27
|
const DEFAULT_AIWG_SERVE = 'http://127.0.0.1:7337';
|
|
28
|
+
/** Optional input: no source keeps registration/cache behavior, even at a terminal. */
|
|
29
|
+
export async function resolveLocalExecutorToken(args, dependencies = {}) {
|
|
30
|
+
const source = sensitiveInputOptions(args, 'token');
|
|
31
|
+
const environment = dependencies.environment ?? process.env;
|
|
32
|
+
const explicit = source.interactive || source.stdin || source.file !== undefined || source.store !== undefined || source.env !== undefined;
|
|
33
|
+
if (!explicit) {
|
|
34
|
+
if (!environment.AIWG_EXECUTOR_TOKEN)
|
|
35
|
+
return undefined;
|
|
36
|
+
source.env = 'AIWG_EXECUTOR_TOKEN';
|
|
37
|
+
}
|
|
38
|
+
return resolveSensitiveInput(source, { ...dependencies, environment });
|
|
39
|
+
}
|
|
27
40
|
/**
|
|
28
41
|
* Resolve or generate-once the executor identity.
|
|
29
42
|
* Written to `.aiwg/local-executor/identity.json` so restarts reuse the same
|
|
@@ -122,6 +135,14 @@ export const localExecutorServeHandler = {
|
|
|
122
135
|
// The subcommand is: `aiwg local-executor serve [flags]`
|
|
123
136
|
// ctx.args[0] === 'serve' (consumed by router) or flags directly
|
|
124
137
|
const flagArgs = ctx.args[0] === 'serve' ? ctx.args.slice(1) : ctx.args;
|
|
138
|
+
let preexistingToken;
|
|
139
|
+
try {
|
|
140
|
+
preexistingToken = await resolveLocalExecutorToken(flagArgs);
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
return { exitCode: 1, message: error.message };
|
|
144
|
+
}
|
|
145
|
+
const redact = (message) => preexistingToken ? message.split(preexistingToken).join('[REDACTED]') : message;
|
|
125
146
|
const opts = parseLocalExecutorArgs(flagArgs);
|
|
126
147
|
warnIfNonLoopback(opts.bind);
|
|
127
148
|
// ── Dynamic imports (same pattern as serve.ts) ──────────────────────────
|
|
@@ -138,7 +159,7 @@ export const localExecutorServeHandler = {
|
|
|
138
159
|
}
|
|
139
160
|
catch (err) {
|
|
140
161
|
return {
|
|
141
|
-
message: `Failed to load executor-shim: ${err.message}`,
|
|
162
|
+
message: `Failed to load executor-shim: ${redact(err.message)}`,
|
|
142
163
|
exitCode: 1,
|
|
143
164
|
};
|
|
144
165
|
}
|
|
@@ -148,7 +169,7 @@ export const localExecutorServeHandler = {
|
|
|
148
169
|
}
|
|
149
170
|
catch (err) {
|
|
150
171
|
return {
|
|
151
|
-
message: `Failed to load DaemonSupervisor: ${err.message}`,
|
|
172
|
+
message: `Failed to load DaemonSupervisor: ${redact(err.message)}`,
|
|
152
173
|
exitCode: 1,
|
|
153
174
|
};
|
|
154
175
|
}
|
|
@@ -208,7 +229,7 @@ export const localExecutorServeHandler = {
|
|
|
208
229
|
restBase,
|
|
209
230
|
wsBase,
|
|
210
231
|
aiwgServeUrl: opts.aiwgServe,
|
|
211
|
-
token:
|
|
232
|
+
token: preexistingToken,
|
|
212
233
|
tokenPath: projectAiwgPath(process.cwd(), 'local-executor', 'tokens', `${createHash('sha256').update(`${opts.aiwgServe}\0${opts.executorId}`).digest('hex')}.json`),
|
|
213
234
|
});
|
|
214
235
|
// Register with aiwg-serve
|
|
@@ -218,7 +239,7 @@ export const localExecutorServeHandler = {
|
|
|
218
239
|
}
|
|
219
240
|
catch (err) {
|
|
220
241
|
return {
|
|
221
|
-
message: `Failed to register with aiwg-serve at ${opts.aiwgServe}: ${err.message}\n` +
|
|
242
|
+
message: `Failed to register with aiwg-serve at ${opts.aiwgServe}: ${redact(err.message)}\n` +
|
|
222
243
|
'Is aiwg serve running? Start it with: aiwg serve --no-open',
|
|
223
244
|
exitCode: 1,
|
|
224
245
|
};
|
|
@@ -235,7 +256,7 @@ export const localExecutorServeHandler = {
|
|
|
235
256
|
catch (err) {
|
|
236
257
|
await shim.deregister();
|
|
237
258
|
return {
|
|
238
|
-
message: `Failed to start executor server: ${err.message}`,
|
|
259
|
+
message: `Failed to start executor server: ${redact(err.message).split(token).join('[REDACTED]')}`,
|
|
239
260
|
exitCode: 1,
|
|
240
261
|
};
|
|
241
262
|
}
|
|
@@ -304,9 +325,15 @@ export const localExecutorHandler = {
|
|
|
304
325
|
' --aiwg-serve <url> Registration target (default: http://127.0.0.1:7337)\n' +
|
|
305
326
|
' --max-concurrency <n> Max concurrent missions (default: 4)\n' +
|
|
306
327
|
' --executor-id <uuid> Override stable executor ID\n' +
|
|
328
|
+
' --token-interactive Read optional existing bearer without echo\n' +
|
|
329
|
+
' --token-stdin Read optional existing bearer from stdin\n' +
|
|
330
|
+
' --token-file <path> Read optional existing bearer from a private file\n' +
|
|
331
|
+
' --token-store <name> Read optional existing bearer from the OS store\n' +
|
|
332
|
+
' --token-env <NAME> Read optional existing bearer from a named variable\n' +
|
|
307
333
|
'\n' +
|
|
308
334
|
'Environment:\n' +
|
|
309
|
-
' AIWG_SERVE_ENDPOINT Override --aiwg-serve default\n'
|
|
335
|
+
' AIWG_SERVE_ENDPOINT Override --aiwg-serve default\n' +
|
|
336
|
+
' AIWG_EXECUTOR_TOKEN Optional preexisting executor bearer\n');
|
|
310
337
|
return { exitCode: 0 };
|
|
311
338
|
}
|
|
312
339
|
return {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OpenRouter fleet spend observation and local activity correlation.
|
|
3
3
|
*
|
|
4
|
-
* Secrets are resolved
|
|
4
|
+
* Secrets are resolved from named sources or the user's
|
|
5
5
|
* ~/.config/aiwg/keys directory. Fleet configuration contains references,
|
|
6
6
|
* never credential values.
|
|
7
7
|
*
|
|
@@ -11,6 +11,7 @@ import { promises as fs } from 'node:fs';
|
|
|
11
11
|
import os from 'node:os';
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import { load as loadYaml } from 'js-yaml';
|
|
14
|
+
import { resolveSensitiveInput } from '../auth/sensitive-input.js';
|
|
14
15
|
const OPENROUTER_API = 'https://openrouter.ai/api/v1';
|
|
15
16
|
const KEY_REF_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
16
17
|
const SECRET_FIELD_PATTERN = /(?:api[_-]?key|token|secret|credential|authorization)/i;
|
|
@@ -60,6 +61,9 @@ function validateFleetConfig(value) {
|
|
|
60
61
|
if (!KEY_REF_PATTERN.test(entry.key_ref)) {
|
|
61
62
|
throw new Error(`fleet[${index}].key_ref contains unsupported characters.`);
|
|
62
63
|
}
|
|
64
|
+
if (entry.key_store !== undefined && (typeof entry.key_store !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/.test(entry.key_store))) {
|
|
65
|
+
throw new Error(`fleet[${index}].key_store must name a native-store entry (1–128 letters, digits, dots, underscores or hyphens).`);
|
|
66
|
+
}
|
|
63
67
|
if (typeof entry.monthly_cap !== 'number' || !Number.isFinite(entry.monthly_cap) || entry.monthly_cap < 0) {
|
|
64
68
|
throw new Error(`fleet[${index}].monthly_cap must be a non-negative number.`);
|
|
65
69
|
}
|
|
@@ -73,6 +77,7 @@ function validateFleetConfig(value) {
|
|
|
73
77
|
bot: entry.bot.trim(),
|
|
74
78
|
machine: entry.machine.trim(),
|
|
75
79
|
key_ref: entry.key_ref,
|
|
80
|
+
...(entry.key_store !== undefined ? { key_store: entry.key_store } : {}),
|
|
76
81
|
monthly_cap: entry.monthly_cap,
|
|
77
82
|
};
|
|
78
83
|
});
|
|
@@ -101,10 +106,16 @@ export async function loadFleetKey(keyRef, options = {}) {
|
|
|
101
106
|
if (!KEY_REF_PATTERN.test(keyRef))
|
|
102
107
|
throw new Error('Invalid key_ref.');
|
|
103
108
|
const env = options.env ?? process.env;
|
|
109
|
+
const dependencies = { environment: env, readStore: options.readStore, isTTY: false };
|
|
110
|
+
// An explicit reference is scoped to this entry. Never probe a global key or
|
|
111
|
+
// another bot's store when the selected source is unavailable.
|
|
112
|
+
if (options.store !== undefined) {
|
|
113
|
+
return resolveSensitiveInput({ prefix: 'key', store: options.store }, dependencies);
|
|
114
|
+
}
|
|
104
115
|
const environmentName = keyEnvironmentName(keyRef);
|
|
105
116
|
const environmentValue = env[environmentName]?.trim();
|
|
106
117
|
if (environmentValue)
|
|
107
|
-
return
|
|
118
|
+
return resolveSensitiveInput({ prefix: 'key', env: environmentName }, dependencies);
|
|
108
119
|
const homeDir = options.homeDir ?? os.homedir();
|
|
109
120
|
const keyDirectory = path.resolve(homeDir, '.config', 'aiwg', 'keys');
|
|
110
121
|
const keyPath = path.resolve(keyDirectory, keyRef);
|
|
@@ -116,7 +127,7 @@ export async function loadFleetKey(keyRef, options = {}) {
|
|
|
116
127
|
}
|
|
117
128
|
catch (error) {
|
|
118
129
|
if (error.code === 'ENOENT') {
|
|
119
|
-
throw new Error(`No secure credential directory exists for key_ref '${keyRef}'; use ${environmentName} or create ~/.config/aiwg/keys with mode 0700.`);
|
|
130
|
+
throw new Error(`No secure credential directory exists for key_ref '${keyRef}'; use ${environmentName}, --key-store <name> (fleet: key_store), or create ~/.config/aiwg/keys with mode 0700.`);
|
|
120
131
|
}
|
|
121
132
|
throw error;
|
|
122
133
|
}
|
|
@@ -129,27 +140,12 @@ export async function loadFleetKey(keyRef, options = {}) {
|
|
|
129
140
|
if (typeof process.getuid === 'function' && directoryStat.uid !== process.getuid()) {
|
|
130
141
|
throw new Error('The AIWG credential directory must be owned by the current user.');
|
|
131
142
|
}
|
|
132
|
-
let stat;
|
|
133
143
|
try {
|
|
134
|
-
|
|
144
|
+
return await resolveSensitiveInput({ prefix: 'key', file: keyPath }, dependencies);
|
|
135
145
|
}
|
|
136
146
|
catch (error) {
|
|
137
|
-
|
|
138
|
-
throw new Error(`No credential found for key_ref '${keyRef}' in the secure key directory or ${environmentName}.`);
|
|
139
|
-
}
|
|
140
|
-
throw error;
|
|
141
|
-
}
|
|
142
|
-
if (stat.isSymbolicLink() || !stat.isFile())
|
|
143
|
-
throw new Error(`Credential for key_ref '${keyRef}' must be a regular file, not a symlink.`);
|
|
144
|
-
if ((stat.mode & 0o077) !== 0)
|
|
145
|
-
throw new Error(`Credential file for key_ref '${keyRef}' must not be accessible by group or other users.`);
|
|
146
|
-
if (typeof process.getuid === 'function' && stat.uid !== process.getuid()) {
|
|
147
|
-
throw new Error(`Credential file for key_ref '${keyRef}' must be owned by the current user.`);
|
|
147
|
+
throw new Error(`Credential for key_ref '${keyRef}': ${error.message} Use ${environmentName}, --key-store <name> (fleet: key_store), or a private ~/.config/aiwg/keys/<key_ref> file.`);
|
|
148
148
|
}
|
|
149
|
-
const value = (await fs.readFile(keyPath, 'utf8')).trim();
|
|
150
|
-
if (!value)
|
|
151
|
-
throw new Error(`Credential file for key_ref '${keyRef}' is empty.`);
|
|
152
|
-
return value;
|
|
153
149
|
}
|
|
154
150
|
function parseActivityLog(raw) {
|
|
155
151
|
const entries = [];
|
|
@@ -202,7 +198,9 @@ async function openRouterGet(endpoint, key, options) {
|
|
|
202
198
|
}
|
|
203
199
|
if (!payload || typeof payload !== 'object' || !payload.data)
|
|
204
200
|
throw new Error('OpenRouter returned an invalid response.');
|
|
205
|
-
|
|
201
|
+
// Generation metadata can reflect a request header in free-text fields. Keep
|
|
202
|
+
// such content out of both structured and human-readable reports.
|
|
203
|
+
return JSON.parse(JSON.stringify(payload.data, (_name, value) => (typeof value === 'string' ? value.split(key).join('[REDACTED]') : value)));
|
|
206
204
|
}
|
|
207
205
|
function correlateGenerations(records) {
|
|
208
206
|
const sessions = new Map();
|
|
@@ -223,7 +221,7 @@ function correlateGenerations(records) {
|
|
|
223
221
|
}
|
|
224
222
|
async function reportBot(entry, activity, options) {
|
|
225
223
|
try {
|
|
226
|
-
const key = await loadFleetKey(entry.key_ref, options);
|
|
224
|
+
const key = await loadFleetKey(entry.key_ref, { ...options, store: entry.key_store });
|
|
227
225
|
const keyData = await openRouterGet('/key', key, options);
|
|
228
226
|
const spend = Number(keyData.usage_monthly ?? keyData.usage ?? 0);
|
|
229
227
|
if (!Number.isFinite(spend) || spend < 0)
|
|
@@ -4,12 +4,12 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
6
|
import { askDecision, setupJev, DecisionAskUsageError, readDecisionContextFile, validateDecisionAskInput, } from './ask.js';
|
|
7
|
-
import { recordDecisionAsk, decisionLedgerHistory, decisionLedgerStats, resolveDecisionSessionId, resolveDecisionSession, DECISION_SESSION_ENV_VARS, parseDecisionLedgerSince, DecisionLedgerSinceError, DECISION_LEDGER_SCHEMA, DECISION_LEDGER_SCHEMA_V1, ATTRIBUTION_FIELD_PATTERN, MAX_ATTRIBUTION_FIELD_LENGTH, MAX_RECORDED_CHOICE_IDS, MAX_RECORD_TEXT_BYTES, MAX_LEDGER_RECORD_BYTES, DECISION_LEDGER_RECORD_TIMEOUT_MS, DECISION_SAVINGS_REASONING_ALLOWANCE_TOKENS, DECISION_SAVINGS_ESTIMATION_METHOD, buildDecisionLedgerRecord, appendDecisionLedgerRecord, filterDecisionLedgerRecords, } from './ledger.js';
|
|
7
|
+
import { recordDecisionAsk, decisionLedgerHistory, decisionLedgerStats, rotateDecisionLedger, parseDecisionLedgerRetention, resolveDecisionSessionId, resolveDecisionSession, DECISION_SESSION_ENV_VARS, parseDecisionLedgerSince, DecisionLedgerSinceError, DECISION_LEDGER_SCHEMA, DECISION_LEDGER_SCHEMA_V1, ATTRIBUTION_FIELD_PATTERN, MAX_ATTRIBUTION_FIELD_LENGTH, MAX_RECORDED_CHOICE_IDS, MAX_RECORD_TEXT_BYTES, MAX_LEDGER_RECORD_BYTES, DECISION_LEDGER_RECORD_TIMEOUT_MS, DECISION_SAVINGS_REASONING_ALLOWANCE_TOKENS, DECISION_SAVINGS_ESTIMATION_METHOD, buildDecisionLedgerRecord, appendDecisionLedgerRecord, filterDecisionLedgerRecords, } from './ledger.js';
|
|
8
8
|
export { DEFAULT_DECISION_SAVINGS_WEIGHTS, decisionSavingsWeights } from './savings.js';
|
|
9
9
|
import { resolveActiveProvider } from '../cli/provider-resolution.js';
|
|
10
10
|
export { appendDecisionOpportunity, buildDecisionOffloadSnapshot, decisionOffloadCategory, DECISION_OFFLOAD_CHECKPOINT_COVERAGE, DECISION_OFFLOAD_EXECUTABLE_COVERAGE, DECISION_OFFLOAD_SNAPSHOT_SCHEMA, DECISION_OPPORTUNITY_SCHEMA, runDecisionOffloadCheckpoint, } from './offload.js';
|
|
11
11
|
import { assertDecisionEvaluateDispatcherConfig, decisionPatternPacks, getDecisionPatternPack, governedPatternArtifacts, listDecisionPatterns, patternInputSchema, patternOutputSchema, planLiveDecisionPattern, runOfflineDecisionPattern, validateAgainstSchema, validateDecisionDocument, validateDefinition, validateRuleset, } from './index.js';
|
|
12
|
-
export { askDecision, setupJev, readDecisionContextFile, validateDecisionAskInput, DecisionAskUsageError, recordDecisionAsk, decisionLedgerHistory, decisionLedgerStats, resolveDecisionSessionId, resolveDecisionSession, DECISION_SESSION_ENV_VARS, parseDecisionLedgerSince, DecisionLedgerSinceError, DECISION_LEDGER_SCHEMA, DECISION_LEDGER_SCHEMA_V1, ATTRIBUTION_FIELD_PATTERN, MAX_ATTRIBUTION_FIELD_LENGTH, MAX_RECORDED_CHOICE_IDS, MAX_RECORD_TEXT_BYTES, MAX_LEDGER_RECORD_BYTES, DECISION_LEDGER_RECORD_TIMEOUT_MS, DECISION_SAVINGS_REASONING_ALLOWANCE_TOKENS, DECISION_SAVINGS_ESTIMATION_METHOD, buildDecisionLedgerRecord, appendDecisionLedgerRecord, filterDecisionLedgerRecords, resolveActiveProvider, };
|
|
12
|
+
export { askDecision, setupJev, readDecisionContextFile, validateDecisionAskInput, DecisionAskUsageError, recordDecisionAsk, decisionLedgerHistory, decisionLedgerStats, rotateDecisionLedger, parseDecisionLedgerRetention, resolveDecisionSessionId, resolveDecisionSession, DECISION_SESSION_ENV_VARS, parseDecisionLedgerSince, DecisionLedgerSinceError, DECISION_LEDGER_SCHEMA, DECISION_LEDGER_SCHEMA_V1, ATTRIBUTION_FIELD_PATTERN, MAX_ATTRIBUTION_FIELD_LENGTH, MAX_RECORDED_CHOICE_IDS, MAX_RECORD_TEXT_BYTES, MAX_LEDGER_RECORD_BYTES, DECISION_LEDGER_RECORD_TIMEOUT_MS, DECISION_SAVINGS_REASONING_ALLOWANCE_TOKENS, DECISION_SAVINGS_ESTIMATION_METHOD, buildDecisionLedgerRecord, appendDecisionLedgerRecord, filterDecisionLedgerRecords, resolveActiveProvider, };
|
|
13
13
|
const DECISION_ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
14
14
|
const VALIDATION_TARGETS = ['request', 'definition', 'ruleset', 'binding'];
|
|
15
15
|
const OFFLINE_ADAPTER = 'agentic/code/addons/decision-engine/examples/fixture-jev-adapter.mjs';
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { resolveDecisionEnablement } from '../config/decision-config.js';
|
|
2
2
|
import { constants } from 'node:fs';
|
|
3
|
-
import { access, chmod,
|
|
3
|
+
import { access, chmod, lstat, mkdir, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { randomUUID } from 'node:crypto';
|
|
7
7
|
import { JevCredentialError } from './adapters/jev.js';
|
|
8
|
+
import { readPrivateValueFile } from '../auth/sensitive-input.js';
|
|
8
9
|
export const JEV_CREDENTIAL_REF = 'typesafe-api';
|
|
9
10
|
export const JEV_DEFAULT_REGION = 'us';
|
|
10
11
|
function env(options) {
|
|
@@ -45,10 +46,22 @@ function parseRecord(raw) {
|
|
|
45
46
|
async function readCredentialFile(options = {}) {
|
|
46
47
|
const credentialPath = jevCredentialPath(options);
|
|
47
48
|
try {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
// Preserve the missing-file result and the existing directory requirement.
|
|
50
|
+
// The shared reader validates the opened descriptor, including ownership
|
|
51
|
+
// and link checks, before any credential record is parsed.
|
|
52
|
+
const [, parentStat] = await Promise.all([lstat(credentialPath), stat(path.dirname(credentialPath))]);
|
|
53
|
+
if (isInsecureMode(parentStat.mode)) {
|
|
54
|
+
throw Object.assign(new JevCredentialError('denied'), { message: 'Jev credential directory must not allow group or other access (chmod 700).' });
|
|
55
|
+
}
|
|
56
|
+
let raw;
|
|
57
|
+
try {
|
|
58
|
+
raw = await readPrivateValueFile(credentialPath, 'Jev credential file');
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
// Shared private-file diagnostics contain no path or credential bytes.
|
|
62
|
+
throw Object.assign(new JevCredentialError('denied'), { message: error.message });
|
|
63
|
+
}
|
|
64
|
+
return parseRecord(raw);
|
|
52
65
|
}
|
|
53
66
|
catch (error) {
|
|
54
67
|
if (error.code === 'ENOENT')
|