fixo-cli 1.0.0
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/LICENSE +201 -0
- package/README.md +530 -0
- package/dist/agent/agent-client.d.ts +108 -0
- package/dist/agent/agent-client.d.ts.map +1 -0
- package/dist/agent/agent-client.js +1247 -0
- package/dist/agent/agent-client.js.map +1 -0
- package/dist/agent/agent-pool.d.ts +20 -0
- package/dist/agent/agent-pool.d.ts.map +1 -0
- package/dist/agent/agent-pool.js +217 -0
- package/dist/agent/agent-pool.js.map +1 -0
- package/dist/agent/background-awareness.d.ts +55 -0
- package/dist/agent/background-awareness.d.ts.map +1 -0
- package/dist/agent/background-awareness.js +104 -0
- package/dist/agent/background-awareness.js.map +1 -0
- package/dist/agent/command-parser.d.ts +33 -0
- package/dist/agent/command-parser.d.ts.map +1 -0
- package/dist/agent/command-parser.js +120 -0
- package/dist/agent/command-parser.js.map +1 -0
- package/dist/agent/context-budget.d.ts +91 -0
- package/dist/agent/context-budget.d.ts.map +1 -0
- package/dist/agent/context-budget.js +219 -0
- package/dist/agent/context-budget.js.map +1 -0
- package/dist/agent/conversation.d.ts +190 -0
- package/dist/agent/conversation.d.ts.map +1 -0
- package/dist/agent/conversation.js +547 -0
- package/dist/agent/conversation.js.map +1 -0
- package/dist/agent/hooks.d.ts +72 -0
- package/dist/agent/hooks.d.ts.map +1 -0
- package/dist/agent/hooks.js +214 -0
- package/dist/agent/hooks.js.map +1 -0
- package/dist/agent/mcp-bridge.d.ts +13 -0
- package/dist/agent/mcp-bridge.d.ts.map +1 -0
- package/dist/agent/mcp-bridge.js +86 -0
- package/dist/agent/mcp-bridge.js.map +1 -0
- package/dist/agent/mcp-client.d.ts +24 -0
- package/dist/agent/mcp-client.d.ts.map +1 -0
- package/dist/agent/mcp-client.js +146 -0
- package/dist/agent/mcp-client.js.map +1 -0
- package/dist/agent/mcp-manager.d.ts +13 -0
- package/dist/agent/mcp-manager.d.ts.map +1 -0
- package/dist/agent/mcp-manager.js +84 -0
- package/dist/agent/mcp-manager.js.map +1 -0
- package/dist/agent/mcp-registry.d.ts +45 -0
- package/dist/agent/mcp-registry.d.ts.map +1 -0
- package/dist/agent/mcp-registry.js +98 -0
- package/dist/agent/mcp-registry.js.map +1 -0
- package/dist/agent/orchestrator.d.ts +14 -0
- package/dist/agent/orchestrator.d.ts.map +1 -0
- package/dist/agent/orchestrator.js +118 -0
- package/dist/agent/orchestrator.js.map +1 -0
- package/dist/agent/parser-adapter.d.ts +120 -0
- package/dist/agent/parser-adapter.d.ts.map +1 -0
- package/dist/agent/parser-adapter.js +265 -0
- package/dist/agent/parser-adapter.js.map +1 -0
- package/dist/agent/parsers/imports.d.ts +11 -0
- package/dist/agent/parsers/imports.d.ts.map +1 -0
- package/dist/agent/parsers/imports.js +94 -0
- package/dist/agent/parsers/imports.js.map +1 -0
- package/dist/agent/parsers/shell.d.ts +23 -0
- package/dist/agent/parsers/shell.d.ts.map +1 -0
- package/dist/agent/parsers/shell.js +200 -0
- package/dist/agent/parsers/shell.js.map +1 -0
- package/dist/agent/parsers/symbols.d.ts +17 -0
- package/dist/agent/parsers/symbols.d.ts.map +1 -0
- package/dist/agent/parsers/symbols.js +103 -0
- package/dist/agent/parsers/symbols.js.map +1 -0
- package/dist/agent/permissions.d.ts +65 -0
- package/dist/agent/permissions.d.ts.map +1 -0
- package/dist/agent/permissions.js +219 -0
- package/dist/agent/permissions.js.map +1 -0
- package/dist/agent/predictive-gate.d.ts +69 -0
- package/dist/agent/predictive-gate.d.ts.map +1 -0
- package/dist/agent/predictive-gate.js +128 -0
- package/dist/agent/predictive-gate.js.map +1 -0
- package/dist/agent/provider-cooldown.d.ts +144 -0
- package/dist/agent/provider-cooldown.d.ts.map +1 -0
- package/dist/agent/provider-cooldown.js +300 -0
- package/dist/agent/provider-cooldown.js.map +1 -0
- package/dist/agent/providers-manager.d.ts +109 -0
- package/dist/agent/providers-manager.d.ts.map +1 -0
- package/dist/agent/providers-manager.js +464 -0
- package/dist/agent/providers-manager.js.map +1 -0
- package/dist/agent/repo-map.d.ts +6 -0
- package/dist/agent/repo-map.d.ts.map +1 -0
- package/dist/agent/repo-map.js +221 -0
- package/dist/agent/repo-map.js.map +1 -0
- package/dist/agent/retry.d.ts +103 -0
- package/dist/agent/retry.d.ts.map +1 -0
- package/dist/agent/retry.js +276 -0
- package/dist/agent/retry.js.map +1 -0
- package/dist/agent/search/index.d.ts +61 -0
- package/dist/agent/search/index.d.ts.map +1 -0
- package/dist/agent/search/index.js +314 -0
- package/dist/agent/search/index.js.map +1 -0
- package/dist/agent/single-agent.d.ts +76 -0
- package/dist/agent/single-agent.d.ts.map +1 -0
- package/dist/agent/single-agent.js +697 -0
- package/dist/agent/single-agent.js.map +1 -0
- package/dist/agent/skills.d.ts +22 -0
- package/dist/agent/skills.d.ts.map +1 -0
- package/dist/agent/skills.js +139 -0
- package/dist/agent/skills.js.map +1 -0
- package/dist/agent/stream-glue.d.ts +85 -0
- package/dist/agent/stream-glue.d.ts.map +1 -0
- package/dist/agent/stream-glue.js +120 -0
- package/dist/agent/stream-glue.js.map +1 -0
- package/dist/agent/subagent.d.ts +72 -0
- package/dist/agent/subagent.d.ts.map +1 -0
- package/dist/agent/subagent.js +193 -0
- package/dist/agent/subagent.js.map +1 -0
- package/dist/agent/telemetry.d.ts +192 -0
- package/dist/agent/telemetry.d.ts.map +1 -0
- package/dist/agent/telemetry.js +400 -0
- package/dist/agent/telemetry.js.map +1 -0
- package/dist/agent/tokenizer.d.ts +42 -0
- package/dist/agent/tokenizer.d.ts.map +1 -0
- package/dist/agent/tokenizer.js +107 -0
- package/dist/agent/tokenizer.js.map +1 -0
- package/dist/agent/tool-executor.d.ts +289 -0
- package/dist/agent/tool-executor.d.ts.map +1 -0
- package/dist/agent/tool-executor.js +2519 -0
- package/dist/agent/tool-executor.js.map +1 -0
- package/dist/agent/web-impl.d.ts +2 -0
- package/dist/agent/web-impl.d.ts.map +1 -0
- package/dist/agent/web-impl.js +34 -0
- package/dist/agent/web-impl.js.map +1 -0
- package/dist/agent/web.d.ts +8 -0
- package/dist/agent/web.d.ts.map +1 -0
- package/dist/agent/web.js +8 -0
- package/dist/agent/web.js.map +1 -0
- package/dist/agent/worker-agent.d.ts +27 -0
- package/dist/agent/worker-agent.d.ts.map +1 -0
- package/dist/agent/worker-agent.js +503 -0
- package/dist/agent/worker-agent.js.map +1 -0
- package/dist/config.d.ts +162 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +138 -0
- package/dist/config.js.map +1 -0
- package/dist/context/fixo-md-watcher.d.ts +42 -0
- package/dist/context/fixo-md-watcher.d.ts.map +1 -0
- package/dist/context/fixo-md-watcher.js +126 -0
- package/dist/context/fixo-md-watcher.js.map +1 -0
- package/dist/context/fixo-md.d.ts +50 -0
- package/dist/context/fixo-md.d.ts.map +1 -0
- package/dist/context/fixo-md.js +118 -0
- package/dist/context/fixo-md.js.map +1 -0
- package/dist/context/todo.d.ts +65 -0
- package/dist/context/todo.d.ts.map +1 -0
- package/dist/context/todo.js +194 -0
- package/dist/context/todo.js.map +1 -0
- package/dist/git/git-manager.d.ts +33 -0
- package/dist/git/git-manager.d.ts.map +1 -0
- package/dist/git/git-manager.js +293 -0
- package/dist/git/git-manager.js.map +1 -0
- package/dist/git/git-ops.d.ts +10 -0
- package/dist/git/git-ops.d.ts.map +1 -0
- package/dist/git/git-ops.js +131 -0
- package/dist/git/git-ops.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +352 -0
- package/dist/index.js.map +1 -0
- package/dist/indexer.d.ts +30 -0
- package/dist/indexer.d.ts.map +1 -0
- package/dist/indexer.js +273 -0
- package/dist/indexer.js.map +1 -0
- package/dist/lsp/lsp-client.d.ts +24 -0
- package/dist/lsp/lsp-client.d.ts.map +1 -0
- package/dist/lsp/lsp-client.js +205 -0
- package/dist/lsp/lsp-client.js.map +1 -0
- package/dist/lsp/lsp-manager.d.ts +17 -0
- package/dist/lsp/lsp-manager.d.ts.map +1 -0
- package/dist/lsp/lsp-manager.js +154 -0
- package/dist/lsp/lsp-manager.js.map +1 -0
- package/dist/lsp/lsp-pre-save.d.ts +137 -0
- package/dist/lsp/lsp-pre-save.d.ts.map +1 -0
- package/dist/lsp/lsp-pre-save.js +245 -0
- package/dist/lsp/lsp-pre-save.js.map +1 -0
- package/dist/lsp/syntax-fallback.d.ts +83 -0
- package/dist/lsp/syntax-fallback.d.ts.map +1 -0
- package/dist/lsp/syntax-fallback.js +275 -0
- package/dist/lsp/syntax-fallback.js.map +1 -0
- package/dist/model-outcomes.d.ts +12 -0
- package/dist/model-outcomes.d.ts.map +1 -0
- package/dist/model-outcomes.js +46 -0
- package/dist/model-outcomes.js.map +1 -0
- package/dist/planner.d.ts +32 -0
- package/dist/planner.d.ts.map +1 -0
- package/dist/planner.js +163 -0
- package/dist/planner.js.map +1 -0
- package/dist/project-memory.d.ts +29 -0
- package/dist/project-memory.d.ts.map +1 -0
- package/dist/project-memory.js +349 -0
- package/dist/project-memory.js.map +1 -0
- package/dist/review.d.ts +2 -0
- package/dist/review.d.ts.map +1 -0
- package/dist/review.js +61 -0
- package/dist/review.js.map +1 -0
- package/dist/runtime/background-jobs.d.ts +97 -0
- package/dist/runtime/background-jobs.d.ts.map +1 -0
- package/dist/runtime/background-jobs.js +331 -0
- package/dist/runtime/background-jobs.js.map +1 -0
- package/dist/runtime/credential-vault.d.ts +124 -0
- package/dist/runtime/credential-vault.d.ts.map +1 -0
- package/dist/runtime/credential-vault.js +184 -0
- package/dist/runtime/credential-vault.js.map +1 -0
- package/dist/runtime/loop-trap.d.ts +197 -0
- package/dist/runtime/loop-trap.d.ts.map +1 -0
- package/dist/runtime/loop-trap.js +420 -0
- package/dist/runtime/loop-trap.js.map +1 -0
- package/dist/runtime/policy.d.ts +15 -0
- package/dist/runtime/policy.d.ts.map +1 -0
- package/dist/runtime/policy.js +60 -0
- package/dist/runtime/policy.js.map +1 -0
- package/dist/runtime/redaction.d.ts +66 -0
- package/dist/runtime/redaction.d.ts.map +1 -0
- package/dist/runtime/redaction.js +155 -0
- package/dist/runtime/redaction.js.map +1 -0
- package/dist/runtime/session-snapshots.d.ts +76 -0
- package/dist/runtime/session-snapshots.d.ts.map +1 -0
- package/dist/runtime/session-snapshots.js +166 -0
- package/dist/runtime/session-snapshots.js.map +1 -0
- package/dist/runtime/staging.d.ts +205 -0
- package/dist/runtime/staging.d.ts.map +1 -0
- package/dist/runtime/staging.js +526 -0
- package/dist/runtime/staging.js.map +1 -0
- package/dist/runtime/task-session.d.ts +95 -0
- package/dist/runtime/task-session.d.ts.map +1 -0
- package/dist/runtime/task-session.js +263 -0
- package/dist/runtime/task-session.js.map +1 -0
- package/dist/runtime/worktree.d.ts +55 -0
- package/dist/runtime/worktree.d.ts.map +1 -0
- package/dist/runtime/worktree.js +175 -0
- package/dist/runtime/worktree.js.map +1 -0
- package/dist/setup-wizard.d.ts +8 -0
- package/dist/setup-wizard.d.ts.map +1 -0
- package/dist/setup-wizard.js +73 -0
- package/dist/setup-wizard.js.map +1 -0
- package/dist/shared/content.d.ts +43 -0
- package/dist/shared/content.d.ts.map +1 -0
- package/dist/shared/content.js +61 -0
- package/dist/shared/content.js.map +1 -0
- package/dist/shared/types.d.ts +217 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/types.js +3 -0
- package/dist/shared/types.js.map +1 -0
- package/dist/test-runner.d.ts +5 -0
- package/dist/test-runner.d.ts.map +1 -0
- package/dist/test-runner.js +42 -0
- package/dist/test-runner.js.map +1 -0
- package/dist/types.d.ts +85 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/ui/ascii.d.ts +23 -0
- package/dist/ui/ascii.d.ts.map +1 -0
- package/dist/ui/ascii.js +45 -0
- package/dist/ui/ascii.js.map +1 -0
- package/dist/ui/colors.d.ts +111 -0
- package/dist/ui/colors.d.ts.map +1 -0
- package/dist/ui/colors.js +166 -0
- package/dist/ui/colors.js.map +1 -0
- package/dist/ui/image-attach.d.ts +27 -0
- package/dist/ui/image-attach.d.ts.map +1 -0
- package/dist/ui/image-attach.js +100 -0
- package/dist/ui/image-attach.js.map +1 -0
- package/dist/ui/index.d.ts +18 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +18 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/markdown-stream.d.ts +91 -0
- package/dist/ui/markdown-stream.d.ts.map +1 -0
- package/dist/ui/markdown-stream.js +524 -0
- package/dist/ui/markdown-stream.js.map +1 -0
- package/dist/ui/plan-renderer.d.ts +36 -0
- package/dist/ui/plan-renderer.d.ts.map +1 -0
- package/dist/ui/plan-renderer.js +79 -0
- package/dist/ui/plan-renderer.js.map +1 -0
- package/dist/ui/prompt.d.ts +11 -0
- package/dist/ui/prompt.d.ts.map +1 -0
- package/dist/ui/prompt.js +1960 -0
- package/dist/ui/prompt.js.map +1 -0
- package/dist/ui/render-primitives.d.ts +117 -0
- package/dist/ui/render-primitives.d.ts.map +1 -0
- package/dist/ui/render-primitives.js +322 -0
- package/dist/ui/render-primitives.js.map +1 -0
- package/dist/ui/render.d.ts +133 -0
- package/dist/ui/render.d.ts.map +1 -0
- package/dist/ui/render.js +547 -0
- package/dist/ui/render.js.map +1 -0
- package/dist/ui/session-header.d.ts +30 -0
- package/dist/ui/session-header.d.ts.map +1 -0
- package/dist/ui/session-header.js +74 -0
- package/dist/ui/session-header.js.map +1 -0
- package/dist/workspace-guard.d.ts +68 -0
- package/dist/workspace-guard.d.ts.map +1 -0
- package/dist/workspace-guard.js +168 -0
- package/dist/workspace-guard.js.map +1 -0
- package/dist/workspace-lock.d.ts +27 -0
- package/dist/workspace-lock.d.ts.map +1 -0
- package/dist/workspace-lock.js +95 -0
- package/dist/workspace-lock.js.map +1 -0
- package/package.json +63 -0
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* background-jobs.ts — Async command execution registry.
|
|
3
|
+
*
|
|
4
|
+
* Phase 3.1: lets the agent spawn long-running commands
|
|
5
|
+
* (`run_command_async`) and poll their status without blocking
|
|
6
|
+
* the REPL or the agent loop. Every job is keyed by a
|
|
7
|
+
* `job_<short-uuid>` and stored in an in-memory `Map` so a
|
|
8
|
+
* crashed CLI invocation can rebuild the tail buffers from the
|
|
9
|
+
* on-disk JSON snapshot.
|
|
10
|
+
*
|
|
11
|
+
* Safety:
|
|
12
|
+
* - `spawn` reuses `isCommandSafe` from `command-parser.ts` so
|
|
13
|
+
* a workspace-escape or sensitive-file read is rejected
|
|
14
|
+
* before any process is started.
|
|
15
|
+
* - The `cwd` argument is resolved through `WorkspaceGuard`
|
|
16
|
+
* so the spawned process cannot chdir outside the workspace.
|
|
17
|
+
* - The 64 KiB per-stream cap matches the staging-manager
|
|
18
|
+
* contract — no unbounded buffer growth.
|
|
19
|
+
* - A 1-hour reaper (`setInterval` in `startReaper`) kills any
|
|
20
|
+
* job whose `pid` is no longer alive, even if the executor
|
|
21
|
+
* never received an `exit` event.
|
|
22
|
+
*/
|
|
23
|
+
import { spawn } from 'node:child_process';
|
|
24
|
+
import fs from 'node:fs';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
import { randomUUID } from 'node:crypto';
|
|
27
|
+
import { isCommandSafe } from '../agent/command-parser.js';
|
|
28
|
+
import { WorkspaceGuard } from '../workspace-guard.js';
|
|
29
|
+
import { recordTelemetry, telemetry } from '../agent/telemetry.js';
|
|
30
|
+
/* ──────────────────────── Constants ──────────────────────── */
|
|
31
|
+
const STREAM_CAP_BYTES = 64 * 1024;
|
|
32
|
+
const SNAPSHOT_FLUSH_MS = 5_000;
|
|
33
|
+
const REAPER_INTERVAL_MS = 60_000;
|
|
34
|
+
const REAPER_MAX_AGE_MS = 60 * 60 * 1_000; // 1 hour
|
|
35
|
+
/* ──────────────────────── Helpers ──────────────────────── */
|
|
36
|
+
function appendCapped(buffer, chunk) {
|
|
37
|
+
const next = buffer.text + chunk;
|
|
38
|
+
const bytes = Buffer.byteLength(next, 'utf-8');
|
|
39
|
+
if (bytes > STREAM_CAP_BYTES) {
|
|
40
|
+
// Slice from the END so the user always sees the most recent
|
|
41
|
+
// output, not the oldest. The `totalStdoutBytes` counter
|
|
42
|
+
// preserves the original length so `sinceBytes` can still
|
|
43
|
+
// compute deltas.
|
|
44
|
+
const overflow = bytes - STREAM_CAP_BYTES;
|
|
45
|
+
let startIdx = 0;
|
|
46
|
+
let acc = 0;
|
|
47
|
+
// Walk forward until the cumulative byte count equals the
|
|
48
|
+
// overflow, then slice from there. This is a character-safe
|
|
49
|
+
// approach that avoids splitting a multi-byte UTF-8 char.
|
|
50
|
+
for (let i = 0; i < next.length; i++) {
|
|
51
|
+
acc += Buffer.byteLength(next[i], 'utf-8');
|
|
52
|
+
if (acc > overflow) {
|
|
53
|
+
startIdx = i;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
buffer.text = next.slice(startIdx);
|
|
58
|
+
buffer.truncated = true;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
buffer.text = next;
|
|
62
|
+
}
|
|
63
|
+
buffer.bytes += Buffer.byteLength(chunk, 'utf-8');
|
|
64
|
+
}
|
|
65
|
+
function isPidAlive(pid) {
|
|
66
|
+
try {
|
|
67
|
+
process.kill(pid, 0);
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/* ──────────────────────── Registry ──────────────────────── */
|
|
75
|
+
export class BackgroundJobRegistry {
|
|
76
|
+
jobs = new Map();
|
|
77
|
+
processes = new Map();
|
|
78
|
+
cwd;
|
|
79
|
+
snapshotDir;
|
|
80
|
+
flushTimer = null;
|
|
81
|
+
reaperTimer = null;
|
|
82
|
+
constructor(cwd, opts = {}) {
|
|
83
|
+
this.cwd = cwd;
|
|
84
|
+
this.snapshotDir = opts.snapshotDir ?? path.join(cwd, '.fixo', 'jobs');
|
|
85
|
+
fs.mkdirSync(this.snapshotDir, { recursive: true });
|
|
86
|
+
this.startSnapshotFlusher();
|
|
87
|
+
if (!opts.disableReaper)
|
|
88
|
+
this.startReaper();
|
|
89
|
+
}
|
|
90
|
+
/** Spawn a new background command. Reuses `isCommandSafe` for AST validation. */
|
|
91
|
+
async register(input) {
|
|
92
|
+
if (input.cmd.trim().length === 0) {
|
|
93
|
+
return { ok: false, error: 'cmd is empty' };
|
|
94
|
+
}
|
|
95
|
+
// Resolve the requested cwd through the workspace guard so the
|
|
96
|
+
// child process cannot chdir outside the workspace root.
|
|
97
|
+
const guard = new WorkspaceGuard(this.cwd);
|
|
98
|
+
let resolvedCwd;
|
|
99
|
+
try {
|
|
100
|
+
resolvedCwd = guard.resolve(input.cwd, 'background-job cwd');
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
return { ok: false, error: err.message };
|
|
104
|
+
}
|
|
105
|
+
const safety = await isCommandSafe(`${input.cmd} ${input.args.join(' ')}`.trim(), this.cwd);
|
|
106
|
+
if (!safety.safe) {
|
|
107
|
+
return { ok: false, error: `command rejected by command-parser: ${safety.reason ?? 'unsafe'}` };
|
|
108
|
+
}
|
|
109
|
+
const id = `job_${randomUUID().slice(0, 8)}`;
|
|
110
|
+
const job = {
|
|
111
|
+
id,
|
|
112
|
+
cmd: input.cmd,
|
|
113
|
+
args: [...input.args],
|
|
114
|
+
cwd: resolvedCwd,
|
|
115
|
+
status: 'running',
|
|
116
|
+
startedAt: new Date().toISOString(),
|
|
117
|
+
stdout: '',
|
|
118
|
+
stderr: '',
|
|
119
|
+
totalStdoutBytes: 0,
|
|
120
|
+
totalStderrBytes: 0,
|
|
121
|
+
stdoutTruncated: false,
|
|
122
|
+
stderrTruncated: false,
|
|
123
|
+
};
|
|
124
|
+
let child;
|
|
125
|
+
try {
|
|
126
|
+
child = spawn(input.cmd, input.args, {
|
|
127
|
+
cwd: resolvedCwd,
|
|
128
|
+
env: { ...process.env, FIXO_BACKGROUND_JOB: id },
|
|
129
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
job.status = 'failed';
|
|
134
|
+
job.failureReason = 'spawn threw';
|
|
135
|
+
job.lastError = err.message;
|
|
136
|
+
job.exitedAt = new Date().toISOString();
|
|
137
|
+
this.jobs.set(id, job);
|
|
138
|
+
return { ok: false, jobId: id, error: job.lastError };
|
|
139
|
+
}
|
|
140
|
+
job.pid = child.pid;
|
|
141
|
+
this.jobs.set(id, job);
|
|
142
|
+
this.processes.set(id, child);
|
|
143
|
+
const stdoutBuf = { text: job.stdout, truncated: false, bytes: 0 };
|
|
144
|
+
const stderrBuf = { text: job.stderr, truncated: false, bytes: 0 };
|
|
145
|
+
child.stdout?.setEncoding('utf-8');
|
|
146
|
+
child.stderr?.setEncoding('utf-8');
|
|
147
|
+
child.stdout?.on('data', (chunk) => {
|
|
148
|
+
appendCapped(stdoutBuf, chunk);
|
|
149
|
+
job.stdout = stdoutBuf.text;
|
|
150
|
+
job.stdoutTruncated = stdoutBuf.truncated;
|
|
151
|
+
job.totalStdoutBytes = stdoutBuf.bytes;
|
|
152
|
+
});
|
|
153
|
+
child.stderr?.on('data', (chunk) => {
|
|
154
|
+
appendCapped(stderrBuf, chunk);
|
|
155
|
+
job.stderr = stderrBuf.text;
|
|
156
|
+
job.stderrTruncated = stderrBuf.truncated;
|
|
157
|
+
job.totalStderrBytes = stderrBuf.bytes;
|
|
158
|
+
});
|
|
159
|
+
child.on('error', (err) => {
|
|
160
|
+
job.lastError = err.message;
|
|
161
|
+
// Spawn failures do not emit an `exit` event — flip the
|
|
162
|
+
// status to `failed` here so pollers can detect the
|
|
163
|
+
// terminal state without waiting forever.
|
|
164
|
+
if (job.status === 'running') {
|
|
165
|
+
job.status = 'failed';
|
|
166
|
+
job.failureReason = err.message;
|
|
167
|
+
job.exitedAt = new Date().toISOString();
|
|
168
|
+
this.processes.delete(id);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
child.on('exit', (code, signal) => {
|
|
172
|
+
job.exitedAt = new Date().toISOString();
|
|
173
|
+
if (signal === 'SIGTERM' || signal === 'SIGKILL') {
|
|
174
|
+
job.status = 'killed';
|
|
175
|
+
}
|
|
176
|
+
else if (code === 0) {
|
|
177
|
+
job.status = 'exited';
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
job.status = 'failed';
|
|
181
|
+
job.failureReason = job.lastError ?? `exit code ${String(code)}`;
|
|
182
|
+
}
|
|
183
|
+
job.exitCode = code ?? undefined;
|
|
184
|
+
this.processes.delete(id);
|
|
185
|
+
recordTelemetry(telemetry.asyncSpawn({
|
|
186
|
+
jobId: id,
|
|
187
|
+
cmd: input.cmd,
|
|
188
|
+
pid: job.pid,
|
|
189
|
+
}));
|
|
190
|
+
});
|
|
191
|
+
recordTelemetry(telemetry.asyncSpawn({ jobId: id, cmd: input.cmd, pid: job.pid }));
|
|
192
|
+
return { ok: true, jobId: id, pid: job.pid };
|
|
193
|
+
}
|
|
194
|
+
/** Read a snapshot. Honours `tailLines` and `sinceBytes` for the streams. */
|
|
195
|
+
poll(input) {
|
|
196
|
+
const job = this.jobs.get(input.jobId);
|
|
197
|
+
if (!job)
|
|
198
|
+
return null;
|
|
199
|
+
const tailLines = input.tailLines;
|
|
200
|
+
const sinceBytes = input.sinceBytes ?? 0;
|
|
201
|
+
const sliceTail = (text) => {
|
|
202
|
+
if (tailLines === undefined)
|
|
203
|
+
return text;
|
|
204
|
+
if (tailLines <= 0)
|
|
205
|
+
return '';
|
|
206
|
+
// Trim the trailing empty that comes from a terminal newline
|
|
207
|
+
// so "tail 2" returns the last 2 *content* lines, not
|
|
208
|
+
// "<last content>\n".
|
|
209
|
+
const lines = text.split('\n');
|
|
210
|
+
if (lines.length > 0 && lines[lines.length - 1] === '') {
|
|
211
|
+
lines.pop();
|
|
212
|
+
}
|
|
213
|
+
return lines.slice(Math.max(0, lines.length - tailLines)).join('\n');
|
|
214
|
+
};
|
|
215
|
+
return {
|
|
216
|
+
id: job.id,
|
|
217
|
+
status: job.status,
|
|
218
|
+
exitCode: job.exitCode,
|
|
219
|
+
startedAt: job.startedAt,
|
|
220
|
+
exitedAt: job.exitedAt,
|
|
221
|
+
cmd: job.cmd,
|
|
222
|
+
args: job.args,
|
|
223
|
+
cwd: job.cwd,
|
|
224
|
+
stdout: sliceTail(job.stdout),
|
|
225
|
+
stderr: sliceTail(job.stderr),
|
|
226
|
+
totalStdoutBytes: job.totalStdoutBytes,
|
|
227
|
+
totalStderrBytes: job.totalStderrBytes,
|
|
228
|
+
stdoutTruncated: job.stdoutTruncated,
|
|
229
|
+
stderrTruncated: job.stderrTruncated,
|
|
230
|
+
stdoutDelta: sinceBytes > 0 ? sliceTail(job.stdout.slice(sinceBytes)) : undefined,
|
|
231
|
+
stderrDelta: sinceBytes > 0 ? sliceTail(job.stderr.slice(sinceBytes)) : undefined,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
/** Kill a running job. Returns false if the job was already terminal. */
|
|
235
|
+
kill(jobId) {
|
|
236
|
+
const child = this.processes.get(jobId);
|
|
237
|
+
const job = this.jobs.get(jobId);
|
|
238
|
+
if (!job)
|
|
239
|
+
return { ok: false, error: 'no such job' };
|
|
240
|
+
if (!child)
|
|
241
|
+
return { ok: false, error: `job is ${job.status}; nothing to kill` };
|
|
242
|
+
try {
|
|
243
|
+
child.kill('SIGTERM');
|
|
244
|
+
return { ok: true };
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
return { ok: false, error: err.message };
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/** List all jobs (newest first). */
|
|
251
|
+
list() {
|
|
252
|
+
return Array.from(this.jobs.values()).sort((a, b) => b.startedAt.localeCompare(a.startedAt));
|
|
253
|
+
}
|
|
254
|
+
/** Read a job directly (no tail / sinceBytes shaping). */
|
|
255
|
+
get(jobId) {
|
|
256
|
+
return this.jobs.get(jobId) ?? null;
|
|
257
|
+
}
|
|
258
|
+
/** Stop timers and kill all running children. Called on CLI shutdown. */
|
|
259
|
+
shutdown() {
|
|
260
|
+
if (this.flushTimer) {
|
|
261
|
+
clearInterval(this.flushTimer);
|
|
262
|
+
this.flushTimer = null;
|
|
263
|
+
}
|
|
264
|
+
if (this.reaperTimer) {
|
|
265
|
+
clearInterval(this.reaperTimer);
|
|
266
|
+
this.reaperTimer = null;
|
|
267
|
+
}
|
|
268
|
+
for (const child of this.processes.values()) {
|
|
269
|
+
try {
|
|
270
|
+
child.kill('SIGTERM');
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
// best-effort
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
this.processes.clear();
|
|
277
|
+
}
|
|
278
|
+
/* ──────── internals ──────── */
|
|
279
|
+
startSnapshotFlusher() {
|
|
280
|
+
if (this.flushTimer)
|
|
281
|
+
return;
|
|
282
|
+
this.flushTimer = setInterval(() => {
|
|
283
|
+
this.flushAllSnapshots();
|
|
284
|
+
}, SNAPSHOT_FLUSH_MS);
|
|
285
|
+
// The flusher is a per-process helper; if the event loop exits
|
|
286
|
+
// (process exit), Node tears it down automatically. We still
|
|
287
|
+
// call `.unref()` so a stuck flusher never holds the loop open.
|
|
288
|
+
this.flushTimer.unref?.();
|
|
289
|
+
}
|
|
290
|
+
flushAllSnapshots() {
|
|
291
|
+
for (const job of this.jobs.values()) {
|
|
292
|
+
const file = path.join(this.snapshotDir, `${job.id}.json`);
|
|
293
|
+
try {
|
|
294
|
+
const tmp = `${file}.tmp`;
|
|
295
|
+
fs.writeFileSync(tmp, JSON.stringify(job, null, 2), { encoding: 'utf-8', mode: 0o600 });
|
|
296
|
+
fs.renameSync(tmp, file);
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
// best-effort
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
startReaper() {
|
|
304
|
+
if (this.reaperTimer)
|
|
305
|
+
return;
|
|
306
|
+
this.reaperTimer = setInterval(() => {
|
|
307
|
+
const now = Date.now();
|
|
308
|
+
for (const [id, child] of this.processes.entries()) {
|
|
309
|
+
const job = this.jobs.get(id);
|
|
310
|
+
if (!job)
|
|
311
|
+
continue;
|
|
312
|
+
const ageMs = now - new Date(job.startedAt).getTime();
|
|
313
|
+
const pidDead = job.pid !== undefined && !isPidAlive(job.pid);
|
|
314
|
+
if (ageMs > REAPER_MAX_AGE_MS || pidDead) {
|
|
315
|
+
try {
|
|
316
|
+
child.kill('SIGKILL');
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
// best-effort
|
|
320
|
+
}
|
|
321
|
+
job.status = pidDead ? 'failed' : 'killed';
|
|
322
|
+
job.failureReason = pidDead ? 'pid no longer alive' : 'exceeded 1-hour reaper window';
|
|
323
|
+
job.exitedAt = new Date().toISOString();
|
|
324
|
+
this.processes.delete(id);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}, REAPER_INTERVAL_MS);
|
|
328
|
+
this.reaperTimer.unref?.();
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
//# sourceMappingURL=background-jobs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"background-jobs.js","sourceRoot":"","sources":["../../src/runtime/background-jobs.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Restricted Credential Sandbox — Pillar 4 of the Phase 2 safety
|
|
3
|
+
* refactor. The problem: API keys for OpenAI, Anthropic, Google,
|
|
4
|
+
* AWS, OpenRouter, etc. are long-lived secrets with catastrophic
|
|
5
|
+
* blast radius if leaked into a tool result, an LLM prompt, or a
|
|
6
|
+
* log line. The legacy `ProvidersManager.getDirectConfig(name)`
|
|
7
|
+
* returns the raw key in an object — every caller that touches
|
|
8
|
+
* that object now has the key on its stack, in error messages,
|
|
9
|
+
* in telemetry, and in crash dumps.
|
|
10
|
+
*
|
|
11
|
+
* The fix: a {@link ProviderKeyVault} that owns the credentials
|
|
12
|
+
* in a private, read-only `Map` and exposes them **only** inside
|
|
13
|
+
* a scoped executor block. Outside the block, the vault reveals
|
|
14
|
+
* nothing but metadata (provider names, presence, count). There
|
|
15
|
+
* is no `getApiKey`, no `peek`, no `toJSON`. The only way to
|
|
16
|
+
* read a key is to hand a callback to the vault, and the key
|
|
17
|
+
* is never visible to the caller after the callback returns.
|
|
18
|
+
*
|
|
19
|
+
* The vault is in-memory and process-local; persistence is
|
|
20
|
+
* delegated to `ProvidersManager`, which calls `vault.ingest()`
|
|
21
|
+
* after a disk read. Test code can call `ingest()` directly with
|
|
22
|
+
* fixture credentials.
|
|
23
|
+
*
|
|
24
|
+
* The class is intentionally small (no `any`, no async-only
|
|
25
|
+
* fast paths) so the security boundary is auditable in one read.
|
|
26
|
+
*/
|
|
27
|
+
/** A scoped credential, exposed only inside a vault callback. */
|
|
28
|
+
export interface ProviderCredential {
|
|
29
|
+
/** The provider name as it appears in `PROVIDER_REGISTRY`. */
|
|
30
|
+
readonly providerName: string;
|
|
31
|
+
/** The raw API key. Never log, never stringify, never serialise. */
|
|
32
|
+
readonly apiKey: string;
|
|
33
|
+
/** Provider base URL. */
|
|
34
|
+
readonly baseUrl: string;
|
|
35
|
+
/** Display name (e.g. "OpenAI"). */
|
|
36
|
+
readonly displayName: string;
|
|
37
|
+
}
|
|
38
|
+
/** Callback that receives a key. */
|
|
39
|
+
export type KeyCallback<T> = (key: string) => Promise<T> | T;
|
|
40
|
+
/** Callback that receives a full credential. */
|
|
41
|
+
export type CredentialCallback<T> = (cred: ProviderCredential) => Promise<T> | T;
|
|
42
|
+
/** Options for the vault. */
|
|
43
|
+
export interface ProviderKeyVaultOptions {
|
|
44
|
+
/**
|
|
45
|
+
* When true (default), `ingest()` rejects empty / whitespace-only
|
|
46
|
+
* keys. Disable for tests that exercise the empty-key error path
|
|
47
|
+
* via direct construction.
|
|
48
|
+
*/
|
|
49
|
+
rejectEmptyKeys?: boolean;
|
|
50
|
+
}
|
|
51
|
+
/** Thrown when a requested provider is not in the vault. */
|
|
52
|
+
export declare class ProviderNotInVaultError extends Error {
|
|
53
|
+
readonly providerName: string;
|
|
54
|
+
constructor(providerName: string);
|
|
55
|
+
}
|
|
56
|
+
/** Thrown when ingest() is called with an empty / blank key. */
|
|
57
|
+
export declare class EmptyKeyRejectedError extends Error {
|
|
58
|
+
constructor(providerName: string);
|
|
59
|
+
}
|
|
60
|
+
export declare class ProviderKeyVault {
|
|
61
|
+
#private;
|
|
62
|
+
constructor(options?: ProviderKeyVaultOptions);
|
|
63
|
+
/**
|
|
64
|
+
* Add or replace a credential. Existing entries for the same
|
|
65
|
+
* provider are silently overwritten; the old `ProviderCredential`
|
|
66
|
+
* is dropped, and the previous key becomes unreachable.
|
|
67
|
+
*/
|
|
68
|
+
ingest(providerName: string, apiKey: string, baseUrl: string, displayName?: string): void;
|
|
69
|
+
/**
|
|
70
|
+
* True if the vault has a credential for the given provider.
|
|
71
|
+
* Safe to call from anywhere — does not leak the key.
|
|
72
|
+
*/
|
|
73
|
+
hasProvider(providerName: string): boolean;
|
|
74
|
+
/** Number of providers currently in the vault. */
|
|
75
|
+
size(): number;
|
|
76
|
+
/** Sorted list of configured provider names. */
|
|
77
|
+
listProviderNames(): string[];
|
|
78
|
+
/**
|
|
79
|
+
* Resolve the provider name for a given model id by walking a
|
|
80
|
+
* caller-supplied resolver. The vault itself does not know the
|
|
81
|
+
* provider registry; callers pass a pure function so the vault
|
|
82
|
+
* stays a pure credential container.
|
|
83
|
+
*
|
|
84
|
+
* Returns the resolved provider name or null if `resolveModel`
|
|
85
|
+
* returned null.
|
|
86
|
+
*/
|
|
87
|
+
providerForModel(model: string, resolveModel: (model: string) => string | null): string | null;
|
|
88
|
+
/**
|
|
89
|
+
* The ONLY way to read an API key. Pass a callback; the vault
|
|
90
|
+
* invokes it with the key as the sole argument. The key is
|
|
91
|
+
* never returned, never serialised, never assigned to a wider
|
|
92
|
+
* scope. If the provider is not configured, the callback is
|
|
93
|
+
* never called and {@link ProviderNotInVaultError} is thrown.
|
|
94
|
+
*/
|
|
95
|
+
withApiKey<T>(providerName: string, fn: KeyCallback<T>): Promise<T>;
|
|
96
|
+
/**
|
|
97
|
+
* Synchronous variant of {@link withApiKey}. Throws if the
|
|
98
|
+
* callback returns a promise (use `withApiKey` for async).
|
|
99
|
+
*/
|
|
100
|
+
withApiKeySync<T>(providerName: string, fn: KeyCallback<T>): T;
|
|
101
|
+
/**
|
|
102
|
+
* Like {@link withApiKey} but exposes the entire
|
|
103
|
+
* {@link ProviderCredential} (apiKey, baseUrl, displayName).
|
|
104
|
+
* Use this for the common "build a request to a provider" case
|
|
105
|
+
* where the URL is part of the credential surface.
|
|
106
|
+
*/
|
|
107
|
+
withCredential<T>(providerName: string, fn: CredentialCallback<T>): Promise<T>;
|
|
108
|
+
/** Remove a single provider's credential. */
|
|
109
|
+
evict(providerName: string): boolean;
|
|
110
|
+
/** Remove every credential. Used by `/fixo vault:reset`. */
|
|
111
|
+
clearAll(): void;
|
|
112
|
+
/**
|
|
113
|
+
* Internal: build a `Headers`-shaped object for the provider
|
|
114
|
+
* using a callback. The callback receives the credential and
|
|
115
|
+
* returns a `Record<string, string>`. The callback is the only
|
|
116
|
+
* place where the key is materialised, and the returned object
|
|
117
|
+
* is owned by the caller (which can pass it to `fetch`).
|
|
118
|
+
*/
|
|
119
|
+
buildAuthHeaders(providerName: string, builder: (cred: ProviderCredential) => Record<string, string>): Promise<Record<string, string>>;
|
|
120
|
+
}
|
|
121
|
+
export declare function getProviderKeyVault(): ProviderKeyVault;
|
|
122
|
+
/** Test hook — drop the cached singleton. */
|
|
123
|
+
export declare function resetProviderKeyVault(): void;
|
|
124
|
+
//# sourceMappingURL=credential-vault.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credential-vault.d.ts","sourceRoot":"","sources":["../../src/runtime/credential-vault.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAIH,iEAAiE;AACjE,MAAM,WAAW,kBAAkB;IACjC,8DAA8D;IAC9D,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,oEAAoE;IACpE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,yBAAyB;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,oCAAoC;IACpC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED,oCAAoC;AACpC,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;AAC7D,gDAAgD;AAChD,MAAM,MAAM,kBAAkB,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,kBAAkB,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;AAEjF,6BAA6B;AAC7B,MAAM,WAAW,uBAAuB;IACtC;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAID,4DAA4D;AAC5D,qBAAa,uBAAwB,SAAQ,KAAK;IAChD,SAAgB,YAAY,EAAE,MAAM,CAAC;gBACzB,YAAY,EAAE,MAAM;CAKjC;AAED,gEAAgE;AAChE,qBAAa,qBAAsB,SAAQ,KAAK;gBAClC,YAAY,EAAE,MAAM;CAIjC;AAID,qBAAa,gBAAgB;;gBAUf,OAAO,GAAE,uBAA4B;IAIjD;;;;OAIG;IACI,MAAM,CACX,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,WAAW,CAAC,EAAE,MAAM,GACnB,IAAI;IAeP;;;OAGG;IACI,WAAW,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO;IAIjD,kDAAkD;IAC3C,IAAI,IAAI,MAAM;IAIrB,gDAAgD;IACzC,iBAAiB,IAAI,MAAM,EAAE;IAIpC;;;;;;;;OAQG;IACI,gBAAgB,CACrB,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,GAC7C,MAAM,GAAG,IAAI;IAMhB;;;;;;OAMG;IACU,UAAU,CAAC,CAAC,EACvB,YAAY,EAAE,MAAM,EACpB,EAAE,EAAE,WAAW,CAAC,CAAC,CAAC,GACjB,OAAO,CAAC,CAAC,CAAC;IAMb;;;OAGG;IACI,cAAc,CAAC,CAAC,EAAE,YAAY,EAAE,MAAM,EAAE,EAAE,EAAE,WAAW,CAAC,CAAC,CAAC,GAAG,CAAC;IAYrE;;;;;OAKG;IACU,cAAc,CAAC,CAAC,EAC3B,YAAY,EAAE,MAAM,EACpB,EAAE,EAAE,kBAAkB,CAAC,CAAC,CAAC,GACxB,OAAO,CAAC,CAAC,CAAC;IAMb,6CAA6C;IACtC,KAAK,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO;IAI3C,4DAA4D;IACrD,QAAQ,IAAI,IAAI;IAIvB;;;;;;OAMG;IACU,gBAAgB,CAC3B,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,CAAC,IAAI,EAAE,kBAAkB,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC5D,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAKnC;AAYD,wBAAgB,mBAAmB,IAAI,gBAAgB,CAGtD;AAED,6CAA6C;AAC7C,wBAAgB,qBAAqB,IAAI,IAAI,CAE5C"}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Restricted Credential Sandbox — Pillar 4 of the Phase 2 safety
|
|
3
|
+
* refactor. The problem: API keys for OpenAI, Anthropic, Google,
|
|
4
|
+
* AWS, OpenRouter, etc. are long-lived secrets with catastrophic
|
|
5
|
+
* blast radius if leaked into a tool result, an LLM prompt, or a
|
|
6
|
+
* log line. The legacy `ProvidersManager.getDirectConfig(name)`
|
|
7
|
+
* returns the raw key in an object — every caller that touches
|
|
8
|
+
* that object now has the key on its stack, in error messages,
|
|
9
|
+
* in telemetry, and in crash dumps.
|
|
10
|
+
*
|
|
11
|
+
* The fix: a {@link ProviderKeyVault} that owns the credentials
|
|
12
|
+
* in a private, read-only `Map` and exposes them **only** inside
|
|
13
|
+
* a scoped executor block. Outside the block, the vault reveals
|
|
14
|
+
* nothing but metadata (provider names, presence, count). There
|
|
15
|
+
* is no `getApiKey`, no `peek`, no `toJSON`. The only way to
|
|
16
|
+
* read a key is to hand a callback to the vault, and the key
|
|
17
|
+
* is never visible to the caller after the callback returns.
|
|
18
|
+
*
|
|
19
|
+
* The vault is in-memory and process-local; persistence is
|
|
20
|
+
* delegated to `ProvidersManager`, which calls `vault.ingest()`
|
|
21
|
+
* after a disk read. Test code can call `ingest()` directly with
|
|
22
|
+
* fixture credentials.
|
|
23
|
+
*
|
|
24
|
+
* The class is intentionally small (no `any`, no async-only
|
|
25
|
+
* fast paths) so the security boundary is auditable in one read.
|
|
26
|
+
*/
|
|
27
|
+
/* ──────────────────────── Errors ──────────────────────── */
|
|
28
|
+
/** Thrown when a requested provider is not in the vault. */
|
|
29
|
+
export class ProviderNotInVaultError extends Error {
|
|
30
|
+
providerName;
|
|
31
|
+
constructor(providerName) {
|
|
32
|
+
super(`ProviderKeyVault: no credential for "${providerName}"`);
|
|
33
|
+
this.name = 'ProviderNotInVaultError';
|
|
34
|
+
this.providerName = providerName;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Thrown when ingest() is called with an empty / blank key. */
|
|
38
|
+
export class EmptyKeyRejectedError extends Error {
|
|
39
|
+
constructor(providerName) {
|
|
40
|
+
super(`ProviderKeyVault: refused to ingest empty key for "${providerName}"`);
|
|
41
|
+
this.name = 'EmptyKeyRejectedError';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/* ──────────────────────── ProviderKeyVault ──────────────────────── */
|
|
45
|
+
export class ProviderKeyVault {
|
|
46
|
+
/**
|
|
47
|
+
* Backing store. Marked `readonly` so a misbehaving consumer
|
|
48
|
+
* cannot reassign the map, but the map itself is private so
|
|
49
|
+
* the `ProviderCredential` objects are unreachable from
|
|
50
|
+
* outside the class.
|
|
51
|
+
*/
|
|
52
|
+
#store = new Map();
|
|
53
|
+
#rejectEmpty;
|
|
54
|
+
constructor(options = {}) {
|
|
55
|
+
this.#rejectEmpty = options.rejectEmptyKeys ?? true;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Add or replace a credential. Existing entries for the same
|
|
59
|
+
* provider are silently overwritten; the old `ProviderCredential`
|
|
60
|
+
* is dropped, and the previous key becomes unreachable.
|
|
61
|
+
*/
|
|
62
|
+
ingest(providerName, apiKey, baseUrl, displayName) {
|
|
63
|
+
if (!providerName) {
|
|
64
|
+
throw new Error('ProviderKeyVault.ingest: providerName is required');
|
|
65
|
+
}
|
|
66
|
+
if (this.#rejectEmpty && (!apiKey || !apiKey.trim())) {
|
|
67
|
+
throw new EmptyKeyRejectedError(providerName);
|
|
68
|
+
}
|
|
69
|
+
this.#store.set(providerName, {
|
|
70
|
+
providerName,
|
|
71
|
+
apiKey,
|
|
72
|
+
baseUrl,
|
|
73
|
+
displayName: displayName ?? providerName,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* True if the vault has a credential for the given provider.
|
|
78
|
+
* Safe to call from anywhere — does not leak the key.
|
|
79
|
+
*/
|
|
80
|
+
hasProvider(providerName) {
|
|
81
|
+
return this.#store.has(providerName);
|
|
82
|
+
}
|
|
83
|
+
/** Number of providers currently in the vault. */
|
|
84
|
+
size() {
|
|
85
|
+
return this.#store.size;
|
|
86
|
+
}
|
|
87
|
+
/** Sorted list of configured provider names. */
|
|
88
|
+
listProviderNames() {
|
|
89
|
+
return Array.from(this.#store.keys()).sort();
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Resolve the provider name for a given model id by walking a
|
|
93
|
+
* caller-supplied resolver. The vault itself does not know the
|
|
94
|
+
* provider registry; callers pass a pure function so the vault
|
|
95
|
+
* stays a pure credential container.
|
|
96
|
+
*
|
|
97
|
+
* Returns the resolved provider name or null if `resolveModel`
|
|
98
|
+
* returned null.
|
|
99
|
+
*/
|
|
100
|
+
providerForModel(model, resolveModel) {
|
|
101
|
+
const name = resolveModel(model);
|
|
102
|
+
if (!name)
|
|
103
|
+
return null;
|
|
104
|
+
return this.#store.has(name) ? name : null;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The ONLY way to read an API key. Pass a callback; the vault
|
|
108
|
+
* invokes it with the key as the sole argument. The key is
|
|
109
|
+
* never returned, never serialised, never assigned to a wider
|
|
110
|
+
* scope. If the provider is not configured, the callback is
|
|
111
|
+
* never called and {@link ProviderNotInVaultError} is thrown.
|
|
112
|
+
*/
|
|
113
|
+
async withApiKey(providerName, fn) {
|
|
114
|
+
const cred = this.#store.get(providerName);
|
|
115
|
+
if (!cred)
|
|
116
|
+
throw new ProviderNotInVaultError(providerName);
|
|
117
|
+
return await fn(cred.apiKey);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Synchronous variant of {@link withApiKey}. Throws if the
|
|
121
|
+
* callback returns a promise (use `withApiKey` for async).
|
|
122
|
+
*/
|
|
123
|
+
withApiKeySync(providerName, fn) {
|
|
124
|
+
const cred = this.#store.get(providerName);
|
|
125
|
+
if (!cred)
|
|
126
|
+
throw new ProviderNotInVaultError(providerName);
|
|
127
|
+
const result = fn(cred.apiKey);
|
|
128
|
+
if (result instanceof Promise) {
|
|
129
|
+
throw new Error('ProviderKeyVault.withApiKeySync: callback returned a Promise; use withApiKey() instead');
|
|
130
|
+
}
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Like {@link withApiKey} but exposes the entire
|
|
135
|
+
* {@link ProviderCredential} (apiKey, baseUrl, displayName).
|
|
136
|
+
* Use this for the common "build a request to a provider" case
|
|
137
|
+
* where the URL is part of the credential surface.
|
|
138
|
+
*/
|
|
139
|
+
async withCredential(providerName, fn) {
|
|
140
|
+
const cred = this.#store.get(providerName);
|
|
141
|
+
if (!cred)
|
|
142
|
+
throw new ProviderNotInVaultError(providerName);
|
|
143
|
+
return await fn(cred);
|
|
144
|
+
}
|
|
145
|
+
/** Remove a single provider's credential. */
|
|
146
|
+
evict(providerName) {
|
|
147
|
+
return this.#store.delete(providerName);
|
|
148
|
+
}
|
|
149
|
+
/** Remove every credential. Used by `/fixo vault:reset`. */
|
|
150
|
+
clearAll() {
|
|
151
|
+
this.#store.clear();
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Internal: build a `Headers`-shaped object for the provider
|
|
155
|
+
* using a callback. The callback receives the credential and
|
|
156
|
+
* returns a `Record<string, string>`. The callback is the only
|
|
157
|
+
* place where the key is materialised, and the returned object
|
|
158
|
+
* is owned by the caller (which can pass it to `fetch`).
|
|
159
|
+
*/
|
|
160
|
+
async buildAuthHeaders(providerName, builder) {
|
|
161
|
+
const cred = this.#store.get(providerName);
|
|
162
|
+
if (!cred)
|
|
163
|
+
throw new ProviderNotInVaultError(providerName);
|
|
164
|
+
return builder(cred);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/* ──────────────────────── Process-wide singleton ─────────────────── */
|
|
168
|
+
/**
|
|
169
|
+
* Process-wide vault singleton. Lazily created on first call to
|
|
170
|
+
* {@link getProviderKeyVault}. Tests that need an isolated vault
|
|
171
|
+
* should construct their own {@link ProviderKeyVault} directly
|
|
172
|
+
* and use it without going through this singleton.
|
|
173
|
+
*/
|
|
174
|
+
let cachedVault = null;
|
|
175
|
+
export function getProviderKeyVault() {
|
|
176
|
+
if (!cachedVault)
|
|
177
|
+
cachedVault = new ProviderKeyVault();
|
|
178
|
+
return cachedVault;
|
|
179
|
+
}
|
|
180
|
+
/** Test hook — drop the cached singleton. */
|
|
181
|
+
export function resetProviderKeyVault() {
|
|
182
|
+
cachedVault = null;
|
|
183
|
+
}
|
|
184
|
+
//# sourceMappingURL=credential-vault.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credential-vault.js","sourceRoot":"","sources":["../../src/runtime/credential-vault.ts"],"names":[],"mappings":"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"}
|