@kontextmind/kxm 0.7.51 → 0.7.53
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 +60 -8
- package/docs/configuration.md +1 -1
- package/docs/contracts/migration.md +39 -1
- package/docs/operations.md +195 -19
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/cli.js +105 -22
- package/plugins/kxm/dist/extension.js +4 -3
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/dist/runtime-supervisor.js +6 -81
- package/plugins/kxm/dist/runtime.js +6 -81
- package/plugins/kxm/dist/server.js +6 -105
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/src/cli/hub.ts +84 -5
- package/plugins/kxm/src/database.ts +9 -21
- package/plugins/kxm/src/external-effects.ts +37 -27
- package/plugins/kxm/src/hub-binding.ts +22 -0
- package/plugins/kxm/src/hub-env.ts +28 -0
- package/plugins/kxm/src/intake.ts +6 -11
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/runtime-store.ts +0 -69
- package/plugins/kxm/src/session-work.ts +9 -3
- package/plugins/kxm/src/store.ts +0 -92
|
@@ -17121,7 +17121,7 @@ async function deliverInboxNotification(messageId, delivered, notify) {
|
|
|
17121
17121
|
}
|
|
17122
17122
|
|
|
17123
17123
|
// plugins/kxm/src/mcp-server.ts
|
|
17124
|
-
var VERSION = "0.7.
|
|
17124
|
+
var VERSION = "0.7.53";
|
|
17125
17125
|
var inbox = /* @__PURE__ */ new Map();
|
|
17126
17126
|
var notifiedInbox = /* @__PURE__ */ new Set();
|
|
17127
17127
|
var meshClient;
|
|
@@ -17612,20 +17612,11 @@ function openDatabase(file, description, spec) {
|
|
|
17612
17612
|
database.exec(spec.schema);
|
|
17613
17613
|
database.exec(`PRAGMA user_version = ${spec.version}`);
|
|
17614
17614
|
} else if (version < spec.version) {
|
|
17615
|
-
|
|
17616
|
-
|
|
17617
|
-
|
|
17618
|
-
|
|
17619
|
-
|
|
17620
|
-
"runtime_schema_outdated",
|
|
17621
|
-
file,
|
|
17622
|
-
`${description} schema version ${version} is older than ${spec.version}; no migration lane, backup and restore remain E6`
|
|
17623
|
-
);
|
|
17624
|
-
}
|
|
17625
|
-
step.migrate(database);
|
|
17626
|
-
currentVersion = step.toVersion;
|
|
17627
|
-
database.exec(`PRAGMA user_version = ${currentVersion}`);
|
|
17628
|
-
}
|
|
17615
|
+
throw databaseError(
|
|
17616
|
+
"runtime_schema_outdated",
|
|
17617
|
+
file,
|
|
17618
|
+
`${description} is schema version ${version}; this build requires ${spec.version}. Delete the state file (or re-run \`kxm init\`) to start fresh \u2014 upgrading old state in place is deliberately unsupported`
|
|
17619
|
+
);
|
|
17629
17620
|
}
|
|
17630
17621
|
if (spec.tables) {
|
|
17631
17622
|
verifyExpectedTables(database, file, description, spec.tables);
|
|
@@ -18270,71 +18261,6 @@ CREATE TABLE project_controls (
|
|
|
18270
18261
|
record TEXT NOT NULL
|
|
18271
18262
|
) STRICT;
|
|
18272
18263
|
`;
|
|
18273
|
-
var COORDINATOR_INTAKE_DDL = `
|
|
18274
|
-
CREATE TABLE IF NOT EXISTS coordinators (
|
|
18275
|
-
coordinator_id TEXT PRIMARY KEY,
|
|
18276
|
-
project_id TEXT NOT NULL,
|
|
18277
|
-
role TEXT NOT NULL,
|
|
18278
|
-
channel TEXT NOT NULL,
|
|
18279
|
-
ceiling_hash TEXT NOT NULL,
|
|
18280
|
-
config_revision TEXT NOT NULL,
|
|
18281
|
-
bound_at TEXT NOT NULL,
|
|
18282
|
-
schema TEXT NOT NULL,
|
|
18283
|
-
record TEXT NOT NULL
|
|
18284
|
-
) STRICT;
|
|
18285
|
-
CREATE UNIQUE INDEX IF NOT EXISTS coordinators_slot ON coordinators(project_id, role, channel);
|
|
18286
|
-
CREATE TABLE IF NOT EXISTS intake_messages (
|
|
18287
|
-
message_id TEXT PRIMARY KEY,
|
|
18288
|
-
project_id TEXT NOT NULL,
|
|
18289
|
-
coordinator_id TEXT NOT NULL,
|
|
18290
|
-
idempotency_key TEXT NOT NULL,
|
|
18291
|
-
content_hash TEXT NOT NULL,
|
|
18292
|
-
received_at TEXT NOT NULL,
|
|
18293
|
-
dispatch_state TEXT NOT NULL CHECK (dispatch_state IN ('ready','held_paused','admitted','refused')),
|
|
18294
|
-
schema TEXT NOT NULL,
|
|
18295
|
-
record TEXT NOT NULL
|
|
18296
|
-
) STRICT;
|
|
18297
|
-
CREATE UNIQUE INDEX IF NOT EXISTS intake_idempotency ON intake_messages(project_id, coordinator_id, idempotency_key);
|
|
18298
|
-
CREATE INDEX IF NOT EXISTS intake_dispatch ON intake_messages(project_id, dispatch_state, received_at, message_id);
|
|
18299
|
-
CREATE TABLE IF NOT EXISTS project_controls (
|
|
18300
|
-
project_id TEXT PRIMARY KEY,
|
|
18301
|
-
paused INTEGER NOT NULL CHECK (paused IN (0, 1)),
|
|
18302
|
-
reason TEXT,
|
|
18303
|
-
updated_at TEXT NOT NULL,
|
|
18304
|
-
actor TEXT NOT NULL,
|
|
18305
|
-
schema TEXT NOT NULL,
|
|
18306
|
-
record TEXT NOT NULL
|
|
18307
|
-
) STRICT;
|
|
18308
|
-
`;
|
|
18309
|
-
var DRIVE_RECEIPTS_DDL = `
|
|
18310
|
-
CREATE TABLE IF NOT EXISTS drive_receipts (
|
|
18311
|
-
drive_id TEXT PRIMARY KEY,
|
|
18312
|
-
run_id TEXT NOT NULL,
|
|
18313
|
-
project_id TEXT NOT NULL,
|
|
18314
|
-
opened_sequence INTEGER NOT NULL,
|
|
18315
|
-
last_sequence INTEGER NOT NULL,
|
|
18316
|
-
closed_at TEXT NOT NULL,
|
|
18317
|
-
schema TEXT NOT NULL,
|
|
18318
|
-
receipt TEXT NOT NULL
|
|
18319
|
-
) STRICT;
|
|
18320
|
-
CREATE INDEX IF NOT EXISTS drive_receipts_run ON drive_receipts(run_id);
|
|
18321
|
-
`;
|
|
18322
|
-
var EVENT_STORE_MIGRATIONS = [
|
|
18323
|
-
{
|
|
18324
|
-
fromVersion: 3,
|
|
18325
|
-
toVersion: 4,
|
|
18326
|
-
migrate(database) {
|
|
18327
|
-
database.exec(DRIVE_RECEIPTS_DDL);
|
|
18328
|
-
}
|
|
18329
|
-
},
|
|
18330
|
-
{
|
|
18331
|
-
fromVersion: 4,
|
|
18332
|
-
toVersion: 5,
|
|
18333
|
-
migrate(database) {
|
|
18334
|
-
database.exec(COORDINATOR_INTAKE_DDL);
|
|
18335
|
-
}
|
|
18336
|
-
}
|
|
18337
|
-
];
|
|
18338
18264
|
var KxmRunEventStore = class {
|
|
18339
18265
|
path;
|
|
18340
18266
|
database;
|
|
@@ -18343,8 +18269,7 @@ var KxmRunEventStore = class {
|
|
|
18343
18269
|
this.database = openDatabase(this.path, "run event store", {
|
|
18344
18270
|
schema: EVENT_STORE_SCHEMA,
|
|
18345
18271
|
version: KXM_EVENT_STORE_SCHEMA_VERSION,
|
|
18346
|
-
tables: EVENT_STORE_TABLES
|
|
18347
|
-
migrations: EVENT_STORE_MIGRATIONS
|
|
18272
|
+
tables: EVENT_STORE_TABLES
|
|
18348
18273
|
});
|
|
18349
18274
|
}
|
|
18350
18275
|
close() {
|
|
@@ -17783,20 +17783,11 @@ function openDatabase(file, description, spec) {
|
|
|
17783
17783
|
database.exec(spec.schema);
|
|
17784
17784
|
database.exec(`PRAGMA user_version = ${spec.version}`);
|
|
17785
17785
|
} else if (version < spec.version) {
|
|
17786
|
-
|
|
17787
|
-
|
|
17788
|
-
|
|
17789
|
-
|
|
17790
|
-
|
|
17791
|
-
"runtime_schema_outdated",
|
|
17792
|
-
file,
|
|
17793
|
-
`${description} schema version ${version} is older than ${spec.version}; no migration lane, backup and restore remain E6`
|
|
17794
|
-
);
|
|
17795
|
-
}
|
|
17796
|
-
step.migrate(database);
|
|
17797
|
-
currentVersion = step.toVersion;
|
|
17798
|
-
database.exec(`PRAGMA user_version = ${currentVersion}`);
|
|
17799
|
-
}
|
|
17786
|
+
throw databaseError(
|
|
17787
|
+
"runtime_schema_outdated",
|
|
17788
|
+
file,
|
|
17789
|
+
`${description} is schema version ${version}; this build requires ${spec.version}. Delete the state file (or re-run \`kxm init\`) to start fresh \u2014 upgrading old state in place is deliberately unsupported`
|
|
17790
|
+
);
|
|
17800
17791
|
}
|
|
17801
17792
|
if (spec.tables) {
|
|
17802
17793
|
verifyExpectedTables(database, file, description, spec.tables);
|
|
@@ -18710,71 +18701,6 @@ CREATE TABLE project_controls (
|
|
|
18710
18701
|
record TEXT NOT NULL
|
|
18711
18702
|
) STRICT;
|
|
18712
18703
|
`;
|
|
18713
|
-
var COORDINATOR_INTAKE_DDL = `
|
|
18714
|
-
CREATE TABLE IF NOT EXISTS coordinators (
|
|
18715
|
-
coordinator_id TEXT PRIMARY KEY,
|
|
18716
|
-
project_id TEXT NOT NULL,
|
|
18717
|
-
role TEXT NOT NULL,
|
|
18718
|
-
channel TEXT NOT NULL,
|
|
18719
|
-
ceiling_hash TEXT NOT NULL,
|
|
18720
|
-
config_revision TEXT NOT NULL,
|
|
18721
|
-
bound_at TEXT NOT NULL,
|
|
18722
|
-
schema TEXT NOT NULL,
|
|
18723
|
-
record TEXT NOT NULL
|
|
18724
|
-
) STRICT;
|
|
18725
|
-
CREATE UNIQUE INDEX IF NOT EXISTS coordinators_slot ON coordinators(project_id, role, channel);
|
|
18726
|
-
CREATE TABLE IF NOT EXISTS intake_messages (
|
|
18727
|
-
message_id TEXT PRIMARY KEY,
|
|
18728
|
-
project_id TEXT NOT NULL,
|
|
18729
|
-
coordinator_id TEXT NOT NULL,
|
|
18730
|
-
idempotency_key TEXT NOT NULL,
|
|
18731
|
-
content_hash TEXT NOT NULL,
|
|
18732
|
-
received_at TEXT NOT NULL,
|
|
18733
|
-
dispatch_state TEXT NOT NULL CHECK (dispatch_state IN ('ready','held_paused','admitted','refused')),
|
|
18734
|
-
schema TEXT NOT NULL,
|
|
18735
|
-
record TEXT NOT NULL
|
|
18736
|
-
) STRICT;
|
|
18737
|
-
CREATE UNIQUE INDEX IF NOT EXISTS intake_idempotency ON intake_messages(project_id, coordinator_id, idempotency_key);
|
|
18738
|
-
CREATE INDEX IF NOT EXISTS intake_dispatch ON intake_messages(project_id, dispatch_state, received_at, message_id);
|
|
18739
|
-
CREATE TABLE IF NOT EXISTS project_controls (
|
|
18740
|
-
project_id TEXT PRIMARY KEY,
|
|
18741
|
-
paused INTEGER NOT NULL CHECK (paused IN (0, 1)),
|
|
18742
|
-
reason TEXT,
|
|
18743
|
-
updated_at TEXT NOT NULL,
|
|
18744
|
-
actor TEXT NOT NULL,
|
|
18745
|
-
schema TEXT NOT NULL,
|
|
18746
|
-
record TEXT NOT NULL
|
|
18747
|
-
) STRICT;
|
|
18748
|
-
`;
|
|
18749
|
-
var DRIVE_RECEIPTS_DDL = `
|
|
18750
|
-
CREATE TABLE IF NOT EXISTS drive_receipts (
|
|
18751
|
-
drive_id TEXT PRIMARY KEY,
|
|
18752
|
-
run_id TEXT NOT NULL,
|
|
18753
|
-
project_id TEXT NOT NULL,
|
|
18754
|
-
opened_sequence INTEGER NOT NULL,
|
|
18755
|
-
last_sequence INTEGER NOT NULL,
|
|
18756
|
-
closed_at TEXT NOT NULL,
|
|
18757
|
-
schema TEXT NOT NULL,
|
|
18758
|
-
receipt TEXT NOT NULL
|
|
18759
|
-
) STRICT;
|
|
18760
|
-
CREATE INDEX IF NOT EXISTS drive_receipts_run ON drive_receipts(run_id);
|
|
18761
|
-
`;
|
|
18762
|
-
var EVENT_STORE_MIGRATIONS = [
|
|
18763
|
-
{
|
|
18764
|
-
fromVersion: 3,
|
|
18765
|
-
toVersion: 4,
|
|
18766
|
-
migrate(database) {
|
|
18767
|
-
database.exec(DRIVE_RECEIPTS_DDL);
|
|
18768
|
-
}
|
|
18769
|
-
},
|
|
18770
|
-
{
|
|
18771
|
-
fromVersion: 4,
|
|
18772
|
-
toVersion: 5,
|
|
18773
|
-
migrate(database) {
|
|
18774
|
-
database.exec(COORDINATOR_INTAKE_DDL);
|
|
18775
|
-
}
|
|
18776
|
-
}
|
|
18777
|
-
];
|
|
18778
18704
|
var KxmRunEventStore = class {
|
|
18779
18705
|
path;
|
|
18780
18706
|
database;
|
|
@@ -18783,8 +18709,7 @@ var KxmRunEventStore = class {
|
|
|
18783
18709
|
this.database = openDatabase(this.path, "run event store", {
|
|
18784
18710
|
schema: EVENT_STORE_SCHEMA,
|
|
18785
18711
|
version: KXM_EVENT_STORE_SCHEMA_VERSION,
|
|
18786
|
-
tables: EVENT_STORE_TABLES
|
|
18787
|
-
migrations: EVENT_STORE_MIGRATIONS
|
|
18712
|
+
tables: EVENT_STORE_TABLES
|
|
18788
18713
|
});
|
|
18789
18714
|
}
|
|
18790
18715
|
close() {
|
|
@@ -11504,20 +11504,11 @@ function openDatabase(file, description, spec) {
|
|
|
11504
11504
|
database.exec(spec.schema);
|
|
11505
11505
|
database.exec(`PRAGMA user_version = ${spec.version}`);
|
|
11506
11506
|
} else if (version < spec.version) {
|
|
11507
|
-
|
|
11508
|
-
|
|
11509
|
-
|
|
11510
|
-
|
|
11511
|
-
|
|
11512
|
-
"runtime_schema_outdated",
|
|
11513
|
-
file,
|
|
11514
|
-
`${description} schema version ${version} is older than ${spec.version}; no migration lane, backup and restore remain E6`
|
|
11515
|
-
);
|
|
11516
|
-
}
|
|
11517
|
-
step.migrate(database);
|
|
11518
|
-
currentVersion = step.toVersion;
|
|
11519
|
-
database.exec(`PRAGMA user_version = ${currentVersion}`);
|
|
11520
|
-
}
|
|
11507
|
+
throw databaseError(
|
|
11508
|
+
"runtime_schema_outdated",
|
|
11509
|
+
file,
|
|
11510
|
+
`${description} is schema version ${version}; this build requires ${spec.version}. Delete the state file (or re-run \`kxm init\`) to start fresh \u2014 upgrading old state in place is deliberately unsupported`
|
|
11511
|
+
);
|
|
11521
11512
|
}
|
|
11522
11513
|
if (spec.tables) {
|
|
11523
11514
|
verifyExpectedTables(database, file, description, spec.tables);
|
|
@@ -11683,100 +11674,10 @@ var HUB_STORE_SCHEMA_V3 = `
|
|
|
11683
11674
|
) STRICT;
|
|
11684
11675
|
CREATE INDEX IF NOT EXISTS context_items_project ON context_items(project);
|
|
11685
11676
|
`;
|
|
11686
|
-
var HUB_STORE_MIGRATIONS = Object.freeze([
|
|
11687
|
-
{
|
|
11688
|
-
fromVersion: 1,
|
|
11689
|
-
toVersion: 2,
|
|
11690
|
-
migrate: (db) => {
|
|
11691
|
-
db.exec(`
|
|
11692
|
-
CREATE TABLE IF NOT EXISTS agents (
|
|
11693
|
-
id TEXT PRIMARY KEY,
|
|
11694
|
-
record TEXT NOT NULL
|
|
11695
|
-
) STRICT;
|
|
11696
|
-
CREATE TABLE IF NOT EXISTS messages (
|
|
11697
|
-
id TEXT PRIMARY KEY,
|
|
11698
|
-
record TEXT NOT NULL
|
|
11699
|
-
) STRICT;
|
|
11700
|
-
CREATE TABLE IF NOT EXISTS workflow_runs (
|
|
11701
|
-
id TEXT PRIMARY KEY,
|
|
11702
|
-
definition_id TEXT NOT NULL,
|
|
11703
|
-
delivery_id TEXT NOT NULL,
|
|
11704
|
-
record TEXT NOT NULL,
|
|
11705
|
-
UNIQUE(definition_id, delivery_id)
|
|
11706
|
-
) STRICT;
|
|
11707
|
-
CREATE TABLE IF NOT EXISTS workflow_journal (
|
|
11708
|
-
id TEXT PRIMARY KEY,
|
|
11709
|
-
run_id TEXT NOT NULL,
|
|
11710
|
-
category TEXT NOT NULL,
|
|
11711
|
-
area TEXT NOT NULL,
|
|
11712
|
-
record TEXT NOT NULL
|
|
11713
|
-
) STRICT;
|
|
11714
|
-
CREATE INDEX IF NOT EXISTS workflow_journal_run_id ON workflow_journal(run_id);
|
|
11715
|
-
`);
|
|
11716
|
-
}
|
|
11717
|
-
},
|
|
11718
|
-
{
|
|
11719
|
-
fromVersion: 2,
|
|
11720
|
-
toVersion: 3,
|
|
11721
|
-
migrate: (db) => {
|
|
11722
|
-
db.exec(`
|
|
11723
|
-
CREATE TABLE IF NOT EXISTS agents (
|
|
11724
|
-
id TEXT PRIMARY KEY,
|
|
11725
|
-
record TEXT NOT NULL
|
|
11726
|
-
) STRICT;
|
|
11727
|
-
CREATE TABLE IF NOT EXISTS messages (
|
|
11728
|
-
id TEXT PRIMARY KEY,
|
|
11729
|
-
record TEXT NOT NULL
|
|
11730
|
-
) STRICT;
|
|
11731
|
-
CREATE TABLE IF NOT EXISTS workflow_runs (
|
|
11732
|
-
id TEXT PRIMARY KEY,
|
|
11733
|
-
definition_id TEXT NOT NULL,
|
|
11734
|
-
delivery_id TEXT NOT NULL,
|
|
11735
|
-
record TEXT NOT NULL,
|
|
11736
|
-
UNIQUE(definition_id, delivery_id)
|
|
11737
|
-
) STRICT;
|
|
11738
|
-
CREATE TABLE IF NOT EXISTS workflow_journal (
|
|
11739
|
-
id TEXT PRIMARY KEY,
|
|
11740
|
-
run_id TEXT NOT NULL,
|
|
11741
|
-
category TEXT NOT NULL,
|
|
11742
|
-
area TEXT NOT NULL,
|
|
11743
|
-
record TEXT NOT NULL
|
|
11744
|
-
) STRICT;
|
|
11745
|
-
CREATE INDEX IF NOT EXISTS workflow_journal_run_id ON workflow_journal(run_id);
|
|
11746
|
-
CREATE TABLE IF NOT EXISTS context_items (
|
|
11747
|
-
id TEXT PRIMARY KEY,
|
|
11748
|
-
project TEXT NOT NULL,
|
|
11749
|
-
kind TEXT NOT NULL,
|
|
11750
|
-
record TEXT NOT NULL
|
|
11751
|
-
) STRICT;
|
|
11752
|
-
CREATE INDEX IF NOT EXISTS context_items_project ON context_items(project);
|
|
11753
|
-
CREATE UNIQUE INDEX IF NOT EXISTS messages_from_idempotency
|
|
11754
|
-
ON messages(
|
|
11755
|
-
json_extract(record, '$.from'),
|
|
11756
|
-
json_extract(record, '$.idempotencyKey')
|
|
11757
|
-
) WHERE json_extract(record, '$.idempotencyKey') IS NOT NULL;
|
|
11758
|
-
CREATE INDEX IF NOT EXISTS messages_to_seq
|
|
11759
|
-
ON messages(
|
|
11760
|
-
json_extract(record, '$.to'),
|
|
11761
|
-
COALESCE(json_extract(record, '$.seq'), 0)
|
|
11762
|
-
);
|
|
11763
|
-
CREATE TABLE IF NOT EXISTS consumer_cursors (
|
|
11764
|
-
agent_id TEXT PRIMARY KEY,
|
|
11765
|
-
cursor INTEGER NOT NULL
|
|
11766
|
-
) STRICT;
|
|
11767
|
-
CREATE TABLE IF NOT EXISTS agent_sequences (
|
|
11768
|
-
agent_id TEXT PRIMARY KEY,
|
|
11769
|
-
next_seq INTEGER NOT NULL
|
|
11770
|
-
) STRICT;
|
|
11771
|
-
`);
|
|
11772
|
-
}
|
|
11773
|
-
}
|
|
11774
|
-
]);
|
|
11775
11677
|
var HUB_STORE_SCHEMA_SPEC = Object.freeze({
|
|
11776
11678
|
schema: HUB_STORE_SCHEMA_V3,
|
|
11777
11679
|
version: HUB_STORE_SCHEMA_VERSION,
|
|
11778
|
-
tables: HUB_STORE_TABLES
|
|
11779
|
-
migrations: HUB_STORE_MIGRATIONS
|
|
11680
|
+
tables: HUB_STORE_TABLES
|
|
11780
11681
|
});
|
|
11781
11682
|
var MessageMap = class extends Map {
|
|
11782
11683
|
store;
|
package/plugins/kxm/package.json
CHANGED
|
@@ -3,7 +3,7 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync
|
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
4
|
import { redactSecrets } from "../redact.ts";
|
|
5
5
|
import { defaultProjectName } from "../project-name.ts";
|
|
6
|
-
import { resolveClientHubAuthToken } from "../hub-env.ts";
|
|
6
|
+
import { hasClientHubCredential, resolveClientHubAuthToken } from "../hub-env.ts";
|
|
7
7
|
import { agentWorker, type Worker } from "../envelope.ts";
|
|
8
8
|
import { MESH_TUI_PANELS, runMeshTui, type MeshTuiPanel } from "../tui.ts";
|
|
9
9
|
import { formatSessionBriefText, loadSessionBriefAsync, type SessionHubStatus } from "../session-work.ts";
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
HubBindingError,
|
|
13
13
|
hubBindingFile,
|
|
14
14
|
probeHubHealth,
|
|
15
|
+
hubBindingScope,
|
|
15
16
|
readHubBinding,
|
|
16
17
|
removeHubBinding,
|
|
17
18
|
validateHubUrl,
|
|
@@ -104,8 +105,22 @@ export async function refreshKxmUpdateNotice(runtime: Runtime, config?: KxmUpdat
|
|
|
104
105
|
export async function cmdStatus(runtime: Runtime): Promise<number> {
|
|
105
106
|
const health = await hubGet(`${runtime.serverUrl}/health`, runtime.fetchImpl);
|
|
106
107
|
const ready = await hubGet(`${runtime.serverUrl}/ready`, runtime.fetchImpl);
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
// Scope on the status line deliberately: "attached across a network" and "attached on
|
|
109
|
+
// this box" are otherwise indistinguishable, and only one of them ships a token.
|
|
110
|
+
// Scope is a property of the URL actually contacted, not of whichever file the
|
|
111
|
+
// binding came from: KXM_SERVER_URL overrides the binding, and labelling the binding
|
|
112
|
+
// while probing an override would report "loopback" about a remote request.
|
|
113
|
+
const effectiveScope = hubBindingScope(runtime.serverUrl);
|
|
114
|
+
const overridden = Boolean(runtime.boundHubUrl && runtime.boundHubUrl !== runtime.serverUrl);
|
|
115
|
+
const payload = {
|
|
116
|
+
ok: health.ok && ready.ok,
|
|
117
|
+
command: "hub view",
|
|
118
|
+
target: { url: runtime.serverUrl, scope: effectiveScope, ...(overridden ? { source: "env" } : {}) },
|
|
119
|
+
health: health.body,
|
|
120
|
+
ready: ready.body,
|
|
121
|
+
};
|
|
122
|
+
print(runtime.io, runtime.json, payload,
|
|
123
|
+
`hub health=${health.ok} ready=${ready.ok} · ${effectiveScope} hub${overridden ? " (KXM_SERVER_URL)" : ""}`);
|
|
109
124
|
return payload.ok ? 0 : 1;
|
|
110
125
|
}
|
|
111
126
|
|
|
@@ -191,6 +206,10 @@ export function formatHubBindHealth(health: HubHealth): string {
|
|
|
191
206
|
return "health=unknown (no reply within 300 ms)";
|
|
192
207
|
}
|
|
193
208
|
|
|
209
|
+
/** Kept in one place so the JSON payload and the prose line cannot drift apart. */
|
|
210
|
+
const HUB_BIND_UNAUTHENTICATED_HINT =
|
|
211
|
+
"export KXM_AUTH_TOKEN (or point KXM_STATE_HOME at the hub-env record that already holds one), then re-run; the hub itself requires a token beyond loopback";
|
|
212
|
+
|
|
194
213
|
export async function cmdHubBind(runtime: Runtime, rawUrl: string): Promise<number> {
|
|
195
214
|
let url: string;
|
|
196
215
|
try {
|
|
@@ -207,14 +226,73 @@ export async function cmdHubBind(runtime: Runtime, rawUrl: string): Promise<numb
|
|
|
207
226
|
}
|
|
208
227
|
throw error;
|
|
209
228
|
}
|
|
229
|
+
const scope = hubBindingScope(url);
|
|
230
|
+
// Scope-scoped on purpose, and only for **this command**. A remote binding puts a
|
|
231
|
+
// bearer on a network path, so it is refused when nothing can authenticate it; a
|
|
232
|
+
// stored-but-unusable URL otherwise reads later like a network fault and gets debugged
|
|
233
|
+
// as one. Loopback is not consulted here because a loopback URL puts nothing on a wire —
|
|
234
|
+
// not because credentials are never resolved locally: other client paths still call
|
|
235
|
+
// `resolveClientHubAuthToken` regardless of scope, so a damaged host record can still
|
|
236
|
+
// fail `kxm peer list` on loopback. Narrowing the claim is the point; the first version
|
|
237
|
+
// of this guard checked the record *before* the scope and refused loopback binds that had
|
|
238
|
+
// always worked, which is a regression against behaviour predating this slice.
|
|
239
|
+
if (scope === "remote") {
|
|
240
|
+
// The project that will actually authenticate: a record holding only another
|
|
241
|
+
// project's token cannot authorise this one.
|
|
242
|
+
const bindProject = defaultProjectName(runtime.dirs.workdir, runtime.env) || "project";
|
|
243
|
+
let credentialReady = false;
|
|
244
|
+
try {
|
|
245
|
+
credentialReady = hasClientHubCredential(runtime.env, bindProject);
|
|
246
|
+
} catch (error) {
|
|
247
|
+
// A malformed record is a readable configuration failure, not an uncaught throw
|
|
248
|
+
// past a user-facing entry point.
|
|
249
|
+
print(
|
|
250
|
+
runtime.io,
|
|
251
|
+
runtime.json,
|
|
252
|
+
{
|
|
253
|
+
ok: false,
|
|
254
|
+
command: "hub bind",
|
|
255
|
+
error: "hub_credential_unreadable",
|
|
256
|
+
url,
|
|
257
|
+
scope,
|
|
258
|
+
nextAction: "repair_hub_env_record",
|
|
259
|
+
hint: `${error instanceof Error ? error.message : String(error)}; no binding was written`,
|
|
260
|
+
},
|
|
261
|
+
`cannot read the hub credential: ${error instanceof Error ? error.message : String(error)}`,
|
|
262
|
+
);
|
|
263
|
+
return 2;
|
|
264
|
+
}
|
|
265
|
+
if (!credentialReady) {
|
|
266
|
+
print(
|
|
267
|
+
runtime.io,
|
|
268
|
+
runtime.json,
|
|
269
|
+
{
|
|
270
|
+
ok: false,
|
|
271
|
+
command: "hub bind",
|
|
272
|
+
error: "hub_bind_unauthenticated",
|
|
273
|
+
url,
|
|
274
|
+
scope,
|
|
275
|
+
project: bindProject,
|
|
276
|
+
// The hint belongs in the payload, not only the prose line: under --json the
|
|
277
|
+
// prose is suppressed, and a refusal that names no next step gets debugged by
|
|
278
|
+
// reading source.
|
|
279
|
+
nextAction: "export_kxm_auth_token",
|
|
280
|
+
hint: `${HUB_BIND_UNAUTHENTICATED_HINT} (needs a token for project ${bindProject})`,
|
|
281
|
+
},
|
|
282
|
+
`refusing to bind remote hub ${url} with no credential for project ${bindProject}; ${HUB_BIND_UNAUTHENTICATED_HINT}`,
|
|
283
|
+
);
|
|
284
|
+
return 2;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
210
287
|
const file = hubBindingFile(runtime.env);
|
|
211
288
|
if (runtime.dryRun) {
|
|
212
|
-
print(runtime.io, runtime.json, { ok: true, command: "hub bind", dryRun: true, url, file }, `would bind hub ${url}`);
|
|
289
|
+
print(runtime.io, runtime.json, { ok: true, command: "hub bind", dryRun: true, url, scope, file }, `would bind hub ${url} (${scope})`);
|
|
213
290
|
return 0;
|
|
214
291
|
}
|
|
215
292
|
writeHubBinding({ schema: HUB_BINDING_SCHEMA, url, boundAt: new Date().toISOString() }, runtime.env);
|
|
216
293
|
const { health, probeMs } = await probeHubHealth(url, runtime.fetchImpl);
|
|
217
|
-
print(runtime.io, runtime.json, { ok: true, command: "hub bind", url, file, health, probeMs },
|
|
294
|
+
print(runtime.io, runtime.json, { ok: true, command: "hub bind", url, scope, file, health, probeMs },
|
|
295
|
+
`bound hub ${url} · ${scope} · ${formatHubBindHealth(health)}${scope === "remote" ? " · token leaves this machine" : ""}`);
|
|
218
296
|
return 0;
|
|
219
297
|
}
|
|
220
298
|
|
|
@@ -540,6 +618,7 @@ export async function cmdSessionBrief(runtime: Runtime, options: { status?: bool
|
|
|
540
618
|
evidence: health === "unknown" ? "timeout" : "probed",
|
|
541
619
|
online: health === "on",
|
|
542
620
|
url: targetUrl,
|
|
621
|
+
scope: hubBindingScope(targetUrl),
|
|
543
622
|
};
|
|
544
623
|
} else {
|
|
545
624
|
hub = { state: "off", evidence: "unconfigured", online: false };
|
|
@@ -14,17 +14,10 @@ import { basename, dirname, join, resolve } from "node:path";
|
|
|
14
14
|
import { DatabaseSync } from "./sqlite.ts";
|
|
15
15
|
import { KxmConfigError, type KxmConfigIssue } from "./project-config.ts";
|
|
16
16
|
|
|
17
|
-
export interface DatabaseMigrationStep {
|
|
18
|
-
fromVersion: number;
|
|
19
|
-
toVersion: number;
|
|
20
|
-
migrate: (database: DatabaseSync) => void;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
17
|
export interface DatabaseSchemaSpec {
|
|
24
18
|
schema: string;
|
|
25
19
|
version: number;
|
|
26
20
|
tables?: Readonly<Record<string, readonly string[]>>;
|
|
27
|
-
migrations?: readonly DatabaseMigrationStep[];
|
|
28
21
|
timeoutMs?: number;
|
|
29
22
|
}
|
|
30
23
|
|
|
@@ -179,20 +172,15 @@ export function openDatabase(file: string, description: string, spec: DatabaseSc
|
|
|
179
172
|
database.exec(spec.schema);
|
|
180
173
|
database.exec(`PRAGMA user_version = ${spec.version}`);
|
|
181
174
|
} else if (version < spec.version) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
step.migrate(database);
|
|
193
|
-
currentVersion = step.toVersion;
|
|
194
|
-
database.exec(`PRAGMA user_version = ${currentVersion}`);
|
|
195
|
-
}
|
|
175
|
+
// No migration lanes. This is a single-operator tool: an older database is
|
|
176
|
+
// re-initialised, not upgraded in place, and the code never carries two schema
|
|
177
|
+
// shapes at once. Silently accepting an older file would mean every query has to
|
|
178
|
+
// work against shapes it no longer tests.
|
|
179
|
+
throw databaseError(
|
|
180
|
+
"runtime_schema_outdated",
|
|
181
|
+
file,
|
|
182
|
+
`${description} is schema version ${version}; this build requires ${spec.version}. Delete the state file (or re-run \`kxm init\`) to start fresh — upgrading old state in place is deliberately unsupported`,
|
|
183
|
+
);
|
|
196
184
|
}
|
|
197
185
|
|
|
198
186
|
if (spec.tables) {
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
lstatSync,
|
|
17
17
|
} from "node:fs";
|
|
18
18
|
import { dirname, join, resolve } from "node:path";
|
|
19
|
+
import { openDatabase } from "./database.ts";
|
|
19
20
|
import { spawnSync } from "node:child_process";
|
|
20
21
|
|
|
21
22
|
export const EXTERNAL_EFFECT_SCHEMA = "kxm.external-effect-receipt.v1" as const;
|
|
@@ -114,6 +115,33 @@ export function computeEffectKey(
|
|
|
114
115
|
return `eff_${createHash("sha256").update(raw).digest("hex").slice(0, 16)}`;
|
|
115
116
|
}
|
|
116
117
|
|
|
118
|
+
const EXTERNAL_EFFECTS_SCHEMA_VERSION = 1;
|
|
119
|
+
|
|
120
|
+
const EXTERNAL_EFFECTS_DDL = `
|
|
121
|
+
CREATE TABLE IF NOT EXISTS external_effects (
|
|
122
|
+
effect_key TEXT PRIMARY KEY,
|
|
123
|
+
run_id TEXT NOT NULL,
|
|
124
|
+
step_id TEXT NOT NULL,
|
|
125
|
+
attempt_id TEXT NOT NULL,
|
|
126
|
+
action_kind TEXT NOT NULL,
|
|
127
|
+
target_ref TEXT NOT NULL,
|
|
128
|
+
status TEXT NOT NULL,
|
|
129
|
+
payload_hash TEXT NOT NULL,
|
|
130
|
+
receipt_payload TEXT NOT NULL,
|
|
131
|
+
executed_at TEXT NOT NULL,
|
|
132
|
+
last_heartbeat_at TEXT,
|
|
133
|
+
completed_at TEXT
|
|
134
|
+
);
|
|
135
|
+
CREATE INDEX IF NOT EXISTS idx_ext_effects_run ON external_effects(run_id);
|
|
136
|
+
`;
|
|
137
|
+
|
|
138
|
+
const EXTERNAL_EFFECTS_SHAPE = {
|
|
139
|
+
external_effects: [
|
|
140
|
+
"effect_key", "run_id", "step_id", "attempt_id", "action_kind", "target_ref",
|
|
141
|
+
"status", "payload_hash", "receipt_payload", "executed_at", "last_heartbeat_at", "completed_at",
|
|
142
|
+
],
|
|
143
|
+
} as const;
|
|
144
|
+
|
|
117
145
|
export class ExternalEffectsLedger {
|
|
118
146
|
private db: DatabaseSync;
|
|
119
147
|
|
|
@@ -121,33 +149,15 @@ export class ExternalEffectsLedger {
|
|
|
121
149
|
if (dbPath !== ":memory:") {
|
|
122
150
|
mkdirSync(dirname(dbPath), { recursive: true });
|
|
123
151
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
step_id TEXT NOT NULL,
|
|
134
|
-
attempt_id TEXT NOT NULL,
|
|
135
|
-
action_kind TEXT NOT NULL,
|
|
136
|
-
target_ref TEXT NOT NULL,
|
|
137
|
-
status TEXT NOT NULL,
|
|
138
|
-
payload_hash TEXT NOT NULL,
|
|
139
|
-
receipt_payload TEXT NOT NULL,
|
|
140
|
-
executed_at TEXT NOT NULL,
|
|
141
|
-
last_heartbeat_at TEXT,
|
|
142
|
-
completed_at TEXT
|
|
143
|
-
);
|
|
144
|
-
CREATE INDEX IF NOT EXISTS idx_ext_effects_run ON external_effects(run_id);
|
|
145
|
-
`);
|
|
146
|
-
try {
|
|
147
|
-
this.db.exec(`ALTER TABLE external_effects ADD COLUMN last_heartbeat_at TEXT;`);
|
|
148
|
-
} catch {
|
|
149
|
-
// Column already present in schema
|
|
150
|
-
}
|
|
152
|
+
// Goes through the shared opener, not a bare `new DatabaseSync`: that is what gives this
|
|
153
|
+
// store the same contract as every other one — path checks, WAL, the `user_version` gate
|
|
154
|
+
// that refuses an older stamp outright, and the column-shape check that turns a missing
|
|
155
|
+
// column into `runtime_schema_shape_invalid` here instead of a bare SQL error on first use.
|
|
156
|
+
this.db = openDatabase(dbPath, "external effects ledger", {
|
|
157
|
+
schema: EXTERNAL_EFFECTS_DDL,
|
|
158
|
+
version: EXTERNAL_EFFECTS_SCHEMA_VERSION,
|
|
159
|
+
tables: EXTERNAL_EFFECTS_SHAPE,
|
|
160
|
+
});
|
|
151
161
|
}
|
|
152
162
|
|
|
153
163
|
/**
|
|
@@ -7,6 +7,15 @@ export const HUB_HEALTH_PROBE_MS = 300;
|
|
|
7
7
|
|
|
8
8
|
export type HubHealth = "on" | "off" | "unknown";
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Where a bound hub sits relative to this machine — a trust question, not a cosmetic one.
|
|
12
|
+
* A loopback URL never puts a bearer on a network; a remote URL means the operator chose
|
|
13
|
+
* to. `kxm hub bind` therefore refuses a remote URL it cannot authenticate, mirroring the
|
|
14
|
+
* rule the hub applies to its own listener ("KXM_AUTH_TOKEN is required when binding
|
|
15
|
+
* beyond localhost").
|
|
16
|
+
*/
|
|
17
|
+
export type HubBindingScope = "loopback" | "remote";
|
|
18
|
+
|
|
10
19
|
export interface HubBindingRecord {
|
|
11
20
|
schema: typeof HUB_BINDING_SCHEMA;
|
|
12
21
|
url: string;
|
|
@@ -62,6 +71,19 @@ export function validateHubUrl(raw: string): string {
|
|
|
62
71
|
return parsed.href.replace(/\/$/, "");
|
|
63
72
|
}
|
|
64
73
|
|
|
74
|
+
/** Loopback literals only; `0.0.0.0`, a LAN address or a hostname are all remote. */
|
|
75
|
+
export function hubBindingScope(url: string): HubBindingScope {
|
|
76
|
+
let host: string;
|
|
77
|
+
try {
|
|
78
|
+
host = new URL(url).hostname.toLowerCase();
|
|
79
|
+
} catch {
|
|
80
|
+
return "remote";
|
|
81
|
+
}
|
|
82
|
+
if (host === "localhost" || host === "::1" || host === "[::1]" || host.endsWith(".localhost")) return "loopback";
|
|
83
|
+
const v4 = /^127\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/.exec(host);
|
|
84
|
+
return v4 && [v4[1], v4[2], v4[3]].every((part) => Number(part) <= 255) ? "loopback" : "remote";
|
|
85
|
+
}
|
|
86
|
+
|
|
65
87
|
function isIsoTimestamp(value: string): boolean {
|
|
66
88
|
if (Number.isNaN(Date.parse(value))) return false;
|
|
67
89
|
return value === new Date(value).toISOString();
|