@ccmsg/cli 0.11.3 → 0.11.5
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/README.md +8 -12
- package/package.json +1 -1
- package/src/auth/auth.ts +23 -23
- package/src/auth/http.ts +3 -3
- package/src/auth/records.ts +2 -2
- package/src/auth/webauthn.ts +1 -1
- package/src/cli.ts +10 -10
- package/src/daemon/control.ts +1 -1
- package/src/daemon/registry.ts +11 -11
- package/src/daemon/snapshot.ts +1 -1
- package/src/daemon/supervise.ts +4 -4
- package/src/dispatch/caller.ts +1 -1
- package/src/dispatch/dispatch.ts +4 -4
- package/src/dispatch/handler.ts +4 -4
- package/src/dispatch/identity.ts +1 -1
- package/src/dispatch/result.ts +2 -2
- package/src/files/containment.ts +2 -2
- package/src/harness/index.ts +1 -1
- package/src/instance/client.ts +1 -1
- package/src/instance/config.ts +13 -13
- package/src/instance/instance.ts +42 -42
- package/src/instance/lock.ts +2 -2
- package/src/instance/log.ts +1 -1
- package/src/instance/paths.ts +6 -6
- package/src/instance/socket.ts +1 -1
- package/src/kv/store.ts +1 -1
- package/src/mesh/instances.ts +2 -2
- package/src/mesh/keys.ts +7 -7
- package/src/mesh/mesh.ts +67 -67
- package/src/mesh/relay.ts +8 -8
- package/src/mesh/wire.ts +2 -2
- package/src/messaging/delivery.ts +24 -24
- package/src/messaging/direct.ts +23 -23
- package/src/messaging/inbox.ts +6 -6
- package/src/messaging/notify.ts +5 -5
- package/src/plugin/codex.ts +1 -1
- package/src/sessions/classify.ts +6 -6
- package/src/sessions/dump.ts +1 -1
- package/src/sessions/handlers.ts +2 -2
- package/src/sessions/harness.ts +12 -12
- package/src/sessions/last-live.ts +4 -4
- package/src/sessions/registry.ts +32 -32
- package/src/sessions/status.ts +7 -7
- package/src/sessions/terminals.ts +1 -1
- package/src/transcript/files.ts +4 -4
- package/src/transcript/fold.ts +9 -9
- package/src/transcript/items/classify.ts +56 -8
- package/src/transcript/read.ts +1 -1
- package/src/transcript/tail.ts +3 -3
- package/src/transcript/transcripts.ts +7 -7
- package/src/translate/helper.ts +2 -2
- package/src/transport/conn.ts +3 -3
- package/src/transport/dial.ts +1 -1
- package/src/transport/driver.ts +2 -2
- package/src/transport/entry.ts +1 -1
- package/src/transport/framing.ts +1 -1
- package/src/transport/listener.ts +3 -3
- package/src/transport/uds.ts +2 -2
- package/src/transport/ws.ts +2 -2
- package/src/upstream/events.ts +3 -3
- package/src/upstream/gateway.ts +6 -6
- package/src/upstream/json.ts +1 -1
- package/src/upstream/requests.ts +6 -6
- package/src/upstream/stats.ts +1 -1
- package/src/upstream/status.ts +4 -4
- package/src/upstream/usage.ts +1 -1
- package/src/upstream/webhook.ts +1 -1
package/README.md
CHANGED
|
@@ -2,19 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
> 🇯🇵 [README-ja.md](./README-ja.md)
|
|
4
4
|
|
|
5
|
-
The daemon that serves one **instance** (= one config home) as an endpoint of the contract,
|
|
6
|
-
together with its CLI and the plugins it hands to agents.
|
|
5
|
+
The daemon that serves one **instance** (= one config home) as an endpoint of the contract, together with its CLI and the plugins it hands to agents.
|
|
7
6
|
|
|
8
7
|
- `daemon` — holds sessions, delivery, topics and the mesh, and answers the contract's ops over UDS / WS
|
|
9
8
|
- `cli` — the entry point a session calls its instance through (`ccmsg`)
|
|
10
|
-
- `plugin` — the plugin an agent receives (`ccmsg plugin install claude`). Claude Code is the only
|
|
11
|
-
agent that has one; a codex plugin is not implemented, and what it would take is tracked in
|
|
12
|
-
[`docs/issue/2026-09-09-codex-plugin-delivery-via-thread-queue.md`](./docs/issue/2026-09-09-codex-plugin-delivery-via-thread-queue.md)
|
|
9
|
+
- `plugin` — the plugin an agent receives (`ccmsg plugin install claude`). Claude Code is the only agent that has one; a codex plugin is not implemented, and what it would take is tracked in [`docs/issue/2026-09-09-codex-plugin-delivery-via-thread-queue.md`](./docs/issue/2026-09-09-codex-plugin-delivery-via-thread-queue.md)
|
|
13
10
|
|
|
14
|
-
The wire contract is [`@ccmsg/protocol`](https://github.com/kawaz/ccmsg-protocol), pinned to a
|
|
15
|
-
version here. Who may call what, and what comes back, is decided by the contract's attribute
|
|
16
|
-
table and schemas; the daemon reads them rather than carrying validation or authorization
|
|
17
|
-
branches of its own.
|
|
11
|
+
The wire contract is [`@ccmsg/protocol`](https://github.com/kawaz/ccmsg-protocol), pinned to a version here. Who may call what, and what comes back, is decided by the contract's attribute table and schemas; the daemon reads them rather than carrying validation or authorization branches of its own.
|
|
18
12
|
|
|
19
13
|
## What it does not do
|
|
20
14
|
|
|
@@ -23,12 +17,14 @@ branches of its own.
|
|
|
23
17
|
- Re-derive an upstream judgment (the gateway's severity, Claude Code's permission decisions) or observe another config home
|
|
24
18
|
- Carry validation of its own, or serve v1 alongside — the contract's validator is called, and the new lineage stands as a separate instance
|
|
25
19
|
|
|
26
|
-
Authenticating a person is not on that list: the daemon answers "who came" itself, with a
|
|
27
|
-
passkey. [docs/DESIGN.md](./docs/DESIGN.md) §9 carries the reason each of these ties back to.
|
|
20
|
+
Authenticating a person is not on that list: the daemon answers "who came" itself, with a passkey. [docs/DESIGN.md](./docs/DESIGN.md) §8.6 carries the reason each of these ties back to.
|
|
28
21
|
|
|
29
22
|
## Documentation
|
|
30
23
|
|
|
31
|
-
- [docs/DESIGN.md](./docs/DESIGN.md) — layers,
|
|
24
|
+
- [docs/DESIGN.md](./docs/DESIGN.md) — **what it is now**: the purpose and what is not grown, the contract and the layers, authentication, the state model, transcripts and dumps, topics and delivery, the mesh, operation, and how it is tested
|
|
25
|
+
- [docs/decisions/](./docs/decisions/INDEX.md) — **the record of the judgments (DR)**: why something was decided that way, and what was set aside
|
|
26
|
+
- [docs/ROADMAP.md](./docs/ROADMAP.md) — the bundles of work in the order they are taken, pointing at the issues across the three repos
|
|
27
|
+
- [docs/design/](./docs/design/README.md) — the design details that stand on their own (authentication between instances, the item types of a dump)
|
|
32
28
|
|
|
33
29
|
## License
|
|
34
30
|
|
package/package.json
CHANGED
package/src/auth/auth.ts
CHANGED
|
@@ -44,7 +44,7 @@ import { CborError } from "./cbor.ts";
|
|
|
44
44
|
|
|
45
45
|
/** How long an access token is accepted, and how long a refresh token is.
|
|
46
46
|
*
|
|
47
|
-
* Chosen rather than derived, within the DR's "hours" and "days" (§2.4). The
|
|
47
|
+
* Chosen rather than derived, within the DR's "hours" and "days" (DR-0001 §2.4). The
|
|
48
48
|
* access token's life is also a connection's: a client renews on the
|
|
49
49
|
* connection it already holds, so the period is what bounds a stolen token
|
|
50
50
|
* rather than how often a person is interrupted. The refresh token's life is
|
|
@@ -84,7 +84,7 @@ export const CODE_ATTEMPTS = 5;
|
|
|
84
84
|
/** How many `/auth/*` requests are answered per second, over all callers.
|
|
85
85
|
*
|
|
86
86
|
* The routes are reached before anything is proven, like the mesh's key
|
|
87
|
-
* endpoint (§6), and the work behind them is a signature verification. The cap
|
|
87
|
+
* endpoint (mesh-peer-auth §6), and the work behind them is a signature verification. The cap
|
|
88
88
|
* is far above what a person at a keyboard produces and far below what would
|
|
89
89
|
* cost this instance anything. */
|
|
90
90
|
export const AUTH_RATE_LIMIT = 30;
|
|
@@ -102,7 +102,7 @@ interface Pending {
|
|
|
102
102
|
attempts: number;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
/** One challenge this instance issued, good once (§2.6). */
|
|
105
|
+
/** One challenge this instance issued, good once (DR-0001 §2.6). */
|
|
106
106
|
interface Issued {
|
|
107
107
|
readonly expiresAt: Timestamp;
|
|
108
108
|
}
|
|
@@ -194,7 +194,7 @@ export class Auth {
|
|
|
194
194
|
return (this.deps.now ?? Date.now)();
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
-
// --- issuing a registration URL (§2.2) ---
|
|
197
|
+
// --- issuing a registration URL (DR-0001 §2.2) ---
|
|
198
198
|
|
|
199
199
|
/** Make one registration URL and the code that goes with it.
|
|
200
200
|
*
|
|
@@ -345,7 +345,7 @@ export class Auth {
|
|
|
345
345
|
for (const sub of removed) this.disconnect(sub);
|
|
346
346
|
}
|
|
347
347
|
|
|
348
|
-
// --- challenges (§2.6) ---
|
|
348
|
+
// --- challenges (DR-0001 §2.6) ---
|
|
349
349
|
|
|
350
350
|
challenge(): AuthChallengeResult {
|
|
351
351
|
this.#forget();
|
|
@@ -365,7 +365,7 @@ export class Auth {
|
|
|
365
365
|
}
|
|
366
366
|
|
|
367
367
|
/** Spend a challenge wherever it was issued: here, or at the instance the
|
|
368
|
-
* caller says issued it (§2.4, behind a load balancer either may be
|
|
368
|
+
* caller says issued it (DR-0001 §2.4, behind a load balancer either may be
|
|
369
369
|
* reached). */
|
|
370
370
|
async #spendAnywhere(challenge: AuthChallenge): Promise<void> {
|
|
371
371
|
if (challenge.issuer === this.deps.self) {
|
|
@@ -402,13 +402,13 @@ export class Auth {
|
|
|
402
402
|
return ask(iss, op, args);
|
|
403
403
|
}
|
|
404
404
|
|
|
405
|
-
// --- registration (§2.2) ---
|
|
405
|
+
// --- registration (DR-0001 §2.2) ---
|
|
406
406
|
|
|
407
407
|
/** Verify a registration and write the credential down.
|
|
408
408
|
*
|
|
409
409
|
* The registration URL is checked where its secret is, which may be another
|
|
410
410
|
* instance; everything else — the WebAuthn verification, the record — is done
|
|
411
|
-
* here, by whoever the browser reached (§2.6). */
|
|
411
|
+
* here, by whoever the browser reached (DR-0001 §2.6). */
|
|
412
412
|
async register(
|
|
413
413
|
args: AuthRegisterArgs,
|
|
414
414
|
from: { ip?: string; userAgent?: string; path?: string } = {},
|
|
@@ -493,7 +493,7 @@ export class Auth {
|
|
|
493
493
|
*
|
|
494
494
|
* The claims come back from the issuer having been checked and consumed, and
|
|
495
495
|
* everything after this — the WebAuthn verification, the record — is done by
|
|
496
|
-
* whichever instance the browser actually reached (§2.6). */
|
|
496
|
+
* whichever instance the browser actually reached (DR-0001 §2.6). */
|
|
497
497
|
async #claimsOf(args: AuthRegisterArgs): Promise<RegisterClaims> {
|
|
498
498
|
const stated = claimsOf(args.token);
|
|
499
499
|
if (stated.iss === this.deps.self) return this.resolveRegistration(args.token, args.code);
|
|
@@ -530,7 +530,7 @@ export class Auth {
|
|
|
530
530
|
if (code === undefined || !equalStrings(code, held.code)) {
|
|
531
531
|
held.attempts += 1;
|
|
532
532
|
// The URL itself is spent once the tries are gone, so guessing the code
|
|
533
|
-
// costs the whole registration rather than one attempt (§2.2).
|
|
533
|
+
// costs the whole registration rather than one attempt (DR-0001 §2.2).
|
|
534
534
|
if (held.attempts >= CODE_ATTEMPTS) {
|
|
535
535
|
this.#pending.delete(stated.jti);
|
|
536
536
|
throw new OpError(
|
|
@@ -544,7 +544,7 @@ export class Auth {
|
|
|
544
544
|
return held.claims;
|
|
545
545
|
}
|
|
546
546
|
|
|
547
|
-
// --- assertion (§2.5) ---
|
|
547
|
+
// --- assertion (DR-0001 §2.5) ---
|
|
548
548
|
|
|
549
549
|
async assert(
|
|
550
550
|
args: AuthAssertArgs,
|
|
@@ -620,7 +620,7 @@ export class Auth {
|
|
|
620
620
|
*
|
|
621
621
|
* The one the credential was registered under, which the record carries: a
|
|
622
622
|
* passkey only ever answers for the domain it was made under, and the
|
|
623
|
-
* endpoint being reached says nothing about that (§2.3). Nothing is widened
|
|
623
|
+
* endpoint being reached says nothing about that (DR-0001 §2.3). Nothing is widened
|
|
624
624
|
* to a suffix, and a record from before the field existed names the host of
|
|
625
625
|
* the endpoint it was registered for. */
|
|
626
626
|
#rpIdFor(record: CredentialRecord): string[] {
|
|
@@ -633,9 +633,9 @@ export class Auth {
|
|
|
633
633
|
*
|
|
634
634
|
* Its own is there because a browser may land here holding a URL another
|
|
635
635
|
* instance issued — the page it runs the exchange from is then this
|
|
636
|
-
* instance's, and the issuer is only asked to spend the URL (§2.6).
|
|
636
|
+
* instance's, and the issuer is only asked to spend the URL (DR-0001 §2.6).
|
|
637
637
|
*
|
|
638
|
-
* Read by the HTTP carrier, which compares them whole (§2.3). Not the relying
|
|
638
|
+
* Read by the HTTP carrier, which compares them whole (DR-0001 §2.3). Not the relying
|
|
639
639
|
* party: an RP ID is a domain, so a page at any host under it would be let in
|
|
640
640
|
* — and `/auth/refresh` answers a cookie the browser attaches by domain, so a
|
|
641
641
|
* sibling subdomain admitted here would read a person's access token. What
|
|
@@ -651,7 +651,7 @@ export class Auth {
|
|
|
651
651
|
return [...origins];
|
|
652
652
|
}
|
|
653
653
|
|
|
654
|
-
// --- tokens (§2.4) ---
|
|
654
|
+
// --- tokens (DR-0001 §2.4) ---
|
|
655
655
|
|
|
656
656
|
/** Make a family for this person, minted by this instance. */
|
|
657
657
|
mint(sub: Subject): MintedSession {
|
|
@@ -673,7 +673,7 @@ export class Auth {
|
|
|
673
673
|
* A family is written by its `iss` alone, so a rotation that landed here for
|
|
674
674
|
* a family minted elsewhere is carried there rather than done here — two
|
|
675
675
|
* instances rotating one family in parallel would merge by last write and
|
|
676
|
-
* read exactly like a stolen token being replayed (§2.4). */
|
|
676
|
+
* read exactly like a stolen token being replayed (DR-0001 §2.4). */
|
|
677
677
|
async refreshToken(value: Base64Url, from: RefreshFrom = {}): Promise<MintedSession> {
|
|
678
678
|
const held = this.deps.records.byRefresh(value);
|
|
679
679
|
if (held === undefined) {
|
|
@@ -783,7 +783,7 @@ export class Auth {
|
|
|
783
783
|
|
|
784
784
|
/** A value that is nobody's standing token but was somebody's: the family it
|
|
785
785
|
* belonged to is failed, because a token in use twice is a token that was
|
|
786
|
-
* taken (§2.4).
|
|
786
|
+
* taken (DR-0001 §2.4).
|
|
787
787
|
*
|
|
788
788
|
* Recognised three ways: the standing refresh token past its expiry, the one
|
|
789
789
|
* before it past its grace, and any generation this instance rotated away
|
|
@@ -813,7 +813,7 @@ export class Auth {
|
|
|
813
813
|
}
|
|
814
814
|
}
|
|
815
815
|
|
|
816
|
-
// --- connections (§2.5) ---
|
|
816
|
+
// --- connections (DR-0001 §2.5) ---
|
|
817
817
|
|
|
818
818
|
/** Whether an access token opens a connection, and until when. */
|
|
819
819
|
admits(access: Base64Url): { sub: Subject; expiresAt: Timestamp } | undefined {
|
|
@@ -861,7 +861,7 @@ export class Auth {
|
|
|
861
861
|
return this.#authorized.get(conn)?.expiresAt;
|
|
862
862
|
}
|
|
863
863
|
|
|
864
|
-
/** Extend a live connection with a token got from `/auth/refresh` (§2.5). */
|
|
864
|
+
/** Extend a live connection with a token got from `/auth/refresh` (DR-0001 §2.5). */
|
|
865
865
|
extend(conn: Requester, args: AuthExtendArgs): AuthExtendResult {
|
|
866
866
|
const held = this.#authorized.get(conn);
|
|
867
867
|
if (held === undefined) {
|
|
@@ -879,7 +879,7 @@ export class Auth {
|
|
|
879
879
|
return { auth_expires_at: admitted.expiresAt };
|
|
880
880
|
}
|
|
881
881
|
|
|
882
|
-
// --- the rate limit the unauthenticated routes share (§2.4) ---
|
|
882
|
+
// --- the rate limit the unauthenticated routes share (DR-0001 §2.4) ---
|
|
883
883
|
|
|
884
884
|
allowRequest(): boolean {
|
|
885
885
|
const now = this.#now();
|
|
@@ -929,7 +929,7 @@ export function authHandlers(auth: Auth) {
|
|
|
929
929
|
}
|
|
930
930
|
// The digits arrive unjudged from wherever the browser landed, and are
|
|
931
931
|
// checked here — this is the instance holding both the secret that signed
|
|
932
|
-
// the URL and the count of tries against it (§2.2).
|
|
932
|
+
// the URL and the count of tries against it (DR-0001 §2.2).
|
|
933
933
|
return { kind: "register", claims: auth.resolveRegistration(args.token, args.code) };
|
|
934
934
|
},
|
|
935
935
|
"auth.rotate": (input: HandlerInput): AuthRotateResult => {
|
|
@@ -1011,7 +1011,7 @@ function token(): Base64Url {
|
|
|
1011
1011
|
* A JWS with HS256, because the value travels in a URL fragment and has to
|
|
1012
1012
|
* survive being carried there: the shape is the conventional one, and the
|
|
1013
1013
|
* verifier is the issuer itself, so nothing about it is a key anyone else
|
|
1014
|
-
* needs (§2.2). */
|
|
1014
|
+
* needs (DR-0001 §2.2). */
|
|
1015
1015
|
function sign(claims: RegisterClaims, secret: Buffer): string {
|
|
1016
1016
|
const header = Buffer.from(JSON.stringify({ alg: "HS256", typ: "JWT" })).toString("base64url");
|
|
1017
1017
|
const body = Buffer.from(JSON.stringify(claims)).toString("base64url");
|
|
@@ -1060,7 +1060,7 @@ function challengeIn(clientDataJson: Base64Url): Base64Url {
|
|
|
1060
1060
|
return challenge;
|
|
1061
1061
|
}
|
|
1062
1062
|
|
|
1063
|
-
/** The host an endpoint names, which is the relying party by default (§2.3). */
|
|
1063
|
+
/** The host an endpoint names, which is the relying party by default (DR-0001 §2.3). */
|
|
1064
1064
|
export function hostOf(endpoint: Endpoint): string {
|
|
1065
1065
|
return new URL(endpoint).hostname;
|
|
1066
1066
|
}
|
package/src/auth/http.ts
CHANGED
|
@@ -60,7 +60,7 @@ export function endpointPath(pathname: string): string {
|
|
|
60
60
|
/** The cookie path for a request: everything up to and including its `/auth/`.
|
|
61
61
|
*
|
|
62
62
|
* It narrows what the browser sends where, and nothing more — same-origin
|
|
63
|
-
* script can fetch any path, so this is not an authorization boundary (§2.4).
|
|
63
|
+
* script can fetch any path, so this is not an authorization boundary (DR-0001 §2.4).
|
|
64
64
|
* What it is for is that two endpoints behind one origin (`/` and `/personal`)
|
|
65
65
|
* get cookies of their own. */
|
|
66
66
|
export function cookiePath(pathname: string): string {
|
|
@@ -102,7 +102,7 @@ export interface AuthRoutesDeps {
|
|
|
102
102
|
*
|
|
103
103
|
* Everything unauthenticated shares one rate limit and one origin check: these
|
|
104
104
|
* routes are reachable before anything is proven, and the work behind them is a
|
|
105
|
-
* signature verification (§2.4). */
|
|
105
|
+
* signature verification (DR-0001 §2.4). */
|
|
106
106
|
export async function handleAuth(
|
|
107
107
|
request: Request,
|
|
108
108
|
deps: AuthRoutesDeps,
|
|
@@ -116,7 +116,7 @@ export async function handleAuth(
|
|
|
116
116
|
// anything else, including the preflight that would tell it to try. Compared
|
|
117
117
|
// whole rather than by domain: `/auth/refresh` answers with a person's access
|
|
118
118
|
// token, and a browser attaches the cookie it is asked for by domain, so a
|
|
119
|
-
// sibling subdomain let in here could read that token (§2.3).
|
|
119
|
+
// sibling subdomain let in here could read that token (DR-0001 §2.3).
|
|
120
120
|
if (origin !== null && !deps.auth.knownOrigins().includes(origin)) {
|
|
121
121
|
return new Response("Forbidden", { status: 403 });
|
|
122
122
|
}
|
package/src/auth/records.ts
CHANGED
|
@@ -57,7 +57,7 @@ function covers(tombstone: string, key: string): boolean {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
export interface RecordsDeps {
|
|
60
|
-
/** Where the set is written down (§
|
|
60
|
+
/** Where the set is written down (DESIGN §2.5). */
|
|
61
61
|
readonly dir: string;
|
|
62
62
|
/** This instance's id, which is the one thing that makes a record arriving
|
|
63
63
|
* from a peer refusable on sight: a family this instance minted is written by
|
|
@@ -76,7 +76,7 @@ export interface RecordsDeps {
|
|
|
76
76
|
* write to the keys it covers, so a peer coming back from a partition cannot
|
|
77
77
|
* carry a revoked credential in as news.
|
|
78
78
|
*
|
|
79
|
-
* Written down for the same reason the store is (§
|
|
79
|
+
* Written down for the same reason the store is (DESIGN §2.5): none of it is derived
|
|
80
80
|
* from anything else this instance holds. A credential exists nowhere but here
|
|
81
81
|
* and in the authenticator, and losing a family logs its person out. */
|
|
82
82
|
export class AuthRecords {
|
package/src/auth/webauthn.ts
CHANGED
|
@@ -245,7 +245,7 @@ export const SUPPORTED_ALGORITHMS: readonly number[] = [ES256, EdDSA, RS256];
|
|
|
245
245
|
* Done at registration rather than at the first assertion: a key that cannot be
|
|
246
246
|
* imported is a credential that can never be used, and finding that out when
|
|
247
247
|
* the person tries to sign in leaves a record nobody can explain. The imported
|
|
248
|
-
* key itself is thrown away — an assertion imports its own (§2.10). */
|
|
248
|
+
* key itself is thrown away — an assertion imports its own (DR-0001 §2.10). */
|
|
249
249
|
export async function checkPublicKey(cose: Uint8Array): Promise<void> {
|
|
250
250
|
const key = decodeCborWhole(cose);
|
|
251
251
|
const alg = mapEntry(key, 3);
|
package/src/cli.ts
CHANGED
|
@@ -76,7 +76,7 @@ import {
|
|
|
76
76
|
import { type Run, runCommand, serviceFor } from "./service/index.ts";
|
|
77
77
|
import { VERSION } from "./version.ts";
|
|
78
78
|
|
|
79
|
-
/** The session this process runs inside, as its environment says (§
|
|
79
|
+
/** The session this process runs inside, as its environment says (DESIGN §4.1).
|
|
80
80
|
*
|
|
81
81
|
* One reading for every command that speaks as a session: which harness
|
|
82
82
|
* claimed the process settles both who the sender is and which instance it
|
|
@@ -644,9 +644,9 @@ async function runInstance(given: string | undefined): Promise<unknown> {
|
|
|
644
644
|
// The directory is handed over rather than put in the environment: the
|
|
645
645
|
// instance would otherwise read it back through the question "which session
|
|
646
646
|
// is this process inside", and a `daemon run` issued from a session of
|
|
647
|
-
// another harness would answer for that session's config home (§
|
|
647
|
+
// another harness would answer for that session's config home (DESIGN §4.1).
|
|
648
648
|
// With a supervisor up, it is the one that reads the files and writes down
|
|
649
|
-
// what held (§8.2); this start reads what it applied. With none, this
|
|
649
|
+
// what held (DESIGN §8.2); this start reads what it applied. With none, this
|
|
650
650
|
// process is the only one there is, so it does both.
|
|
651
651
|
const outcome = await start({ configHome: home, settle: !(await reachable()) });
|
|
652
652
|
if (!isRunning(outcome)) {
|
|
@@ -657,7 +657,7 @@ async function runInstance(given: string | undefined): Promise<unknown> {
|
|
|
657
657
|
}
|
|
658
658
|
const instance = outcome;
|
|
659
659
|
// A signal is a request to leave, and leaving is the ordered shutdown of
|
|
660
|
-
// §8.5 — the same one `instance.shutdown` runs, so a client sees the same
|
|
660
|
+
// DESIGN §8.5 — the same one `instance.shutdown` runs, so a client sees the same
|
|
661
661
|
// departure either way. The listeners are removed once it has run, because a
|
|
662
662
|
// signal listener keeps the event loop alive and the process would sit at an
|
|
663
663
|
// empty loop instead of exiting.
|
|
@@ -689,7 +689,7 @@ async function supervise(): Promise<unknown> {
|
|
|
689
689
|
* The file first and the supervisor second, because the file is what survives:
|
|
690
690
|
* a host with no supervisor running still gets the config home added, and the
|
|
691
691
|
* next supervisor starts it. Told rather than left to be discovered, because
|
|
692
|
-
* the supervisor reads the list once (
|
|
692
|
+
* the supervisor reads the list once (DR-0004) and would otherwise not know
|
|
693
693
|
* until it is restarted. */
|
|
694
694
|
async function added(args: readonly string[]): Promise<unknown> {
|
|
695
695
|
const { named, rest } = options(args, ["harness", "port"]);
|
|
@@ -722,11 +722,11 @@ async function added(args: readonly string[]): Promise<unknown> {
|
|
|
722
722
|
*
|
|
723
723
|
* Its own command rather than one under `daemon`, because what it edits is not
|
|
724
724
|
* one instance's anything: every instance of this host is in the same mesh
|
|
725
|
-
* (§7.1), so the list is the host's. `peers` is what a session list is called,
|
|
725
|
+
* (DESIGN §7.1), so the list is the host's. `peers` is what a session list is called,
|
|
726
726
|
* which is why this is called what the thing itself is called.
|
|
727
727
|
*
|
|
728
728
|
* An addition takes effect when the instances next start, for the reason
|
|
729
|
-
* nothing else reloads either (
|
|
729
|
+
* nothing else reloads either (DR-0004). A removal is told to whoever is running
|
|
730
730
|
* as well as written down: an endpoint taken off the list is one this host is
|
|
731
731
|
* not to be talking to, and leaving a live link up until the next restart would
|
|
732
732
|
* be leaving exactly the connection that was just revoked. */
|
|
@@ -829,7 +829,7 @@ async function configShow(args: readonly string[]): Promise<unknown> {
|
|
|
829
829
|
}
|
|
830
830
|
// A dry run: the files are read and the settings functions are called, and
|
|
831
831
|
// nothing is written. They are expected to have no side effects for exactly
|
|
832
|
-
// this reason (§8.2).
|
|
832
|
+
// this reason (DESIGN §8.2).
|
|
833
833
|
const read = await evaluate(resolveConfigDir());
|
|
834
834
|
if (read.satisfied === undefined) {
|
|
835
835
|
throw new CommandError(
|
|
@@ -1437,7 +1437,7 @@ function notify(args: readonly string[]): Promise<unknown> {
|
|
|
1437
1437
|
|
|
1438
1438
|
/** `ccmsg stopping`: this session is about to go.
|
|
1439
1439
|
*
|
|
1440
|
-
* What it buys is the difference between Paused and Disappeared (§
|
|
1440
|
+
* What it buys is the difference between Paused and Disappeared (DESIGN §4.3): the
|
|
1441
1441
|
* instance holds the declaration until the connection closes, and the entry it
|
|
1442
1442
|
* then writes carries the instant it was told. The connection closing is the
|
|
1443
1443
|
* second half of that, so the command says its piece and leaves — which is
|
|
@@ -1570,7 +1570,7 @@ async function plugin(
|
|
|
1570
1570
|
// The config home is that agent's own, and not whichever variable happens to
|
|
1571
1571
|
// be set: a Codex session started from a Claude Code session carries both,
|
|
1572
1572
|
// and an install that read the wrong one would write Codex's hooks into
|
|
1573
|
-
// Claude Code's config home (§
|
|
1573
|
+
// Claude Code's config home (DESIGN §4.1). The marker check is what says the
|
|
1574
1574
|
// directory really is that agent's.
|
|
1575
1575
|
const home = configHome(homeOf(which), which);
|
|
1576
1576
|
const paths = resolvePathsFor(home);
|
package/src/daemon/control.ts
CHANGED
|
@@ -64,7 +64,7 @@ export function greetAsUser(conn: Conn): Promise<Record<string, unknown>> {
|
|
|
64
64
|
* and everything else.
|
|
65
65
|
*
|
|
66
66
|
* By `request_id` rather than by arrival order, because the two are not the
|
|
67
|
-
* same stream: an instance pushes topic frames of its own accord (§6), and one
|
|
67
|
+
* same stream: an instance pushes topic frames of its own accord (DESIGN §6), and one
|
|
68
68
|
* of those landing between a request and its reply would otherwise be read as
|
|
69
69
|
* the reply. It is not a rare window — greeting an instance that has mesh peers
|
|
70
70
|
* is enough, since a peer connecting moves a row on `instances`. */
|
package/src/daemon/registry.ts
CHANGED
|
@@ -32,7 +32,7 @@ import { CommandError } from "./link.ts";
|
|
|
32
32
|
*
|
|
33
33
|
* The harness's own settings file is what says the directory is a config home
|
|
34
34
|
* rather than any directory somebody typed, so which file is looked for
|
|
35
|
-
* follows which harness the directory runs (§
|
|
35
|
+
* follows which harness the directory runs (DESIGN §4.1). Checked where a directory
|
|
36
36
|
* is named — `add` and `run` — rather than at every use, so the mistake is
|
|
37
37
|
* caught when it is made. */
|
|
38
38
|
export function configHome(dir: string, harness: Harness = DEFAULT_HARNESS): string {
|
|
@@ -49,7 +49,7 @@ export function configHome(dir: string, harness: Harness = DEFAULT_HARNESS): str
|
|
|
49
49
|
|
|
50
50
|
/** Which harness a registered config home runs, as its own file says.
|
|
51
51
|
*
|
|
52
|
-
* Read from the same file the instance itself will read (§8.2), so a command
|
|
52
|
+
* Read from the same file the instance itself will read (DESIGN §8.2), so a command
|
|
53
53
|
* that has to know before anything is running — `run`, and the supervisor's
|
|
54
54
|
* own start — reaches the same answer the instance does. A directory no file
|
|
55
55
|
* names runs whatever the defaults say, which is what an unregistered
|
|
@@ -99,7 +99,7 @@ export interface InstanceRow {
|
|
|
99
99
|
* settings for has none. */
|
|
100
100
|
readonly name?: string;
|
|
101
101
|
readonly dir: string;
|
|
102
|
-
/** The address it binds, and the one its peers dial (§7.1). */
|
|
102
|
+
/** The address it binds, and the one its peers dial (DESIGN §7.1). */
|
|
103
103
|
readonly port?: number;
|
|
104
104
|
readonly endpoint?: string;
|
|
105
105
|
readonly running: boolean;
|
|
@@ -113,7 +113,7 @@ export interface StatusRow extends InstanceRow {
|
|
|
113
113
|
* what is written. */
|
|
114
114
|
readonly config_problems?: readonly ConfigProblem[];
|
|
115
115
|
/** What this config home's instance is configured with, after the shared
|
|
116
|
-
* file's defaults and its own entry are merged (§8.2).
|
|
116
|
+
* file's defaults and its own entry are merged (DESIGN §8.2).
|
|
117
117
|
*
|
|
118
118
|
* Answered whether or not anything is running, and read from the file rather
|
|
119
119
|
* than asked of the instance: this is what a restart would apply, which is
|
|
@@ -249,7 +249,7 @@ export function nameFor(dir: string): string {
|
|
|
249
249
|
*
|
|
250
250
|
* The marker file is the evidence: Claude Code keeps `settings.json` and Codex
|
|
251
251
|
* keeps `config.toml`, so a directory that holds one of them is that harness's
|
|
252
|
-
* (§
|
|
252
|
+
* (DESIGN §4.1). A directory holding both, or neither, is not answered for — the
|
|
253
253
|
* first is two answers and the second is none, and guessing either way writes
|
|
254
254
|
* down a setting the instance will act on for the whole of its life. */
|
|
255
255
|
export function harnessOf(dir: string): Harness {
|
|
@@ -333,7 +333,7 @@ export async function add(env: Env, dir: string, options: AddOptions = {}): Prom
|
|
|
333
333
|
// is keyed by, and a fresh one gets its id here rather than at its first
|
|
334
334
|
// start (DR-0001 §2.1).
|
|
335
335
|
const id = instanceIdentity(targetFor(env, home).paths.instanceIdFile);
|
|
336
|
-
// Every instance listens, because an instance is an entry of the mesh (§7.1)
|
|
336
|
+
// Every instance listens, because an instance is an entry of the mesh (DESIGN §7.1)
|
|
337
337
|
// and a mesh is reached over the entry: what `--port` settles is which
|
|
338
338
|
// address, not whether there is one.
|
|
339
339
|
const port =
|
|
@@ -353,7 +353,7 @@ export async function add(env: Env, dir: string, options: AddOptions = {}): Prom
|
|
|
353
353
|
);
|
|
354
354
|
// The loopback address, because that is the one this host is certainly
|
|
355
355
|
// reached at. A proxy in front of it is a deployment fact nothing here can
|
|
356
|
-
// see, so an operator who has one edits this row (§8.2).
|
|
356
|
+
// see, so an operator who has one edits this row (DESIGN §8.2).
|
|
357
357
|
saveEndpoints(paths.configDir, [
|
|
358
358
|
...readEndpointRows(paths.configDir).filter((row) => row.id !== id),
|
|
359
359
|
{ id, endpoint: `http://127.0.0.1:${String(port)}/` as EndpointRow["endpoint"] },
|
|
@@ -563,7 +563,7 @@ export async function status(target: Target): Promise<StatusRow> {
|
|
|
563
563
|
config: own?.config ?? DEFAULT_CONFIG,
|
|
564
564
|
// What a person has to be told even though the instance is running: an
|
|
565
565
|
// edit that did not check out is not applied, and the only sign of it
|
|
566
|
-
// otherwise is a setting that did not take (§8.3).
|
|
566
|
+
// otherwise is a setting that did not take (DESIGN §8.3).
|
|
567
567
|
...(read.problems.length === 0 ? {} : { config_problems: read.problems }),
|
|
568
568
|
};
|
|
569
569
|
const conn = await connect(target.paths.socket);
|
|
@@ -596,7 +596,7 @@ export async function status(target: Target): Promise<StatusRow> {
|
|
|
596
596
|
*
|
|
597
597
|
* The contract's op rather than a signal, so the request goes through the same
|
|
598
598
|
* authorization every other op does and the caller is told it was accepted
|
|
599
|
-
* before the process goes down (§8.5). */
|
|
599
|
+
* before the process goes down (DESIGN §8.5). */
|
|
600
600
|
export async function stop(target: Target): Promise<{ dir: string; stopped: boolean }> {
|
|
601
601
|
const conn = await connect(target.paths.socket);
|
|
602
602
|
if (conn === undefined) {
|
|
@@ -639,7 +639,7 @@ export const spawnInstance: SpawnInstance = (dir, env) => {
|
|
|
639
639
|
// The directory is an argument and not an environment variable: `daemon run`
|
|
640
640
|
// takes it from there and hands it to the instance by value, so which config
|
|
641
641
|
// home the child answers for cannot depend on which session the supervisor
|
|
642
|
-
// was started from (§
|
|
642
|
+
// was started from (DESIGN §4.1).
|
|
643
643
|
const proc = Bun.spawn([process.execPath, ENTRY, "daemon", "run", dir], {
|
|
644
644
|
stdio: ["ignore", "ignore", "ignore"],
|
|
645
645
|
env: { ...env } as Record<string, string>,
|
|
@@ -679,7 +679,7 @@ export function awaitSocket(paths: InstancePaths, timeoutMs: number): Promise<vo
|
|
|
679
679
|
}
|
|
680
680
|
|
|
681
681
|
/** Wait for the lock to be released, which is the last thing a departing
|
|
682
|
-
* instance does (§8.5). */
|
|
682
|
+
* instance does (DESIGN §8.5). */
|
|
683
683
|
export function awaitGone(paths: InstancePaths, timeoutMs: number): Promise<void> {
|
|
684
684
|
return awaitEntry(paths, timeoutMs, () => {
|
|
685
685
|
const pid = lockHolder(paths.lockFile);
|
package/src/daemon/snapshot.ts
CHANGED
|
@@ -41,7 +41,7 @@ interface Greeted {
|
|
|
41
41
|
* leave.
|
|
42
42
|
*
|
|
43
43
|
* A subscription opens with one `snapshot: true` frame per instance that has a
|
|
44
|
-
* value (§6.2), so the current value of a topic is read by subscribing and
|
|
44
|
+
* value (DESIGN §6.2), so the current value of a topic is read by subscribing and
|
|
45
45
|
* stopping at the frames rather than by an op of its own. The local instance's
|
|
46
46
|
* frame is deferred behind the acknowledgement and is therefore already on the
|
|
47
47
|
* wire; the relayed ones are what the budget is for.
|
package/src/daemon/supervise.ts
CHANGED
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
* still holds the lock needs only the moment that takes, so the first retry
|
|
31
31
|
* is short enough to be invisible.
|
|
32
32
|
* - `maxMs` bounds a spin. The failure this is really for is a config the
|
|
33
|
-
* instance refuses to start with (
|
|
33
|
+
* instance refuses to start with (DR-0004): it fails in milliseconds and will
|
|
34
34
|
* fail again identically until somebody edits the file, so the cap is the
|
|
35
35
|
* rate at which the supervisor is willing to say so in its log while waiting
|
|
36
36
|
* for that edit.
|
|
@@ -93,7 +93,7 @@ class Supervised {
|
|
|
93
93
|
/** The foreground supervisor: the instances the shared file lists, kept up, and
|
|
94
94
|
* a socket over which they are asked about.
|
|
95
95
|
*
|
|
96
|
-
* The list is read once, when this starts (
|
|
96
|
+
* The list is read once, when this starts (DR-0004). What changes it afterwards
|
|
97
97
|
* is a request — `daemon add` and `daemon remove` tell the supervisor as well
|
|
98
98
|
* as the file — rather than the file being re-read, so the supervisor's idea of
|
|
99
99
|
* which instances there are and the file's cannot silently disagree about
|
|
@@ -122,7 +122,7 @@ export class Supervisor {
|
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
/** Read the edited files, check them, write down what held, and look after
|
|
125
|
-
* what it names (§8.2).
|
|
125
|
+
* what it names (DESIGN §8.2).
|
|
126
126
|
*
|
|
127
127
|
* This is the one thing that writes the applied settings: the children read
|
|
128
128
|
* them and write nothing, so nothing races over the file and there is one
|
|
@@ -316,7 +316,7 @@ export class Supervisor {
|
|
|
316
316
|
/** Stop one child, and leave it stopped.
|
|
317
317
|
*
|
|
318
318
|
* Asked over its own socket rather than signalled, so what runs is the
|
|
319
|
-
* ordered shutdown of §8.5 — the same departure a client sees from an
|
|
319
|
+
* ordered shutdown of DESIGN §8.5 — the same departure a client sees from an
|
|
320
320
|
* `instance.shutdown`. */
|
|
321
321
|
async stopOne(dir: string): Promise<{ dir: string; stopped: boolean }> {
|
|
322
322
|
const unit = this.#units.get(dir);
|
package/src/dispatch/caller.ts
CHANGED
|
@@ -10,7 +10,7 @@ export class CallerError extends Error {}
|
|
|
10
10
|
* The role is read here because it decides which fields the value has to
|
|
11
11
|
* carry — `sid` is present exactly when the role is `session` — and not
|
|
12
12
|
* whether anything is allowed: what the identity may do is decided afterwards,
|
|
13
|
-
* by dispatch, against the attribute table (§
|
|
13
|
+
* by dispatch, against the attribute table (DESIGN §2.2). It is the same question
|
|
14
14
|
* `hello` asks of a greeting, whose shape depends on its role in the same way
|
|
15
15
|
* and for the same reason: one schema covers all three roles, so the schema
|
|
16
16
|
* cannot state the rule and the instance does.
|
package/src/dispatch/dispatch.ts
CHANGED
|
@@ -20,7 +20,7 @@ export interface DispatchDeps {
|
|
|
20
20
|
readonly capabilities: ReadonlySet<Capability>;
|
|
21
21
|
/** The instance that owns the subject of an `instance-local` op, or
|
|
22
22
|
* `undefined` when no other instance owns it and we answer ourselves.
|
|
23
|
-
* The routing table behind this is the `peers` topic (
|
|
23
|
+
* The routing table behind this is the `peers` topic (DESIGN §7.3). */
|
|
24
24
|
readonly resolveInstance: (op: OpName, frame: Record<string, unknown>) => InstanceId | undefined;
|
|
25
25
|
readonly handlers: Handlers;
|
|
26
26
|
}
|
|
@@ -31,7 +31,7 @@ function isOpName(op: string): op is OpName {
|
|
|
31
31
|
|
|
32
32
|
/** Decide one frame.
|
|
33
33
|
*
|
|
34
|
-
* The six steps of
|
|
34
|
+
* The six steps of DESIGN §2.2 are written once, here, and read the op
|
|
35
35
|
* attribute table for every op. Adding an op is a row in the table plus a
|
|
36
36
|
* schema and an implementation — never a check in this function (M1). */
|
|
37
37
|
export async function dispatch(
|
|
@@ -101,7 +101,7 @@ export async function dispatch(
|
|
|
101
101
|
//
|
|
102
102
|
// A request that has already been here is dropped before that: a cycle in
|
|
103
103
|
// the routing would otherwise send it round the same instances until every
|
|
104
|
-
// deadline expired (§7.3). It is answered rather than left unanswered,
|
|
104
|
+
// deadline expired (DESIGN §7.3). It is answered rather than left unanswered,
|
|
105
105
|
// because the caller learns the same thing sooner and the code is the one
|
|
106
106
|
// the contract gives a destination that could not be reached.
|
|
107
107
|
const hops = fields["hops"];
|
|
@@ -123,7 +123,7 @@ export async function dispatch(
|
|
|
123
123
|
args: fields,
|
|
124
124
|
conn,
|
|
125
125
|
identity: identity.state === "settled" ? identity : undefined,
|
|
126
|
-
// The one route by which a role reaches an implementation (§
|
|
126
|
+
// The one route by which a role reaches an implementation (DESIGN §2.2).
|
|
127
127
|
role: attrs.scope === "role" && identity.state === "settled" ? identity.role : undefined,
|
|
128
128
|
});
|
|
129
129
|
return reply(requestId, body);
|
package/src/dispatch/handler.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { ConnIdentity, SettledIdentity } from "./identity.ts";
|
|
|
5
5
|
*
|
|
6
6
|
* It is the `Conn` transport accepted, narrowed to what an op may do with it:
|
|
7
7
|
* read the identity, push frames, and learn that the connection is gone. A
|
|
8
|
-
* subscription is held by a connection and ends with it (
|
|
8
|
+
* subscription is held by a connection and ends with it (DESIGN §6.3), so
|
|
9
9
|
* this is what the topic mechanism keys its subscribers on. Declared here
|
|
10
10
|
* rather than imported from transport because dispatch sits below it. */
|
|
11
11
|
export interface Requester {
|
|
@@ -25,12 +25,12 @@ export interface Requester {
|
|
|
25
25
|
/** What an op implementation receives.
|
|
26
26
|
*
|
|
27
27
|
* The arguments are already validated and the caller is already allowed
|
|
28
|
-
* (
|
|
28
|
+
* (DESIGN §2.2): a handler starts from "this may be run", so it holds no
|
|
29
29
|
* check of its own. */
|
|
30
30
|
export interface HandlerInput {
|
|
31
31
|
readonly op: OpName;
|
|
32
32
|
/** The connection the request arrived on. Ops that hold something for the
|
|
33
|
-
* length of a connection — the subscriptions of
|
|
33
|
+
* length of a connection — the subscriptions of DESIGN §6.3 — need it;
|
|
34
34
|
* ops that only answer ignore it. */
|
|
35
35
|
readonly conn: Requester;
|
|
36
36
|
/** The whole request frame, validated against the op's request schema. */
|
|
@@ -40,7 +40,7 @@ export interface HandlerInput {
|
|
|
40
40
|
/** Set only for ops the attribute table marks `scope: "role"`, where the role
|
|
41
41
|
* changes what the reply may contain rather than whether the call is allowed.
|
|
42
42
|
* This is the only route by which a role reaches an implementation
|
|
43
|
-
* (
|
|
43
|
+
* (DESIGN §2.2). */
|
|
44
44
|
readonly role?: Role;
|
|
45
45
|
}
|
|
46
46
|
|
package/src/dispatch/identity.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Role, Sid } from "@ccmsg/protocol";
|
|
|
2
2
|
|
|
3
3
|
/** What dispatch knows about the connection a frame arrived on.
|
|
4
4
|
*
|
|
5
|
-
* transport settles this (
|
|
5
|
+
* transport settles this (DESIGN §2.1): a connection starts anonymous and
|
|
6
6
|
* becomes `settled` when `hello` binds a role, and a session's `sid`, to it.
|
|
7
7
|
* Nothing else about the connection reaches dispatch — the authorization steps
|
|
8
8
|
* read the op attribute table, not the connection. */
|
package/src/dispatch/result.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { ErrorCode, ErrorResponse, InstanceId } from "@ccmsg/protocol";
|
|
|
3
3
|
/** What dispatch decided about one frame.
|
|
4
4
|
*
|
|
5
5
|
* `forward` is the only outcome that is not an answer: the op belongs to
|
|
6
|
-
* another instance and mesh has to carry it there (
|
|
6
|
+
* another instance and mesh has to carry it there (DESIGN §2.2 step 6).
|
|
7
7
|
* There is no mesh yet, so dispatch names the destination and stops. */
|
|
8
8
|
export type DispatchResult =
|
|
9
9
|
/** The frame was not a request and has no answer. The mesh handshake's own
|
|
@@ -32,7 +32,7 @@ export class OpError extends Error {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/** The reply envelope, built here and nowhere else so the wire shape stays in
|
|
35
|
-
* one place (
|
|
35
|
+
* one place (DESIGN §9.1). */
|
|
36
36
|
export function reply(
|
|
37
37
|
requestId: string,
|
|
38
38
|
body: unknown,
|