cotal-ai 0.96.1 → 0.97.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.
@@ -21,12 +21,12 @@ import { join } from "node:path";
21
21
  import { contractDigest, resolveAuthProvider, ServiceAuthorityTransportError, singleDocumentClosure, VOID_SCHEMA_DIGEST } from "@cotal-ai/core";
22
22
  import { claimIdentityRecord, findMesh, homeCotalDir, spaceSegment, userAuthStateDir, workspaceSecretStore } from "@cotal-ai/workspace";
23
23
  import { connectCheck, DAEMON_ENDPOINT } from "./connect.js";
24
- /** The daemon's contract: one cluster with one read command, declared for the later telemetry slice.
25
- * It is ephemeral, untargeted and ungoverned, the only shape the service door admits. Nothing serves it yet. */
24
+ /** The daemon's existing void liveness contract. It reports no machine metadata or vitals.
25
+ * It is ephemeral, untargeted and ungoverned, the shape the service door admits. */
26
26
  const CLUSTER = { urn: DAEMON_ENDPOINT, revision: 1, attributes: [], events: [],
27
27
  commands: [{ name: "machine-status", class: "ephemeral", targeted: false, capability: "read", inputDigest: VOID_SCHEMA_DIGEST, outputDigest: VOID_SCHEMA_DIGEST }] };
28
28
  const closure = singleDocumentClosure(CLUSTER);
29
- const REGISTRATION = { clusterDigests: [closure.closureDigest], contractArtifacts: [closure.manifest, CLUSTER] };
29
+ export const DAEMON_REGISTRATION = { clusterDigests: [closure.closureDigest], contractArtifacts: [closure.manifest, CLUSTER] };
30
30
  const KIND = "cotal-daemon-register-request";
