cotal-ai 0.95.0 → 0.95.1
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/dist/daemon/client.js +17 -0
- package/dist/daemon/command.js +54 -8
- package/dist/daemon/protocol.js +3 -2
- package/dist/daemon/register.js +138 -0
- package/dist/daemon/server.js +10 -1
- package/package.json +18 -18
- package/seeded-connectors/claude/dist/mcp.cjs +3 -3
- package/seeded-connectors/claude/package.json +1 -1
- package/seeded-connectors/codex/dist/host.js +3 -3
- package/seeded-connectors/codex/package.json +1 -1
- package/seeded-connectors/hermes/dist/launch.js +3 -3
- package/seeded-connectors/hermes/package.json +1 -1
- package/seeded-connectors/hermes/plugin/cotal/_sidecar/standalone.cjs +3 -3
- package/seeded-connectors/jcode/dist/host.js +3 -3
- package/seeded-connectors/jcode/dist/mcp.js +3 -3
- package/seeded-connectors/jcode/package.json +1 -1
- package/seeded-connectors/opencode/dist/plugin/index.js +3 -3
- package/seeded-connectors/opencode/package.json +1 -1
- package/seeded-connectors/pi/dist/index.js +3 -3
- package/seeded-connectors/pi/dist/standalone.js +3 -3
- package/seeded-connectors/pi/package.json +1 -1
- package/seeded-connectors/web/package.json +1 -1
package/dist/daemon/client.js
CHANGED
|
@@ -27,6 +27,23 @@ export async function connectCheckViaDaemon(opts) {
|
|
|
27
27
|
sock.destroy();
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
+
/** Ask the daemon to submit its own service registration for `space` and report the space's answer. */
|
|
31
|
+
export async function registerViaDaemon(opts) {
|
|
32
|
+
const sock = await openVerified({ ...opts, timeoutMs: 5000 });
|
|
33
|
+
try {
|
|
34
|
+
sock.write(encode({ v: DAEMON_PROTOCOL_VERSION, id: randomUUID().replaceAll("-", ""), op: "register", space: opts.space }));
|
|
35
|
+
// Readiness read plus the stock register call, each bounded by the stock client (30 s).
|
|
36
|
+
const reply = JSON.parse(await readLine(sock, 65_000));
|
|
37
|
+
if (!reply.ok)
|
|
38
|
+
return { ok: false, code: reply.error.code, message: reply.error.message };
|
|
39
|
+
if ("event" in reply || !("outcome" in reply.result) || !("space" in reply.result))
|
|
40
|
+
return { ok: false, code: "BAD_REPLY", message: "the daemon answered the registration with something else" };
|
|
41
|
+
return { ok: true, result: reply.result };
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
sock.destroy();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
30
47
|
function connectTo(path, timeoutMs) {
|
|
31
48
|
return new Promise((resolve, reject) => {
|
|
32
49
|
const s = netConnect(path);
|
package/dist/daemon/command.js
CHANGED
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
* in an `implementations/*` package so it can later wire `@cotal-ai/auth` and `@cotal-ai/manager`
|
|
4
4
|
* without one implementation importing another (AGENTS.md, repository map).
|
|
5
5
|
*
|
|
6
|
-
* This slice serves a local, authenticated endpoint for status, sign-in
|
|
7
|
-
* nothing else: no
|
|
6
|
+
* This slice serves a local, authenticated endpoint for status, sign-in, a connection check and a
|
|
7
|
+
* registration request, and nothing else: no activation or serving, no Manager, no mesh or Cloud
|
|
8
|
+
* connection, no service install. Linux
|
|
8
9
|
* only. Sign-in runs inside the
|
|
9
10
|
* daemon through stock `establishIdpSession`, so the session lands in the machine-home Cotal
|
|
10
11
|
* directory where `cotal login` keeps it, and no token ever crosses the local endpoint. The
|
|
11
12
|
* connection check (`connect.ts`) reads the space's public account identity and keeps the daemon's
|
|
12
|
-
* own per-space identity, never a Manager's.
|
|
13
|
+
* own per-space identity, never a Manager's. The registration request (`register.ts`) sends one stock
|
|
14
|
+
* `register` for that identity and reports the space's answer; approval stays with the space owner. It is a
|
|
13
15
|
* foreground foundation, not a resilient machine service: a daemon killed without cleanup leaves
|
|
14
16
|
* its socket behind, and the next start refuses until the operator removes it (see the
|
|
15
17
|
* ALREADY_RUNNING remedy). `state: "ready"` means the local control listener is bound and
|
|
@@ -24,7 +26,7 @@ import { peerCredentials } from "@cotal-ai/seat";
|
|
|
24
26
|
import { normalizeIdpUrl } from "@cotal-ai/auth";
|
|
25
27
|
import { EndpointRefusal, daemonPaths } from "./endpoint.js";
|
|
26
28
|
import { serveDaemon } from "./server.js";
|
|
27
|
-
import { connectCheckViaDaemon, loginViaDaemon, queryDaemon } from "./client.js";
|
|
29
|
+
import { connectCheckViaDaemon, loginViaDaemon, queryDaemon, registerViaDaemon } from "./client.js";
|
|
28
30
|
export const daemonFlags = [
|
|
29
31
|
jsonFlag,
|
|
30
32
|
plainFlag,
|
|
@@ -187,6 +189,47 @@ async function connectCheckCommand(args) {
|
|
|
187
189
|
] },
|
|
188
190
|
] }));
|
|
189
191
|
}
|
|
192
|
+
/** Ask the running daemon to submit its own service registration for a space. The space owner's
|
|
193
|
+
* approval and the operator's name binding happen elsewhere; this never activates or serves. */
|
|
194
|
+
async function registerCommand(args) {
|
|
195
|
+
const presentation = presentationOf(args.values);
|
|
196
|
+
const space = args.values.space;
|
|
197
|
+
if (typeof space !== "string" || !space)
|
|
198
|
+
throw new CommandFailure("refused", "USAGE_INVALID", "cotal daemon register needs --space <s>");
|
|
199
|
+
let reply;
|
|
200
|
+
try {
|
|
201
|
+
reply = await registerViaDaemon({ paths: daemonPaths(), peerUid, space });
|
|
202
|
+
}
|
|
203
|
+
catch (e) {
|
|
204
|
+
throw asFailure(e);
|
|
205
|
+
}
|
|
206
|
+
if (!reply.ok)
|
|
207
|
+
throw new CommandFailure("refused", "USAGE_INVALID", `the daemon refused the registration request: ${reply.code} ${reply.message}`, { native: { code: reply.code } });
|
|
208
|
+
const r = reply.result;
|
|
209
|
+
const native = { native: { code: (r.outcome === "refused" && "reason" in r ? r.reason : r.outcome).toUpperCase().replaceAll("-", "_") } };
|
|
210
|
+
if (r.outcome === "not-registered")
|
|
211
|
+
throw new CommandFailure("refused", "TARGET_NOT_FOUND", r.message, native);
|
|
212
|
+
if (r.outcome === "not-user-auth" || r.outcome === "unsupported-by-space")
|
|
213
|
+
throw new CommandFailure("refused", "UNSUPPORTED", r.message, native);
|
|
214
|
+
if (r.outcome === "not-signed-in")
|
|
215
|
+
throw new CommandFailure("refused", "AUTH_REQUIRED", r.message, native);
|
|
216
|
+
if (r.outcome === "unreachable")
|
|
217
|
+
throw new CommandFailure("refused", "DEPENDENCY_UNAVAILABLE", r.message, native);
|
|
218
|
+
if (r.outcome === "account-mismatch" || r.outcome === "conflict")
|
|
219
|
+
throw new CommandFailure("refused", "CONFLICT", r.message, native);
|
|
220
|
+
// Only a refusal carrying the space's stock reason is a denial; one without is the daemon's own failure.
|
|
221
|
+
if (r.outcome === "refused")
|
|
222
|
+
throw new CommandFailure("refused", "reason" in r ? "PERMISSION_DENIED" : "DEPENDENCY_UNAVAILABLE", r.message, native);
|
|
223
|
+
if (r.outcome !== "approval-required" && r.outcome !== "pending" && r.outcome !== "registered")
|
|
224
|
+
throw new CommandFailure("unknown", "OPERATION_UNKNOWN", r.message, native);
|
|
225
|
+
const title = r.outcome === "approval-required" ? "Waiting for the space owner" : r.outcome === "pending" ? "Registration in progress" : "Registered, not connected";
|
|
226
|
+
writeResult(presentation, { command: "daemon register", target: { space }, result: r }, () => ({ sections: [
|
|
227
|
+
{ title, blocks: [
|
|
228
|
+
{ kind: "fields", rows: [["Space", r.space], ["Service", r.endpoint], ["Instance", r.instanceId], ["Request", r.requestId]] },
|
|
229
|
+
{ kind: "lines", lines: [{ text: r.message, tone: "hint" }] },
|
|
230
|
+
] },
|
|
231
|
+
] }));
|
|
232
|
+
}
|
|
190
233
|
async function serve() {
|
|
191
234
|
let running;
|
|
192
235
|
try {
|
|
@@ -214,23 +257,26 @@ async function run(args) {
|
|
|
214
257
|
return login(args);
|
|
215
258
|
if (mode === "connect-check")
|
|
216
259
|
return connectCheckCommand(args);
|
|
217
|
-
|
|
260
|
+
if (mode === "register")
|
|
261
|
+
return registerCommand(args);
|
|
262
|
+
throw new CommandFailure("refused", "USAGE_INVALID", mode === undefined ? "cotal daemon needs a mode: status, serve, login, connect-check or register" : `unknown daemon mode ${JSON.stringify(mode)}`, { remedy: "cotal daemon status" });
|
|
218
263
|
}
|
|
219
264
|
const daemonCommand = {
|
|
220
265
|
kind: "command",
|
|
221
266
|
name: "daemon",
|
|
222
267
|
group: "Manager",
|
|
223
|
-
summary: "the local machine daemon (Linux): serve its control endpoint, read its status, sign it in, check
|
|
224
|
-
positionals: "<status|serve|login|connect-check>",
|
|
268
|
+
summary: "the local machine daemon (Linux): serve its control endpoint, read its status, sign it in, check and request its own registration with a space",
|
|
269
|
+
positionals: "<status|serve|login|connect-check|register>",
|
|
225
270
|
flags: daemonFlags,
|
|
226
271
|
subcommands: [
|
|
227
272
|
{ name: "status", summary: "ask the running daemon for its status over the local endpoint", flagNames: ["json", "plain"], outputs: ["json", "plain"], reads: true, prepareMeshTarget: false },
|
|
228
273
|
{ name: "serve", summary: "run the daemon in the foreground and serve its local endpoint (refusals honor --json)", flagNames: ["json", "plain"], outputs: ["json", "plain"], prepareMeshTarget: false },
|
|
229
274
|
{ name: "login", summary: "sign the running daemon in to a provider with device approval; the daemon keeps the session", flagNames: ["idp", "client-id", "json", "plain"], outputs: ["json", "plain"], prepareMeshTarget: false },
|
|
230
275
|
{ name: "connect-check", summary: "ask the running daemon whether it could register with a space as its own service; registers nothing", flagNames: ["space", "json", "plain"], outputs: ["json", "plain"], reads: true, prepareMeshTarget: false },
|
|
276
|
+
{ name: "register", summary: "ask the running daemon to request its own service registration with a space; the space owner must approve it, and it does not connect", flagNames: ["space", "json", "plain"], outputs: ["json", "plain"], prepareMeshTarget: false },
|
|
231
277
|
],
|
|
232
278
|
prepareMeshTarget: false,
|
|
233
|
-
examples: ["cotal daemon serve", "cotal daemon status", "cotal daemon status --json", "cotal daemon login --idp https://example.org/api/auth", "cotal daemon connect-check --space acme"],
|
|
279
|
+
examples: ["cotal daemon serve", "cotal daemon status", "cotal daemon status --json", "cotal daemon login --idp https://example.org/api/auth", "cotal daemon connect-check --space acme", "cotal daemon register --space acme"],
|
|
234
280
|
run,
|
|
235
281
|
};
|
|
236
282
|
registry.register(daemonCommand);
|
package/dist/daemon/protocol.js
CHANGED
|
@@ -13,13 +13,14 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { normalizeIdpUrl } from "@cotal-ai/auth";
|
|
15
15
|
import { DAEMON_PROTOCOL_VERSION, MAX_LINE_BYTES } from "./endpoint.js";
|
|
16
|
-
export const DAEMON_OPS = ["status", "login-start", "login-cancel", "connect-check"];
|
|
16
|
+
export const DAEMON_OPS = ["status", "login-start", "login-cancel", "connect-check", "register"];
|
|
17
17
|
/** The fields each op takes besides `v`, `id` and `op`. Required ones must be present. */
|
|
18
18
|
const OP_FIELDS = {
|
|
19
19
|
status: { required: [], optional: [] },
|
|
20
20
|
"login-start": { required: ["idp"], optional: ["clientId"] },
|
|
21
21
|
"login-cancel": { required: ["attempt"], optional: [] },
|
|
22
22
|
"connect-check": { required: ["space"], optional: [] },
|
|
23
|
+
register: { required: ["space"], optional: [] },
|
|
23
24
|
};
|
|
24
25
|
const ENVELOPE_KEYS = new Set(["v", "id", "op"]);
|
|
25
26
|
const ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
|
|
@@ -64,7 +65,7 @@ export function parseRequest(line) {
|
|
|
64
65
|
return bad(id, "attempt must be the 22-character attempt id from login-prompt");
|
|
65
66
|
return { ok: true, request: { v, id, op, attempt: obj.attempt } };
|
|
66
67
|
}
|
|
67
|
-
if (op === "connect-check") {
|
|
68
|
+
if (op === "connect-check" || op === "register") {
|
|
68
69
|
if (typeof obj.space !== "string" || !SPACE_PATTERN.test(obj.space))
|
|
69
70
|
return bad(id, "space must be 1-64 characters of [A-Za-z0-9_-]");
|
|
70
71
|
return { ok: true, request: { v, id, op, space: obj.space } };
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The daemon's own service registration (A2b.2). It submits ONE stock `register` request for the
|
|
3
|
+
* daemon's A2b.1 identity and reports the stock answer. It never activates, serves, holds a bearer,
|
|
4
|
+
* decides, binds a name or grants a caller, and it never touches a Manager.
|
|
5
|
+
*
|
|
6
|
+
* It runs the A2b.1 readiness check first. Any outcome other than `ready-to-register` is returned
|
|
7
|
+
* unchanged, so a space without service endpoints gets 0 POST and no identity.
|
|
8
|
+
*
|
|
9
|
+
* Request identity follows the stock rule (docs/embedding.md, "Service authority contracts"): one
|
|
10
|
+
* requestId per instance and body. It is persisted with the body digest before the first send, through
|
|
11
|
+
* stock `claimIdentityRecord`, and replayed unchanged on every later call, restarts included. The host
|
|
12
|
+
* bounds an instance to 64 distinct requests, so a fresh key is never minted for the same instance. A
|
|
13
|
+
* different body for the same instance is refused locally.
|
|
14
|
+
*
|
|
15
|
+
* The request goes through the registered auth provider's `serviceEndpointAuthority` with no proof
|
|
16
|
+
* argument, so the stock client presents the machine's signed-in IdP proof itself. The daemon never
|
|
17
|
+
* reads or relays that proof.
|
|
18
|
+
*/
|
|
19
|
+
import { randomBytes } from "node:crypto";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import { contractDigest, resolveAuthProvider, ServiceAuthorityTransportError, singleDocumentClosure, VOID_SCHEMA_DIGEST } from "@cotal-ai/core";
|
|
22
|
+
import { claimIdentityRecord, findMesh, homeCotalDir, spaceSegment, userAuthStateDir, workspaceSecretStore } from "@cotal-ai/workspace";
|
|
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. */
|
|
26
|
+
const CLUSTER = { urn: DAEMON_ENDPOINT, revision: 1, attributes: [], events: [],
|
|
27
|
+
commands: [{ name: "machine-status", class: "ephemeral", targeted: false, capability: "read", inputDigest: VOID_SCHEMA_DIGEST, outputDigest: VOID_SCHEMA_DIGEST }] };
|
|
28
|
+
const closure = singleDocumentClosure(CLUSTER);
|
|
29
|
+
const REGISTRATION = { clusterDigests: [closure.closureDigest], contractArtifacts: [closure.manifest, CLUSTER] };
|
|
30
|
+
const KIND = "cotal-daemon-register-request";
|
|
31
|
+
export function daemonRegisterFile(space, home = homeCotalDir()) {
|
|
32
|
+
return join(home, "daemon", spaceSegment(space), "register-request.json");
|
|
33
|
+
}
|
|
34
|
+
const parseRecord = (space) => (raw) => {
|
|
35
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
36
|
+
return undefined;
|
|
37
|
+
const o = raw;
|
|
38
|
+
if (Object.keys(o).sort().join(",") !== "bodyDigest,instanceId,kind,requestId,space,v")
|
|
39
|
+
return undefined;
|
|
40
|
+
if (o.v !== 1 || o.kind !== KIND || o.space !== space || typeof o.instanceId !== "string" || typeof o.bodyDigest !== "string")
|
|
41
|
+
return undefined;
|
|
42
|
+
if (typeof o.requestId !== "string" || !/^[0-9a-f]{32}$/.test(o.requestId))
|
|
43
|
+
return undefined;
|
|
44
|
+
return o;
|
|
45
|
+
};
|
|
46
|
+
/** Fixed sentences for the stock register denial codes (`service-endpoint-authority.ts` register
|
|
47
|
+
* door and its host). Any other code is reported as {@link UNRECOGNIZED}, so no remote text crosses
|
|
48
|
+
* the endpoint. */
|
|
49
|
+
const DENIED = {
|
|
50
|
+
"proposal-refused": "the space owner refused this daemon; a new proposal needs a new request, which this command does not send",
|
|
51
|
+
"proposal-expired": "the space owner did not decide before the proposal expired; a new proposal needs a new request, which this command does not send",
|
|
52
|
+
"approval-revoked": "the space owner revoked this daemon's approval",
|
|
53
|
+
"permission-denied": "the signed-in account may not register services in this space",
|
|
54
|
+
conflict: "the space already holds a different registration for this daemon's instance",
|
|
55
|
+
expired: "the space's approval for this daemon has expired",
|
|
56
|
+
"bad-request": "the space refused the registration request as malformed",
|
|
57
|
+
"closure-invalid": "the space refused the daemon's contract",
|
|
58
|
+
"unsupported-runtime": "the space refused the daemon's contract",
|
|
59
|
+
unsupported: "the space does not support this registration",
|
|
60
|
+
unauthenticated: "the space did not accept this machine's sign-in",
|
|
61
|
+
"rate-limited": "the space is rate-limiting registrations; try again later",
|
|
62
|
+
"resource-exhausted": "the space has no room left for this daemon's requests",
|
|
63
|
+
};
|
|
64
|
+
const UNRECOGNIZED = "unrecognized";
|
|
65
|
+
/** The stock register pending phase (`approved-unregistered`); any other is reported as {@link UNRECOGNIZED}. */
|
|
66
|
+
const PHASES = new Set(["approved-unregistered"]);
|
|
67
|
+
/** A stock proposal id: 64 lowercase hex characters (`service-host-ops.ts`, `hexN(proposalId, 64)`). */
|
|
68
|
+
const PROPOSAL_ID = /^[0-9a-f]{64}$/;
|
|
69
|
+
const NOT_SIGNED_IN = "this machine's sign-in was not accepted by the space's provider (expired, revoked or missing); run `cotal daemon login --idp <url>`";
|
|
70
|
+
const NOT_SENT = "the daemon could not send the registration request; nothing was sent and nothing is connected";
|
|
71
|
+
const UNUSABLE = "the space answered the registration with something the daemon does not use; run the same command again";
|
|
72
|
+
export async function registerDaemon(space, home = homeCotalDir()) {
|
|
73
|
+
const ready = await connectCheck(space, home);
|
|
74
|
+
if (ready.outcome !== "ready-to-register")
|
|
75
|
+
return ready;
|
|
76
|
+
const entry = findMesh(space);
|
|
77
|
+
const base = {
|
|
78
|
+
v: 1, kind: "service-endpoint-authority", operation: "register", space,
|
|
79
|
+
accountPublicKey: ready.accountPublicKey, endpoint: ready.endpoint, instanceId: ready.instanceId,
|
|
80
|
+
registration: REGISTRATION, expected: { state: "unregistered" },
|
|
81
|
+
};
|
|
82
|
+
const bodyDigest = contractDigest(base);
|
|
83
|
+
const record = claimIdentityRecord(daemonRegisterFile(space, home), "daemon register request", parseRecord(space), () => ({
|
|
84
|
+
v: 1, kind: KIND, space, instanceId: ready.instanceId, requestId: randomBytes(16).toString("hex"), bodyDigest,
|
|
85
|
+
}));
|
|
86
|
+
if (record.instanceId !== ready.instanceId || record.bodyDigest !== bodyDigest)
|
|
87
|
+
return { outcome: "conflict", space, requestId: record.requestId,
|
|
88
|
+
message: "this daemon already sent a different registration for this space; nothing was sent. Confirm the change before removing its register request file by hand" };
|
|
89
|
+
const request = { ...base, requestId: record.requestId };
|
|
90
|
+
const ids = { space, requestId: record.requestId, endpoint: ready.endpoint, instanceId: ready.instanceId };
|
|
91
|
+
let result;
|
|
92
|
+
try {
|
|
93
|
+
const provider = resolveAuthProvider();
|
|
94
|
+
if (!provider.serviceEndpointAuthority)
|
|
95
|
+
return { outcome: "unsupported-by-space", space, message: "the space's auth service does not host service endpoints, so the daemon cannot register with it" };
|
|
96
|
+
result = (await provider.serviceEndpointAuthority({ store: workspaceSecretStore(entry.root), dir: userAuthStateDir(entry.root, space, { readOnly: true }), request })).result;
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
const status = e.httpStatus;
|
|
100
|
+
if (status === 404)
|
|
101
|
+
return { outcome: "unsupported-by-space", space, message: "the space's auth service does not host service endpoints, so the daemon cannot register with it" };
|
|
102
|
+
// Only the stock transport's own unknown outcome may have reached the space.
|
|
103
|
+
if (e instanceof ServiceAuthorityTransportError && e.outcome === "unknown")
|
|
104
|
+
return { outcome: "unconfirmed", ...ids,
|
|
105
|
+
message: "the space did not confirm the registration request; run the same command again to replay it unchanged" };
|
|
106
|
+
// Everything else failed before any send. A sign-in the IdP rejected (stock `fetchIdpJwt`'s fixed
|
|
107
|
+
// 401 sentence; it carries no code) or none at all (`signed-out`) needs a new sign-in.
|
|
108
|
+
if (e.code === "signed-out" || /rejected the cached session/.test(e.message ?? ""))
|
|
109
|
+
return { outcome: "not-signed-in", space, message: NOT_SIGNED_IN };
|
|
110
|
+
return { outcome: "refused", space, message: NOT_SENT };
|
|
111
|
+
}
|
|
112
|
+
return registerAnswer(result, ids);
|
|
113
|
+
}
|
|
114
|
+
/** The daemon's outcome for one stock register answer. */
|
|
115
|
+
export function registerAnswer(result, ids) {
|
|
116
|
+
switch (result.state) {
|
|
117
|
+
case "approval-required":
|
|
118
|
+
if (!PROPOSAL_ID.test(result.proposalId))
|
|
119
|
+
return { outcome: "unconfirmed", ...ids, message: UNUSABLE };
|
|
120
|
+
return { outcome: "approval-required", ...ids, proposalId: result.proposalId,
|
|
121
|
+
message: "the space owner must approve this daemon and the space operator must bind its service name; nothing is connected" };
|
|
122
|
+
case "pending":
|
|
123
|
+
return { outcome: "pending", ...ids, phase: PHASES.has(result.phase) ? result.phase : UNRECOGNIZED, message: "the space is processing an approved registration; run the same command again" };
|
|
124
|
+
case "registered": {
|
|
125
|
+
const w = result.witness;
|
|
126
|
+
return { outcome: "registered", ...ids,
|
|
127
|
+
witness: { accountPublicKey: w.accountPublicKey, space: w.space, endpoint: w.endpoint, instanceId: w.instanceId, registrationRevision: w.registrationRevision, processEpoch: w.processEpoch },
|
|
128
|
+
message: "registered with the space; not activated or serving, so this machine is not connected yet" };
|
|
129
|
+
}
|
|
130
|
+
case "denied": {
|
|
131
|
+
const known = Object.hasOwn(DENIED, result.reason);
|
|
132
|
+
return { outcome: "refused", ...ids, reason: known ? result.reason : UNRECOGNIZED,
|
|
133
|
+
message: known ? DENIED[result.reason] : "the space refused the registration for a reason the daemon does not recognize" };
|
|
134
|
+
}
|
|
135
|
+
default:
|
|
136
|
+
return { outcome: "unconfirmed", ...ids, message: UNUSABLE };
|
|
137
|
+
}
|
|
138
|
+
}
|
package/dist/daemon/server.js
CHANGED
|
@@ -16,6 +16,7 @@ import { DAEMON_PROTOCOL_VERSION, MAX_LINE_BYTES, EndpointRefusal, assertLinux,
|
|
|
16
16
|
import { encode, parseRequest } from "./protocol.js";
|
|
17
17
|
import { createLoginCoordinator } from "./login.js";
|
|
18
18
|
import { connectCheck } from "./connect.js";
|
|
19
|
+
import { registerDaemon } from "./register.js";
|
|
19
20
|
import { homeCotalDir } from "@cotal-ai/workspace";
|
|
20
21
|
/** Requests one connection may send. A sign-in needs two (start, cancel); status polling a few. */
|
|
21
22
|
export const MAX_REQUESTS_PER_CONNECTION = 64;
|
|
@@ -41,7 +42,7 @@ export async function serveDaemon(opts) {
|
|
|
41
42
|
const { paths } = opts;
|
|
42
43
|
const dirOutcome = ensureDir(paths.dir, uid);
|
|
43
44
|
refuseExisting(paths.socket);
|
|
44
|
-
const counters = { accepted: 0, peerRejected: 0, framesDecoded: 0, requestsRefused: 0, statusAnswered: 0, loginsStarted: 0, loginsRefused: 0, cancelsAnswered: 0, connectChecksAnswered: 0 };
|
|
45
|
+
const counters = { accepted: 0, peerRejected: 0, framesDecoded: 0, requestsRefused: 0, statusAnswered: 0, loginsStarted: 0, loginsRefused: 0, cancelsAnswered: 0, connectChecksAnswered: 0, registersAnswered: 0 };
|
|
45
46
|
const machineHome = opts.sessionDir ?? homeCotalDir();
|
|
46
47
|
const login = opts.login ?? createLoginCoordinator({ sessionDir: machineHome });
|
|
47
48
|
const startedAt = new Date();
|
|
@@ -119,6 +120,14 @@ export async function serveDaemon(opts) {
|
|
|
119
120
|
});
|
|
120
121
|
return;
|
|
121
122
|
}
|
|
123
|
+
if (req.op === "register") {
|
|
124
|
+
void registerDaemon(req.space, machineHome).catch(() => ({ outcome: "refused", space: req.space, message: "the daemon could not complete the registration request; nothing is connected" }))
|
|
125
|
+
.then((result) => {
|
|
126
|
+
counters.registersAnswered++;
|
|
127
|
+
return write({ v: DAEMON_PROTOCOL_VERSION, id: req.id, ok: true, result });
|
|
128
|
+
});
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
122
131
|
if (req.op === "login-cancel") {
|
|
123
132
|
void login.cancel(req.attempt).then((result) => {
|
|
124
133
|
counters.cancelsAnswered++;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cotal-ai",
|
|
3
|
-
"version": "0.95.
|
|
3
|
+
"version": "0.95.1",
|
|
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.95.
|
|
37
|
-
"@cotal-ai/cli": "0.95.
|
|
38
|
-
"@cotal-ai/connector-core": "0.95.
|
|
39
|
-
"@cotal-ai/core": "0.95.
|
|
40
|
-
"@cotal-ai/delivery": "0.95.
|
|
41
|
-
"@cotal-ai/manager": "0.95.
|
|
42
|
-
"@cotal-ai/runtime": "0.95.
|
|
43
|
-
"@cotal-ai/seat": "0.95.
|
|
44
|
-
"@cotal-ai/workspace": "0.95.
|
|
36
|
+
"@cotal-ai/auth": "0.95.1",
|
|
37
|
+
"@cotal-ai/cli": "0.95.1",
|
|
38
|
+
"@cotal-ai/connector-core": "0.95.1",
|
|
39
|
+
"@cotal-ai/core": "0.95.1",
|
|
40
|
+
"@cotal-ai/delivery": "0.95.1",
|
|
41
|
+
"@cotal-ai/manager": "0.95.1",
|
|
42
|
+
"@cotal-ai/runtime": "0.95.1",
|
|
43
|
+
"@cotal-ai/seat": "0.95.1",
|
|
44
|
+
"@cotal-ai/workspace": "0.95.1"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"@cotal-ai/connector-claude-code": "0.95.
|
|
48
|
-
"@cotal-ai/connector-codex": "0.95.
|
|
49
|
-
"@cotal-ai/connector-hermes": "0.95.
|
|
50
|
-
"@cotal-ai/connector-jcode": "0.95.
|
|
51
|
-
"@cotal-ai/connector-opencode": "0.95.
|
|
52
|
-
"@cotal-ai/linear": "0.95.
|
|
53
|
-
"@cotal-ai/pi": "0.95.
|
|
47
|
+
"@cotal-ai/connector-claude-code": "0.95.1",
|
|
48
|
+
"@cotal-ai/connector-codex": "0.95.1",
|
|
49
|
+
"@cotal-ai/connector-hermes": "0.95.1",
|
|
50
|
+
"@cotal-ai/connector-jcode": "0.95.1",
|
|
51
|
+
"@cotal-ai/connector-opencode": "0.95.1",
|
|
52
|
+
"@cotal-ai/linear": "0.95.1",
|
|
53
|
+
"@cotal-ai/pi": "0.95.1",
|
|
54
54
|
"@cotal-ai/smoke-kit": "0.0.0",
|
|
55
|
-
"@cotal-ai/web": "0.95.
|
|
55
|
+
"@cotal-ai/web": "0.95.1",
|
|
56
56
|
"@nats-io/jetstream": "^3.4.0",
|
|
57
57
|
"@nats-io/transport-node": "^3.4.0",
|
|
58
58
|
"typescript": "^5.9.3"
|