@kontextmind/kxm 0.6.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/.claude-plugin/marketplace.json +19 -0
- package/.kxm/README.md +14 -0
- package/.kxm/assets/README.md +5 -0
- package/.kxm/assets/retrospectives/README.md +5 -0
- package/.kxm/config/README.md +5 -0
- package/.kxm/config/agents.json +43 -0
- package/.kxm/config/env.example +56 -0
- package/.kxm/config/update.example.yaml +9 -0
- package/.kxm/config/workflows/fix.json +160 -0
- package/.kxm/config/workflows/jira-development.json +116 -0
- package/.kxm/config/workflows/provenance-quorum.json +150 -0
- package/.kxm/config/workflows/v04-dogfood.json +72 -0
- package/CHANGELOG.md +465 -0
- package/LICENSE +21 -0
- package/README.md +306 -0
- package/SECURITY.md +72 -0
- package/docs/README.md +48 -0
- package/docs/agent-communication-envelopes-and-gates.md +553 -0
- package/docs/architecture.md +242 -0
- package/docs/assignment-runner.md +241 -0
- package/docs/configuration.md +361 -0
- package/docs/continuous-improvement.md +114 -0
- package/docs/getting-started.md +253 -0
- package/docs/kxm-handbook.md +1090 -0
- package/docs/operations.md +205 -0
- package/docs/provenance-gates.md +291 -0
- package/docs/skills.md +45 -0
- package/docs/templates/README.md +95 -0
- package/docs/templates/adr.md +88 -0
- package/docs/templates/architecture.md +120 -0
- package/docs/templates/bug-fix.md +109 -0
- package/docs/templates/feature.md +108 -0
- package/docs/templates/handoff.md +72 -0
- package/docs/templates/postmortem.md +77 -0
- package/docs/templates/research.md +100 -0
- package/docs/templates/review.md +85 -0
- package/docs/templates/runbook.md +73 -0
- package/docs/templates/test-plan.md +87 -0
- package/docs/templates/test-report.md +72 -0
- package/docs/test-matrix.md +121 -0
- package/docs/troubleshooting.md +249 -0
- package/docs/vnext/README.md +62 -0
- package/docs/vnext/architecture.md +185 -0
- package/docs/vnext/effects-and-recovery.md +172 -0
- package/docs/vnext/lifecycles.md +235 -0
- package/docs/vnext/migration.md +220 -0
- package/docs/vnext/routing.md +184 -0
- package/docs/vnext/synchronization.md +172 -0
- package/docs/vnext/terminology.md +240 -0
- package/docs/vnext/validation.md +335 -0
- package/docs/webhook-workflows.md +240 -0
- package/docs/workflow-guide.md +1150 -0
- package/examples/README.md +102 -0
- package/examples/provenance-workflow.json +40 -0
- package/examples/requester.ts +30 -0
- package/examples/reviewer-agent.ts +29 -0
- package/examples/roundtrip.ts +46 -0
- package/examples/vnext/.kxm/agents/coordinator.yaml +16 -0
- package/examples/vnext/.kxm/agents/critic-1.yaml +16 -0
- package/examples/vnext/.kxm/agents/critic-2.yaml +15 -0
- package/examples/vnext/.kxm/agents/critic-3.yaml +15 -0
- package/examples/vnext/.kxm/agents/implementer.yaml +15 -0
- package/examples/vnext/.kxm/agents/planner.yaml +13 -0
- package/examples/vnext/.kxm/agents/reproducer.yaml +15 -0
- package/examples/vnext/.kxm/agents/reviewer.yaml +15 -0
- package/examples/vnext/.kxm/gates.yaml +8 -0
- package/examples/vnext/.kxm/models/critic-claude.yaml +11 -0
- package/examples/vnext/.kxm/models/critic-gemini.yaml +11 -0
- package/examples/vnext/.kxm/models/critic-grok.yaml +12 -0
- package/examples/vnext/.kxm/models/implementation.yaml +14 -0
- package/examples/vnext/.kxm/models/primary.yaml +17 -0
- package/examples/vnext/.kxm/prices.yaml +111 -0
- package/examples/vnext/.kxm/project/env.yaml +7 -0
- package/examples/vnext/.kxm/project.yaml +32 -0
- package/examples/vnext/.kxm/repo/repo.yaml +8 -0
- package/examples/vnext/.kxm/workflows/default.yaml +92 -0
- package/examples/vnext/.kxm/workflows/fix.yaml +376 -0
- package/examples/vnext/.kxm/workflows/improve.yaml +57 -0
- package/examples/vnext/README.md +53 -0
- package/examples/vnext/records/assignment-result-recorded.json +63 -0
- package/examples/vnext/records/assignment-result.json +46 -0
- package/examples/vnext/records/context-candidate.json +42 -0
- package/examples/vnext/records/delivery-manifest.json +66 -0
- package/examples/vnext/records/effect-uncertainty-resolved-sync.json +67 -0
- package/examples/vnext/records/effect-uncertainty-resolved.json +62 -0
- package/examples/vnext/records/run-created.json +54 -0
- package/examples/vnext/records/sync-event.json +65 -0
- package/examples/vnext/repositories/api/.kxm/repo/env.yaml +7 -0
- package/examples/vnext/repositories/api/.kxm/repo/repo.yaml +8 -0
- package/examples/vnext/repositories/web/.kxm/repo/repo.yaml +8 -0
- package/examples/workflow-signal.ts +63 -0
- package/package.json +129 -0
- package/plugins/kxm/.claude-plugin/plugin.json +73 -0
- package/plugins/kxm/.mcp.json +19 -0
- package/plugins/kxm/README.md +93 -0
- package/plugins/kxm/dist/cli.js +42853 -0
- package/plugins/kxm/dist/client.js +416 -0
- package/plugins/kxm/dist/core.js +1823 -0
- package/plugins/kxm/dist/extension.js +3797 -0
- package/plugins/kxm/dist/mcp-server.js +17104 -0
- package/plugins/kxm/dist/runtime.js +23361 -0
- package/plugins/kxm/dist/server.js +13640 -0
- package/plugins/kxm/dist/vnext-runtime-supervisor.js +21109 -0
- package/plugins/kxm/package.json +12 -0
- package/plugins/kxm/skills/kxm/SKILL.md +97 -0
- package/plugins/kxm/skills/kxm/references/protocol.md +103 -0
- package/plugins/kxm/skills/kxm-session/SKILL.md +53 -0
- package/plugins/kxm/src/arbiter.ts +355 -0
- package/plugins/kxm/src/artifacts-exist.ts +62 -0
- package/plugins/kxm/src/autocomplete.ts +236 -0
- package/plugins/kxm/src/cli.ts +3707 -0
- package/plugins/kxm/src/client.ts +614 -0
- package/plugins/kxm/src/commands.ts +1063 -0
- package/plugins/kxm/src/config.ts +290 -0
- package/plugins/kxm/src/context/providers.ts +101 -0
- package/plugins/kxm/src/context-packet.ts +332 -0
- package/plugins/kxm/src/context.ts +499 -0
- package/plugins/kxm/src/core.ts +6 -0
- package/plugins/kxm/src/database.ts +563 -0
- package/plugins/kxm/src/diagnostics.ts +184 -0
- package/plugins/kxm/src/envelope.ts +118 -0
- package/plugins/kxm/src/extension.ts +895 -0
- package/plugins/kxm/src/external-effects.ts +299 -0
- package/plugins/kxm/src/github-watch.ts +255 -0
- package/plugins/kxm/src/hub-binding.ts +160 -0
- package/plugins/kxm/src/hub.ts +2502 -0
- package/plugins/kxm/src/improve.ts +383 -0
- package/plugins/kxm/src/inbox.ts +10 -0
- package/plugins/kxm/src/kxm-install-kind.ts +113 -0
- package/plugins/kxm/src/kxm-update-config.ts +39 -0
- package/plugins/kxm/src/kxm-update.ts +238 -0
- package/plugins/kxm/src/local-snapshot.ts +406 -0
- package/plugins/kxm/src/logger.ts +198 -0
- package/plugins/kxm/src/mcp-server.ts +143 -0
- package/plugins/kxm/src/memory.ts +385 -0
- package/plugins/kxm/src/nous-pi.ts +287 -0
- package/plugins/kxm/src/nous-provider.ts +729 -0
- package/plugins/kxm/src/price-calc.ts +87 -0
- package/plugins/kxm/src/prices.ts +121 -0
- package/plugins/kxm/src/protocol.ts +172 -0
- package/plugins/kxm/src/recovery.ts +211 -0
- package/plugins/kxm/src/redact.ts +26 -0
- package/plugins/kxm/src/retrospective.ts +400 -0
- package/plugins/kxm/src/routing.ts +830 -0
- package/plugins/kxm/src/runtime.ts +9 -0
- package/plugins/kxm/src/server.ts +117 -0
- package/plugins/kxm/src/session-work.ts +571 -0
- package/plugins/kxm/src/session.ts +184 -0
- package/plugins/kxm/src/skills.ts +535 -0
- package/plugins/kxm/src/state.ts +326 -0
- package/plugins/kxm/src/store.ts +637 -0
- package/plugins/kxm/src/studio-layout.ts +268 -0
- package/plugins/kxm/src/suggest.ts +162 -0
- package/plugins/kxm/src/task-manager.ts +244 -0
- package/plugins/kxm/src/telemetry.ts +116 -0
- package/plugins/kxm/src/tui.ts +1046 -0
- package/plugins/kxm/src/vnext-bindings.ts +403 -0
- package/plugins/kxm/src/vnext-config.ts +1646 -0
- package/plugins/kxm/src/vnext-engine-artifacts.ts +86 -0
- package/plugins/kxm/src/vnext-engine-command.ts +533 -0
- package/plugins/kxm/src/vnext-engine-compile.ts +722 -0
- package/plugins/kxm/src/vnext-engine-evidence.ts +273 -0
- package/plugins/kxm/src/vnext-engine-fold.ts +1400 -0
- package/plugins/kxm/src/vnext-engine-gate-records.ts +583 -0
- package/plugins/kxm/src/vnext-engine-plan.ts +717 -0
- package/plugins/kxm/src/vnext-engine.ts +2458 -0
- package/plugins/kxm/src/vnext-gate-hash.ts +10 -0
- package/plugins/kxm/src/vnext-harness.ts +1142 -0
- package/plugins/kxm/src/vnext-init.ts +430 -0
- package/plugins/kxm/src/vnext-migrate.ts +1848 -0
- package/plugins/kxm/src/vnext-oneshot-producer.ts +424 -0
- package/plugins/kxm/src/vnext-permission.ts +936 -0
- package/plugins/kxm/src/vnext-pi-producer.ts +628 -0
- package/plugins/kxm/src/vnext-repair.ts +1094 -0
- package/plugins/kxm/src/vnext-runtime-owner.ts +320 -0
- package/plugins/kxm/src/vnext-runtime-store.ts +1560 -0
- package/plugins/kxm/src/vnext-runtime-supervisor.ts +586 -0
- package/plugins/kxm/src/vnext-runtime.ts +663 -0
- package/plugins/kxm/src/vnext-template.ts +247 -0
- package/plugins/kxm/src/wiki.ts +313 -0
- package/plugins/kxm/src/workflow.ts +1548 -0
- package/schemas/vnext/README.md +46 -0
- package/schemas/vnext/agent.schema.json +40 -0
- package/schemas/vnext/assignment-result.schema.json +66 -0
- package/schemas/vnext/backup-manifest.schema.json +89 -0
- package/schemas/vnext/candidate.schema.json +109 -0
- package/schemas/vnext/common.schema.json +422 -0
- package/schemas/vnext/context-candidate.schema.json +76 -0
- package/schemas/vnext/context-packet.schema.json +192 -0
- package/schemas/vnext/delivery-manifest.schema.json +159 -0
- package/schemas/vnext/environment.schema.json +66 -0
- package/schemas/vnext/gate-registry.schema.json +109 -0
- package/schemas/vnext/handoff-manifest.schema.json +146 -0
- package/schemas/vnext/init-operation.schema.json +61 -0
- package/schemas/vnext/local-repository-bindings.schema.json +30 -0
- package/schemas/vnext/memory-record.schema.json +45 -0
- package/schemas/vnext/migration-decision.schema.json +26 -0
- package/schemas/vnext/migration-plan.schema.json +123 -0
- package/schemas/vnext/migration-receipt.schema.json +52 -0
- package/schemas/vnext/model.schema.json +42 -0
- package/schemas/vnext/permission-diff.schema.json +57 -0
- package/schemas/vnext/prices.schema.json +115 -0
- package/schemas/vnext/project.schema.json +85 -0
- package/schemas/vnext/repository.schema.json +24 -0
- package/schemas/vnext/run-event.schema.json +460 -0
- package/schemas/vnext/session-brief.schema.json +153 -0
- package/schemas/vnext/sync-event.schema.json +234 -0
- package/schemas/vnext/template-provenance.schema.json +38 -0
- package/schemas/vnext/workflow.schema.json +248 -0
- package/scripts/assignment-run.d.mts +354 -0
- package/scripts/assignment-run.mjs +4451 -0
- package/scripts/build-runtime.mjs +56 -0
- package/scripts/check-generated.mjs +77 -0
- package/scripts/check-versions.mjs +34 -0
- package/scripts/emit-codex-artifacts.d.mts +9 -0
- package/scripts/emit-codex-artifacts.mjs +91 -0
- package/scripts/harness-run.d.mts +83 -0
- package/scripts/harness-run.mjs +2095 -0
- package/scripts/kxm-hub.mjs +105 -0
- package/scripts/kxm-publish-npm.mjs +327 -0
- package/scripts/kxm-release-github.mjs +472 -0
- package/scripts/kxm-runtime-supervisor.mjs +7 -0
- package/scripts/kxm-worker.mjs +1127 -0
- package/scripts/kxm.mjs +27 -0
- package/scripts/roster-policy.d.mts +20 -0
- package/scripts/roster-policy.mjs +161 -0
- package/scripts/smoke-multi-pi.mjs +479 -0
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
3
|
+
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
|
5
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { loadVnextProject, VnextConfigError, type VnextConfigOptions } from "./vnext-config.ts";
|
|
8
|
+
|
|
9
|
+
const repoRoot = resolve(fileURLToPath(new URL("../../../", import.meta.url)));
|
|
10
|
+
import {
|
|
11
|
+
VnextRuntimeRegistry,
|
|
12
|
+
projectRuntimeKey,
|
|
13
|
+
runtimeError,
|
|
14
|
+
vnextRuntimePaths,
|
|
15
|
+
type VnextRuntimePaths,
|
|
16
|
+
} from "./vnext-runtime-store.ts";
|
|
17
|
+
import {
|
|
18
|
+
acceptVnextRun,
|
|
19
|
+
cancelVnextRun,
|
|
20
|
+
closeVnextRuntimeContext,
|
|
21
|
+
foldStoredVnextRun,
|
|
22
|
+
openVnextRuntimeContext,
|
|
23
|
+
rebuildVnextRunProjection,
|
|
24
|
+
type VnextRuntimeContext,
|
|
25
|
+
} from "./vnext-runtime.ts";
|
|
26
|
+
import { recoverVnextRun } from "./vnext-engine.ts";
|
|
27
|
+
|
|
28
|
+
/* ------------------------------------------------------------------ *
|
|
29
|
+
* Token management
|
|
30
|
+
* ------------------------------------------------------------------ */
|
|
31
|
+
|
|
32
|
+
export function vnextSupervisorTokenFile(paths: VnextRuntimePaths): string {
|
|
33
|
+
return join(paths.runtimeDir, "supervisor.token");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Atomically publish the supervisor token (temp file + rename). */
|
|
37
|
+
function publishVnextSupervisorToken(paths: VnextRuntimePaths, token: string): void {
|
|
38
|
+
const file = vnextSupervisorTokenFile(paths);
|
|
39
|
+
mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
|
|
40
|
+
const temp = `${file}.${process.pid}.tmp`;
|
|
41
|
+
writeFileSync(temp, `${token}\n`, { encoding: "utf8", mode: 0o600 });
|
|
42
|
+
try { chmodSync(temp, 0o600); } catch { /* Windows may ignore modes */ }
|
|
43
|
+
renameSync(temp, file);
|
|
44
|
+
try { chmodSync(file, 0o600); } catch { /* Windows may ignore modes */ }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function hashVnextSupervisorToken(token: string): string {
|
|
48
|
+
return `sha256:${createHash("sha256").update(`kxm-runtime-supervisor\0${token}`, "utf8").digest("hex")}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function readVnextSupervisorToken(paths: VnextRuntimePaths): string | undefined {
|
|
52
|
+
const file = vnextSupervisorTokenFile(paths);
|
|
53
|
+
const stat = lstatSync(file, { throwIfNoEntry: false });
|
|
54
|
+
if (!stat) return undefined;
|
|
55
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
56
|
+
throw runtimeError("runtime_path_invalid", file, "supervisor token file must be a regular file, not a link");
|
|
57
|
+
}
|
|
58
|
+
const token = readFileSync(file, "utf8").trim();
|
|
59
|
+
return token.length >= 32 ? token : undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* ------------------------------------------------------------------ *
|
|
63
|
+
* Supervisor process state + liveness
|
|
64
|
+
* ------------------------------------------------------------------ */
|
|
65
|
+
|
|
66
|
+
export interface VnextSupervisorStatus {
|
|
67
|
+
running: boolean;
|
|
68
|
+
runtimeId?: string;
|
|
69
|
+
pid?: number;
|
|
70
|
+
port?: number;
|
|
71
|
+
state?: string;
|
|
72
|
+
heartbeatAt?: string;
|
|
73
|
+
startedAt?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function processAlive(pid: number): boolean {
|
|
77
|
+
try {
|
|
78
|
+
process.kill(pid, 0);
|
|
79
|
+
return true;
|
|
80
|
+
} catch (error) {
|
|
81
|
+
return (error as NodeJS.ErrnoException).code === "EPERM";
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const HEARTBEAT_STALE_MS = 15_000;
|
|
86
|
+
const SUPERVISOR_ERROR_MAX_AGE_MS = 30_000;
|
|
87
|
+
|
|
88
|
+
function supervisorErrorFile(paths: VnextRuntimePaths): string {
|
|
89
|
+
return join(paths.runtimeDir, "supervisor.error");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function clearSupervisorError(paths: VnextRuntimePaths): void {
|
|
93
|
+
const file = supervisorErrorFile(paths);
|
|
94
|
+
if (existsSync(file)) rmSync(file, { force: true });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function recordSupervisorError(paths: VnextRuntimePaths, message: string): void {
|
|
98
|
+
try {
|
|
99
|
+
mkdirSync(paths.runtimeDir, { recursive: true, mode: 0o700 });
|
|
100
|
+
writeFileSync(supervisorErrorFile(paths), `${message}\n`, { encoding: "utf8", mode: 0o600 });
|
|
101
|
+
} catch { /* best effort */ }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function readRecentSupervisorError(paths: VnextRuntimePaths): string | undefined {
|
|
105
|
+
const file = supervisorErrorFile(paths);
|
|
106
|
+
const stat = lstatSync(file, { throwIfNoEntry: false });
|
|
107
|
+
if (!stat) return undefined;
|
|
108
|
+
const ageMs = Date.now() - stat.mtimeMs;
|
|
109
|
+
if (ageMs > SUPERVISOR_ERROR_MAX_AGE_MS) return undefined;
|
|
110
|
+
try {
|
|
111
|
+
return readFileSync(file, "utf8").trim();
|
|
112
|
+
} catch {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function vnextSupervisorStatus(paths: VnextRuntimePaths): VnextSupervisorStatus {
|
|
118
|
+
if (!existsSync(paths.registryDb)) return { running: false };
|
|
119
|
+
const registry = new VnextRuntimeRegistry(paths.registryDb);
|
|
120
|
+
try {
|
|
121
|
+
const record = registry.supervisor();
|
|
122
|
+
if (!record) return { running: false };
|
|
123
|
+
// Pid-only liveness is not enough: after a crash the pid may be reused by
|
|
124
|
+
// an unrelated process. A stale heartbeat is authoritative.
|
|
125
|
+
const heartbeatAgeMs = Date.now() - Date.parse(record.heartbeatAt);
|
|
126
|
+
const fresh = Number.isFinite(heartbeatAgeMs) && heartbeatAgeMs < HEARTBEAT_STALE_MS;
|
|
127
|
+
const alive = record.state === "running" && fresh && processAlive(record.pid);
|
|
128
|
+
return {
|
|
129
|
+
running: alive,
|
|
130
|
+
runtimeId: record.runtimeId,
|
|
131
|
+
pid: record.pid,
|
|
132
|
+
port: record.port,
|
|
133
|
+
state: alive ? record.state : "dead",
|
|
134
|
+
heartbeatAt: record.heartbeatAt,
|
|
135
|
+
startedAt: record.startedAt,
|
|
136
|
+
};
|
|
137
|
+
} finally {
|
|
138
|
+
registry.close();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function probeSupervisorWithRetry(port: number, expectedRuntimeId: string, token: string): Promise<boolean> {
|
|
143
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
144
|
+
if (await probeSupervisor(port, expectedRuntimeId, token)) return true;
|
|
145
|
+
if (attempt < 2) await new Promise((resolveWait) => setTimeout(resolveWait, 250));
|
|
146
|
+
}
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function probeSupervisor(port: number, expectedRuntimeId: string, token: string, timeoutMs = 750): Promise<boolean> {
|
|
151
|
+
const controller = new AbortController();
|
|
152
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
153
|
+
try {
|
|
154
|
+
const nonce = randomBytes(16).toString("hex");
|
|
155
|
+
const response = await fetch(`http://127.0.0.1:${port}/healthz?nonce=${nonce}`, { signal: controller.signal });
|
|
156
|
+
if (!response.ok) return false;
|
|
157
|
+
const payload = await response.json() as { runtimeId?: string; tokenProof?: string };
|
|
158
|
+
// The server must prove it knows the current token: an impostor that
|
|
159
|
+
// merely binds the stale port cannot answer the keyed challenge.
|
|
160
|
+
const expectedProof = hashVnextTokenProof(token, nonce);
|
|
161
|
+
return payload.runtimeId === expectedRuntimeId && payload.tokenProof === expectedProof;
|
|
162
|
+
} catch {
|
|
163
|
+
return false;
|
|
164
|
+
} finally {
|
|
165
|
+
clearTimeout(timeout);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function hashVnextTokenProof(token: string, nonce: string): string {
|
|
170
|
+
return createHmac("sha256", token).update(`kxm-runtime-token-proof\0${nonce}`, "utf8").digest("hex");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export interface VnextSupervisorHandle {
|
|
174
|
+
runtimeId: string;
|
|
175
|
+
port: number;
|
|
176
|
+
token: string;
|
|
177
|
+
started: boolean;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Ensure a supervisor is running: reuse a live one, otherwise auto-start. */
|
|
181
|
+
export async function ensureVnextSupervisor(
|
|
182
|
+
options: { stateRoot?: string; env?: NodeJS.ProcessEnv; spawnImpl?: (scriptPath: string, env: NodeJS.ProcessEnv) => number } = {},
|
|
183
|
+
): Promise<VnextSupervisorHandle> {
|
|
184
|
+
const paths = vnextRuntimePaths(options.stateRoot !== undefined ? { stateRoot: options.stateRoot } : { ...(options.env ? { env: options.env } : {}) });
|
|
185
|
+
const status = vnextSupervisorStatus(paths);
|
|
186
|
+
if (status.running && status.port) {
|
|
187
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
188
|
+
const token = readVnextSupervisorToken(paths);
|
|
189
|
+
if (token && (await probeSupervisorWithRetry(status.port, status.runtimeId as string, token))) {
|
|
190
|
+
return { runtimeId: status.runtimeId as string, port: status.port, token, started: false };
|
|
191
|
+
}
|
|
192
|
+
// The token may be mid-publish in the claim/rename window; retry shortly.
|
|
193
|
+
if (attempt < 2) await new Promise((resolveWait) => setTimeout(resolveWait, 150));
|
|
194
|
+
}
|
|
195
|
+
// A live, fresh supervisor that cannot be probed is not spawnable:
|
|
196
|
+
// spawning a competitor guarantees a conflict and locks out the state root.
|
|
197
|
+
throw runtimeError("runtime_supervisor_unreachable", paths.registryDb, `runtime supervisor pid ${status.pid} is registered as running but cannot be probed`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Auto-start: claim the singleton, spawn detached, wait for readiness.
|
|
201
|
+
clearSupervisorError(paths);
|
|
202
|
+
const scriptPath = join(repoRoot, "scripts", "kxm-runtime-supervisor.mjs");
|
|
203
|
+
const spawnImpl = options.spawnImpl ?? ((script: string, env: NodeJS.ProcessEnv): number => {
|
|
204
|
+
const child = spawn(process.execPath, [script], {
|
|
205
|
+
detached: true,
|
|
206
|
+
stdio: "ignore",
|
|
207
|
+
env,
|
|
208
|
+
windowsHide: true,
|
|
209
|
+
});
|
|
210
|
+
child.unref();
|
|
211
|
+
if (child.pid === undefined) throw runtimeError("runtime_supervisor_spawn_failed", script, "could not spawn the runtime supervisor");
|
|
212
|
+
return child.pid;
|
|
213
|
+
});
|
|
214
|
+
const pid = spawnImpl(scriptPath, {
|
|
215
|
+
...process.env,
|
|
216
|
+
KXM_STATE_HOME: paths.stateRoot,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const deadline = Date.now() + 10_000;
|
|
220
|
+
while (Date.now() < deadline) {
|
|
221
|
+
await new Promise((resolveWait) => setTimeout(resolveWait, 100));
|
|
222
|
+
const next = vnextSupervisorStatus(paths);
|
|
223
|
+
if (next.running && next.port) {
|
|
224
|
+
const token = readVnextSupervisorToken(paths);
|
|
225
|
+
if (token && (await probeSupervisor(next.port, next.runtimeId as string, token))) {
|
|
226
|
+
// Entry status had no live supervisor: this call caused a start
|
|
227
|
+
// (ours or a concurrent winner's), which the probe just confirmed.
|
|
228
|
+
return { runtimeId: next.runtimeId as string, port: next.port, token, started: true };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const errorDetail = readRecentSupervisorError(paths);
|
|
232
|
+
if (errorDetail) {
|
|
233
|
+
throw runtimeError("runtime_supervisor_start_failed", scriptPath, `runtime supervisor failed at startup: ${errorDetail}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
throw runtimeError("runtime_supervisor_start_failed", scriptPath, `runtime supervisor (pid ${pid}) did not become ready in time`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/* ------------------------------------------------------------------ *
|
|
240
|
+
* Supervisor server (runs inside the supervisor process)
|
|
241
|
+
* ------------------------------------------------------------------ */
|
|
242
|
+
|
|
243
|
+
interface JsonBody {
|
|
244
|
+
[key: string]: unknown;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function readJsonBody(request: IncomingMessage, limit = 64 * 1024): Promise<JsonBody> {
|
|
248
|
+
const chunks: Buffer[] = [];
|
|
249
|
+
let size = 0;
|
|
250
|
+
let tooLarge = false;
|
|
251
|
+
for await (const chunk of request) {
|
|
252
|
+
const buffer = chunk as Buffer;
|
|
253
|
+
size += buffer.byteLength;
|
|
254
|
+
if (size > limit) {
|
|
255
|
+
// Drain the remaining body so the client sees the 400 response instead
|
|
256
|
+
// of a reset connection.
|
|
257
|
+
tooLarge = true;
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (!tooLarge) chunks.push(buffer);
|
|
261
|
+
}
|
|
262
|
+
if (tooLarge) throw runtimeError("runtime_request_too_large", request.url ?? "<request>", "request body exceeds the limit");
|
|
263
|
+
if (chunks.length === 0) return {};
|
|
264
|
+
try {
|
|
265
|
+
const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8")) as unknown;
|
|
266
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
267
|
+
throw runtimeError("runtime_request_invalid", request.url ?? "<request>", "request body must be a JSON object");
|
|
268
|
+
}
|
|
269
|
+
return parsed as JsonBody;
|
|
270
|
+
} catch (error) {
|
|
271
|
+
if (error instanceof VnextConfigError) throw error;
|
|
272
|
+
throw runtimeError("runtime_request_invalid", request.url ?? "<request>", "request body is not valid JSON");
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function sendJson(response: ServerResponse, status: number, payload: unknown): void {
|
|
277
|
+
const body = JSON.stringify(payload);
|
|
278
|
+
response.writeHead(status, { "content-type": "application/json", "content-length": Buffer.byteLength(body) });
|
|
279
|
+
response.end(body);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export interface VnextRuntimeSupervisor {
|
|
283
|
+
server: Server;
|
|
284
|
+
port: number;
|
|
285
|
+
runtimeId: string;
|
|
286
|
+
stop: () => Promise<void>;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export async function startVnextRuntimeSupervisor(
|
|
290
|
+
options: { stateRoot?: string; port?: number; now?: () => string } = {},
|
|
291
|
+
): Promise<VnextRuntimeSupervisor> {
|
|
292
|
+
const now = options.now ?? (() => new Date().toISOString());
|
|
293
|
+
const paths = vnextRuntimePaths(options.stateRoot !== undefined ? { stateRoot: options.stateRoot } : {});
|
|
294
|
+
try {
|
|
295
|
+
return await startVnextRuntimeSupervisorInner(paths, options.port, now);
|
|
296
|
+
} catch (error) {
|
|
297
|
+
// Startup failures are recorded so auto-start clients see the real cause
|
|
298
|
+
// instead of a bare timeout. Conflict is not an error: the winner is
|
|
299
|
+
// probed and reused instead.
|
|
300
|
+
if (!(error instanceof VnextConfigError && error.issues.some((issue) => issue.code === "runtime_supervisor_conflict"))) {
|
|
301
|
+
recordSupervisorError(paths, (error as Error).message);
|
|
302
|
+
}
|
|
303
|
+
throw error;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function startVnextRuntimeSupervisorInner(
|
|
308
|
+
paths: VnextRuntimePaths,
|
|
309
|
+
requestedPortOption: number | undefined,
|
|
310
|
+
now: () => string,
|
|
311
|
+
): Promise<VnextRuntimeSupervisor> {
|
|
312
|
+
mkdirSync(paths.runtimeDir, { recursive: true, mode: 0o700 });
|
|
313
|
+
mkdirSync(paths.projectsDir, { recursive: true, mode: 0o700 });
|
|
314
|
+
// The token is generated in memory and written only after the supervisor
|
|
315
|
+
// singleton is owned: a losing supervisor can never overwrite a winner's
|
|
316
|
+
// token and lock every client out.
|
|
317
|
+
const token = randomBytes(32).toString("hex");
|
|
318
|
+
const tokenHash = hashVnextSupervisorToken(token);
|
|
319
|
+
const registry = new VnextRuntimeRegistry(paths.registryDb);
|
|
320
|
+
|
|
321
|
+
const runtimeId = `rtm_${createHash("sha256").update(`${paths.stateRoot}\0${process.pid}\0${now()}\0${randomBytes(16).toString("hex")}`, "utf8").digest("hex").slice(0, 24)}`;
|
|
322
|
+
const requestedPort = requestedPortOption ?? 0;
|
|
323
|
+
let activeRuntimeId = runtimeId;
|
|
324
|
+
|
|
325
|
+
const contexts = new Map<string, VnextRuntimeContext>();
|
|
326
|
+
const contextFor = (projectRoot: string): VnextRuntimeContext => {
|
|
327
|
+
if (!isAbsolute(projectRoot)) {
|
|
328
|
+
throw runtimeError("runtime_request_invalid", "projectRoot", "projectRoot must be an absolute path");
|
|
329
|
+
}
|
|
330
|
+
const key = projectRuntimeKey(projectRoot);
|
|
331
|
+
const existing = contexts.get(key);
|
|
332
|
+
if (existing) return existing;
|
|
333
|
+
const context = openVnextRuntimeContext(projectRoot, { homeRuntimeId: activeRuntimeId, stateRoot: paths.stateRoot });
|
|
334
|
+
contexts.set(key, context);
|
|
335
|
+
return context;
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const requireAuth = (request: IncomingMessage): void => {
|
|
339
|
+
const header = request.headers.authorization ?? "";
|
|
340
|
+
const presented = header.startsWith("Bearer ") ? header.slice("Bearer ".length) : "";
|
|
341
|
+
const presentedHash = presented.length > 0 ? Buffer.from(hashVnextSupervisorToken(presented), "utf8") : Buffer.alloc(0);
|
|
342
|
+
const expectedHash = Buffer.from(tokenHash, "utf8");
|
|
343
|
+
if (presentedHash.length !== expectedHash.length || !timingSafeEqual(presentedHash, expectedHash)) {
|
|
344
|
+
throw runtimeError("runtime_auth_failed", request.url ?? "<request>", "missing or invalid supervisor token");
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
const server = createServer((request, response) => {
|
|
349
|
+
void (async () => {
|
|
350
|
+
try {
|
|
351
|
+
const url = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
352
|
+
if (request.method === "GET" && url.pathname === "/healthz") {
|
|
353
|
+
const nonce = url.searchParams.get("nonce") ?? "";
|
|
354
|
+
sendJson(response, 200, {
|
|
355
|
+
ok: true,
|
|
356
|
+
runtimeId: activeRuntimeId,
|
|
357
|
+
state: "running",
|
|
358
|
+
// Proof of token knowledge: an impostor that merely binds the
|
|
359
|
+
// stale port cannot answer the keyed challenge.
|
|
360
|
+
tokenProof: nonce.length > 0 ? hashVnextTokenProof(token, nonce) : undefined,
|
|
361
|
+
});
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
requireAuth(request);
|
|
365
|
+
|
|
366
|
+
if (request.method === "POST" && url.pathname === "/v1/shutdown") {
|
|
367
|
+
sendJson(response, 200, { ok: true, stopping: true });
|
|
368
|
+
setImmediate(() => { void stop(); });
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (request.method === "POST" && url.pathname === "/v1/runs") {
|
|
373
|
+
const body = await readJsonBody(request);
|
|
374
|
+
const projectRoot = typeof body.projectRoot === "string" ? body.projectRoot : "";
|
|
375
|
+
const workflowId = typeof body.workflowId === "string" ? body.workflowId : "";
|
|
376
|
+
const prompt = typeof body.prompt === "string" ? body.prompt : "";
|
|
377
|
+
if (!projectRoot || !workflowId) {
|
|
378
|
+
sendJson(response, 400, { ok: false, error: "runtime_request_invalid", message: "projectRoot and workflowId are required" });
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
const context = contextFor(projectRoot);
|
|
382
|
+
const bundle = loadVnextProject(projectRoot, {});
|
|
383
|
+
const acceptance = acceptVnextRun(context, bundle, {
|
|
384
|
+
...(typeof body.commandId === "string" && body.commandId.length > 0 ? { commandId: body.commandId } : {}),
|
|
385
|
+
workflowId,
|
|
386
|
+
prompt,
|
|
387
|
+
});
|
|
388
|
+
sendJson(response, 200, {
|
|
389
|
+
ok: true,
|
|
390
|
+
idempotent: acceptance.idempotent,
|
|
391
|
+
run: acceptance.run,
|
|
392
|
+
event: acceptance.event,
|
|
393
|
+
});
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const runMatch = /^\/v1\/runs\/([A-Za-z0-9_-]+)(?:\/(events|cancel|signal|wait))?$/.exec(url.pathname);
|
|
398
|
+
if (runMatch) {
|
|
399
|
+
const runId = runMatch[1] as string;
|
|
400
|
+
const sub = runMatch[2];
|
|
401
|
+
const projectRoot = url.searchParams.get("projectRoot") ?? "";
|
|
402
|
+
if (!projectRoot) {
|
|
403
|
+
sendJson(response, 400, { ok: false, error: "runtime_request_invalid", message: "projectRoot query parameter is required" });
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
const context = contextFor(projectRoot);
|
|
407
|
+
if (request.method === "GET" && !sub) {
|
|
408
|
+
const projected = rebuildVnextRunProjection(context, runId);
|
|
409
|
+
sendJson(response, 200, { ok: true, run: projected });
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if (request.method === "GET" && sub === "events") {
|
|
413
|
+
const after = Number.parseInt(url.searchParams.get("after") ?? "0", 10);
|
|
414
|
+
const events = context.eventStore.events(runId, Number.isFinite(after) && after >= 0 ? after : 0, 500);
|
|
415
|
+
sendJson(response, 200, { ok: true, events });
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
if (request.method === "POST" && sub === "cancel") {
|
|
419
|
+
const body = await readJsonBody(request);
|
|
420
|
+
const result = cancelVnextRun(context, runId, {
|
|
421
|
+
...(typeof body.commandId === "string" && body.commandId.length > 0 ? { commandId: body.commandId } : {}),
|
|
422
|
+
});
|
|
423
|
+
sendJson(response, 200, { ok: true, idempotent: result.idempotent, run: result.run, events: result.events });
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
if (request.method === "POST" && sub === "signal") {
|
|
427
|
+
const body = await readJsonBody(request);
|
|
428
|
+
const run = context.eventStore.run(runId);
|
|
429
|
+
if (!run) throw runtimeError("run_unknown", runId, "run not found");
|
|
430
|
+
const state = foldStoredVnextRun(context, run);
|
|
431
|
+
let unblocked = false;
|
|
432
|
+
if (state.status === "blocked_uncertain" || state.currentStep?.effectState === "blocked_uncertain") {
|
|
433
|
+
const rec = recoverVnextRun(context, runId, {
|
|
434
|
+
action: "unblock",
|
|
435
|
+
reason: typeof body.summary === "string" ? body.summary : `signal_${body.signalKey ?? "callback"}`,
|
|
436
|
+
});
|
|
437
|
+
unblocked = rec.unblocked;
|
|
438
|
+
}
|
|
439
|
+
sendJson(response, 200, { ok: true, runId, signalKey: body.signalKey, status: body.status, unblocked });
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (request.method === "POST" && sub === "wait") {
|
|
443
|
+
const body = await readJsonBody(request);
|
|
444
|
+
const run = context.eventStore.run(runId);
|
|
445
|
+
if (!run) throw runtimeError("run_unknown", runId, "run not found");
|
|
446
|
+
sendJson(response, 200, { ok: true, runId, stageId: body.stageId, signalKey: body.signalKey, waiting: true });
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const projectRunsMatch = /^\/v1\/projects\/(prj_[A-Za-z0-9_-]+)\/runs$/.exec(url.pathname);
|
|
452
|
+
if (request.method === "GET" && projectRunsMatch) {
|
|
453
|
+
const projectRoot = url.searchParams.get("projectRoot") ?? "";
|
|
454
|
+
if (!projectRoot) {
|
|
455
|
+
sendJson(response, 400, { ok: false, error: "runtime_request_invalid", message: "projectRoot query parameter is required" });
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
const context = contextFor(projectRoot);
|
|
459
|
+
const requestedProjectId = projectRunsMatch[1] as string;
|
|
460
|
+
if (requestedProjectId !== context.projectId) {
|
|
461
|
+
sendJson(response, 400, { ok: false, error: "runtime_request_invalid", message: `project ${requestedProjectId} is not the bound project ${context.projectId}` });
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
const runs = context.eventStore.runsForProject(requestedProjectId, 50);
|
|
465
|
+
sendJson(response, 200, { ok: true, runs });
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
sendJson(response, 404, { ok: false, error: "runtime_not_found" });
|
|
470
|
+
} catch (error) {
|
|
471
|
+
if (error instanceof VnextConfigError) {
|
|
472
|
+
const code = error.issues[0]?.code ?? "runtime_error";
|
|
473
|
+
const status = code === "runtime_auth_failed" ? 401 : code === "run_unknown" ? 404 : 400;
|
|
474
|
+
sendJson(response, status, { ok: false, error: code, issues: error.issues });
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
sendJson(response, 500, { ok: false, error: "runtime_internal" });
|
|
478
|
+
}
|
|
479
|
+
})();
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
const port = await new Promise<number>((resolvePort, rejectPort) => {
|
|
483
|
+
server.once("error", rejectPort);
|
|
484
|
+
server.listen(requestedPort, "127.0.0.1", () => {
|
|
485
|
+
const address = server.address();
|
|
486
|
+
if (!address || typeof address !== "object") {
|
|
487
|
+
rejectPort(new Error("runtime supervisor did not bind"));
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
resolvePort(address.port);
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
// Claim or take over the supervisor singleton.
|
|
495
|
+
const claim = registry.claimSupervisor({ runtimeId, pid: process.pid, port, tokenHash, now: now() });
|
|
496
|
+
if (!claim.claimed) {
|
|
497
|
+
const existing = claim.record;
|
|
498
|
+
// Only a genuinely live supervisor (running, fresh heartbeat, live pid)
|
|
499
|
+
// conflicts. A cleanly stopped or stale supervisor may be replaced.
|
|
500
|
+
const heartbeatAgeMs = Date.now() - Date.parse(existing.heartbeatAt);
|
|
501
|
+
const fresh = Number.isFinite(heartbeatAgeMs) && heartbeatAgeMs < HEARTBEAT_STALE_MS;
|
|
502
|
+
if (existing.state === "running" && fresh && processAlive(existing.pid)) {
|
|
503
|
+
registry.close();
|
|
504
|
+
server.close();
|
|
505
|
+
throw runtimeError("runtime_supervisor_conflict", paths.registryDb, `runtime supervisor is already managed by PID ${existing.pid}`);
|
|
506
|
+
}
|
|
507
|
+
try {
|
|
508
|
+
const takeover = registry.takeoverSupervisor({ runtimeId, pid: process.pid, port, tokenHash, now: now(), observedDeadPid: existing.pid, observedHeartbeatAt: existing.heartbeatAt });
|
|
509
|
+
activeRuntimeId = takeover.runtimeId;
|
|
510
|
+
} catch (error) {
|
|
511
|
+
registry.close();
|
|
512
|
+
server.close();
|
|
513
|
+
throw error;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Ownership is established: publish the token atomically so concurrent
|
|
518
|
+
// readers never see a truncated file, and clear any stale startup error.
|
|
519
|
+
try {
|
|
520
|
+
publishVnextSupervisorToken(paths, token);
|
|
521
|
+
clearSupervisorError(paths);
|
|
522
|
+
} catch (error) {
|
|
523
|
+
registry.close();
|
|
524
|
+
server.close();
|
|
525
|
+
throw error;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const heartbeat = setInterval(() => {
|
|
529
|
+
try {
|
|
530
|
+
const changes = registry.heartbeat(process.pid, now());
|
|
531
|
+
if (changes === 0 && !stopping) {
|
|
532
|
+
// The singleton was taken over by another process: this supervisor is
|
|
533
|
+
// superseded and must not linger as an unreachable zombie.
|
|
534
|
+
void stop().finally(() => process.exit(0));
|
|
535
|
+
}
|
|
536
|
+
} catch { /* registry closed during shutdown */ }
|
|
537
|
+
}, 1000);
|
|
538
|
+
heartbeat.unref();
|
|
539
|
+
|
|
540
|
+
let stopping = false;
|
|
541
|
+
const stop = async (): Promise<void> => {
|
|
542
|
+
if (stopping) return;
|
|
543
|
+
stopping = true;
|
|
544
|
+
clearInterval(heartbeat);
|
|
545
|
+
try { registry.markStopping(process.pid, now()); } catch { /* best effort */ }
|
|
546
|
+
for (const context of contexts.values()) closeVnextRuntimeContext(context);
|
|
547
|
+
contexts.clear();
|
|
548
|
+
const closed = new Promise<void>((resolveStop) => server.close(() => resolveStop()));
|
|
549
|
+
server.closeIdleConnections?.();
|
|
550
|
+
await closed;
|
|
551
|
+
try { registry.markStopped(process.pid, now()); } catch { /* best effort */ }
|
|
552
|
+
registry.close();
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
process.once("SIGINT", () => { void stop().finally(() => process.exit(0)); });
|
|
556
|
+
process.once("SIGTERM", () => { void stop().finally(() => process.exit(0)); });
|
|
557
|
+
|
|
558
|
+
return { server, port, runtimeId: activeRuntimeId, stop };
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/* ------------------------------------------------------------------ *
|
|
562
|
+
* Thin client used by the CLI
|
|
563
|
+
* ------------------------------------------------------------------ */
|
|
564
|
+
|
|
565
|
+
export async function vnextRuntimeRequest(
|
|
566
|
+
handle: VnextSupervisorHandle,
|
|
567
|
+
method: "GET" | "POST",
|
|
568
|
+
path: string,
|
|
569
|
+
body?: unknown,
|
|
570
|
+
): Promise<Record<string, unknown>> {
|
|
571
|
+
const response = await fetch(`http://127.0.0.1:${handle.port}${path}`, {
|
|
572
|
+
method,
|
|
573
|
+
headers: {
|
|
574
|
+
authorization: `Bearer ${handle.token}`,
|
|
575
|
+
...(body !== undefined ? { "content-type": "application/json" } : {}),
|
|
576
|
+
},
|
|
577
|
+
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
|
|
578
|
+
});
|
|
579
|
+
const payload = await response.json() as Record<string, unknown>;
|
|
580
|
+
if (!response.ok) {
|
|
581
|
+
const code = typeof payload.error === "string" ? payload.error : "runtime_request_failed";
|
|
582
|
+
const message = typeof payload.message === "string" ? payload.message : `runtime request failed with HTTP ${response.status}`;
|
|
583
|
+
throw runtimeError(code, path, message);
|
|
584
|
+
}
|
|
585
|
+
return payload;
|
|
586
|
+
}
|