31
31
  export function daemonRegisterFile(space, home = homeCotalDir()) {
32
32
  return join(home, "daemon", spaceSegment(space), "register-request.json");
@@ -92,7 +92,7 @@ async function sendRegister(space, home) {
92
92
  const base = {
93
93
  v: 1, kind: "service-endpoint-authority", operation: "register", space,
94
94
  accountPublicKey: ready.accountPublicKey, endpoint: ready.endpoint, instanceId: ready.instanceId,
95
- registration: REGISTRATION, expected: { state: "unregistered" },
95
+ registration: DAEMON_REGISTRATION, expected: { state: "unregistered" },
96
96
  };
97
97
  const bodyDigest = contractDigest(base);
98
98
  const record = claimIdentityRecord(daemonRegisterFile(space, home), "daemon register request", parseRecord(space), () => ({
@@ -17,16 +17,18 @@
17
17
  * or close). Either going down withdraws `connected` at once, as `connecting`; it is claimed again
18
18
  * only after the broker is back and the space answers a fresh `ready`.
19
19
  *
20
- * No liveness command is served: the registered `machine-status` command has void output and a
21
- * service connection gets no stock serve grant (see the connect contract). No token, proof or
22
- * provider text leaves this module; every message is fixed text.
20
+ * The stock guest seam verifies each pulled credential against the registered authority's issuer
21
+ * and keys and this daemon's embedded closure. Each opened transport binds the existing void
22
+ * `machine-status` responder before ready. Its public descriptor includes the endpoint, derived
23
+ * owner token and command schemas; broker caller grants still control invoke access. It reports no vitals or machine metadata. No token, proof
24
+ * or provider text leaves this module; every message is fixed text.
23
25
  */
24
26
  import { join } from "node:path";
25
27
  import { existsSync } from "node:fs";
26
28
  import { randomBytes } from "node:crypto";
27
- import { CotalEndpoint, resolveAuthProvider, ServiceAuthorityTransportError, serviceWitnessDigest, } from "@cotal-ai/core";
29
+ import { CotalEndpoint, authorizeGuestServeGrant, compileContract, resolveAuthProvider, serveAuthenticityOf, serveEndpoint, ServiceAuthorityTransportError, serviceWitnessDigest, VOID_SCHEMA, } from "@cotal-ai/core";
28
30
  import { FsSecretStore, findMesh, homeCotalDir, spaceSegment, userAuthStateDir, workspaceSecretStore } from "@cotal-ai/workspace";
29
- import { daemonRegisterFile, daemonWitness } from "./register.js";
31
+ import { DAEMON_REGISTRATION, daemonRegisterFile, daemonWitness } from "./register.js";
30
32
  const SENTENCE = {
31
33
  "not-requested": "this daemon has not requested registration with this space; run `cotal daemon register --space <s>` first",
32
34
  "not-registered": "this space is not registered on this machine; add it with `cotal meshes add`",
@@ -114,7 +116,11 @@ export function createServiceConnections(home = homeCotalDir()) {
114
116
  l.endpoint = undefined;
115
117
  l.source = undefined;
116
118
  l.brokerUp = false;
119
+ l.flush = undefined;
120
+ const drains = [...l.serving].map((handle) => handle.stop().catch(() => { }));
117
121
  await ep?.stop().catch(() => { });
122
+ await Promise.all(drains);
123
+ l.serving.clear();
118
124
  await src?.close().catch(() => { });
119
125
  })();
120
126
  await l.closing;
@@ -138,6 +144,12 @@ export function createServiceConnections(home = homeCotalDir()) {
138
144
  * one persisted ready request per witness. Sets `connected` only if the broker is still up. */
139
145
  async function ready(l, space, witness, store, dir) {
140
146
  const provider = resolveAuthProvider();
147
+ const flush = l.flush;
148
+ if (!flush) {
149
+ l.state = "unavailable";
150
+ return;
151
+ }
152
+ await flush();
141
153
  const requestId = await readyRequestId(store, witness);
142
154
  if (requestId === undefined) {
143
155
  l.state = "unavailable";
@@ -162,7 +174,7 @@ export function createServiceConnections(home = homeCotalDir()) {
162
174
  return;
163
175
  }
164
176
  if (answer.state === "ready") {
165
- l.state = l.brokerUp && l.endpoint ? "connected" : "connecting";
177
+ l.state = l.brokerUp && l.endpoint && l.flush === flush && l.serving.size > 0 ? "connected" : "connecting";
166
178
  return;
167
179
  }
168
180
  if (answer.state === "denied" && answer.reason === "rate-limited") {
@@ -217,7 +229,7 @@ export function createServiceConnections(home = homeCotalDir()) {
217
229
  return result(space);
218
230
  if (current)
219
231
  await teardown(current);
220
- const l = current ?? { state: "connecting", brokerUp: false };
232
+ const l = current ?? { state: "connecting", brokerUp: false, serving: new Set() };
221
233
  l.state = "connecting";
222
234
  l.reason = undefined;
223
235
  live.set(space, l);
@@ -238,7 +250,7 @@ export function createServiceConnections(home = homeCotalDir()) {
238
250
  const witness = w.witness;
239
251
  l.instanceId = witness.instanceId;
240
252
  const provider = resolveAuthProvider();
241
- if (!provider.serviceEndpointRenewal || !provider.serviceEndpointAuthority) {
253
+ if (!provider.serviceEndpointRenewal || !provider.serviceEndpointAuthority || !provider.serviceEndpointInfo) {
242
254
  l.state = "unsupported";
243
255
  return result(space);
244
256
  }
@@ -246,8 +258,18 @@ export function createServiceConnections(home = homeCotalDir()) {
246
258
  const dir = userAuthStateDir(entry.root, space, { readOnly: true });
247
259
  const source = provider.serviceEndpointRenewal({ store, dir, witness });
248
260
  l.source = source;
261
+ let grant;
262
+ const pull = async () => {
263
+ const credential = await source.credential();
264
+ const info = await provider.serviceEndpointInfo({ store, dir, space });
265
+ if (info.accountPublicKey !== witness.accountPublicKey)
266
+ throw new Error("the service authority account changed");
267
+ grant = await authorizeGuestServeGrant({ accountPublicKey: witness.accountPublicKey, result: credential,
268
+ registration: DAEMON_REGISTRATION, authenticity: serveAuthenticityOf(info) });
269
+ return credential.accessBearer;
270
+ };
249
271
  try {
250
- await source.bearer();
272
+ await pull();
251
273
  }
252
274
  catch {
253
275
  sourceFailed(l);
@@ -264,11 +286,20 @@ export function createServiceConnections(home = homeCotalDir()) {
264
286
  const endpoint = new CotalEndpoint({
265
287
  space, servers: entry.server, sentinelCreds: source.sentinelCreds,
266
288
  bearer: async () => { try {
267
- return await source.bearer();
289
+ return await pull();
268
290
  }
269
291
  finally {
270
292
  onEnd();
271
293
  } },
294
+ onConnection: (nc) => {
295
+ if (l.endpoint !== endpoint)
296
+ return;
297
+ const contract = compileContract({ root: VOID_SCHEMA });
298
+ const handle = serveEndpoint(nc, space, grant, [{ command: "machine-status", contract: { input: contract, output: contract }, handler: () => null }], { public: true });
299
+ l.serving.add(handle);
300
+ l.flush = () => nc.flush();
301
+ void nc.closed().then(() => handle.stop()).catch(() => { }).finally(() => { l.serving.delete(handle); });
302
+ },
272
303
  card: { name: "cotal-daemon", kind: "endpoint", owner: witness.principal.owner, actor: witness.principal.actor },
273
304
  channels: [], registerPresence: false, watchPresence: false, watchChannels: false, consume: false,
274
305
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cotal-ai",
3
- "version": "0.96.1",
3
+ "version": "0.97.0",
4
4
  "description": "Cotal — lateral agent coordination over NATS. Run `npx cotal-ai` for the CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
@@ -33,26 +33,26 @@
33
33
  "node": ">=22"
34
34
  },
35
35
  "dependencies": {
36
- "@cotal-ai/auth": "0.96.1",
37
- "@cotal-ai/cli": "0.96.1",
38
- "@cotal-ai/connector-core": "0.96.1",
39
- "@cotal-ai/core": "0.96.1",
40
- "@cotal-ai/delivery": "0.96.1",
41
- "@cotal-ai/manager": "0.96.1",
42
- "@cotal-ai/runtime": "0.96.1",
43
- "@cotal-ai/seat": "0.96.1",
44
- "@cotal-ai/workspace": "0.96.1"
36
+ "@cotal-ai/auth": "0.97.0",
37
+ "@cotal-ai/cli": "0.97.0",
38
+ "@cotal-ai/connector-core": "0.97.0",
39
+ "@cotal-ai/core": "0.97.0",
40
+ "@cotal-ai/delivery": "0.97.0",
41
+ "@cotal-ai/manager": "0.97.0",
42
+ "@cotal-ai/runtime": "0.97.0",
43
+ "@cotal-ai/seat": "0.97.0",
44
+ "@cotal-ai/workspace": "0.97.0"
45
45
  },
46
46
  "devDependencies": {
47
- "@cotal-ai/connector-claude-code": "0.96.1",
48
- "@cotal-ai/connector-codex": "0.96.1",
49
- "@cotal-ai/connector-hermes": "0.96.1",
50
- "@cotal-ai/connector-jcode": "0.96.1",
51
- "@cotal-ai/connector-opencode": "0.96.1",
52
- "@cotal-ai/linear": "0.96.1",
53
- "@cotal-ai/pi": "0.96.1",
47
+ "@cotal-ai/connector-claude-code": "0.97.0",
48
+ "@cotal-ai/connector-codex": "0.97.0",
49
+ "@cotal-ai/connector-hermes": "0.97.0",
50
+ "@cotal-ai/connector-jcode": "0.97.0",
51
+ "@cotal-ai/connector-opencode": "0.97.0",
52
+ "@cotal-ai/linear": "0.97.0",
53
+ "@cotal-ai/pi": "0.97.0",
54
54
  "@cotal-ai/smoke-kit": "0.0.0",
55
- "@cotal-ai/web": "0.96.1",
55
+ "@cotal-ai/web": "0.97.0",
56
56
  "@nats-io/jetstream": "^3.4.0",
57
57
  "@nats-io/transport-node": "^3.4.0",
58
58
  "typescript": "^5.9.3"