@cotal-ai/core 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,315 @@
1
+ /**
2
+ * Authoritative channel-membership feed — the broker-sourced "who is subscribed to each channel"
3
+ * the graph view draws (incl. silent readers and `live` channels that keep no enumerable roster).
4
+ *
5
+ * This is the NATS-client layer of the feature (so it lives in core, like `setupSpaceStreams`); the
6
+ * delivery daemon is the thin composition root that loads the two scoped creds + the account id and
7
+ * calls {@link startMembershipFeed}. It owns TWO connections — NATS accounts are a hard isolation
8
+ * boundary, so the `$SYS` CONNZ read (conn A, system account) and the data-account KV (conn B) cannot
9
+ * share a principal — and merges them IN-PROCESS:
10
+ *
11
+ * conn A (SYSTEM) — poll `$SYS.REQ.ACCOUNT.<id>.CONNZ {subscriptions,auth}` (fans out: 1 reply/server
12
+ * → per-server paginate → union-dedupe by nkey); sub CONNECT/DISCONNECT as re-poll triggers.
13
+ * conn B (DATA) — read the members registry (durable arm) + read/write the derived feed bucket.
14
+ * merge — per agent: live (CONNZ patterns, wildcards kept) ∪ durable (members registry);
15
+ * diff-before-put on the normalized {live,durable}; prune departed agents.
16
+ *
17
+ * CONNZ is authoritative for the live half; presence only *enriches* (name/role/status) at the
18
+ * dashboard, never gates here (a momentarily-lapsed heartbeat must not drop a live core-sub). The feed
19
+ * is **display-only** — never an input to delivery/ACL/authorization. Any failure here logs and degrades
20
+ * the graph only; it shares nothing with Plane-3 delivery.
21
+ *
22
+ * Placement note (fowler): every other core connect-site is one-shot (connect → op → drain). This is the
23
+ * FIRST persistently-connected, timer-driven service in core — a new category, deliberately split:
24
+ * **core owns the mechanism + connection lifecycle** (the engine, the two conns, the poll loop), and the
25
+ * **implementation (delivery daemon) owns the DECISION to run it** — creds source, lifetime, N=1, fail-
26
+ * soft. Don't read "it touches NATS → put it in core" and migrate, say, the Plane-3 writer up here; that
27
+ * would undo the daemon's least-privilege extraction. The barrel exports {@link startMembershipFeed}, but
28
+ * the **scoped creds are the real gate**: with no system-account observer cred it simply cannot connect.
29
+ */
30
+ import { connect, credsAuthenticator } from "@nats-io/transport-node";
31
+ import { Kvm } from "@nats-io/kv";
32
+ import { membershipBucket, membershipKey, MEMBERSHIP_FEED_KEY, MEMBERSHIP_INBOX_PREFIX, connzRequestSubject, accountConnectSubject, accountDisconnectSubject, channelFromChatSubscription, } from "./subjects.js";
33
+ import { openMembersRegistry, listMembers } from "./members.js";
34
+ import { idFromCreds } from "./identity.js";
35
+ const enc = (s) => new TextEncoder().encode(s);
36
+ const MAX_PAGES = 64; // fan-out pagination guard (64 × 1024 = 65k conns/server before a loud under-report)
37
+ /** Connect, wire the triggers + safety poll, and run an immediate first reconcile. */
38
+ export async function startMembershipFeed(opts) {
39
+ const log = opts.log ?? ((m) => console.error(`! membership: ${m}`));
40
+ const intervalMs = opts.intervalMs ?? 15_000;
41
+ const debounceMs = opts.debounceMs ?? 400;
42
+ const settleMs = opts.settleMs ?? 250;
43
+ const maxWaitMs = opts.maxWaitMs ?? 1_500;
44
+ const pageLimit = opts.pageLimit ?? 1024;
45
+ const { space, accountId } = opts;
46
+ const connA = await connect({
47
+ servers: opts.servers,
48
+ authenticator: credsAuthenticator(enc(opts.observerCreds)),
49
+ name: "cotal-membership-observer",
50
+ inboxPrefix: MEMBERSHIP_INBOX_PREFIX, // scoped reply inboxes — the cred only allows `<prefix>.>`
51
+ maxReconnectAttempts: -1,
52
+ });
53
+ connA.closed().then((err) => { if (err)
54
+ log(`conn A (system) closed: ${err.message}`); });
55
+ const rwSelfId = idFromCreds(opts.rwCreds); // conn B's own nkey — the data-account self-presence check below
56
+ const connB = await connect({
57
+ servers: opts.servers,
58
+ authenticator: credsAuthenticator(enc(opts.rwCreds)),
59
+ name: "cotal-membership-rw",
60
+ // The rw cred's sub.allow is `_INBOX_<id>.>`, so the connection's inbox prefix MUST match it — else
61
+ // every KV reply / ordered-consumer delivery (kv.get/keys/watch) lands on a subject it can't subscribe.
62
+ inboxPrefix: `_INBOX_${rwSelfId}`,
63
+ maxReconnectAttempts: -1,
64
+ });
65
+ connB.closed().then((err) => { if (err)
66
+ log(`conn B (data) closed: ${err.message}`); });
67
+ const kvm = new Kvm(connB);
68
+ const feedKv = await kvm.open(membershipBucket(space));
69
+ const membersKv = await openMembersRegistry(connB, space);
70
+ let stopped = false;
71
+ let polling = false;
72
+ let rerun = false; // a trigger fired mid-poll → run once more after
73
+ let reqSeq = 0;
74
+ let clusterWarned = false; // log the multi-server completeness limit at most once (never fires at N=1)
75
+ /** One CONNZ round: publish the account request, collect every server's reply within the window. */
76
+ async function connzRound(offset) {
77
+ return new Promise((resolve) => {
78
+ const inbox = `${MEMBERSHIP_INBOX_PREFIX}.${reqSeq++}`;
79
+ const out = [];
80
+ let settle;
81
+ let done = false;
82
+ const finish = () => {
83
+ if (done)
84
+ return;
85
+ done = true;
86
+ if (settle)
87
+ clearTimeout(settle);
88
+ clearTimeout(hard);
89
+ try {
90
+ sub.unsubscribe();
91
+ }
92
+ catch { /* draining */ }
93
+ resolve(out);
94
+ };
95
+ const sub = connA.subscribe(inbox, {
96
+ callback: (err, msg) => {
97
+ if (err)
98
+ return;
99
+ try {
100
+ out.push(msg.json());
101
+ }
102
+ catch { /* skip undecodable */ }
103
+ if (settle)
104
+ clearTimeout(settle);
105
+ settle = setTimeout(finish, settleMs);
106
+ },
107
+ });
108
+ const hard = setTimeout(finish, maxWaitMs);
109
+ connA.publish(connzRequestSubject(accountId), enc(JSON.stringify({ subscriptions: true, auth: true, offset, limit: pageLimit })), { reply: inbox });
110
+ });
111
+ }
112
+ /** Fan-out + per-server pagination + union-dedupe → nkey → live channel-subscription patterns.
113
+ * God-view taps (a connection holding the whole-chat/space wildcard) are excluded entirely. Returns
114
+ * `complete:false` for a sweep that didn't fully drain (zero replies = broker unreachable/denied, or a
115
+ * MAX_PAGES truncation) so the caller can skip the write — a PARTIAL CONNZ read must never prune real
116
+ * members or stamp a fresh heartbeat (truthium). */
117
+ async function liveFromConnz() {
118
+ const live = new Map();
119
+ const serverMore = new Set(); // server ids still reporting a full page this round
120
+ const serversSeen = new Set(); // distinct responders across the whole sweep
121
+ let gotReply = false, exhausted = false, seenSelf = false;
122
+ for (let page = 0; page < MAX_PAGES; page++) {
123
+ const offset = page * pageLimit;
124
+ const replies = await connzRound(offset);
125
+ if (replies.length === 0) {
126
+ if (page === 0)
127
+ log(`CONNZ returned no replies (offset 0) — broker unreachable or cred denied; keeping last membership this tick`);
128
+ break;
129
+ }
130
+ gotReply = true;
131
+ serverMore.clear();
132
+ for (const r of replies) {
133
+ const sid = r.server?.id ?? r.data?.server_id ?? "?";
134
+ serversSeen.add(sid);
135
+ const conns = r.data?.connections ?? [];
136
+ for (const c of conns) {
137
+ if (c.authorized_user === rwSelfId)
138
+ seenSelf = true; // our own conn B must be in a complete read
139
+ addConn(space, live, c);
140
+ }
141
+ const total = r.data?.total ?? conns.length;
142
+ // A server has more ONLY if it returned a FULL page that hasn't reached its total. A short page
143
+ // (len < requested limit) means exhausted regardless of `total` — this is filter-proof: if a
144
+ // server-side filter_subject is ever added, `total` stays the pre-filter account total and
145
+ // `offset+len >= total` would never trip, but the short page still terminates the loop (truthium).
146
+ if (conns.length >= pageLimit && offset + conns.length < total)
147
+ serverMore.add(sid);
148
+ }
149
+ if (serverMore.size === 0) {
150
+ exhausted = true;
151
+ break;
152
+ }
153
+ if (page === MAX_PAGES - 1)
154
+ log(`CONNZ still paginating after ${MAX_PAGES} pages (servers ${[...serverMore].join(",")}) — UNDER-REPORTING; skipping this sweep`);
155
+ }
156
+ // SELF-PRESENCE completeness check (socrates): the data account ALWAYS holds at least conn B, so a
157
+ // sweep that doesn't even include our own rw connection missed connections (a mid-reconnect blip, or
158
+ // the server hosting conn B staying silent) — treat it as incomplete so reconcile() neither prunes nor
159
+ // restamps. 1-BROKER SCOPE (truthium): this is sufficient at N=1 (canary == full coverage), but only
160
+ // NECESSARY at cluster scale — conn B is pinned to ONE server, so a DIFFERENT silent server's agents
161
+ // would still pass this canary. The sufficient multi-server check is `distinct responding server_ids
162
+ // == expected server count` (expected set discovered via $SYS.REQ.SERVER.PING); deferred with the rest
163
+ // of multi-broker support — a conscious deferral, not a single-server bake-in.
164
+ if (gotReply && exhausted && !seenSelf)
165
+ log(`CONNZ sweep omitted our own rw connection — treating as incomplete (keeping last membership)`);
166
+ // NO-SILENT-DEGRADATION (socrates): in a real cluster the conn-B floor only proves conn B's OWN server
167
+ // answered — a DIFFERENT silent server would still pass `complete` yet under-report its agents. Until
168
+ // multi-broker responder-accounting ships, surface that limit LOUDLY (once) rather than degrade quietly.
169
+ if (serversSeen.size > 1 && !clusterWarned) {
170
+ clusterWarned = true;
171
+ log(`multi-server cluster detected (${serversSeen.size} responders) — membership completeness uses the conn-B floor only; a silent peer server can under-report (multi-broker accounting deferred, see core-sub-fabric.md)`);
172
+ }
173
+ return { live, complete: gotReply && exhausted && seenSelf };
174
+ }
175
+ /** The durable arm: open, activated (non-tombstoned) members from the privileged registry. Mirrors
176
+ * endpoint `channelMembers()` so the daemon's union and the manager surface agree. */
177
+ async function durableFromMembers() {
178
+ const durable = new Map();
179
+ for (const r of await listMembers(membersKv)) {
180
+ if (r.leaveCursor !== undefined || r.activated !== true)
181
+ continue;
182
+ (durable.get(r.owner) ?? durable.set(r.owner, new Set()).get(r.owner)).add(r.channel);
183
+ }
184
+ return durable;
185
+ }
186
+ async function reconcile() {
187
+ const { live, complete } = await liveFromConnz();
188
+ // A partial CONNZ sweep (unreachable / truncated) would prune real members and lie about freshness —
189
+ // keep the last good state untouched and don't stamp the heartbeat. Self-heals on the next full poll.
190
+ if (!complete)
191
+ return;
192
+ const durable = await durableFromMembers();
193
+ const observedAt = Date.now();
194
+ // Merge per agent: CONNZ live patterns ∪ durable concrete channels. An agent with neither is omitted.
195
+ const next = new Map();
196
+ for (const id of new Set([...live.keys(), ...durable.keys()])) {
197
+ const liveArr = [...(live.get(id) ?? [])].sort();
198
+ const durableArr = [...(durable.get(id) ?? [])].sort();
199
+ if (liveArr.length === 0 && durableArr.length === 0)
200
+ continue;
201
+ next.set(id, { live: liveArr, durable: durableArr, observedAt });
202
+ }
203
+ // Diff-before-put on the normalized {live,durable} (NOT observedAt), then prune departed agents — so a
204
+ // quiet poll bumps no revision and wakes no watcher. Feed-wide freshness rides the heartbeat key below.
205
+ const existing = new Set();
206
+ for await (const k of await feedKv.keys())
207
+ if (k !== MEMBERSHIP_FEED_KEY)
208
+ existing.add(k);
209
+ for (const [id, rec] of next) {
210
+ const key = membershipKey(id);
211
+ existing.delete(key);
212
+ const cur = await feedKv.get(key);
213
+ let same = false;
214
+ if (cur && cur.operation !== "DEL" && cur.operation !== "PURGE") {
215
+ try {
216
+ same = sameMembership(cur.json(), rec);
217
+ }
218
+ catch { /* re-write on garble */ }
219
+ }
220
+ if (!same)
221
+ await feedKv.put(key, enc(JSON.stringify(rec)));
222
+ }
223
+ for (const stale of existing)
224
+ await feedKv.delete(stale);
225
+ // Heartbeat: re-stamp every successful poll (even with zero membership change) so the dashboard can
226
+ // distinguish "feed is live" from "feed is stale/dead" — the diff-before-put above would otherwise
227
+ // freeze every observedAt and make a healthy feed read stale.
228
+ await feedKv.put(MEMBERSHIP_FEED_KEY, enc(JSON.stringify({ observedAt, count: next.size })));
229
+ }
230
+ async function poll() {
231
+ if (stopped)
232
+ return;
233
+ if (polling) {
234
+ rerun = true;
235
+ return;
236
+ } // a poll is in flight — coalesce, run once more after it
237
+ polling = true;
238
+ try {
239
+ do {
240
+ rerun = false;
241
+ await reconcile();
242
+ } while (rerun && !stopped);
243
+ }
244
+ catch (e) {
245
+ log(`poll failed (graph membership degraded; delivery unaffected): ${e.message}`);
246
+ }
247
+ finally {
248
+ polling = false;
249
+ }
250
+ }
251
+ // Re-poll triggers — debounced. There is NO SUB/UNSUB event, so these only shorten join/leave-the-mesh
252
+ // latency; the interval is the real reconcile. A connect storm coalesces into one debounced poll.
253
+ let debounce;
254
+ const trigger = () => {
255
+ if (stopped)
256
+ return;
257
+ if (debounce)
258
+ clearTimeout(debounce);
259
+ debounce = setTimeout(() => void poll(), debounceMs);
260
+ };
261
+ const subConnect = connA.subscribe(accountConnectSubject(accountId), { callback: () => trigger() });
262
+ const subDisconnect = connA.subscribe(accountDisconnectSubject(accountId), { callback: () => trigger() });
263
+ const timer = setInterval(() => void poll(), intervalMs);
264
+ await poll(); // first reconcile now
265
+ return {
266
+ poll,
267
+ async stop() {
268
+ stopped = true;
269
+ clearInterval(timer);
270
+ if (debounce)
271
+ clearTimeout(debounce);
272
+ try {
273
+ subConnect.unsubscribe();
274
+ subDisconnect.unsubscribe();
275
+ }
276
+ catch { /* draining */ }
277
+ await Promise.allSettled([connA.drain(), connB.drain()]);
278
+ },
279
+ };
280
+ }
281
+ /** Fold one CONNZ connection into the live map: keyed by `authorized_user` (the nkey = `card.id`),
282
+ * unioning its chat-subscription patterns (wildcards kept, e.g. `team.>` or a whole-chat `>`).
283
+ *
284
+ * Infra taps SELF-EXCLUDE — no shape heuristic needed (review-general, socrates): the web dashboard taps
285
+ * `cotal.<space>.>` (spaceWildcard) and `cotal console` taps `cotal.<space>.chat.>` (chatWildcard), both
286
+ * of which {@link channelFromChatSubscription} maps to `null` (the former isn't `.chat.`-prefixed; the
287
+ * latter has no channel token after `chat.`), so they contribute zero channels here; conn B / the
288
+ * delivery cred / the manager hold no chat sub at all. The ONLY subscription that yields the whole-chat
289
+ * `>` pattern is an AGENT's own `chat.*.>` (allowSubscribe `[">"]` — e.g. the default persona), which is
290
+ * a legitimate broad reader the feed MUST surface (the source-of-truth goal), NOT drop. So no shape-based
291
+ * exclusion: a `>` pattern is recorded as-is and the dashboard renders it as a "reads-all" node (a badge,
292
+ * not a spoke to every hub) rather than expanding it. */
293
+ function addConn(space, live, c) {
294
+ const subs = c.subscriptions_list ?? [];
295
+ const id = c.authorized_user;
296
+ if (!id)
297
+ return; // no authenticated identity (open mode) — best-effort handled at the dashboard, not here
298
+ const patterns = subs
299
+ .map((s) => channelFromChatSubscription(space, s))
300
+ .filter((x) => x !== null);
301
+ if (patterns.length === 0)
302
+ return; // connected but subscribed to no channel — member of nothing
303
+ const set = live.get(id) ?? live.set(id, new Set()).get(id);
304
+ for (const p of patterns)
305
+ set.add(p);
306
+ }
307
+ /** Equal on the normalized membership (sorted live + durable), IGNORING `observedAt` — the diff that
308
+ * decides whether a poll re-writes an agent's key (so a quiet poll wakes no watcher). */
309
+ function sameMembership(a, b) {
310
+ return arrEq(a.live, b.live) && arrEq(a.durable, b.durable);
311
+ }
312
+ function arrEq(a, b) {
313
+ return a.length === b.length && a.every((x, i) => x === b[i]);
314
+ }
315
+ //# sourceMappingURL=membership-feed.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"membership-feed.js","sourceRoot":"","sources":["../src/membership-feed.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The registry of running meshes: one record per broker `cotal up` started on this machine, so a
3
+ * `cotal spawn` from *any* directory can find which mesh to join, with which creds and personas.
4
+ *
5
+ * Stored as **one JSON file per mesh** (`~/.cotal/meshes/<space>.json`) rather than a single
6
+ * `meshes.json`: concurrent `up`/`down` never read-modify-write the same file (no lost-update race),
7
+ * a crash damages at most one entry, and it mirrors the existing per-process pid files under
8
+ * `~/.cotal`. A separate `~/.cotal/current-mesh` holds the default space for the N-running case
9
+ * (the kubectl `current-context` analogue).
10
+ *
11
+ * Each record stores the mesh's **root path**, not its secrets — trust material stays in that
12
+ * project's `.cotal/auth`; the registry just makes it findable from elsewhere.
13
+ */
14
+ export interface MeshEntry {
15
+ /** The space name — also the registry filename stem. */
16
+ space: string;
17
+ /** The broker URL, e.g. `nats://127.0.0.1:4222`. */
18
+ server: string;
19
+ /** Absolute path whose `.cotal/{auth,agents}` hold this mesh's trust material + personas. */
20
+ root: string;
21
+ mode: "auth" | "open";
22
+ /** ISO timestamp of when the record was written. */
23
+ ts: string;
24
+ }
25
+ /** The cotal machine-home dir (`~/.cotal`), overridable via `COTAL_HOME` so tests sandbox it and
26
+ * never touch the real one. The single source of that path for the registry, the current pointer,
27
+ * and the onboard marker. */
28
+ export declare function homeCotalDir(): string;
29
+ /** Directory holding the per-mesh registry files (`~/.cotal/meshes`). */
30
+ export declare function meshesDir(): string;
31
+ /** Record (or refresh) a running mesh — atomic write, 0600 (the file points at a secrets dir). */
32
+ export declare function recordMesh(m: MeshEntry): void;
33
+ /** Drop a mesh from the registry (on `cotal down` / a stale-entry prune). Absent ⇒ no-op. */
34
+ export declare function removeMesh(space: string): void;
35
+ /** All currently-recorded meshes. An unparseable/partially-written entry is skipped, not fatal —
36
+ * one bad file must not hide the rest. */
37
+ export declare function loadMeshes(): MeshEntry[];
38
+ export declare function findMesh(space: string): MeshEntry | undefined;
39
+ /** The default mesh's space name, set by `cotal use` (and by the first `cotal up`). Undefined when
40
+ * unset or empty. The pointer can dangle (its mesh went down); callers treat a `findMesh` miss as
41
+ * "no current". */
42
+ export declare function getCurrent(): string | undefined;
43
+ export declare function setCurrent(space: string): void;
44
+ export declare function clearCurrent(): void;
45
+ //# sourceMappingURL=mesh-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mesh-registry.d.ts","sourceRoot":"","sources":["../src/mesh-registry.ts"],"names":[],"mappings":"AAWA;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,SAAS;IACxB,wDAAwD;IACxD,KAAK,EAAE,MAAM,CAAC;IACd,oDAAoD;IACpD,MAAM,EAAE,MAAM,CAAC;IACf,6FAA6F;IAC7F,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IACtB,oDAAoD;IACpD,EAAE,EAAE,MAAM,CAAC;CACZ;AAED;;8BAE8B;AAC9B,wBAAgB,YAAY,IAAI,MAAM,CAErC;AAED,yEAAyE;AACzE,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAUD,kGAAkG;AAClG,wBAAgB,UAAU,CAAC,CAAC,EAAE,SAAS,GAAG,IAAI,CAU7C;AAED,6FAA6F;AAC7F,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAE9C;AAED;2CAC2C;AAC3C,wBAAgB,UAAU,IAAI,SAAS,EAAE,CAgBxC;AAED,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CAE7D;AAED;;oBAEoB;AACpB,wBAAgB,UAAU,IAAI,MAAM,GAAG,SAAS,CAM/C;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAG9C;AAED,wBAAgB,YAAY,IAAI,IAAI,CAEnC"}
@@ -0,0 +1,78 @@
1
+ import { mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync, } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ /** The cotal machine-home dir (`~/.cotal`), overridable via `COTAL_HOME` so tests sandbox it and
5
+ * never touch the real one. The single source of that path for the registry, the current pointer,
6
+ * and the onboard marker. */
7
+ export function homeCotalDir() {
8
+ return process.env.COTAL_HOME ?? join(homedir(), ".cotal");
9
+ }
10
+ /** Directory holding the per-mesh registry files (`~/.cotal/meshes`). */
11
+ export function meshesDir() {
12
+ return join(homeCotalDir(), "meshes");
13
+ }
14
+ function meshFile(space) {
15
+ return join(meshesDir(), `${encodeURIComponent(space)}.json`);
16
+ }
17
+ function currentFile() {
18
+ return join(homeCotalDir(), "current-mesh");
19
+ }
20
+ /** Record (or refresh) a running mesh — atomic write, 0600 (the file points at a secrets dir). */
21
+ export function recordMesh(m) {
22
+ // 0700: the filenames in here ARE the space names, so a world-traversable dir would leak them to
23
+ // other local users even though the file contents are 0600. Keep the dir readable only by us.
24
+ mkdirSync(meshesDir(), { recursive: true, mode: 0o700 });
25
+ const file = meshFile(m.space);
26
+ // Per-process temp name so two concurrent `up`s for the same space can't stomp each other's
27
+ // half-written file before the rename.
28
+ const tmp = `${file}.${process.pid}.tmp`;
29
+ writeFileSync(tmp, JSON.stringify(m, null, 2), { mode: 0o600 });
30
+ renameSync(tmp, file); // atomic replace — a reader never sees a half-written record
31
+ }
32
+ /** Drop a mesh from the registry (on `cotal down` / a stale-entry prune). Absent ⇒ no-op. */
33
+ export function removeMesh(space) {
34
+ rmSync(meshFile(space), { force: true });
35
+ }
36
+ /** All currently-recorded meshes. An unparseable/partially-written entry is skipped, not fatal —
37
+ * one bad file must not hide the rest. */
38
+ export function loadMeshes() {
39
+ let files;
40
+ try {
41
+ files = readdirSync(meshesDir()).filter((f) => f.endsWith(".json"));
42
+ }
43
+ catch {
44
+ return []; // no registry yet
45
+ }
46
+ const out = [];
47
+ for (const f of files.sort()) {
48
+ try {
49
+ out.push(JSON.parse(readFileSync(join(meshesDir(), f), "utf8")));
50
+ }
51
+ catch {
52
+ /* skip a corrupt/half-written entry rather than fail the whole listing */
53
+ }
54
+ }
55
+ return out;
56
+ }
57
+ export function findMesh(space) {
58
+ return loadMeshes().find((m) => m.space === space);
59
+ }
60
+ /** The default mesh's space name, set by `cotal use` (and by the first `cotal up`). Undefined when
61
+ * unset or empty. The pointer can dangle (its mesh went down); callers treat a `findMesh` miss as
62
+ * "no current". */
63
+ export function getCurrent() {
64
+ try {
65
+ return readFileSync(currentFile(), "utf8").trim() || undefined;
66
+ }
67
+ catch {
68
+ return undefined;
69
+ }
70
+ }
71
+ export function setCurrent(space) {
72
+ mkdirSync(homeCotalDir(), { recursive: true, mode: 0o700 });
73
+ writeFileSync(currentFile(), space, { mode: 0o600 });
74
+ }
75
+ export function clearCurrent() {
76
+ rmSync(currentFile(), { force: true });
77
+ }
78
+ //# sourceMappingURL=mesh-registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mesh-registry.js","sourceRoot":"","sources":["../src/mesh-registry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ import { type SpaceAuth } from "./provision.js";
2
+ /**
3
+ * One coherent answer to "which mesh does this command act on, and where do its creds + personas
4
+ * live" — resolved identically for both, so a spawn can never authenticate to mesh A while loading
5
+ * mesh B's persona. Replaces the scattered `loadSpaceAuth(authDir(cotalRoot()))` + `resolveSpace` +
6
+ * `DEFAULT_SERVER` guesswork that silently mistook `~/.cotal` for a space.
7
+ *
8
+ * Pure and offline (no network): `<TAB>` completion uses it as-is; `spawn` adds a reachability
9
+ * preflight (`probeConnect`) on top.
10
+ */
11
+ export interface MeshTarget {
12
+ /** Absolute dir whose `.cotal/{auth,agents}` hold trust + personas. */
13
+ root: string;
14
+ server: string;
15
+ space: string;
16
+ /** Trust material, or undefined for an open mesh. */
17
+ auth?: SpaceAuth;
18
+ /** `<root>/.cotal/agents` — the persona catalog for this mesh. */
19
+ personaRoot: string;
20
+ /** Where the target came from — this also carries OWNERSHIP for pruning. Every value except
21
+ * `local-space` and `flag-server` (the two non-registry escape hatches) means the server + mode
22
+ * came from a registry record, so a stale-broker failure prunes it. `local-recorded` is a local
23
+ * project matched to a registry entry by root: registry-owned for pruning, but quiet on the
24
+ * success line (the target is self-evident from cwd, like `local-space`). */
25
+ source: "flag-server" | "flag-space" | "local-space" | "local-recorded" | "registry" | "current";
26
+ }
27
+ export interface ResolveFlags {
28
+ /** `--server <url>` — raw broker escape hatch. */
29
+ server?: string;
30
+ /** `--space <name>` — pick a specific running mesh from the registry. */
31
+ space?: string;
32
+ }
33
+ /**
34
+ * Resolve the mesh target by precedence (first match wins):
35
+ * 1. `--space` — registry lookup (errors if that space isn't running).
36
+ * 2. `--server` — registry entry on that server (for creds/personas), else the local project.
37
+ * 3. A genuine local project (`cwd` walks up to a real `.cotal/`) — local wins, like `git config`.
38
+ * 4. The registry: 0 ⇒ error; 1 ⇒ use it; N ⇒ `current` if set, else error naming each + its root.
39
+ * No silent fallback — an unresolved target throws one human sentence.
40
+ */
41
+ export declare function resolveMeshTarget(cwd: string, flags?: ResolveFlags): MeshTarget;
42
+ //# sourceMappingURL=mesh-target.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mesh-target.d.ts","sourceRoot":"","sources":["../src/mesh-target.ts"],"names":[],"mappings":"AAGA,OAAO,EAAyC,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAUvF;;;;;;;;GAQG;AACH,MAAM,WAAW,UAAU;IACzB,uEAAuE;IACvE,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,qDAAqD;IACrD,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,kEAAkE;IAClE,WAAW,EAAE,MAAM,CAAC;IACpB;;;;kFAI8E;IAC9E,MAAM,EACF,aAAa,GACb,YAAY,GACZ,aAAa,GACb,gBAAgB,GAChB,UAAU,GACV,SAAS,CAAC;CACf;AAED,MAAM,WAAW,YAAY;IAC3B,kDAAkD;IAClD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yEAAyE;IACzE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA8CD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,GAAE,YAAiB,GAAG,UAAU,CAgDnF"}
@@ -0,0 +1,95 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { DEFAULT_SERVER, DEFAULT_SPACE } from "./endpoint.js";
4
+ import { authDir, findCotalRoot, loadSpaceAuth } from "./provision.js";
5
+ import { findMesh, getCurrent, homeCotalDir, loadMeshes, removeMesh, } from "./mesh-registry.js";
6
+ function personaRoot(root) {
7
+ return join(root, ".cotal", "agents");
8
+ }
9
+ function targetFromEntry(m, server, source) {
10
+ // Honor the recorded mode: an OPEN mesh connects credlessly even if its root still has auth
11
+ // material on disk (e.g. a root that once ran auth mode). Loading it would make `spawn` mint
12
+ // creds against a broker that takes none.
13
+ let auth;
14
+ if (m.mode === "auth") {
15
+ auth = loadSpaceAuth(authDir(m.root));
16
+ // Defense in depth: the root's on-disk auth must still be for THIS space. A divergence (the root
17
+ // was re-`up`ed as a different space without re-recording) would otherwise mint mesh-A creds
18
+ // against the entry for space B. Prune the stale entry and fail loud rather than connect wrong.
19
+ if (auth && auth.space !== m.space) {
20
+ removeMesh(m.space);
21
+ throw new Error(`registry entry "${m.space}" points at ${m.root}, whose auth is now for "${auth.space}" — stale entry removed; re-run \`cotal up\` or check \`cotal meshes\``);
22
+ }
23
+ }
24
+ return {
25
+ root: m.root,
26
+ server,
27
+ space: m.space,
28
+ auth,
29
+ personaRoot: personaRoot(m.root),
30
+ source,
31
+ };
32
+ }
33
+ function localTarget(root, server, source) {
34
+ const auth = loadSpaceAuth(authDir(root));
35
+ return { root, server, space: auth?.space ?? DEFAULT_SPACE, auth, personaRoot: personaRoot(root), source };
36
+ }
37
+ /** A `.cotal/` that a user actually created here — not the machine-home dir the cwd walk-up lands on
38
+ * from outside any project (which has no space, just the daemon's pid/onboard files). */
39
+ function isGenuineSpace(root) {
40
+ // Normalize both sides — COTAL_HOME may be relative or non-canonical, and a raw string compare
41
+ // would then let the real `~/.cotal` masquerade as a project space (or vice-versa).
42
+ return resolve(join(root, ".cotal")) !== resolve(homeCotalDir()) && existsSync(join(root, ".cotal"));
43
+ }
44
+ /**
45
+ * Resolve the mesh target by precedence (first match wins):
46
+ * 1. `--space` — registry lookup (errors if that space isn't running).
47
+ * 2. `--server` — registry entry on that server (for creds/personas), else the local project.
48
+ * 3. A genuine local project (`cwd` walks up to a real `.cotal/`) — local wins, like `git config`.
49
+ * 4. The registry: 0 ⇒ error; 1 ⇒ use it; N ⇒ `current` if set, else error naming each + its root.
50
+ * No silent fallback — an unresolved target throws one human sentence.
51
+ */
52
+ export function resolveMeshTarget(cwd, flags = {}) {
53
+ if (flags.space) {
54
+ const m = findMesh(flags.space);
55
+ if (!m)
56
+ throw new Error(`no mesh named "${flags.space}" is running — see \`cotal meshes\``);
57
+ return targetFromEntry(m, flags.server ?? m.server, "flag-space");
58
+ }
59
+ if (flags.server) {
60
+ const m = loadMeshes().find((e) => e.server === flags.server);
61
+ if (m)
62
+ return targetFromEntry(m, flags.server, "flag-server");
63
+ return localTarget(findCotalRoot(cwd), flags.server, "flag-server");
64
+ }
65
+ const root = findCotalRoot(cwd);
66
+ if (isGenuineSpace(root)) {
67
+ // Local project wins by root — but if its mesh is in the registry, use the RECORDED server +
68
+ // mode, not DEFAULT_SERVER: a project started with `--server …:4333` must spawn against :4333,
69
+ // and a recorded OPEN mesh must not mint creds off stale `.cotal/auth` left on disk. Fall back
70
+ // to the local default only when nothing is recorded for this root.
71
+ const recorded = loadMeshes().find((m) => resolve(m.root) === resolve(root));
72
+ if (recorded)
73
+ return targetFromEntry(recorded, recorded.server, "local-recorded");
74
+ // No record for this root (migration, or our broker went down and the entry was just pruned).
75
+ // Before guessing DEFAULT_SERVER, refuse if a DIFFERENT mesh is recorded there — otherwise the
76
+ // fallback would silently join someone else's mesh on the default port with our persona (the
77
+ // exact silent-wrong-mesh outcome this feature exists to prevent).
78
+ const onDefault = loadMeshes().find((m) => m.server === DEFAULT_SERVER && resolve(m.root) !== resolve(root));
79
+ if (onDefault)
80
+ throw new Error(`another mesh ("${onDefault.space}") is running at ${DEFAULT_SERVER} — run \`cotal up\` here to start yours, or \`--space ${onDefault.space}\` to join it`);
81
+ return localTarget(root, DEFAULT_SERVER, "local-space");
82
+ }
83
+ const meshes = loadMeshes();
84
+ if (meshes.length === 0)
85
+ throw new Error("no mesh running — run `cotal up` in a project, or pass `--server`");
86
+ if (meshes.length === 1)
87
+ return targetFromEntry(meshes[0], meshes[0].server, "registry");
88
+ const current = getCurrent();
89
+ const cur = current ? findMesh(current) : undefined;
90
+ if (cur)
91
+ return targetFromEntry(cur, cur.server, "current");
92
+ const names = meshes.map((m) => `${m.space} (${m.root})`).join(", ");
93
+ throw new Error(`multiple meshes running — ${names}. Pick one with \`--space <name>\` or set a default with \`cotal use <name>\`.`);
94
+ }
95
+ //# sourceMappingURL=mesh-target.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mesh-target.js","sourceRoot":"","sources":["../src/mesh-target.ts"],"names":[],"mappings":"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"}