@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.
@@ -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 an already-stored coordinator express this ceiling?
105
+ * Does a stored coordinator already express the ceiling being requested?
106
106
  *
107
- * A row written before set normalisation existed carries a fingerprint that
108
- * `kxmCeilingHash` no longer reproduces, so comparing the stored hash alone is
109
- * not enough: recompute over the authority it kept. Every path that asks this
110
- * question — the initial slot lookup and **both** lost-write read-backs — must
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 || kxmCeilingHash(stored.authority) === 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.51";
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
- if (hub?.state === "on" || (hub?.state === undefined && hub?.online === true)) return "kxm hub:on";
69
- if (hub?.state === "off" || (hub?.state === undefined && hub?.online === false)) return "kxm hub:off";
70
- if (hub?.state === "unknown") return "kxm hub:unknown";
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
 
@@ -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 {