@kontextmind/kxm 0.7.40 → 0.7.43
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 +1 -1
- package/CHANGELOG.md +41 -7
- package/docs/README.md +5 -3
- package/docs/agent-skills.md +1 -1
- package/docs/architecture.md +1 -1
- package/docs/configuration.md +28 -5
- package/docs/{vnext → contracts}/README.md +7 -7
- package/docs/{vnext → contracts}/architecture.md +2 -2
- package/docs/{vnext → contracts}/migration.md +25 -25
- package/docs/{vnext → contracts}/routing.md +2 -2
- package/docs/{vnext → contracts}/synchronization.md +1 -1
- package/docs/{vnext → contracts}/validation.md +7 -7
- package/docs/getting-started.md +1 -1
- package/docs/kb/qa-authentik-authentication.md +47 -0
- package/docs/kb/qa-extension-install-and-hub-bootstrap.md +68 -0
- package/docs/kb/qa-hub-on-a-public-host.md +31 -0
- package/docs/kb/qa-sqlite-vs-duckdb.md +18 -0
- package/docs/kb/qa-what-the-hub-stores.md +47 -0
- package/docs/packages.md +91 -0
- package/docs/templates/architecture.md +1 -1
- package/docs/test-matrix.md +6 -6
- package/docs/tui-components.md +131 -0
- package/examples/README.md +2 -2
- package/examples/{vnext → project}/.kxm/agents/critic-3.yaml +1 -1
- package/examples/{vnext → project}/README.md +4 -4
- package/package.json +18 -8
- package/packages/core/tui/CHANGELOG.md +19 -0
- package/packages/core/tui/LICENSE +21 -0
- package/packages/core/tui/README.md +45 -0
- package/packages/core/tui/dist/index.js +1439 -0
- package/packages/core/tui/src/adapters/pi.ts +73 -0
- package/packages/core/tui/src/adapters/terminal.ts +106 -0
- package/packages/core/tui/src/exports/index.ts +30 -0
- package/packages/core/tui/src/services/registry.ts +274 -0
- package/packages/core/tui/src/tui/keys.ts +130 -0
- package/packages/core/tui/src/tui/layout.ts +117 -0
- package/packages/core/tui/src/tui/panel.ts +435 -0
- package/packages/core/tui/src/tui/panelComponent.ts +208 -0
- package/packages/core/tui/src/tui/render.ts +309 -0
- package/packages/core/tui/src/tui/theme.ts +145 -0
- package/packages/core/tui/src/types/surface.ts +407 -0
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/cli.js +681 -553
- package/plugins/kxm/dist/extension.js +18 -18
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/dist/{vnext-runtime-supervisor.js → runtime-supervisor.js} +553 -553
- package/plugins/kxm/dist/runtime.js +608 -608
- package/plugins/kxm/dist/server.js +13 -13
- package/plugins/kxm/package.json +2 -2
- package/plugins/kxm/skills/kxm-project-setup/SKILL.md +2 -2
- package/plugins/kxm/skills/kxm-runs/SKILL.md +3 -3
- package/plugins/kxm/skills/kxm-session/SKILL.md +1 -1
- package/plugins/kxm/skills/kxm-workflow/SKILL.md +1 -1
- package/plugins/kxm/src/autocomplete.ts +4 -4
- package/plugins/kxm/src/{vnext-bindings.ts → bindings.ts} +48 -48
- package/plugins/kxm/src/cli/hub.ts +3 -3
- package/plugins/kxm/src/cli/{vnext.ts → project.ts} +106 -106
- package/plugins/kxm/src/cli/roles.ts +9 -9
- package/plugins/kxm/src/cli/system.ts +2 -2
- package/plugins/kxm/src/cli/tasks.ts +8 -8
- package/plugins/kxm/src/cli/workflows.ts +8 -8
- package/plugins/kxm/src/cli.ts +47 -47
- package/plugins/kxm/src/config.ts +69 -14
- package/plugins/kxm/src/database.ts +4 -4
- package/plugins/kxm/src/{vnext-engine-artifacts.ts → engine-artifacts.ts} +7 -7
- package/plugins/kxm/src/{vnext-engine-command.ts → engine-command.ts} +40 -40
- package/plugins/kxm/src/{vnext-engine-compile.ts → engine-compile.ts} +64 -64
- package/plugins/kxm/src/{vnext-engine-evidence.ts → engine-evidence.ts} +26 -26
- package/plugins/kxm/src/{vnext-engine-fold.ts → engine-fold.ts} +99 -99
- package/plugins/kxm/src/{vnext-engine-gate-records.ts → engine-gate-records.ts} +105 -105
- package/plugins/kxm/src/{vnext-engine-plan.ts → engine-plan.ts} +69 -69
- package/plugins/kxm/src/{vnext-engine.ts → engine.ts} +502 -502
- package/plugins/kxm/src/gate-hash.ts +10 -0
- package/plugins/kxm/src/{vnext-harness.ts → harness.ts} +1 -1
- package/plugins/kxm/src/hub.ts +1 -1
- package/plugins/kxm/src/init-guide-setup.ts +3 -3
- package/plugins/kxm/src/{vnext-init.ts → init.ts} +93 -93
- package/plugins/kxm/src/kxm-install-kind.ts +1 -1
- package/plugins/kxm/src/kxm-update-config.ts +2 -2
- package/plugins/kxm/src/local-snapshot.ts +23 -23
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/{vnext-migrate.ts → migrate.ts} +123 -123
- package/plugins/kxm/src/model-inventory.ts +1 -1
- package/plugins/kxm/src/{vnext-oneshot-evidence.ts → oneshot-evidence.ts} +2 -2
- package/plugins/kxm/src/{vnext-oneshot-process.ts → oneshot-process.ts} +4 -4
- package/plugins/kxm/src/{vnext-oneshot-producer.ts → oneshot-producer.ts} +24 -24
- package/plugins/kxm/src/{vnext-permission.ts → permission.ts} +86 -86
- package/plugins/kxm/src/{vnext-pi-producer.ts → pi-producer.ts} +13 -13
- package/plugins/kxm/src/{vnext-config.ts → project-config.ts} +177 -177
- package/plugins/kxm/src/{vnext-repair.ts → repair.ts} +159 -159
- package/plugins/kxm/src/restricted-yaml.d.mts +3 -3
- package/plugins/kxm/src/restricted-yaml.mjs +4 -4
- package/plugins/kxm/src/{vnext-runtime-owner.ts → runtime-owner.ts} +35 -35
- package/plugins/kxm/src/{vnext-runtime.ts → runtime-service.ts} +136 -136
- package/plugins/kxm/src/{vnext-runtime-store.ts → runtime-store.ts} +145 -145
- package/plugins/kxm/src/{vnext-runtime-supervisor.ts → runtime-supervisor.ts} +80 -80
- package/plugins/kxm/src/runtime.ts +6 -6
- package/plugins/kxm/src/session-work.ts +3 -3
- package/plugins/kxm/src/studio-layout.ts +6 -6
- package/plugins/kxm/src/{vnext-template.ts → template.ts} +33 -33
- package/plugins/kxm/src/tui.ts +10 -24
- package/schemas/{vnext/README.md → README.md} +3 -3
- package/schemas/{vnext/agent.schema.json → agent.schema.json} +1 -1
- package/schemas/{vnext/assignment-result.schema.json → assignment-result.schema.json} +1 -1
- package/schemas/{vnext/backup-manifest.schema.json → backup-manifest.schema.json} +1 -1
- package/schemas/{vnext/candidate.schema.json → candidate.schema.json} +1 -1
- package/schemas/{vnext/common.schema.json → common.schema.json} +2 -2
- package/schemas/{vnext/context-candidate.schema.json → context-candidate.schema.json} +1 -1
- package/schemas/{vnext/context-packet.schema.json → context-packet.schema.json} +1 -1
- package/schemas/{vnext/delivery-manifest.schema.json → delivery-manifest.schema.json} +1 -1
- package/schemas/{vnext/drive-receipt.schema.json → drive-receipt.schema.json} +2 -2
- package/schemas/{vnext/environment.schema.json → environment.schema.json} +1 -1
- package/schemas/{vnext/gate-registry.schema.json → gate-registry.schema.json} +1 -1
- package/schemas/{vnext/handoff-manifest.schema.json → handoff-manifest.schema.json} +1 -1
- package/schemas/{vnext/init-operation.schema.json → init-operation.schema.json} +1 -1
- package/schemas/{vnext/local-repository-bindings.schema.json → local-repository-bindings.schema.json} +1 -1
- package/schemas/{vnext/memory-record.schema.json → memory-record.schema.json} +1 -1
- package/schemas/{vnext/migration-decision.schema.json → migration-decision.schema.json} +1 -1
- package/schemas/{vnext/migration-plan.schema.json → migration-plan.schema.json} +1 -1
- package/schemas/{vnext/migration-receipt.schema.json → migration-receipt.schema.json} +1 -1
- package/schemas/{vnext/model.schema.json → model.schema.json} +1 -1
- package/schemas/{vnext/modes.schema.json → modes.schema.json} +1 -1
- package/schemas/{vnext/permission-diff.schema.json → permission-diff.schema.json} +1 -1
- package/schemas/policy-draft/README.md +2 -2
- package/schemas/{vnext/prices.schema.json → prices.schema.json} +1 -1
- package/schemas/{vnext/project.schema.json → project.schema.json} +1 -1
- package/schemas/{vnext/repository.schema.json → repository.schema.json} +1 -1
- package/schemas/{vnext/role.schema.json → role.schema.json} +1 -1
- package/schemas/{vnext/run-event.schema.json → run-event.schema.json} +1 -1
- package/schemas/{vnext/session-brief.schema.json → session-brief.schema.json} +2 -2
- package/schemas/{vnext/sync-event.schema.json → sync-event.schema.json} +1 -1
- package/schemas/{vnext/template-provenance.schema.json → template-provenance.schema.json} +1 -1
- package/schemas/{vnext/workflow.schema.json → workflow.schema.json} +1 -1
- package/scripts/build-package.mjs +59 -0
- package/scripts/build-runtime.mjs +3 -2
- package/scripts/check-generated.mjs +2 -1
- package/scripts/check-versions.mjs +12 -1
- package/scripts/docker-install-smoke.mjs +268 -0
- package/scripts/emit-codex-artifacts.mjs +1 -1
- package/scripts/kxm-bump-version.mjs +53 -3
- package/scripts/kxm-runtime-supervisor.mjs +3 -3
- package/scripts/package-surfaces.mjs +39 -0
- package/plugins/kxm/src/vnext-gate-hash.ts +0 -10
- /package/docs/{vnext → contracts}/effects-and-recovery.md +0 -0
- /package/docs/{vnext → contracts}/lifecycles.md +0 -0
- /package/docs/{vnext → contracts}/terminology.md +0 -0
- /package/examples/{vnext → project}/.kxm/agents/coordinator.yaml +0 -0
- /package/examples/{vnext → project}/.kxm/agents/critic-1.yaml +0 -0
- /package/examples/{vnext → project}/.kxm/agents/critic-2.yaml +0 -0
- /package/examples/{vnext → project}/.kxm/agents/implementer.yaml +0 -0
- /package/examples/{vnext → project}/.kxm/agents/planner.yaml +0 -0
- /package/examples/{vnext → project}/.kxm/agents/reproducer.yaml +0 -0
- /package/examples/{vnext → project}/.kxm/agents/reviewer.yaml +0 -0
- /package/examples/{vnext → project}/.kxm/gates.yaml +0 -0
- /package/examples/{vnext → project}/.kxm/models/critic-claude.yaml +0 -0
- /package/examples/{vnext → project}/.kxm/models/critic-gemini.yaml +0 -0
- /package/examples/{vnext → project}/.kxm/models/critic-grok.yaml +0 -0
- /package/examples/{vnext → project}/.kxm/models/implementation.yaml +0 -0
- /package/examples/{vnext → project}/.kxm/models/primary.yaml +0 -0
- /package/examples/{vnext → project}/.kxm/prices.yaml +0 -0
- /package/examples/{vnext → project}/.kxm/project/env.yaml +0 -0
- /package/examples/{vnext → project}/.kxm/project.yaml +0 -0
- /package/examples/{vnext → project}/.kxm/repo/repo.yaml +0 -0
- /package/examples/{vnext → project}/.kxm/workflows/default.yaml +0 -0
- /package/examples/{vnext → project}/.kxm/workflows/fix.yaml +0 -0
- /package/examples/{vnext → project}/.kxm/workflows/improve.yaml +0 -0
- /package/examples/{vnext → project}/records/assignment-result-recorded.json +0 -0
- /package/examples/{vnext → project}/records/assignment-result.json +0 -0
- /package/examples/{vnext → project}/records/context-candidate.json +0 -0
- /package/examples/{vnext → project}/records/delivery-manifest.json +0 -0
- /package/examples/{vnext → project}/records/effect-uncertainty-resolved-sync.json +0 -0
- /package/examples/{vnext → project}/records/effect-uncertainty-resolved.json +0 -0
- /package/examples/{vnext → project}/records/run-created.json +0 -0
- /package/examples/{vnext → project}/records/sync-event.json +0 -0
- /package/examples/{vnext → project}/repositories/api/.kxm/repo/env.yaml +0 -0
- /package/examples/{vnext → project}/repositories/api/.kxm/repo/repo.yaml +0 -0
- /package/examples/{vnext → project}/repositories/web/.kxm/repo/repo.yaml +0 -0
|
@@ -4,40 +4,40 @@ import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, renameSync,
|
|
|
4
4
|
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
|
5
5
|
import { dirname, isAbsolute, join } from "node:path";
|
|
6
6
|
import { findKxmRepoRoot } from "./repo-root.ts";
|
|
7
|
-
import {
|
|
7
|
+
import { loadKxmProject, KxmConfigError, type KxmConfigOptions } from "./project-config.ts";
|
|
8
8
|
import {
|
|
9
|
-
|
|
9
|
+
KxmRuntimeRegistry,
|
|
10
10
|
projectRuntimeKey,
|
|
11
11
|
runtimeError,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
type
|
|
15
|
-
} from "./
|
|
12
|
+
verifyKxmDriveReceipt,
|
|
13
|
+
kxmRuntimePaths,
|
|
14
|
+
type KxmRuntimePaths,
|
|
15
|
+
} from "./runtime-store.ts";
|
|
16
16
|
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
type
|
|
24
|
-
} from "./
|
|
25
|
-
import {
|
|
17
|
+
acceptKxmRun,
|
|
18
|
+
cancelKxmRun,
|
|
19
|
+
closeKxmRuntimeContext,
|
|
20
|
+
foldStoredKxmRun,
|
|
21
|
+
openKxmRuntimeContext,
|
|
22
|
+
projectKxmRunReadOnly,
|
|
23
|
+
type KxmRuntimeContext,
|
|
24
|
+
} from "./runtime-service.ts";
|
|
25
|
+
import { createKxmOneShotProducer } from "./oneshot-producer.ts";
|
|
26
26
|
import { isRouteAdmitted } from "./routes.ts";
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
27
|
+
import { KxmRunScheduler, createKxmSimulatedProducer, recordDriveReceipt, recoverKxmRun, kxmDrivePollProjection } from "./engine.ts";
|
|
28
|
+
import { kxmDriveSession, kxmOpenDriveSessions } from "./runtime-owner.ts";
|
|
29
29
|
|
|
30
30
|
/* ------------------------------------------------------------------ *
|
|
31
31
|
* Token management
|
|
32
32
|
* ------------------------------------------------------------------ */
|
|
33
33
|
|
|
34
|
-
export function
|
|
34
|
+
export function kxmSupervisorTokenFile(paths: KxmRuntimePaths): string {
|
|
35
35
|
return join(paths.runtimeDir, "supervisor.token");
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
/** Atomically publish the supervisor token (temp file + rename). */
|
|
39
|
-
function
|
|
40
|
-
const file =
|
|
39
|
+
function publishKxmSupervisorToken(paths: KxmRuntimePaths, token: string): void {
|
|
40
|
+
const file = kxmSupervisorTokenFile(paths);
|
|
41
41
|
mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
|
|
42
42
|
const temp = `${file}.${process.pid}.tmp`;
|
|
43
43
|
writeFileSync(temp, `${token}\n`, { encoding: "utf8", mode: 0o600 });
|
|
@@ -46,12 +46,12 @@ function publishVnextSupervisorToken(paths: VnextRuntimePaths, token: string): v
|
|
|
46
46
|
try { chmodSync(file, 0o600); } catch { /* Windows may ignore modes */ }
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
export function
|
|
49
|
+
export function hashKxmSupervisorToken(token: string): string {
|
|
50
50
|
return `sha256:${createHash("sha256").update(`kxm-runtime-supervisor\0${token}`, "utf8").digest("hex")}`;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
export function
|
|
54
|
-
const file =
|
|
53
|
+
export function readKxmSupervisorToken(paths: KxmRuntimePaths): string | undefined {
|
|
54
|
+
const file = kxmSupervisorTokenFile(paths);
|
|
55
55
|
const stat = lstatSync(file, { throwIfNoEntry: false });
|
|
56
56
|
if (!stat) return undefined;
|
|
57
57
|
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
@@ -65,7 +65,7 @@ export function readVnextSupervisorToken(paths: VnextRuntimePaths): string | und
|
|
|
65
65
|
* Supervisor process state + liveness
|
|
66
66
|
* ------------------------------------------------------------------ */
|
|
67
67
|
|
|
68
|
-
export interface
|
|
68
|
+
export interface KxmSupervisorStatus {
|
|
69
69
|
running: boolean;
|
|
70
70
|
runtimeId?: string;
|
|
71
71
|
pid?: number;
|
|
@@ -87,23 +87,23 @@ function processAlive(pid: number): boolean {
|
|
|
87
87
|
const HEARTBEAT_STALE_MS = 15_000;
|
|
88
88
|
const SUPERVISOR_ERROR_MAX_AGE_MS = 30_000;
|
|
89
89
|
|
|
90
|
-
function supervisorErrorFile(paths:
|
|
90
|
+
function supervisorErrorFile(paths: KxmRuntimePaths): string {
|
|
91
91
|
return join(paths.runtimeDir, "supervisor.error");
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
function clearSupervisorError(paths:
|
|
94
|
+
function clearSupervisorError(paths: KxmRuntimePaths): void {
|
|
95
95
|
const file = supervisorErrorFile(paths);
|
|
96
96
|
if (existsSync(file)) rmSync(file, { force: true });
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
function recordSupervisorError(paths:
|
|
99
|
+
function recordSupervisorError(paths: KxmRuntimePaths, message: string): void {
|
|
100
100
|
try {
|
|
101
101
|
mkdirSync(paths.runtimeDir, { recursive: true, mode: 0o700 });
|
|
102
102
|
writeFileSync(supervisorErrorFile(paths), `${message}\n`, { encoding: "utf8", mode: 0o600 });
|
|
103
103
|
} catch { /* best effort */ }
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
function readRecentSupervisorError(paths:
|
|
106
|
+
function readRecentSupervisorError(paths: KxmRuntimePaths): string | undefined {
|
|
107
107
|
const file = supervisorErrorFile(paths);
|
|
108
108
|
const stat = lstatSync(file, { throwIfNoEntry: false });
|
|
109
109
|
if (!stat) return undefined;
|
|
@@ -116,9 +116,9 @@ function readRecentSupervisorError(paths: VnextRuntimePaths): string | undefined
|
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
export function
|
|
119
|
+
export function kxmSupervisorStatus(paths: KxmRuntimePaths): KxmSupervisorStatus {
|
|
120
120
|
if (!existsSync(paths.registryDb)) return { running: false };
|
|
121
|
-
const registry = new
|
|
121
|
+
const registry = new KxmRuntimeRegistry(paths.registryDb);
|
|
122
122
|
try {
|
|
123
123
|
const record = registry.supervisor();
|
|
124
124
|
if (!record) return { running: false };
|
|
@@ -159,7 +159,7 @@ async function probeSupervisor(port: number, expectedRuntimeId: string, token: s
|
|
|
159
159
|
const payload = await response.json() as { runtimeId?: string; tokenProof?: string };
|
|
160
160
|
// The server must prove it knows the current token: an impostor that
|
|
161
161
|
// merely binds the stale port cannot answer the keyed challenge.
|
|
162
|
-
const expectedProof =
|
|
162
|
+
const expectedProof = hashKxmTokenProof(token, nonce);
|
|
163
163
|
return payload.runtimeId === expectedRuntimeId && payload.tokenProof === expectedProof;
|
|
164
164
|
} catch {
|
|
165
165
|
return false;
|
|
@@ -168,11 +168,11 @@ async function probeSupervisor(port: number, expectedRuntimeId: string, token: s
|
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
-
export function
|
|
171
|
+
export function hashKxmTokenProof(token: string, nonce: string): string {
|
|
172
172
|
return createHmac("sha256", token).update(`kxm-runtime-token-proof\0${nonce}`, "utf8").digest("hex");
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
export interface
|
|
175
|
+
export interface KxmSupervisorHandle {
|
|
176
176
|
runtimeId: string;
|
|
177
177
|
port: number;
|
|
178
178
|
token: string;
|
|
@@ -180,14 +180,14 @@ export interface VnextSupervisorHandle {
|
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
/** Ensure a supervisor is running: reuse a live one, otherwise auto-start. */
|
|
183
|
-
export async function
|
|
183
|
+
export async function ensureKxmSupervisor(
|
|
184
184
|
options: { stateRoot?: string; env?: NodeJS.ProcessEnv; spawnImpl?: (scriptPath: string, env: NodeJS.ProcessEnv) => number } = {},
|
|
185
|
-
): Promise<
|
|
186
|
-
const paths =
|
|
187
|
-
const status =
|
|
185
|
+
): Promise<KxmSupervisorHandle> {
|
|
186
|
+
const paths = kxmRuntimePaths(options.stateRoot !== undefined ? { stateRoot: options.stateRoot } : { ...(options.env ? { env: options.env } : {}) });
|
|
187
|
+
const status = kxmSupervisorStatus(paths);
|
|
188
188
|
if (status.running && status.port) {
|
|
189
189
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
190
|
-
const token =
|
|
190
|
+
const token = readKxmSupervisorToken(paths);
|
|
191
191
|
if (token && (await probeSupervisorWithRetry(status.port, status.runtimeId as string, token))) {
|
|
192
192
|
return { runtimeId: status.runtimeId as string, port: status.port, token, started: false };
|
|
193
193
|
}
|
|
@@ -221,9 +221,9 @@ export async function ensureVnextSupervisor(
|
|
|
221
221
|
const deadline = Date.now() + 10_000;
|
|
222
222
|
while (Date.now() < deadline) {
|
|
223
223
|
await new Promise((resolveWait) => setTimeout(resolveWait, 100));
|
|
224
|
-
const next =
|
|
224
|
+
const next = kxmSupervisorStatus(paths);
|
|
225
225
|
if (next.running && next.port) {
|
|
226
|
-
const token =
|
|
226
|
+
const token = readKxmSupervisorToken(paths);
|
|
227
227
|
if (token && (await probeSupervisor(next.port, next.runtimeId as string, token))) {
|
|
228
228
|
// Entry status had no live supervisor: this call caused a start
|
|
229
229
|
// (ours or a concurrent winner's), which the probe just confirmed.
|
|
@@ -270,7 +270,7 @@ async function readJsonBody(request: IncomingMessage, limit = 64 * 1024): Promis
|
|
|
270
270
|
}
|
|
271
271
|
return parsed as JsonBody;
|
|
272
272
|
} catch (error) {
|
|
273
|
-
if (error instanceof
|
|
273
|
+
if (error instanceof KxmConfigError) throw error;
|
|
274
274
|
throw runtimeError("runtime_request_invalid", request.url ?? "<request>", "request body is not valid JSON");
|
|
275
275
|
}
|
|
276
276
|
}
|
|
@@ -329,58 +329,58 @@ async function driveSessionStillPending(settled: Promise<unknown>): Promise<bool
|
|
|
329
329
|
return pending;
|
|
330
330
|
}
|
|
331
331
|
|
|
332
|
-
export interface
|
|
332
|
+
export interface KxmRuntimeSupervisor {
|
|
333
333
|
server: Server;
|
|
334
334
|
port: number;
|
|
335
335
|
runtimeId: string;
|
|
336
336
|
stop: () => Promise<void>;
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
-
export async function
|
|
339
|
+
export async function startKxmRuntimeSupervisor(
|
|
340
340
|
options: { stateRoot?: string; port?: number; now?: () => string } = {},
|
|
341
|
-
): Promise<
|
|
341
|
+
): Promise<KxmRuntimeSupervisor> {
|
|
342
342
|
const now = options.now ?? (() => new Date().toISOString());
|
|
343
|
-
const paths =
|
|
343
|
+
const paths = kxmRuntimePaths(options.stateRoot !== undefined ? { stateRoot: options.stateRoot } : {});
|
|
344
344
|
try {
|
|
345
|
-
return await
|
|
345
|
+
return await startKxmRuntimeSupervisorInner(paths, options.port, now);
|
|
346
346
|
} catch (error) {
|
|
347
347
|
// Startup failures are recorded so auto-start clients see the real cause
|
|
348
348
|
// instead of a bare timeout. Conflict is not an error: the winner is
|
|
349
349
|
// probed and reused instead.
|
|
350
|
-
if (!(error instanceof
|
|
350
|
+
if (!(error instanceof KxmConfigError && error.issues.some((issue) => issue.code === "runtime_supervisor_conflict"))) {
|
|
351
351
|
recordSupervisorError(paths, (error as Error).message);
|
|
352
352
|
}
|
|
353
353
|
throw error;
|
|
354
354
|
}
|
|
355
355
|
}
|
|
356
356
|
|
|
357
|
-
async function
|
|
358
|
-
paths:
|
|
357
|
+
async function startKxmRuntimeSupervisorInner(
|
|
358
|
+
paths: KxmRuntimePaths,
|
|
359
359
|
requestedPortOption: number | undefined,
|
|
360
360
|
now: () => string,
|
|
361
|
-
): Promise<
|
|
361
|
+
): Promise<KxmRuntimeSupervisor> {
|
|
362
362
|
mkdirSync(paths.runtimeDir, { recursive: true, mode: 0o700 });
|
|
363
363
|
mkdirSync(paths.projectsDir, { recursive: true, mode: 0o700 });
|
|
364
364
|
// The token is generated in memory and written only after the supervisor
|
|
365
365
|
// singleton is owned: a losing supervisor can never overwrite a winner's
|
|
366
366
|
// token and lock every client out.
|
|
367
367
|
const token = randomBytes(32).toString("hex");
|
|
368
|
-
const tokenHash =
|
|
369
|
-
const registry = new
|
|
368
|
+
const tokenHash = hashKxmSupervisorToken(token);
|
|
369
|
+
const registry = new KxmRuntimeRegistry(paths.registryDb);
|
|
370
370
|
|
|
371
371
|
const runtimeId = `rtm_${createHash("sha256").update(`${paths.stateRoot}\0${process.pid}\0${now()}\0${randomBytes(16).toString("hex")}`, "utf8").digest("hex").slice(0, 24)}`;
|
|
372
372
|
const requestedPort = requestedPortOption ?? 0;
|
|
373
373
|
let activeRuntimeId = runtimeId;
|
|
374
374
|
|
|
375
|
-
const contexts = new Map<string,
|
|
376
|
-
const contextFor = (projectRoot: string):
|
|
375
|
+
const contexts = new Map<string, KxmRuntimeContext>();
|
|
376
|
+
const contextFor = (projectRoot: string): KxmRuntimeContext => {
|
|
377
377
|
if (!isAbsolute(projectRoot)) {
|
|
378
378
|
throw runtimeError("runtime_request_invalid", "projectRoot", "projectRoot must be an absolute path");
|
|
379
379
|
}
|
|
380
380
|
const key = projectRuntimeKey(projectRoot);
|
|
381
381
|
const existing = contexts.get(key);
|
|
382
382
|
if (existing) return existing;
|
|
383
|
-
const context =
|
|
383
|
+
const context = openKxmRuntimeContext(projectRoot, { homeRuntimeId: activeRuntimeId, stateRoot: paths.stateRoot });
|
|
384
384
|
contexts.set(key, context);
|
|
385
385
|
return context;
|
|
386
386
|
};
|
|
@@ -388,7 +388,7 @@ async function startVnextRuntimeSupervisorInner(
|
|
|
388
388
|
const requireAuth = (request: IncomingMessage): void => {
|
|
389
389
|
const header = request.headers.authorization ?? "";
|
|
390
390
|
const presented = header.startsWith("Bearer ") ? header.slice("Bearer ".length) : "";
|
|
391
|
-
const presentedHash = presented.length > 0 ? Buffer.from(
|
|
391
|
+
const presentedHash = presented.length > 0 ? Buffer.from(hashKxmSupervisorToken(presented), "utf8") : Buffer.alloc(0);
|
|
392
392
|
const expectedHash = Buffer.from(tokenHash, "utf8");
|
|
393
393
|
if (presentedHash.length !== expectedHash.length || !timingSafeEqual(presentedHash, expectedHash)) {
|
|
394
394
|
throw runtimeError("runtime_auth_failed", request.url ?? "<request>", "missing or invalid supervisor token");
|
|
@@ -407,7 +407,7 @@ async function startVnextRuntimeSupervisorInner(
|
|
|
407
407
|
state: "running",
|
|
408
408
|
// Proof of token knowledge: an impostor that merely binds the
|
|
409
409
|
// stale port cannot answer the keyed challenge.
|
|
410
|
-
tokenProof: nonce.length > 0 ?
|
|
410
|
+
tokenProof: nonce.length > 0 ? hashKxmTokenProof(token, nonce) : undefined,
|
|
411
411
|
});
|
|
412
412
|
return;
|
|
413
413
|
}
|
|
@@ -429,8 +429,8 @@ async function startVnextRuntimeSupervisorInner(
|
|
|
429
429
|
return;
|
|
430
430
|
}
|
|
431
431
|
const context = contextFor(projectRoot);
|
|
432
|
-
const bundle =
|
|
433
|
-
const acceptance =
|
|
432
|
+
const bundle = loadKxmProject(projectRoot, {});
|
|
433
|
+
const acceptance = acceptKxmRun(context, bundle, {
|
|
434
434
|
...(typeof body.commandId === "string" && body.commandId.length > 0 ? { commandId: body.commandId } : {}),
|
|
435
435
|
workflowId,
|
|
436
436
|
prompt,
|
|
@@ -454,11 +454,11 @@ async function startVnextRuntimeSupervisorInner(
|
|
|
454
454
|
return;
|
|
455
455
|
}
|
|
456
456
|
const context = contextFor(projectRoot);
|
|
457
|
-
const bundle =
|
|
457
|
+
const bundle = loadKxmProject(projectRoot, {});
|
|
458
458
|
if (request.method === "GET" && !sub) {
|
|
459
|
-
const projected =
|
|
460
|
-
const folded =
|
|
461
|
-
const drive =
|
|
459
|
+
const projected = projectKxmRunReadOnly(context, runId);
|
|
460
|
+
const folded = foldStoredKxmRun(context, projected);
|
|
461
|
+
const drive = kxmDrivePollProjection(context, runId, folded);
|
|
462
462
|
sendJson(response, 200, { ok: true, run: projected, ...(drive !== undefined ? { drive } : {}) });
|
|
463
463
|
return;
|
|
464
464
|
}
|
|
@@ -473,7 +473,7 @@ async function startVnextRuntimeSupervisorInner(
|
|
|
473
473
|
|
|
474
474
|
const delayMs = typeof body.delayMs === "number" && body.delayMs > 0 ? body.delayMs : 0;
|
|
475
475
|
const createProducer = () => body.mode === "live"
|
|
476
|
-
?
|
|
476
|
+
? createKxmOneShotProducer({
|
|
477
477
|
projectRoot,
|
|
478
478
|
defaultHarness: String(bundle.project.value.defaultHarness ?? "pi"),
|
|
479
479
|
resolveHarness: (agentId) => {
|
|
@@ -493,17 +493,17 @@ async function startVnextRuntimeSupervisorInner(
|
|
|
493
493
|
return { provider, model: modelName };
|
|
494
494
|
},
|
|
495
495
|
})
|
|
496
|
-
:
|
|
496
|
+
: createKxmSimulatedProducer(async () => {
|
|
497
497
|
if (delayMs > 0) {
|
|
498
498
|
await new Promise((r) => setTimeout(r, delayMs));
|
|
499
499
|
}
|
|
500
500
|
return { outcome: "passed" };
|
|
501
501
|
});
|
|
502
502
|
|
|
503
|
-
let session: Awaited<ReturnType<
|
|
503
|
+
let session: Awaited<ReturnType<KxmRunScheduler["openDriveSession"]>> | undefined;
|
|
504
504
|
let earlyError: unknown;
|
|
505
505
|
try {
|
|
506
|
-
const scheduler =
|
|
506
|
+
const scheduler = KxmRunScheduler.for(context, bundle);
|
|
507
507
|
const opening = scheduler.openDriveSession(runId, {
|
|
508
508
|
mode: body.mode,
|
|
509
509
|
allowLimits: false,
|
|
@@ -557,7 +557,7 @@ async function startVnextRuntimeSupervisorInner(
|
|
|
557
557
|
const run = context.eventStore.run(runId);
|
|
558
558
|
if (!run) throw runtimeError("run_unknown", runId, "run not found");
|
|
559
559
|
|
|
560
|
-
const activeSession =
|
|
560
|
+
const activeSession = kxmDriveSession(context.eventStore.path, runId);
|
|
561
561
|
const session = activeSession
|
|
562
562
|
? {
|
|
563
563
|
driveId: activeSession.driveId,
|
|
@@ -572,7 +572,7 @@ async function startVnextRuntimeSupervisorInner(
|
|
|
572
572
|
}
|
|
573
573
|
if (request.method === "POST" && sub === "cancel") {
|
|
574
574
|
const body = await readJsonBody(request);
|
|
575
|
-
const result =
|
|
575
|
+
const result = cancelKxmRun(context, runId, {
|
|
576
576
|
...(typeof body.commandId === "string" && body.commandId.length > 0 ? { commandId: body.commandId } : {}),
|
|
577
577
|
});
|
|
578
578
|
sendJson(response, 200, { ok: true, idempotent: result.idempotent, run: result.run, events: result.events });
|
|
@@ -582,11 +582,11 @@ async function startVnextRuntimeSupervisorInner(
|
|
|
582
582
|
const body = await readJsonBody(request);
|
|
583
583
|
const run = context.eventStore.run(runId);
|
|
584
584
|
if (!run) throw runtimeError("run_unknown", runId, "run not found");
|
|
585
|
-
const state =
|
|
585
|
+
const state = foldStoredKxmRun(context, run);
|
|
586
586
|
let unblocked = false;
|
|
587
587
|
if (state.status === "blocked_uncertain" || state.status === "cancelling" || state.currentStep?.effectState === "blocked_uncertain") {
|
|
588
588
|
const action = body.action === "cancel" || body.action === "fail" || body.action === "retry" || body.action === "unblock" ? body.action : "unblock";
|
|
589
|
-
const rec =
|
|
589
|
+
const rec = recoverKxmRun(context, runId, {
|
|
590
590
|
action,
|
|
591
591
|
reason: typeof body.summary === "string" ? body.summary : `signal_${body.signalKey ?? "callback"}`,
|
|
592
592
|
});
|
|
@@ -633,9 +633,9 @@ async function startVnextRuntimeSupervisorInner(
|
|
|
633
633
|
sendJson(response, 200, { ok: true, receipt, verified: false, divergence: "run missing" });
|
|
634
634
|
return;
|
|
635
635
|
}
|
|
636
|
-
const state =
|
|
636
|
+
const state = foldStoredKxmRun(context, stored);
|
|
637
637
|
const events = context.eventStore.events(runId, 0, 1_000_000);
|
|
638
|
-
const checked =
|
|
638
|
+
const checked = verifyKxmDriveReceipt(receipt, events, state.status, { runId, driveId });
|
|
639
639
|
sendJson(response, 200, {
|
|
640
640
|
ok: true,
|
|
641
641
|
receipt,
|
|
@@ -665,7 +665,7 @@ async function startVnextRuntimeSupervisorInner(
|
|
|
665
665
|
|
|
666
666
|
sendJson(response, 404, { ok: false, error: "runtime_not_found" });
|
|
667
667
|
} catch (error) {
|
|
668
|
-
if (error instanceof
|
|
668
|
+
if (error instanceof KxmConfigError) {
|
|
669
669
|
const code = error.issues[0]?.code ?? "runtime_error";
|
|
670
670
|
const status = code === "runtime_auth_failed" ? 401 : code === "run_unknown" ? 404 : 400;
|
|
671
671
|
sendJson(response, status, { ok: false, error: code, issues: error.issues });
|
|
@@ -714,7 +714,7 @@ async function startVnextRuntimeSupervisorInner(
|
|
|
714
714
|
// Ownership is established: publish the token atomically so concurrent
|
|
715
715
|
// readers never see a truncated file, and clear any stale startup error.
|
|
716
716
|
try {
|
|
717
|
-
|
|
717
|
+
publishKxmSupervisorToken(paths, token);
|
|
718
718
|
clearSupervisorError(paths);
|
|
719
719
|
} catch (error) {
|
|
720
720
|
registry.close();
|
|
@@ -741,11 +741,11 @@ async function startVnextRuntimeSupervisorInner(
|
|
|
741
741
|
clearInterval(heartbeat);
|
|
742
742
|
try { registry.markStopping(process.pid, now()); } catch { /* best effort */ }
|
|
743
743
|
const openSessions = [...contexts.values()].flatMap((context) => (
|
|
744
|
-
|
|
744
|
+
kxmOpenDriveSessions(context.eventStore.path).map((session) => ({ context, session }))
|
|
745
745
|
));
|
|
746
746
|
for (const { context, session } of openSessions) {
|
|
747
747
|
try {
|
|
748
|
-
|
|
748
|
+
cancelKxmRun(context, session.runId, { reason: "runtime_shutdown" });
|
|
749
749
|
} catch {
|
|
750
750
|
// Terminal or already cancelling runs still release on session settle.
|
|
751
751
|
}
|
|
@@ -765,7 +765,7 @@ async function startVnextRuntimeSupervisorInner(
|
|
|
765
765
|
producerClosed: false,
|
|
766
766
|
});
|
|
767
767
|
}
|
|
768
|
-
for (const context of contexts.values())
|
|
768
|
+
for (const context of contexts.values()) closeKxmRuntimeContext(context);
|
|
769
769
|
contexts.clear();
|
|
770
770
|
const closed = new Promise<void>((resolveStop) => server.close(() => resolveStop()));
|
|
771
771
|
server.closeIdleConnections?.();
|
|
@@ -784,8 +784,8 @@ async function startVnextRuntimeSupervisorInner(
|
|
|
784
784
|
* Thin client used by the CLI
|
|
785
785
|
* ------------------------------------------------------------------ */
|
|
786
786
|
|
|
787
|
-
export async function
|
|
788
|
-
handle:
|
|
787
|
+
export async function kxmRuntimeRequest(
|
|
788
|
+
handle: KxmSupervisorHandle,
|
|
789
789
|
method: "GET" | "POST",
|
|
790
790
|
path: string,
|
|
791
791
|
body?: unknown,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
export * from "./
|
|
2
|
-
export * from "./
|
|
3
|
-
export * from "./
|
|
4
|
-
export * from "./
|
|
5
|
-
export * from "./
|
|
6
|
-
export * from "./
|
|
1
|
+
export * from "./runtime-service.ts";
|
|
2
|
+
export * from "./runtime-store.ts";
|
|
3
|
+
export * from "./runtime-supervisor.ts";
|
|
4
|
+
export * from "./pi-producer.ts";
|
|
5
|
+
export * from "./oneshot-producer.ts";
|
|
6
|
+
export * from "./harness.ts";
|
|
7
7
|
export * from "./database.ts";
|
|
8
8
|
export * from "./logger.ts";
|
|
9
9
|
export * from "./improve.ts";
|
|
@@ -53,7 +53,7 @@ export interface SessionBrief {
|
|
|
53
53
|
schema: "kxm.session-brief.v1";
|
|
54
54
|
generatedAt: string;
|
|
55
55
|
staleSeconds: number;
|
|
56
|
-
source: "legacy" | "
|
|
56
|
+
source: "legacy" | "runtime" | "both";
|
|
57
57
|
hub: SessionHubStatus;
|
|
58
58
|
stats: SessionWorkStats;
|
|
59
59
|
tasks: SessionWorkItem[];
|
|
@@ -257,14 +257,14 @@ export function formatSessionWidget(
|
|
|
257
257
|
}
|
|
258
258
|
|
|
259
259
|
export function buildSessionBrief(
|
|
260
|
-
snapshot: Pick<LocalMeshSnapshot, "runs" | "plans" | "openMessageTotal" | "runTotal"> & { source?: "legacy" | "
|
|
260
|
+
snapshot: Pick<LocalMeshSnapshot, "runs" | "plans" | "openMessageTotal" | "runTotal"> & { source?: "legacy" | "runtime" | "both" | undefined },
|
|
261
261
|
current?: SessionWorkItem,
|
|
262
262
|
hub?: SessionHubStatus,
|
|
263
263
|
ship?: SessionShipStatus,
|
|
264
264
|
updateLatest?: string,
|
|
265
265
|
cost?: string,
|
|
266
266
|
sessionToken?: string,
|
|
267
|
-
source?: "legacy" | "
|
|
267
|
+
source?: "legacy" | "runtime" | "both" | undefined,
|
|
268
268
|
staleSeconds = DEFAULT_SESSION_BRIEF_STALE_SECONDS,
|
|
269
269
|
): SessionBrief {
|
|
270
270
|
const resolvedSource = source ?? snapshot.source ?? "legacy";
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
import { createServer, type IncomingMessage, type ServerResponse, type Server } from "node:http";
|
|
8
8
|
import { randomUUID } from "node:crypto";
|
|
9
|
-
import type {
|
|
10
|
-
import type {
|
|
9
|
+
import type { KxmCompiledPlan, KxmCompiledStep } from "./engine-compile.ts";
|
|
10
|
+
import type { KxmRunState } from "./engine-fold.ts";
|
|
11
11
|
|
|
12
12
|
export const STUDIO_LAYOUT_SCHEMA = "kxm.studio-layout.v1" as const;
|
|
13
13
|
|
|
@@ -76,8 +76,8 @@ export interface StudioLayoutPayload {
|
|
|
76
76
|
* without requiring any manual layout coordinates in the YAML workflow file.
|
|
77
77
|
*/
|
|
78
78
|
export function generateStudioLayout(
|
|
79
|
-
plan:
|
|
80
|
-
state?:
|
|
79
|
+
plan: KxmCompiledPlan,
|
|
80
|
+
state?: KxmRunState | undefined,
|
|
81
81
|
): StudioLayoutPayload {
|
|
82
82
|
const generatedAt = new Date().toISOString();
|
|
83
83
|
const stepIds = Object.keys(plan.steps);
|
|
@@ -287,8 +287,8 @@ export interface StudioServerOptions {
|
|
|
287
287
|
host?: string | undefined;
|
|
288
288
|
projectRoot?: string | undefined;
|
|
289
289
|
sessionToken?: string | undefined;
|
|
290
|
-
planProvider?: (() =>
|
|
291
|
-
stateProvider?: (() =>
|
|
290
|
+
planProvider?: (() => KxmCompiledPlan | undefined) | undefined;
|
|
291
|
+
stateProvider?: (() => KxmRunState | undefined) | undefined;
|
|
292
292
|
onMutation?: ((command: string, args: Record<string, unknown>) => Promise<{ ok: boolean; result?: unknown; error?: string }> | { ok: boolean; result?: unknown; error?: string }) | undefined;
|
|
293
293
|
}
|
|
294
294
|
|
|
@@ -1,37 +1,37 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { stringify } from "yaml";
|
|
3
|
-
import type { JsonObject, JsonValue } from "./
|
|
3
|
+
import type { JsonObject, JsonValue } from "./project-config.ts";
|
|
4
4
|
|
|
5
|
-
export const
|
|
6
|
-
export const
|
|
7
|
-
export const
|
|
8
|
-
export const
|
|
5
|
+
export const KXM_TEMPLATE_ID = "builtin-minimal";
|
|
6
|
+
export const KXM_TEMPLATE_PROVENANCE_PATH = ".kxm/template-provenance.yaml";
|
|
7
|
+
export const CURRENT_KXM_TEMPLATE_VARIANT: KxmTemplateVariant = "v4-registry";
|
|
8
|
+
export const SUPPORTED_KXM_TEMPLATE_VARIANTS = ["v1", "v2", "v3-policy", "v4-registry"] as const;
|
|
9
9
|
|
|
10
|
-
export type
|
|
10
|
+
export type KxmTemplateVariant = typeof SUPPORTED_KXM_TEMPLATE_VARIANTS[number];
|
|
11
11
|
|
|
12
|
-
export interface
|
|
12
|
+
export interface KxmTemplateFileRecord {
|
|
13
13
|
path: string;
|
|
14
14
|
sha256: string;
|
|
15
15
|
authoritySha256: string;
|
|
16
16
|
bytes: number;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export interface
|
|
19
|
+
export interface KxmTemplateProvenance {
|
|
20
20
|
schema: "kxm.template-provenance.v1";
|
|
21
|
-
templateId: typeof
|
|
21
|
+
templateId: typeof KXM_TEMPLATE_ID;
|
|
22
22
|
templateRevision: string;
|
|
23
23
|
inputs: {
|
|
24
24
|
projectId: string;
|
|
25
25
|
projectName: string;
|
|
26
26
|
};
|
|
27
|
-
files: readonly
|
|
27
|
+
files: readonly KxmTemplateFileRecord[];
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
export interface
|
|
30
|
+
export interface KxmRenderedTemplate {
|
|
31
31
|
projectId: string;
|
|
32
32
|
projectName: string;
|
|
33
33
|
templateRevision: string;
|
|
34
|
-
provenance:
|
|
34
|
+
provenance: KxmTemplateProvenance;
|
|
35
35
|
files: ReadonlyMap<string, Buffer>;
|
|
36
36
|
values: ReadonlyMap<string, JsonObject>;
|
|
37
37
|
}
|
|
@@ -49,7 +49,7 @@ function canonicalJson(value: JsonValue): string {
|
|
|
49
49
|
.map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key] as JsonValue)}`).join(",")}}`;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
export function
|
|
52
|
+
export function kxmContentSha256(input: string | Uint8Array): string {
|
|
53
53
|
return `sha256:${createHash("sha256").update(input).digest("hex")}`;
|
|
54
54
|
}
|
|
55
55
|
|
|
@@ -62,11 +62,11 @@ function authorityProjection(value: JsonObject): JsonObject {
|
|
|
62
62
|
return projected;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
export function
|
|
66
|
-
return
|
|
65
|
+
export function kxmAuthoritySha256(value: JsonObject): string {
|
|
66
|
+
return kxmContentSha256(canonicalJson(authorityProjection(value)));
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
function coreTemplate(projectId: string, projectName: string, variant:
|
|
69
|
+
function coreTemplate(projectId: string, projectName: string, variant: KxmTemplateVariant): ReadonlyMap<string, JsonObject> {
|
|
70
70
|
const files = new Map<string, JsonObject>([
|
|
71
71
|
[".kxm/project.yaml", {
|
|
72
72
|
schema: "kxm.project.v1",
|
|
@@ -182,8 +182,8 @@ function coreTemplate(projectId: string, projectName: string, variant: VnextTemp
|
|
|
182
182
|
return files;
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
-
function provenanceRevision(files: readonly
|
|
186
|
-
return
|
|
185
|
+
function provenanceRevision(files: readonly KxmTemplateFileRecord[]): string {
|
|
186
|
+
return kxmContentSha256(canonicalJson(files.map((file) => ({
|
|
187
187
|
path: file.path,
|
|
188
188
|
sha256: file.sha256,
|
|
189
189
|
authoritySha256: file.authoritySha256,
|
|
@@ -191,11 +191,11 @@ function provenanceRevision(files: readonly VnextTemplateFileRecord[]): string {
|
|
|
191
191
|
}))));
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
-
export function
|
|
194
|
+
export function renderKxmTemplate(
|
|
195
195
|
projectId: string,
|
|
196
196
|
projectName: string,
|
|
197
|
-
variant:
|
|
198
|
-
):
|
|
197
|
+
variant: KxmTemplateVariant = CURRENT_KXM_TEMPLATE_VARIANT,
|
|
198
|
+
): KxmRenderedTemplate {
|
|
199
199
|
const values = coreTemplate(projectId, projectName, variant);
|
|
200
200
|
const coreFiles = new Map<string, Buffer>();
|
|
201
201
|
const records = [...values.entries()].map(([path, value]) => {
|
|
@@ -203,22 +203,22 @@ export function renderVnextTemplate(
|
|
|
203
203
|
coreFiles.set(path, bytes);
|
|
204
204
|
return {
|
|
205
205
|
path,
|
|
206
|
-
sha256:
|
|
207
|
-
authoritySha256:
|
|
206
|
+
sha256: kxmContentSha256(bytes),
|
|
207
|
+
authoritySha256: kxmAuthoritySha256(value),
|
|
208
208
|
bytes: bytes.byteLength,
|
|
209
|
-
} satisfies
|
|
209
|
+
} satisfies KxmTemplateFileRecord;
|
|
210
210
|
}).sort((left, right) => compareCodeUnits(left.path, right.path));
|
|
211
211
|
const templateRevision = provenanceRevision(records);
|
|
212
|
-
const provenance:
|
|
212
|
+
const provenance: KxmTemplateProvenance = {
|
|
213
213
|
schema: "kxm.template-provenance.v1",
|
|
214
|
-
templateId:
|
|
214
|
+
templateId: KXM_TEMPLATE_ID,
|
|
215
215
|
templateRevision,
|
|
216
216
|
inputs: { projectId, projectName },
|
|
217
217
|
files: records,
|
|
218
218
|
};
|
|
219
219
|
const fileEntries: [string, Buffer][] = [...coreFiles.entries()];
|
|
220
220
|
fileEntries.push([
|
|
221
|
-
|
|
221
|
+
KXM_TEMPLATE_PROVENANCE_PATH,
|
|
222
222
|
Buffer.from(stringify(provenance as unknown as JsonObject, { lineWidth: 0 }), "utf8"),
|
|
223
223
|
]);
|
|
224
224
|
fileEntries.sort(([left], [right]) => compareCodeUnits(left, right));
|
|
@@ -226,18 +226,18 @@ export function renderVnextTemplate(
|
|
|
226
226
|
return { projectId, projectName, templateRevision, provenance, files, values };
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
-
export function validatedTemplateProvenance(value: JsonObject):
|
|
230
|
-
if (value.schema !== "kxm.template-provenance.v1" || value.templateId !==
|
|
231
|
-
const candidate = value as unknown as
|
|
229
|
+
export function validatedTemplateProvenance(value: JsonObject): KxmTemplateProvenance | undefined {
|
|
230
|
+
if (value.schema !== "kxm.template-provenance.v1" || value.templateId !== KXM_TEMPLATE_ID) return undefined;
|
|
231
|
+
const candidate = value as unknown as KxmTemplateProvenance;
|
|
232
232
|
return provenanceRevision(candidate.files) === candidate.templateRevision ? candidate : undefined;
|
|
233
233
|
}
|
|
234
234
|
|
|
235
235
|
/** Resolve provenance only when its complete manifest equals a built-in baseline. */
|
|
236
|
-
export function
|
|
236
|
+
export function resolveKxmTemplateBaseline(value: JsonObject): KxmTemplateProvenance | undefined {
|
|
237
237
|
const candidate = validatedTemplateProvenance(value);
|
|
238
238
|
if (!candidate) return undefined;
|
|
239
|
-
for (const variant of
|
|
240
|
-
const known =
|
|
239
|
+
for (const variant of SUPPORTED_KXM_TEMPLATE_VARIANTS) {
|
|
240
|
+
const known = renderKxmTemplate(candidate.inputs.projectId, candidate.inputs.projectName, variant).provenance;
|
|
241
241
|
if (known.templateRevision === candidate.templateRevision
|
|
242
242
|
&& canonicalJson(known as unknown as JsonObject) === canonicalJson(candidate as unknown as JsonObject)) {
|
|
243
243
|
return candidate;
|