@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
|
@@ -200,6 +200,34 @@ export function resolveHubCredentials(options: ResolveHubCredentialsOptions = {}
|
|
|
200
200
|
* matches the credential precedence `kxm hub start` announces, so a hub
|
|
201
201
|
* started fresh (generated token persisted) accepts authenticated client
|
|
202
202
|
* commands without the operator exporting the token. */
|
|
203
|
+
/**
|
|
204
|
+
* Can this machine authenticate to a hub at all, following the same precedence one-shot
|
|
205
|
+
* clients use: explicit `KXM_AUTH_TOKEN`, else the persisted record's admin token, else a
|
|
206
|
+
* persisted **project** token. Read-only on purpose — a refusal has to tell the operator
|
|
207
|
+
* to configure something, not reveal that the tool quietly configured it for them.
|
|
208
|
+
*
|
|
209
|
+
* Pass `project` when the caller knows which project will authenticate. A record holding
|
|
210
|
+
* only another project's token cannot authorise this one, and a guard that counts it as a
|
|
211
|
+
* credential stores a binding that will fail exactly like the one it prevented.
|
|
212
|
+
*/
|
|
213
|
+
export function hasClientHubCredential(env: NodeJS.ProcessEnv = process.env, project?: string): boolean {
|
|
214
|
+
if (env.KXM_AUTH_TOKEN?.trim()) return true;
|
|
215
|
+
let record: HubEnvRecord | undefined;
|
|
216
|
+
try {
|
|
217
|
+
record = readHubEnvRecord(env);
|
|
218
|
+
} catch (error) {
|
|
219
|
+
// A malformed or invalid record is a configuration failure, not "no credential".
|
|
220
|
+
// Letting it surface as an uncaught throw would print neither JSON nor prose.
|
|
221
|
+
throw new HubEnvError(
|
|
222
|
+
`${error instanceof Error ? error.message : String(error)}; refusing to guess a credential — repair or remove ${hubEnvFile(env)}`,
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
if (record?.authToken?.trim()) return true;
|
|
226
|
+
const tokens = record?.projectTokens ?? {};
|
|
227
|
+
if (project !== undefined) return typeof tokens[project] === "string" && tokens[project].trim().length > 0;
|
|
228
|
+
return Object.values(tokens).some((token) => typeof token === "string" && token.trim().length > 0);
|
|
229
|
+
}
|
|
230
|
+
|
|
203
231
|
export function resolveClientHubAuthToken(env: NodeJS.ProcessEnv, project: string): string | undefined {
|
|
204
232
|
const envToken = env.KXM_AUTH_TOKEN?.trim();
|
|
205
233
|
if (envToken) return envToken;
|
|
@@ -102,20 +102,15 @@ export function kxmCeilingHash(authority: KxmCoordinatorAuthority): string {
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
/**
|
|
105
|
-
* Does
|
|
105
|
+
* Does a stored coordinator already express the ceiling being requested?
|
|
106
106
|
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
* go through here, or an upgrade makes the same row equivalent on lookup and a
|
|
112
|
-
* `coordinator_write_lost` conflict on the race path.
|
|
113
|
-
*
|
|
114
|
-
* A legacy row is returned as stored, so its `ceilingHash` is historical: a
|
|
115
|
-
* caller must not assume every persisted hash uses today's algorithm.
|
|
107
|
+
* Stored fingerprint only. Recomputing the fingerprint over the stored authority — the
|
|
108
|
+
* previous behaviour — existed to tolerate rows written before set canonicalisation, and
|
|
109
|
+
* that compatibility is gone with the single-operator decision: no migration lanes, no
|
|
110
|
+
* second schema or hash shape carried forward. A stale row is re-bound, not forgiven.
|
|
116
111
|
*/
|
|
117
112
|
function ceilingsMatch(stored: KxmCoordinatorRecord, ceilingHash: string): boolean {
|
|
118
|
-
return stored.ceilingHash === ceilingHash
|
|
113
|
+
return stored.ceilingHash === ceilingHash;
|
|
119
114
|
}
|
|
120
115
|
|
|
121
116
|
function normalizeAuthority(authority: KxmCoordinatorAuthority): KxmCoordinatorAuthority {
|
|
@@ -8,7 +8,7 @@ import { AGENT_COMMANDS_MAP, enforceToolPolicy, getMcpTools, reconcileInbox } fr
|
|
|
8
8
|
import { deliverInboxNotification } from "./inbox.ts";
|
|
9
9
|
import type { HubEvent, MessageRecord } from "./protocol.ts";
|
|
10
10
|
|
|
11
|
-
const VERSION = "0.7.
|
|
11
|
+
const VERSION = "0.7.53";
|
|
12
12
|
const inbox = new Map<string, MessageRecord>();
|
|
13
13
|
const notifiedInbox = new Set<string>();
|
|
14
14
|
let meshClient: HubClient | undefined;
|
|
@@ -782,74 +782,6 @@ CREATE TABLE project_controls (
|
|
|
782
782
|
) STRICT;
|
|
783
783
|
`;
|
|
784
784
|
|
|
785
|
-
const COORDINATOR_INTAKE_DDL = `
|
|
786
|
-
CREATE TABLE IF NOT EXISTS coordinators (
|
|
787
|
-
coordinator_id TEXT PRIMARY KEY,
|
|
788
|
-
project_id TEXT NOT NULL,
|
|
789
|
-
role TEXT NOT NULL,
|
|
790
|
-
channel TEXT NOT NULL,
|
|
791
|
-
ceiling_hash TEXT NOT NULL,
|
|
792
|
-
config_revision TEXT NOT NULL,
|
|
793
|
-
bound_at TEXT NOT NULL,
|
|
794
|
-
schema TEXT NOT NULL,
|
|
795
|
-
record TEXT NOT NULL
|
|
796
|
-
) STRICT;
|
|
797
|
-
CREATE UNIQUE INDEX IF NOT EXISTS coordinators_slot ON coordinators(project_id, role, channel);
|
|
798
|
-
CREATE TABLE IF NOT EXISTS intake_messages (
|
|
799
|
-
message_id TEXT PRIMARY KEY,
|
|
800
|
-
project_id TEXT NOT NULL,
|
|
801
|
-
coordinator_id TEXT NOT NULL,
|
|
802
|
-
idempotency_key TEXT NOT NULL,
|
|
803
|
-
content_hash TEXT NOT NULL,
|
|
804
|
-
received_at TEXT NOT NULL,
|
|
805
|
-
dispatch_state TEXT NOT NULL CHECK (dispatch_state IN ('ready','held_paused','admitted','refused')),
|
|
806
|
-
schema TEXT NOT NULL,
|
|
807
|
-
record TEXT NOT NULL
|
|
808
|
-
) STRICT;
|
|
809
|
-
CREATE UNIQUE INDEX IF NOT EXISTS intake_idempotency ON intake_messages(project_id, coordinator_id, idempotency_key);
|
|
810
|
-
CREATE INDEX IF NOT EXISTS intake_dispatch ON intake_messages(project_id, dispatch_state, received_at, message_id);
|
|
811
|
-
CREATE TABLE IF NOT EXISTS project_controls (
|
|
812
|
-
project_id TEXT PRIMARY KEY,
|
|
813
|
-
paused INTEGER NOT NULL CHECK (paused IN (0, 1)),
|
|
814
|
-
reason TEXT,
|
|
815
|
-
updated_at TEXT NOT NULL,
|
|
816
|
-
actor TEXT NOT NULL,
|
|
817
|
-
schema TEXT NOT NULL,
|
|
818
|
-
record TEXT NOT NULL
|
|
819
|
-
) STRICT;
|
|
820
|
-
`;
|
|
821
|
-
|
|
822
|
-
const DRIVE_RECEIPTS_DDL = `
|
|
823
|
-
CREATE TABLE IF NOT EXISTS drive_receipts (
|
|
824
|
-
drive_id TEXT PRIMARY KEY,
|
|
825
|
-
run_id TEXT NOT NULL,
|
|
826
|
-
project_id TEXT NOT NULL,
|
|
827
|
-
opened_sequence INTEGER NOT NULL,
|
|
828
|
-
last_sequence INTEGER NOT NULL,
|
|
829
|
-
closed_at TEXT NOT NULL,
|
|
830
|
-
schema TEXT NOT NULL,
|
|
831
|
-
receipt TEXT NOT NULL
|
|
832
|
-
) STRICT;
|
|
833
|
-
CREATE INDEX IF NOT EXISTS drive_receipts_run ON drive_receipts(run_id);
|
|
834
|
-
`;
|
|
835
|
-
|
|
836
|
-
const EVENT_STORE_MIGRATIONS = [
|
|
837
|
-
{
|
|
838
|
-
fromVersion: 3,
|
|
839
|
-
toVersion: 4,
|
|
840
|
-
migrate(database: DatabaseSync): void {
|
|
841
|
-
database.exec(DRIVE_RECEIPTS_DDL);
|
|
842
|
-
},
|
|
843
|
-
},
|
|
844
|
-
{
|
|
845
|
-
fromVersion: 4,
|
|
846
|
-
toVersion: 5,
|
|
847
|
-
migrate(database: DatabaseSync): void {
|
|
848
|
-
database.exec(COORDINATOR_INTAKE_DDL);
|
|
849
|
-
},
|
|
850
|
-
},
|
|
851
|
-
];
|
|
852
|
-
|
|
853
785
|
/** One persisted coordinator identity (`kxm.coordinator.v1`). */
|
|
854
786
|
export interface KxmCoordinatorRow {
|
|
855
787
|
coordinatorId: string;
|
|
@@ -894,7 +826,6 @@ export class KxmRunEventStore {
|
|
|
894
826
|
schema: EVENT_STORE_SCHEMA,
|
|
895
827
|
version: KXM_EVENT_STORE_SCHEMA_VERSION,
|
|
896
828
|
tables: EVENT_STORE_TABLES,
|
|
897
|
-
migrations: EVENT_STORE_MIGRATIONS,
|
|
898
829
|
});
|
|
899
830
|
}
|
|
900
831
|
|
|
@@ -42,6 +42,8 @@ export interface SessionHubStatus {
|
|
|
42
42
|
evidence: "probed" | "bound" | "cached" | "unconfigured" | "process" | "timeout";
|
|
43
43
|
online?: boolean;
|
|
44
44
|
url?: string;
|
|
45
|
+
/** Loopback or remote, so a brief never reads the same on both. */
|
|
46
|
+
scope?: "loopback" | "remote";
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
export interface SessionShipStatus {
|
|
@@ -65,9 +67,13 @@ export interface SessionBrief {
|
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
function hubPrefix(hub?: Partial<SessionHubStatus>): string {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
// A remote hub carries a bearer over a network; a loopback one does not. The JSON
|
|
71
|
+
// said so while every text surface printed the same bytes for both, which is the
|
|
72
|
+
// distinction an operator actually reads.
|
|
73
|
+
const suffix = hub?.scope === "remote" ? "/remote" : "";
|
|
74
|
+
if (hub?.state === "on" || (hub?.state === undefined && hub?.online === true)) return `kxm hub:on${suffix}`;
|
|
75
|
+
if (hub?.state === "off" || (hub?.state === undefined && hub?.online === false)) return `kxm hub:off${suffix}`;
|
|
76
|
+
if (hub?.state === "unknown") return `kxm hub:unknown${suffix}`;
|
|
71
77
|
return "kxm";
|
|
72
78
|
}
|
|
73
79
|
|
package/plugins/kxm/src/store.ts
CHANGED
|
@@ -6,7 +6,6 @@ import type { WorkflowJournalEntry, WorkflowRun } from "./workflow.ts";
|
|
|
6
6
|
import {
|
|
7
7
|
openDatabase,
|
|
8
8
|
withDatabaseTransaction,
|
|
9
|
-
type DatabaseMigrationStep,
|
|
10
9
|
type DatabaseSchemaSpec,
|
|
11
10
|
} from "./database.ts";
|
|
12
11
|
|
|
@@ -73,101 +72,10 @@ export const HUB_STORE_SCHEMA_V3 = `
|
|
|
73
72
|
CREATE INDEX IF NOT EXISTS context_items_project ON context_items(project);
|
|
74
73
|
`;
|
|
75
74
|
|
|
76
|
-
export const HUB_STORE_MIGRATIONS: readonly DatabaseMigrationStep[] = Object.freeze([
|
|
77
|
-
{
|
|
78
|
-
fromVersion: 1,
|
|
79
|
-
toVersion: 2,
|
|
80
|
-
migrate: (db: DatabaseSync) => {
|
|
81
|
-
db.exec(`
|
|
82
|
-
CREATE TABLE IF NOT EXISTS agents (
|
|
83
|
-
id TEXT PRIMARY KEY,
|
|
84
|
-
record TEXT NOT NULL
|
|
85
|
-
) STRICT;
|
|
86
|
-
CREATE TABLE IF NOT EXISTS messages (
|
|
87
|
-
id TEXT PRIMARY KEY,
|
|
88
|
-
record TEXT NOT NULL
|
|
89
|
-
) STRICT;
|
|
90
|
-
CREATE TABLE IF NOT EXISTS workflow_runs (
|
|
91
|
-
id TEXT PRIMARY KEY,
|
|
92
|
-
definition_id TEXT NOT NULL,
|
|
93
|
-
delivery_id TEXT NOT NULL,
|
|
94
|
-
record TEXT NOT NULL,
|
|
95
|
-
UNIQUE(definition_id, delivery_id)
|
|
96
|
-
) STRICT;
|
|
97
|
-
CREATE TABLE IF NOT EXISTS workflow_journal (
|
|
98
|
-
id TEXT PRIMARY KEY,
|
|
99
|
-
run_id TEXT NOT NULL,
|
|
100
|
-
category TEXT NOT NULL,
|
|
101
|
-
area TEXT NOT NULL,
|
|
102
|
-
record TEXT NOT NULL
|
|
103
|
-
) STRICT;
|
|
104
|
-
CREATE INDEX IF NOT EXISTS workflow_journal_run_id ON workflow_journal(run_id);
|
|
105
|
-
`);
|
|
106
|
-
},
|
|
107
|
-
},
|
|
108
|
-
{
|
|
109
|
-
fromVersion: 2,
|
|
110
|
-
toVersion: 3,
|
|
111
|
-
migrate: (db: DatabaseSync) => {
|
|
112
|
-
db.exec(`
|
|
113
|
-
CREATE TABLE IF NOT EXISTS agents (
|
|
114
|
-
id TEXT PRIMARY KEY,
|
|
115
|
-
record TEXT NOT NULL
|
|
116
|
-
) STRICT;
|
|
117
|
-
CREATE TABLE IF NOT EXISTS messages (
|
|
118
|
-
id TEXT PRIMARY KEY,
|
|
119
|
-
record TEXT NOT NULL
|
|
120
|
-
) STRICT;
|
|
121
|
-
CREATE TABLE IF NOT EXISTS workflow_runs (
|
|
122
|
-
id TEXT PRIMARY KEY,
|
|
123
|
-
definition_id TEXT NOT NULL,
|
|
124
|
-
delivery_id TEXT NOT NULL,
|
|
125
|
-
record TEXT NOT NULL,
|
|
126
|
-
UNIQUE(definition_id, delivery_id)
|
|
127
|
-
) STRICT;
|
|
128
|
-
CREATE TABLE IF NOT EXISTS workflow_journal (
|
|
129
|
-
id TEXT PRIMARY KEY,
|
|
130
|
-
run_id TEXT NOT NULL,
|
|
131
|
-
category TEXT NOT NULL,
|
|
132
|
-
area TEXT NOT NULL,
|
|
133
|
-
record TEXT NOT NULL
|
|
134
|
-
) STRICT;
|
|
135
|
-
CREATE INDEX IF NOT EXISTS workflow_journal_run_id ON workflow_journal(run_id);
|
|
136
|
-
CREATE TABLE IF NOT EXISTS context_items (
|
|
137
|
-
id TEXT PRIMARY KEY,
|
|
138
|
-
project TEXT NOT NULL,
|
|
139
|
-
kind TEXT NOT NULL,
|
|
140
|
-
record TEXT NOT NULL
|
|
141
|
-
) STRICT;
|
|
142
|
-
CREATE INDEX IF NOT EXISTS context_items_project ON context_items(project);
|
|
143
|
-
CREATE UNIQUE INDEX IF NOT EXISTS messages_from_idempotency
|
|
144
|
-
ON messages(
|
|
145
|
-
json_extract(record, '$.from'),
|
|
146
|
-
json_extract(record, '$.idempotencyKey')
|
|
147
|
-
) WHERE json_extract(record, '$.idempotencyKey') IS NOT NULL;
|
|
148
|
-
CREATE INDEX IF NOT EXISTS messages_to_seq
|
|
149
|
-
ON messages(
|
|
150
|
-
json_extract(record, '$.to'),
|
|
151
|
-
COALESCE(json_extract(record, '$.seq'), 0)
|
|
152
|
-
);
|
|
153
|
-
CREATE TABLE IF NOT EXISTS consumer_cursors (
|
|
154
|
-
agent_id TEXT PRIMARY KEY,
|
|
155
|
-
cursor INTEGER NOT NULL
|
|
156
|
-
) STRICT;
|
|
157
|
-
CREATE TABLE IF NOT EXISTS agent_sequences (
|
|
158
|
-
agent_id TEXT PRIMARY KEY,
|
|
159
|
-
next_seq INTEGER NOT NULL
|
|
160
|
-
) STRICT;
|
|
161
|
-
`);
|
|
162
|
-
},
|
|
163
|
-
},
|
|
164
|
-
]);
|
|
165
|
-
|
|
166
75
|
export const HUB_STORE_SCHEMA_SPEC: DatabaseSchemaSpec = Object.freeze({
|
|
167
76
|
schema: HUB_STORE_SCHEMA_V3,
|
|
168
77
|
version: HUB_STORE_SCHEMA_VERSION,
|
|
169
78
|
tables: HUB_STORE_TABLES,
|
|
170
|
-
migrations: HUB_STORE_MIGRATIONS,
|
|
171
79
|
});
|
|
172
80
|
|
|
173
81
|
export interface StoredAgent extends AgentRecord {
|