@ccmsg/cli 0.8.2 → 0.9.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.
- package/package.json +2 -2
- package/src/auth/auth.ts +14 -14
- package/src/auth/http.ts +6 -6
- package/src/auth/records.ts +1 -1
- package/src/auth/topic.ts +1 -1
- package/src/cli.ts +21 -24
- package/src/daemon/control.ts +2 -2
- package/src/daemon/registry.ts +7 -7
- package/src/daemon/snapshot.ts +12 -10
- package/src/daemon/supervise.ts +1 -1
- package/src/files/containment.ts +5 -5
- package/src/files/files.ts +12 -12
- package/src/files/sandbox.ts +0 -0
- package/src/greeting/meta.ts +5 -2
- package/src/instance/config.ts +21 -9
- package/src/instance/instance.ts +15 -13
- package/src/kv/store.ts +4 -3
- package/src/launcher/launcher.ts +3 -3
- package/src/mesh/mesh.ts +7 -8
- package/src/mesh/relay.ts +1 -1
- package/src/messaging/delivery.ts +4 -4
- package/src/messaging/direct.ts +2 -2
- package/src/messaging/handlers.ts +4 -4
- package/src/messaging/inbox.ts +1 -1
- package/src/messaging/notify.ts +11 -11
- package/src/sessions/dump.ts +1 -1
- package/src/sessions/handlers.ts +17 -17
- package/src/sessions/harness.ts +1 -1
- package/src/sessions/last-live.ts +1 -1
- package/src/sessions/registry.ts +60 -77
- package/src/sessions/search.ts +1 -1
- package/src/sessions/status.ts +7 -7
- package/src/topics/handlers.ts +2 -2
- package/src/topics/topics.ts +0 -0
- package/src/transcript/items/classify.ts +42 -37
- package/src/transcript/items/document.ts +2 -2
- package/src/transcript/items/ids.ts +0 -0
- package/src/transcript/items/render.ts +24 -24
- package/src/transcript/items/select.ts +5 -5
- package/src/transcript/transcripts.ts +3 -3
- package/src/translate/translate.ts +1 -1
- package/src/transport/driver.ts +13 -11
- package/src/upstream/gateway.ts +3 -3
- package/src/upstream/requests.ts +1 -1
- package/src/upstream/status.ts +1 -1
- package/src/version.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ccmsg/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "The ccmsg daemon, CLI and agent plugins for one instance (= one config home)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "kawaz",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"test": "bun test"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@ccmsg/protocol": "1.
|
|
23
|
+
"@ccmsg/protocol": "1.22.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@types/bun": "^1.3.0",
|
package/src/auth/auth.ts
CHANGED
|
@@ -4,9 +4,9 @@ import type {
|
|
|
4
4
|
AuthChallenge,
|
|
5
5
|
AuthChallengeResult,
|
|
6
6
|
AuthRecord,
|
|
7
|
-
|
|
7
|
+
AuthExtendArgs,
|
|
8
8
|
AuthRefreshReason,
|
|
9
|
-
|
|
9
|
+
AuthExtendResult,
|
|
10
10
|
AuthRegisterArgs,
|
|
11
11
|
AuthResolveArgs,
|
|
12
12
|
AuthResolveResult,
|
|
@@ -339,7 +339,7 @@ export class Auth {
|
|
|
339
339
|
return closed;
|
|
340
340
|
}
|
|
341
341
|
|
|
342
|
-
/** Take what a peer wrote on `
|
|
342
|
+
/** Take what a peer wrote on `auth.records`, and act on the removals in it. */
|
|
343
343
|
merge(records: readonly AuthRecord[]): void {
|
|
344
344
|
const { removed } = this.deps.records.merge(records);
|
|
345
345
|
for (const sub of removed) this.disconnect(sub);
|
|
@@ -372,7 +372,7 @@ export class Auth {
|
|
|
372
372
|
this.spend(challenge.challenge);
|
|
373
373
|
return;
|
|
374
374
|
}
|
|
375
|
-
await this.#atIssuer(challenge.issuer, "
|
|
375
|
+
await this.#atIssuer(challenge.issuer, "auth.resolve", {
|
|
376
376
|
kind: "challenge",
|
|
377
377
|
challenge: challenge.challenge,
|
|
378
378
|
} satisfies AuthResolveArgs);
|
|
@@ -497,7 +497,7 @@ export class Auth {
|
|
|
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);
|
|
500
|
-
const answer = (await this.#atIssuer(stated.iss, "
|
|
500
|
+
const answer = (await this.#atIssuer(stated.iss, "auth.resolve", {
|
|
501
501
|
kind: "register",
|
|
502
502
|
token: args.token,
|
|
503
503
|
code: args.code,
|
|
@@ -510,7 +510,7 @@ export class Auth {
|
|
|
510
510
|
|
|
511
511
|
/** Check a registration URL against the secret that signed it, and spend it.
|
|
512
512
|
*
|
|
513
|
-
* Only the issuer can run this, which is what `
|
|
513
|
+
* Only the issuer can run this, which is what `auth.resolve` is for. The code
|
|
514
514
|
* is checked here too: it was issued with the secret and is held beside it,
|
|
515
515
|
* and letting another instance check it would be putting the one defence
|
|
516
516
|
* against a leaked URL somewhere the URL's holder could reach. */
|
|
@@ -690,7 +690,7 @@ export class Auth {
|
|
|
690
690
|
// remembered as a time and nothing else. The issuer writes them
|
|
691
691
|
// unchecked, as it does the ones it observes itself (contract,
|
|
692
692
|
// `AuthRotateArgs`).
|
|
693
|
-
const answer = (await this.#atIssuer(held.body.iss, "
|
|
693
|
+
const answer = (await this.#atIssuer(held.body.iss, "auth.rotate", {
|
|
694
694
|
refresh_token: value,
|
|
695
695
|
...(from.reason === undefined ? {} : { reason: from.reason }),
|
|
696
696
|
...(from.ip === undefined ? {} : { ip: from.ip }),
|
|
@@ -718,7 +718,7 @@ export class Auth {
|
|
|
718
718
|
return;
|
|
719
719
|
}
|
|
720
720
|
try {
|
|
721
|
-
await this.#atIssuer(owner.body.iss, "
|
|
721
|
+
await this.#atIssuer(owner.body.iss, "auth.rotate", {
|
|
722
722
|
refresh_token: value,
|
|
723
723
|
} satisfies AuthRotateArgs);
|
|
724
724
|
} catch {
|
|
@@ -729,7 +729,7 @@ export class Auth {
|
|
|
729
729
|
}
|
|
730
730
|
|
|
731
731
|
/** Rotate a family this instance minted. The one writer's own operation, and
|
|
732
|
-
* what `
|
|
732
|
+
* what `auth.rotate` runs on its behalf. */
|
|
733
733
|
rotate(value: Base64Url, from: RefreshFrom = {}): AuthRotateResult {
|
|
734
734
|
const held = this.deps.records.byRefresh(value);
|
|
735
735
|
if (held === undefined) {
|
|
@@ -862,7 +862,7 @@ export class Auth {
|
|
|
862
862
|
}
|
|
863
863
|
|
|
864
864
|
/** Extend a live connection with a token got from `/auth/refresh` (§2.5). */
|
|
865
|
-
extend(conn: Requester, args:
|
|
865
|
+
extend(conn: Requester, args: AuthExtendArgs): AuthExtendResult {
|
|
866
866
|
const held = this.#authorized.get(conn);
|
|
867
867
|
if (held === undefined) {
|
|
868
868
|
throw new OpError("auth_invalid", "この接続は token で開かれたものではありません");
|
|
@@ -919,9 +919,9 @@ export class Auth {
|
|
|
919
919
|
* answers for another instance. */
|
|
920
920
|
export function authHandlers(auth: Auth) {
|
|
921
921
|
return {
|
|
922
|
-
|
|
923
|
-
auth.extend(input.conn, input.args as unknown as
|
|
924
|
-
|
|
922
|
+
"auth.extend": (input: HandlerInput): AuthExtendResult =>
|
|
923
|
+
auth.extend(input.conn, input.args as unknown as AuthExtendArgs),
|
|
924
|
+
"auth.resolve": (input: HandlerInput): AuthResolveResult => {
|
|
925
925
|
const args = input.args as unknown as AuthResolveArgs;
|
|
926
926
|
if (args.kind === "challenge") {
|
|
927
927
|
auth.spend(args.challenge);
|
|
@@ -932,7 +932,7 @@ export function authHandlers(auth: Auth) {
|
|
|
932
932
|
// the URL and the count of tries against it (§2.2).
|
|
933
933
|
return { kind: "register", claims: auth.resolveRegistration(args.token, args.code) };
|
|
934
934
|
},
|
|
935
|
-
|
|
935
|
+
"auth.rotate": (input: HandlerInput): AuthRotateResult => {
|
|
936
936
|
const args = input.args as unknown as AuthRotateArgs;
|
|
937
937
|
// The receiving instance's account of the person, taken as stated: it is
|
|
938
938
|
// the only one that saw them, and `last_refresh` is a hint nothing is
|
package/src/auth/http.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import {
|
|
3
3
|
type AuthAssertArgs,
|
|
4
|
-
type
|
|
4
|
+
type AuthTokenRefreshArgs,
|
|
5
5
|
type AuthRegisterArgs,
|
|
6
6
|
type ErrorCode,
|
|
7
7
|
type InstanceId,
|
|
@@ -24,10 +24,10 @@ type Route = (typeof ROUTES)[number];
|
|
|
24
24
|
/** Which op each route carries. The route is a name a proxy can see; the op is
|
|
25
25
|
* what the attribute table and the schemas are keyed by. */
|
|
26
26
|
const OP_OF: Record<Route, OpName> = {
|
|
27
|
-
challenge: "
|
|
28
|
-
register: "
|
|
29
|
-
assert: "
|
|
30
|
-
refresh: "
|
|
27
|
+
challenge: "auth.challenge",
|
|
28
|
+
register: "auth.register",
|
|
29
|
+
assert: "auth.assert",
|
|
30
|
+
refresh: "auth.token.refresh",
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
/** Cap on an `/auth/*` body. Everything these take is a handful of base64url
|
|
@@ -197,7 +197,7 @@ export async function handleAuth(
|
|
|
197
197
|
if (held === undefined) {
|
|
198
198
|
return refusal("auth_invalid", "この要求には refresh token がありません", cors);
|
|
199
199
|
}
|
|
200
|
-
const { reason } = args as unknown as
|
|
200
|
+
const { reason } = args as unknown as AuthTokenRefreshArgs;
|
|
201
201
|
const minted = await deps.auth.refreshToken(held, {
|
|
202
202
|
...(reason === undefined ? {} : { reason }),
|
|
203
203
|
...(seen.ip === undefined ? {} : { ip: seen.ip }),
|
package/src/auth/records.ts
CHANGED
|
@@ -63,7 +63,7 @@ export interface RecordsDeps {
|
|
|
63
63
|
* from a peer refusable on sight: a family this instance minted is written by
|
|
64
64
|
* this instance alone (DR-0001 §2.4). */
|
|
65
65
|
readonly self: InstanceId;
|
|
66
|
-
/** Hand what this instance wrote to the peers, on the `
|
|
66
|
+
/** Hand what this instance wrote to the peers, on the `auth.records` topic. */
|
|
67
67
|
readonly publish: (records: readonly AuthRecord[]) => void;
|
|
68
68
|
readonly now?: () => Timestamp;
|
|
69
69
|
}
|
package/src/auth/topic.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { Requester } from "../dispatch/index.ts";
|
|
|
3
3
|
import type { TopicValue, UpstreamResource } from "../topics/index.ts";
|
|
4
4
|
import type { AuthRecords } from "./records.ts";
|
|
5
5
|
|
|
6
|
-
/** `
|
|
6
|
+
/** `auth.records` as the topic mechanism sees it (DR-0001 §2.6).
|
|
7
7
|
*
|
|
8
8
|
* The records are here whether anyone is subscribed or not — they are what
|
|
9
9
|
* authenticates a person, not a watch on something — so there is nothing to
|
package/src/cli.ts
CHANGED
|
@@ -202,7 +202,7 @@ const ROOT: Command = {
|
|
|
202
202
|
},
|
|
203
203
|
{
|
|
204
204
|
name: "stop",
|
|
205
|
-
summary: "監督者に、子を止めさせる (
|
|
205
|
+
summary: "監督者に、子を止めさせる (instance.shutdown、以後は上げ直さない)",
|
|
206
206
|
usage: "ccmsg daemon stop <dir> | --all",
|
|
207
207
|
run: (args) => supervised("supervise_stop", args),
|
|
208
208
|
},
|
|
@@ -214,7 +214,7 @@ const ROOT: Command = {
|
|
|
214
214
|
},
|
|
215
215
|
{
|
|
216
216
|
name: "status",
|
|
217
|
-
summary: "監督者が各子に
|
|
217
|
+
summary: "監督者が各子に instance.ping して version・network・peers を答える",
|
|
218
218
|
usage: "ccmsg daemon status [dir] | --all",
|
|
219
219
|
bare: true,
|
|
220
220
|
run: (args) => supervised("supervise_status", args, true),
|
|
@@ -395,7 +395,7 @@ const ROOT: Command = {
|
|
|
395
395
|
summary: "この instance が持つ preset の名前と中身を並べる",
|
|
396
396
|
usage: "ccmsg dump presets",
|
|
397
397
|
bare: true,
|
|
398
|
-
run: () => instanceAsk({ op: "
|
|
398
|
+
run: () => instanceAsk({ op: "dump.presets.read" }),
|
|
399
399
|
},
|
|
400
400
|
],
|
|
401
401
|
run: (args) => dump(args),
|
|
@@ -585,7 +585,7 @@ async function runInstance(dir: string | undefined): Promise<unknown> {
|
|
|
585
585
|
}
|
|
586
586
|
const instance = outcome;
|
|
587
587
|
// A signal is a request to leave, and leaving is the ordered shutdown of
|
|
588
|
-
// §8.5 — the same one `
|
|
588
|
+
// §8.5 — the same one `instance.shutdown` runs, so a client sees the same
|
|
589
589
|
// departure either way. The listeners are removed once it has run, because a
|
|
590
590
|
// signal listener keeps the event loop alive and the process would sit at an
|
|
591
591
|
// empty loop instead of exiting.
|
|
@@ -852,8 +852,8 @@ function peers(args: readonly string[]): Promise<unknown> {
|
|
|
852
852
|
"peers",
|
|
853
853
|
parsed.flags.has("all"),
|
|
854
854
|
sid === undefined || sid === ""
|
|
855
|
-
? { op: "hello
|
|
856
|
-
: { op: "hello
|
|
855
|
+
? { op: "hello.user", protocol_version: PROTOCOL_VERSION }
|
|
856
|
+
: { op: "hello.session", sid, protocol_version: PROTOCOL_VERSION, ...statedMeta() },
|
|
857
857
|
);
|
|
858
858
|
}
|
|
859
859
|
|
|
@@ -864,8 +864,7 @@ function peers(args: readonly string[]): Promise<unknown> {
|
|
|
864
864
|
function agents(args: readonly string[]): Promise<unknown> {
|
|
865
865
|
const parsed = options(args, [], ["all", "json"]);
|
|
866
866
|
return topic("agents", parsed.flags.has("all"), {
|
|
867
|
-
op: "hello",
|
|
868
|
-
role: "user",
|
|
867
|
+
op: "hello.user",
|
|
869
868
|
protocol_version: PROTOCOL_VERSION,
|
|
870
869
|
});
|
|
871
870
|
}
|
|
@@ -919,7 +918,7 @@ async function dump(args: readonly string[]): Promise<unknown> {
|
|
|
919
918
|
);
|
|
920
919
|
}
|
|
921
920
|
const written = (await instanceAsk({
|
|
922
|
-
op: "
|
|
921
|
+
op: "session.dump.write",
|
|
923
922
|
...dumpArgs(subject, parsed.named),
|
|
924
923
|
})) as unknown as SessionDumpWriteResult;
|
|
925
924
|
const body = readFileSync(written.path, "utf8");
|
|
@@ -1028,10 +1027,10 @@ function post(args: readonly string[]): Promise<unknown> {
|
|
|
1028
1027
|
* delivered envelope as `ccmsg-from`.
|
|
1029
1028
|
*
|
|
1030
1029
|
* Without `--to` the answer is for a person, because that is the one message
|
|
1031
|
-
* whose reply line carries no addressee: `user` is not a sid and `
|
|
1030
|
+
* whose reply line carries no addressee: `user` is not a sid and `message.send`
|
|
1032
1031
|
* addresses a sid, so there is nothing to send to. The contract leaves how such
|
|
1033
1032
|
* an answer arrives to the instance and names the route — it reaches them as a
|
|
1034
|
-
* notification, which is `
|
|
1033
|
+
* notification, which is `notify.send`. The caller runs the line either way and
|
|
1035
1034
|
* does not have to know which of the two it became. */
|
|
1036
1035
|
function reply(args: readonly string[]): Promise<unknown> {
|
|
1037
1036
|
const parsed = options(args, ["sid", "to"]);
|
|
@@ -1080,7 +1079,7 @@ export async function stopping(args: readonly string[], read?: Read): Promise<un
|
|
|
1080
1079
|
// what the working directory reveals.
|
|
1081
1080
|
return await call(
|
|
1082
1081
|
parsed.named.get("sid") ?? event.sid,
|
|
1083
|
-
{ op: "
|
|
1082
|
+
{ op: "session.stopping", ...(reason === undefined ? {} : { reason }) },
|
|
1084
1083
|
stated(parsed.named, event),
|
|
1085
1084
|
);
|
|
1086
1085
|
}
|
|
@@ -1113,8 +1112,7 @@ export async function hello(args: readonly string[], read?: Read): Promise<unkno
|
|
|
1113
1112
|
if (conn === undefined) return { greeted: false, reason: "no_instance" };
|
|
1114
1113
|
try {
|
|
1115
1114
|
await conn.ask({
|
|
1116
|
-
op: "hello",
|
|
1117
|
-
role: "session",
|
|
1115
|
+
op: "hello.session",
|
|
1118
1116
|
sid,
|
|
1119
1117
|
protocol_version: PROTOCOL_VERSION,
|
|
1120
1118
|
...meta,
|
|
@@ -1224,19 +1222,19 @@ async function plugin(
|
|
|
1224
1222
|
return outcome;
|
|
1225
1223
|
}
|
|
1226
1224
|
|
|
1227
|
-
/** One `
|
|
1225
|
+
/** One `message.send`. */
|
|
1228
1226
|
function send(named: string | undefined, args: MessageSendArgs): Promise<unknown> {
|
|
1229
|
-
return call(named, { op: "
|
|
1227
|
+
return call(named, { op: "message.send", ...args });
|
|
1230
1228
|
}
|
|
1231
1229
|
|
|
1232
|
-
/** One `
|
|
1230
|
+
/** One `notify.send`. */
|
|
1233
1231
|
function announce(named: string | undefined, args: NotifySendArgs): Promise<unknown> {
|
|
1234
|
-
return call(named, { op: "
|
|
1232
|
+
return call(named, { op: "notify.send", ...args });
|
|
1235
1233
|
}
|
|
1236
1234
|
|
|
1237
1235
|
/** One op, spoken as the session this process runs inside.
|
|
1238
1236
|
*
|
|
1239
|
-
* The greeting is `
|
|
1237
|
+
* The greeting is `hello.session` because that is what the caller is: the
|
|
1240
1238
|
* instance takes the sender and the subject from the connection rather than
|
|
1241
1239
|
* from the arguments, so a connection that greeted as anything else has nobody
|
|
1242
1240
|
* to answer and nothing to be about.
|
|
@@ -1259,7 +1257,7 @@ async function call(
|
|
|
1259
1257
|
);
|
|
1260
1258
|
}
|
|
1261
1259
|
return await exchange(
|
|
1262
|
-
{ op: "hello
|
|
1260
|
+
{ op: "hello.session", sid, protocol_version: PROTOCOL_VERSION, ...meta },
|
|
1263
1261
|
request,
|
|
1264
1262
|
);
|
|
1265
1263
|
}
|
|
@@ -1269,7 +1267,7 @@ async function call(
|
|
|
1269
1267
|
* Which is who is asking: reading a transcript is not something a session is a
|
|
1270
1268
|
* party to, and the ops that do it are open to a person and to nobody else. */
|
|
1271
1269
|
function instanceAsk(request: Record<string, unknown>): Promise<unknown> {
|
|
1272
|
-
return exchange({ op: "hello
|
|
1270
|
+
return exchange({ op: "hello.user", protocol_version: PROTOCOL_VERSION }, request);
|
|
1273
1271
|
}
|
|
1274
1272
|
|
|
1275
1273
|
/** Greet this config home's instance, ask it one thing, and answer with what
|
|
@@ -1354,13 +1352,12 @@ async function posted(text: string): Promise<void> {
|
|
|
1354
1352
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1355
1353
|
const record = async (): Promise<void> => {
|
|
1356
1354
|
const greeting = await conn.ask({
|
|
1357
|
-
op: "hello",
|
|
1358
|
-
role: "session",
|
|
1355
|
+
op: "hello.session",
|
|
1359
1356
|
sid,
|
|
1360
1357
|
protocol_version: PROTOCOL_VERSION,
|
|
1361
1358
|
...statedMeta(),
|
|
1362
1359
|
});
|
|
1363
|
-
if (greeting["ok"] === true) await conn.ask({ op: "
|
|
1360
|
+
if (greeting["ok"] === true) await conn.ask({ op: "say.post", text });
|
|
1364
1361
|
};
|
|
1365
1362
|
const budget = new Promise<void>((resolve) => {
|
|
1366
1363
|
timer = setTimeout(resolve, SAY_POST_MS);
|
package/src/daemon/control.ts
CHANGED
|
@@ -49,10 +49,10 @@ export async function connect(path: string): Promise<Conn | undefined> {
|
|
|
49
49
|
|
|
50
50
|
/** Greet as the person running the command.
|
|
51
51
|
*
|
|
52
|
-
* `
|
|
52
|
+
* `hello.user` because that is what the caller is: the lifecycle ops belong
|
|
53
53
|
* to whoever operates the host, not to a session speaking from inside a turn. */
|
|
54
54
|
export function greetAsUser(conn: Conn): Promise<Record<string, unknown>> {
|
|
55
|
-
return conn.ask({ op: "hello
|
|
55
|
+
return conn.ask({ op: "hello.user", protocol_version: PROTOCOL_VERSION });
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
/** Reassemble the replies of one exchange, by arrival order. */
|
package/src/daemon/registry.ts
CHANGED
|
@@ -110,31 +110,31 @@ const STARTING_PRESETS = [
|
|
|
110
110
|
{
|
|
111
111
|
name: "file",
|
|
112
112
|
description: "ファイル操作。読み書きと探索をひとまとめに",
|
|
113
|
-
opts: { types: ["tool
|
|
113
|
+
opts: { types: ["tool.Read", "tool.Write", "tool.Edit", "tool.Glob", "tool.Grep"] },
|
|
114
114
|
},
|
|
115
115
|
{
|
|
116
116
|
name: "howto",
|
|
117
117
|
description: "調査のノウハウだけ。何を考えて何を叩いて何を読み書きしたか",
|
|
118
118
|
opts: {
|
|
119
|
-
types: ["thinking", "message
|
|
119
|
+
types: ["thinking", "message.user", "message.parent", "message.sub", "tool.Bash", "@file"],
|
|
120
120
|
},
|
|
121
121
|
},
|
|
122
122
|
{
|
|
123
123
|
name: "journal",
|
|
124
124
|
description: "日記用。人との往復と worker の答え、思考は要点だけ",
|
|
125
125
|
opts: {
|
|
126
|
-
types: ["message
|
|
126
|
+
types: ["message.user", "message.parent", "message.sub.in", "message.team.in", "thinking"],
|
|
127
127
|
},
|
|
128
128
|
},
|
|
129
129
|
{
|
|
130
130
|
name: "handoff",
|
|
131
131
|
description: "後継セッションへの引き継ぎ。直近の会話と、走っているものの台帳",
|
|
132
|
-
opts: { types: ["message", "system
|
|
132
|
+
opts: { types: ["message", "system.task", "ids"] },
|
|
133
133
|
},
|
|
134
134
|
{
|
|
135
135
|
name: "audit",
|
|
136
136
|
description: "何をしたかの追跡。会話は落として操作と通知だけ",
|
|
137
|
-
opts: { types: ["@file", "tool
|
|
137
|
+
opts: { types: ["@file", "tool.Bash", "notice", "ids"] },
|
|
138
138
|
},
|
|
139
139
|
];
|
|
140
140
|
|
|
@@ -225,7 +225,7 @@ export async function status(target: Target): Promise<StatusRow> {
|
|
|
225
225
|
if (greeting["ok"] !== true) return row;
|
|
226
226
|
const peers =
|
|
227
227
|
(greeting["instances"] as { id: InstanceId; endpoint: Endpoint }[] | undefined) ?? [];
|
|
228
|
-
const answer = await conn.ask({ op: "
|
|
228
|
+
const answer = await conn.ask({ op: "instance.ping" });
|
|
229
229
|
if (answer["ok"] !== true) return row;
|
|
230
230
|
const ping = answer as unknown as InstancePingResult;
|
|
231
231
|
return {
|
|
@@ -259,7 +259,7 @@ export async function stop(target: Target): Promise<{ dir: string; stopped: bool
|
|
|
259
259
|
if (greeting["ok"] !== true) {
|
|
260
260
|
throw new CommandError("forbidden", `hello が拒否されました: ${JSON.stringify(greeting)}`);
|
|
261
261
|
}
|
|
262
|
-
const answer = await conn.ask({ op: "
|
|
262
|
+
const answer = await conn.ask({ op: "instance.shutdown" });
|
|
263
263
|
if (answer["ok"] !== true) {
|
|
264
264
|
throw new CommandError("internal_error", `停止を拒否されました: ${JSON.stringify(answer)}`);
|
|
265
265
|
}
|
package/src/daemon/snapshot.ts
CHANGED
|
@@ -2,12 +2,12 @@ import type { InstanceId } from "@ccmsg/protocol";
|
|
|
2
2
|
import type { Conn } from "./control.ts";
|
|
3
3
|
import { CommandError } from "./link.ts";
|
|
4
4
|
|
|
5
|
-
/**
|
|
5
|
+
/** What one instance opened a topic with, as the frame carried it.
|
|
6
6
|
*
|
|
7
|
-
* The pair rather than the payload alone, because
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* The pair rather than the payload alone, because two instances state the same
|
|
8
|
+
* topic name: a payload with the name of its author taken off could not be told
|
|
9
|
+
* from the other's, and which instance said it is half of what a cluster read
|
|
10
|
+
* answers. */
|
|
11
11
|
export interface Snapshot {
|
|
12
12
|
readonly instance: InstanceId;
|
|
13
13
|
readonly data: unknown;
|
|
@@ -46,17 +46,19 @@ interface Greeted {
|
|
|
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.
|
|
48
48
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
49
|
+
* The opening frame is the whole of what its instance holds, whichever kind of
|
|
50
|
+
* topic it is: a `per_instance_whole` one states that instance's reading
|
|
51
|
+
* entire, and an `element` one opens with every row it has rather than with the
|
|
52
|
+
* rows that just changed. So one frame per instance is a complete read, and
|
|
53
|
+
* nothing after it is waited for — the first frame from every expected instance
|
|
54
|
+
* ends it. */
|
|
53
55
|
export async function snapshots(
|
|
54
56
|
conn: Conn,
|
|
55
57
|
topic: string,
|
|
56
58
|
expected: readonly InstanceId[],
|
|
57
59
|
budgetMs: number = SNAPSHOT_BUDGET_MS,
|
|
58
60
|
): Promise<Snapshot[]> {
|
|
59
|
-
const ack = await conn.ask({ op: "
|
|
61
|
+
const ack = await conn.ask({ op: "topic.subscribe", topic });
|
|
60
62
|
if (ack["ok"] !== true) {
|
|
61
63
|
const error = ack["error"] as { code?: string; msg?: string } | undefined;
|
|
62
64
|
throw new CommandError(
|
package/src/daemon/supervise.ts
CHANGED
|
@@ -273,7 +273,7 @@ export class Supervisor {
|
|
|
273
273
|
*
|
|
274
274
|
* Asked over its own socket rather than signalled, so what runs is the
|
|
275
275
|
* ordered shutdown of §8.5 — the same departure a client sees from an
|
|
276
|
-
* `
|
|
276
|
+
* `instance.shutdown`. */
|
|
277
277
|
async stopOne(dir: string): Promise<{ dir: string; stopped: boolean }> {
|
|
278
278
|
const unit = this.#units.get(dir);
|
|
279
279
|
if (unit === undefined) throw new CommandError("not_found", `${dir} は登録されていません`);
|
package/src/files/containment.ts
CHANGED
|
@@ -16,7 +16,7 @@ export interface SessionRoots {
|
|
|
16
16
|
* session greeted with, or its working directory. Absent when the session
|
|
17
17
|
* greeted with neither, which admits no contained path at all. */
|
|
18
18
|
readonly root?: string;
|
|
19
|
-
/** Where `
|
|
19
|
+
/** Where `file.write` puts a file. Kept apart from `root`, which may be the
|
|
20
20
|
* container above the working copy. */
|
|
21
21
|
readonly cwd?: string;
|
|
22
22
|
/** Absolute folder paths, each admitting its whole subtree. */
|
|
@@ -40,7 +40,7 @@ export interface Located {
|
|
|
40
40
|
readonly path: string;
|
|
41
41
|
/** The same path with its own last segment unresolved: what was named, not
|
|
42
42
|
* what it points at. An op that acts on the name rather than on the file —
|
|
43
|
-
* `
|
|
43
|
+
* `file.delete`, which unlinks a name — asks what kind of thing was named,
|
|
44
44
|
* and only this distinguishes a file from a symlink to one. */
|
|
45
45
|
readonly named: string;
|
|
46
46
|
}
|
|
@@ -101,7 +101,7 @@ export class Containment {
|
|
|
101
101
|
return undefined;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
/** Where `
|
|
104
|
+
/** Where `file.write` writes, which is the one destination no kind names: the
|
|
105
105
|
* session's working directory, and within it the inbox the destination is
|
|
106
106
|
* fixed to (DR-0019). A name that leaves the inbox is refused as unwritable
|
|
107
107
|
* rather than as forbidden — the path is reachable, and only writing there
|
|
@@ -195,7 +195,7 @@ export class Containment {
|
|
|
195
195
|
* about rather than one it guesses a range for. An op with no `scope` states
|
|
196
196
|
* no role, and has none to narrow by: dispatch already settled who may call it.
|
|
197
197
|
*
|
|
198
|
-
* The visible range is one function rather than one per op: `
|
|
198
|
+
* The visible range is one function rather than one per op: `transcript.read`
|
|
199
199
|
* and the file ops narrow by the same rule, and two spellings of it could come
|
|
200
200
|
* apart while both still passing their own tests. */
|
|
201
201
|
export function sees(sid: Sid, viewer: Viewer): boolean {
|
|
@@ -223,7 +223,7 @@ export interface DirArgs {
|
|
|
223
223
|
readonly path?: string;
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
-
/** The directory `
|
|
226
|
+
/** The directory `file.write` writes into, relative to the working directory. */
|
|
227
227
|
const INBOX = join("docs", "inbox");
|
|
228
228
|
|
|
229
229
|
const KINDS = ["contained", "workspace", "external"] as const;
|
package/src/files/files.ts
CHANGED
|
@@ -28,8 +28,8 @@ import type {
|
|
|
28
28
|
FileFindResult,
|
|
29
29
|
FileReadArgs,
|
|
30
30
|
FileReadResult,
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
FileStatArgs,
|
|
32
|
+
FileStatResult,
|
|
33
33
|
FileStatEntry,
|
|
34
34
|
FileWriteArgs,
|
|
35
35
|
FileWriteResult,
|
|
@@ -38,7 +38,7 @@ import type {
|
|
|
38
38
|
import { type HandlerInput, OpError } from "../dispatch/index.ts";
|
|
39
39
|
import type { Containment, Located, Viewer } from "./containment.ts";
|
|
40
40
|
|
|
41
|
-
/** How much of a file `
|
|
41
|
+
/** How much of a file `file.read` carries. Larger files are answered with their
|
|
42
42
|
* head and `truncated`, so one file can never cost the connection more than
|
|
43
43
|
* this however big it grew (DR-0008 §5). */
|
|
44
44
|
const READ_LIMIT = 512 * 1024;
|
|
@@ -65,7 +65,7 @@ export function fileHandlers(paths: Containment) {
|
|
|
65
65
|
const viewer = (input: HandlerInput): Viewer => ({ role: input.role, sid: input.identity?.sid });
|
|
66
66
|
|
|
67
67
|
return {
|
|
68
|
-
|
|
68
|
+
"dir.list": (input: HandlerInput): DirListResult => {
|
|
69
69
|
const args = input.args as unknown as DirListArgs;
|
|
70
70
|
const at = paths.root(args, viewer(input));
|
|
71
71
|
const stat = existing(at);
|
|
@@ -73,7 +73,7 @@ export function fileHandlers(paths: Containment) {
|
|
|
73
73
|
return { sid: args.sid, path: at.path, entries: entriesOf(at.real) };
|
|
74
74
|
},
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
"file.read": (input: HandlerInput): FileReadResult => {
|
|
77
77
|
const args = input.args as unknown as FileReadArgs;
|
|
78
78
|
const at = paths.locate(args, viewer(input));
|
|
79
79
|
const stat = existing(at);
|
|
@@ -95,7 +95,7 @@ export function fileHandlers(paths: Containment) {
|
|
|
95
95
|
};
|
|
96
96
|
},
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
"file.write": (input: HandlerInput): FileWriteResult => {
|
|
99
99
|
const args = input.args as unknown as FileWriteArgs;
|
|
100
100
|
const at = paths.inbox(args.sid, args.path, viewer(input));
|
|
101
101
|
// The inbox takes new notes, so an existing name is refused rather than
|
|
@@ -106,7 +106,7 @@ export function fileHandlers(paths: Containment) {
|
|
|
106
106
|
return { sid: args.sid, path: at.path };
|
|
107
107
|
},
|
|
108
108
|
|
|
109
|
-
|
|
109
|
+
"file.create": (input: HandlerInput): FileCreateResult => {
|
|
110
110
|
const args = input.args as unknown as FileCreateArgs;
|
|
111
111
|
const at = paths.locate(args, viewer(input));
|
|
112
112
|
const parent = dirname(at.real);
|
|
@@ -117,7 +117,7 @@ export function fileHandlers(paths: Containment) {
|
|
|
117
117
|
return { sid: args.sid, path: at.path };
|
|
118
118
|
},
|
|
119
119
|
|
|
120
|
-
|
|
120
|
+
"file.edit": (input: HandlerInput): FileEditResult => {
|
|
121
121
|
const args = input.args as unknown as FileEditArgs;
|
|
122
122
|
const at = paths.locate(args, viewer(input));
|
|
123
123
|
const stat = existing(at);
|
|
@@ -133,7 +133,7 @@ export function fileHandlers(paths: Containment) {
|
|
|
133
133
|
return { sid: args.sid, path: at.path, size: after.size, mtime_at: mtimeOf(after) };
|
|
134
134
|
},
|
|
135
135
|
|
|
136
|
-
|
|
136
|
+
"file.delete": (input: HandlerInput): FileDeleteResult => {
|
|
137
137
|
const args = input.args as unknown as FileDeleteArgs;
|
|
138
138
|
const at = paths.locate(args, viewer(input));
|
|
139
139
|
// What is unlinked is what is named, so this reads the name itself rather
|
|
@@ -150,7 +150,7 @@ export function fileHandlers(paths: Containment) {
|
|
|
150
150
|
return { sid: args.sid, path: at.path };
|
|
151
151
|
},
|
|
152
152
|
|
|
153
|
-
|
|
153
|
+
"file.find": (input: HandlerInput): FileFindResult => {
|
|
154
154
|
const args = input.args as unknown as FileFindArgs;
|
|
155
155
|
const at = paths.root(
|
|
156
156
|
{ sid: args.sid, kind: args.kind, ...(args.root === undefined ? {} : { path: args.root }) },
|
|
@@ -164,8 +164,8 @@ export function fileHandlers(paths: Containment) {
|
|
|
164
164
|
return { sid: args.sid, hits: walk.hits, truncated: walk.truncated };
|
|
165
165
|
},
|
|
166
166
|
|
|
167
|
-
|
|
168
|
-
const args = input.args as unknown as
|
|
167
|
+
"file.stat": (input: HandlerInput): FileStatResult => {
|
|
168
|
+
const args = input.args as unknown as FileStatArgs;
|
|
169
169
|
const results = args.paths.map((path): FileStatEntry | null => {
|
|
170
170
|
const at = paths.identify(args.sid, path, viewer(input));
|
|
171
171
|
if (at === undefined || !isFile(at.real)) return null;
|
package/src/files/sandbox.ts
CHANGED
|
Binary file
|
package/src/greeting/meta.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { basename, dirname } from "node:path";
|
|
2
|
-
import type {
|
|
2
|
+
import type { HelloSessionArgs } from "@ccmsg/protocol";
|
|
3
3
|
|
|
4
4
|
/** What a session can say about itself when it greets: the contract's shared
|
|
5
5
|
* fields, each of them optional because a greeting states what it knows and
|
|
6
6
|
* the instance leaves the rest unknown (contract, `SessionMetaFields`). */
|
|
7
7
|
export type StatedMeta = Partial<
|
|
8
|
-
Pick<
|
|
8
|
+
Pick<
|
|
9
|
+
HelloSessionArgs,
|
|
10
|
+
"repo" | "ws" | "cwd" | "repo_root" | "branch" | "transcript_path" | "title"
|
|
11
|
+
>
|
|
9
12
|
>;
|
|
10
13
|
|
|
11
14
|
/** How a question is put to the version control the session works under. The
|