@chipschat/harness 0.0.0-stage → 1.0.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.
Files changed (79) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +66 -2
  3. package/dist/asks.d.ts +75 -0
  4. package/dist/asks.d.ts.map +1 -0
  5. package/dist/asks.js +103 -0
  6. package/dist/asks.js.map +1 -0
  7. package/dist/connect.d.ts +406 -0
  8. package/dist/connect.d.ts.map +1 -0
  9. package/dist/connect.js +883 -0
  10. package/dist/connect.js.map +1 -0
  11. package/dist/events.d.ts +89 -0
  12. package/dist/events.d.ts.map +1 -0
  13. package/dist/events.js +36 -0
  14. package/dist/events.js.map +1 -0
  15. package/dist/files.d.ts +34 -0
  16. package/dist/files.d.ts.map +1 -0
  17. package/dist/files.js +78 -0
  18. package/dist/files.js.map +1 -0
  19. package/dist/heard.d.ts +22 -0
  20. package/dist/heard.d.ts.map +1 -0
  21. package/dist/heard.js +32 -0
  22. package/dist/heard.js.map +1 -0
  23. package/dist/index.d.ts +56 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +29 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/join.d.ts +101 -0
  28. package/dist/join.d.ts.map +1 -0
  29. package/dist/join.js +289 -0
  30. package/dist/join.js.map +1 -0
  31. package/dist/keep.d.ts +33 -0
  32. package/dist/keep.d.ts.map +1 -0
  33. package/dist/keep.js +160 -0
  34. package/dist/keep.js.map +1 -0
  35. package/dist/lane.d.ts +73 -0
  36. package/dist/lane.d.ts.map +1 -0
  37. package/dist/lane.js +105 -0
  38. package/dist/lane.js.map +1 -0
  39. package/dist/outbox.d.ts +50 -0
  40. package/dist/outbox.d.ts.map +1 -0
  41. package/dist/outbox.js +52 -0
  42. package/dist/outbox.js.map +1 -0
  43. package/dist/project.d.ts +224 -0
  44. package/dist/project.d.ts.map +1 -0
  45. package/dist/project.js +619 -0
  46. package/dist/project.js.map +1 -0
  47. package/dist/prompt.d.ts +61 -0
  48. package/dist/prompt.d.ts.map +1 -0
  49. package/dist/prompt.js +87 -0
  50. package/dist/prompt.js.map +1 -0
  51. package/dist/rules.d.ts +18 -0
  52. package/dist/rules.d.ts.map +1 -0
  53. package/dist/rules.js +25 -0
  54. package/dist/rules.js.map +1 -0
  55. package/dist/run.d.ts +246 -0
  56. package/dist/run.d.ts.map +1 -0
  57. package/dist/run.js +295 -0
  58. package/dist/run.js.map +1 -0
  59. package/dist/sending.d.ts +78 -0
  60. package/dist/sending.d.ts.map +1 -0
  61. package/dist/sending.js +149 -0
  62. package/dist/sending.js.map +1 -0
  63. package/dist/service.d.ts +124 -0
  64. package/dist/service.d.ts.map +1 -0
  65. package/dist/service.js +349 -0
  66. package/dist/service.js.map +1 -0
  67. package/dist/settings.d.ts +137 -0
  68. package/dist/settings.d.ts.map +1 -0
  69. package/dist/settings.js +228 -0
  70. package/dist/settings.js.map +1 -0
  71. package/dist/steps.d.ts +27 -0
  72. package/dist/steps.d.ts.map +1 -0
  73. package/dist/steps.js +26 -0
  74. package/dist/steps.js.map +1 -0
  75. package/dist/store.d.ts +35 -0
  76. package/dist/store.d.ts.map +1 -0
  77. package/dist/store.js +52 -0
  78. package/dist/store.js.map +1 -0
  79. package/package.json +37 -3
package/dist/join.js ADDED
@@ -0,0 +1,289 @@
1
+ /**
2
+ * Join a Chips (ADR 0122): find the server, ask to join, and come back with the four things
3
+ * `connect()` and a harness need — its address, the harness's id, its key and its room — with no
4
+ * token copied by hand.
5
+ *
6
+ * ```ts
7
+ * const me = await join({ link, name: "Tide" });
8
+ * const chips = await connect(me);
9
+ * ```
10
+ *
11
+ * Two ways in. With a link a person made in Chips (`<address>/j/<secret>`), it is allowed already
12
+ * and is taken up at once. With only an address, it asks: the server answers with a code, which
13
+ * `onCode` shows, and a person compares it and allows it; `join()` asks every few seconds until
14
+ * they do. Either way what it is handed is kept — in the keychain, the Secret Service, or a file
15
+ * only its user can read — and the next `join()` of the same name at the same server returns it
16
+ * without asking again. A link it wasn't handed that key by is taken up all the same, and its key
17
+ * kept in place of the old one: a person makes a new link when the old key is no good (the
18
+ * harness removed, Chips started afresh). A link already used or lapsed gives the kept key back,
19
+ * so a command run again as it was first written still starts.
20
+ *
21
+ * The server is found from the link, then `url`, then `CHIPS_URL`, then the `server.json` that
22
+ * `chips up` writes on its own machine.
23
+ */
24
+ import { createHash } from "node:crypto";
25
+ import { readFileSync } from "node:fs";
26
+ import { hostname } from "node:os";
27
+ import { join as joinPath } from "node:path";
28
+ import { HARNESS_CAPABILITIES, JOIN_POLL_SECONDS, parseJoinLink, } from "@chipschat/protocol";
29
+ import { configHome, openKeyStore } from "./keep.js";
30
+ import { reportStep } from "./steps.js";
31
+ /** Why it did not join, with the rule that refused it: `spent`, `expired`, `denied`, `too_many`… */
32
+ export class JoinError extends Error {
33
+ refusal;
34
+ name = "JoinError";
35
+ constructor(refusal, message) {
36
+ super(message);
37
+ this.refusal = refusal;
38
+ }
39
+ }
40
+ /** Where `chips up` says the server on this machine is. */
41
+ function serverJsonUrl(env) {
42
+ try {
43
+ const said = JSON.parse(readFileSync(joinPath(configHome(env), "server.json"), "utf8"));
44
+ return typeof said.url === "string" && said.url !== "" ? said.url : undefined;
45
+ }
46
+ catch {
47
+ return undefined;
48
+ }
49
+ }
50
+ /** The server, and the link's secret when there is one. */
51
+ export function findServer(options, env) {
52
+ if (options.link !== undefined) {
53
+ const parsed = parseJoinLink(options.link);
54
+ if (parsed === undefined) {
55
+ throw new JoinError("invalid", "That is not a join link: it looks like <address>/j/<secret>.");
56
+ }
57
+ return parsed;
58
+ }
59
+ const url = options.url ?? env["CHIPS_URL"] ?? serverJsonUrl(env);
60
+ if (url === undefined || url === "") {
61
+ throw new JoinError("nowhere", "Where is Chips? Pass a link or url, set CHIPS_URL, or run chips up on this machine.");
62
+ }
63
+ return { url: url.replace(/\/$/, "") };
64
+ }
65
+ /** The account a key is kept under: the harness's name, at its server. */
66
+ export function accountOf(url, name) {
67
+ return `harness:${name}@${url.replace(/\/$/, "")}`;
68
+ }
69
+ function joinedOf(answer) {
70
+ return {
71
+ url: answer.url,
72
+ harnessId: answer.harness_id,
73
+ token: answer.token,
74
+ ...(answer.space_id === undefined ? {} : { spaceId: answer.space_id }),
75
+ capabilities: answer.capabilities,
76
+ };
77
+ }
78
+ /** A link's secret as it is kept beside the key: hashed, since a key store is no place for it. */
79
+ function linkHash(secret) {
80
+ return createHash("sha256").update(secret).digest("base64url");
81
+ }
82
+ function keptAt(store, account) {
83
+ if (store === false)
84
+ return undefined;
85
+ try {
86
+ const kept = store.get(account);
87
+ if (kept === undefined)
88
+ return undefined;
89
+ const { from, hello, ...parsed } = JSON.parse(kept);
90
+ if (typeof parsed.token !== "string" || typeof parsed.harnessId !== "string")
91
+ return undefined;
92
+ if (typeof parsed.url !== "string")
93
+ return undefined;
94
+ return {
95
+ joined: { capabilities: [], ...parsed },
96
+ from: typeof from === "string" ? from : undefined,
97
+ };
98
+ }
99
+ catch {
100
+ return undefined;
101
+ }
102
+ }
103
+ /**
104
+ * The key `join()` takes up for a harness of this name when it has no link, at the server it would
105
+ * find (`url`, `CHIPS_URL`, then `server.json`): what `chips connect <id>` with no link joins with,
106
+ * so the harness keeps its rooms (ADR 0218's addendum of 24 October 2026). The server's address
107
+ * comes back as well where one is found, kept key or not.
108
+ */
109
+ export function keptJoin(name, options = {}) {
110
+ // biome-ignore lint/style/noProcessEnv: as `join()`.
111
+ const env = options.env ?? process.env;
112
+ let url;
113
+ try {
114
+ url = findServer(options.url === undefined ? {} : { url: options.url }, env).url;
115
+ }
116
+ catch {
117
+ return {};
118
+ }
119
+ const kept = keptAt(options.keep ?? openKeyStore(env), accountOf(url, name));
120
+ return kept === undefined ? { url } : { url, joined: kept.joined };
121
+ }
122
+ /** The refusals that mean a link was already taken up or has lapsed: the kept key still stands. */
123
+ const USED_UP = new Set(["spent", "expired"]);
124
+ /** A refusal off the wire, as a `JoinError`. */
125
+ async function refused(response) {
126
+ const body = (await response.json().catch(() => ({})));
127
+ return new JoinError(body.error?.refusal ?? body.error?.code ?? `http_${response.status}`, body.error?.message ?? `Chips answered ${response.status}.`);
128
+ }
129
+ const wait = (ms, signal) => new Promise((resolve, reject) => {
130
+ const timer = setTimeout(resolve, ms);
131
+ signal?.addEventListener("abort", () => {
132
+ clearTimeout(timer);
133
+ reject(new JoinError("stopped", "Stopped before a person decided."));
134
+ }, { once: true });
135
+ });
136
+ /** Ask with a code, and wait for a person. */
137
+ async function ask(url, options, send, machine) {
138
+ const asked = await send(`${url}/v1/join`, {
139
+ method: "POST",
140
+ headers: { "content-type": "application/json" },
141
+ body: JSON.stringify({
142
+ kind: "harness",
143
+ name: options.name,
144
+ machine,
145
+ ...(options.software === undefined ? {} : { software: options.software }),
146
+ wants: [...(options.wants ?? [HARNESS_CAPABILITIES.autonomy])],
147
+ }),
148
+ });
149
+ if (asked.status !== 201)
150
+ throw await refused(asked);
151
+ const { request_id, secret, code, interval } = (await asked.json());
152
+ const show = options.onCode ??
153
+ ((shown, where) => process.stderr.write(`Allow ${options.name} in Chips at ${where}. Its code is ${shown}.\n`));
154
+ show(code, url);
155
+ for (;;) {
156
+ await wait((interval ?? JOIN_POLL_SECONDS) * 1000, options.signal);
157
+ const polled = await send(`${url}/v1/join/${request_id}/redeem`, {
158
+ method: "POST",
159
+ headers: { "content-type": "application/json" },
160
+ body: JSON.stringify({ secret }),
161
+ });
162
+ if (polled.status === 202)
163
+ continue;
164
+ if (polled.status === 200)
165
+ return (await polled.json());
166
+ throw await refused(polled);
167
+ }
168
+ }
169
+ /** Take up a link: allowed already, so the answer is the harness's at once. */
170
+ async function redeem(url, secret, options, send, machine) {
171
+ const answered = await send(`${url}/v1/join/redeem`, {
172
+ method: "POST",
173
+ headers: { "content-type": "application/json" },
174
+ body: JSON.stringify({
175
+ secret,
176
+ name: options.name,
177
+ machine,
178
+ ...(options.software === undefined ? {} : { software: options.software }),
179
+ }),
180
+ });
181
+ if (answered.status !== 200)
182
+ throw await refused(answered);
183
+ return (await answered.json());
184
+ }
185
+ /**
186
+ * A request that never reached Chips, said in one line rather than a stack (the new-person
187
+ * sweep's finding 3): a link made on Chips's own address, opened on a computer that can't reach
188
+ * that address, which no answer from the server would ever explain. A cancel is still a cancel.
189
+ */
190
+ function reaching(send, url) {
191
+ return async (input, init) => {
192
+ try {
193
+ return await send(input, init);
194
+ }
195
+ catch (error) {
196
+ if (error instanceof Error && error.name === "AbortError")
197
+ throw error;
198
+ throw new JoinError("unreachable", `Can't reach Chips at ${url} from this computer. It has to be on the same network as Chips, on its tailnet, or Chips has to be at an address this computer can reach.`);
199
+ }
200
+ };
201
+ }
202
+ /** Take up the link, or ask with a code when there is none. */
203
+ function askOrRedeem(server, options) {
204
+ const send = reaching(options.fetch ?? fetch, server.url);
205
+ const machine = options.machine ?? hostname();
206
+ return server.secret === undefined
207
+ ? ask(server.url, options, send, machine)
208
+ : redeem(server.url, server.secret, options, send, machine);
209
+ }
210
+ const usedUp = (error) => error instanceof JoinError && USED_UP.has(error.refusal);
211
+ export async function join(options) {
212
+ // biome-ignore lint/style/noProcessEnv: the environment is where a harness is told where Chips is.
213
+ const env = options.env ?? process.env;
214
+ reportStep("joining", {}, env);
215
+ const server = findServer(options, env);
216
+ const store = options.keep ?? openKeyStore(env);
217
+ const account = accountOf(server.url, options.name);
218
+ const kept = keptAt(store, account);
219
+ const from = server.secret === undefined ? undefined : linkHash(server.secret);
220
+ // No link, or the very link it was handed this key by: the key, and nothing asked.
221
+ if (kept !== undefined && (from === undefined || kept.from === from))
222
+ return kept.joined;
223
+ let answer;
224
+ try {
225
+ answer = await askOrRedeem(server, options);
226
+ }
227
+ catch (error) {
228
+ if (kept !== undefined && usedUp(error))
229
+ return kept.joined;
230
+ throw error;
231
+ }
232
+ const joined = joinedOf(answer);
233
+ // Handed over now, so its first words are due (`helloOnce`).
234
+ if (store !== false)
235
+ store.set(account, JSON.stringify({ ...joined, from, hello: "due" }));
236
+ return joined;
237
+ }
238
+ /**
239
+ * Say a harness's first words in the room it joined, once: the card that followed its link waits
240
+ * for them ("Said hello", ADR 0122), and a harness that only answers would leave it waiting. They
241
+ * are due when `join()` is handed a key, and noted as said beside it, so a restart says nothing
242
+ * again, a new link (a new key and room) says them again there, and a key kept from before says
243
+ * none. `say` posts the words, as the harness or its Main agent; nothing is said when Chips gave it
244
+ * no room. Whether it spoke comes back.
245
+ */
246
+ export async function helloOnce(joined, options) {
247
+ const { spaceId } = joined;
248
+ if (spaceId === undefined)
249
+ return false;
250
+ // biome-ignore lint/style/noProcessEnv: as `join()`.
251
+ const store = options.keep ?? openKeyStore(options.env ?? process.env);
252
+ const account = accountOf(joined.url, options.name);
253
+ let record;
254
+ try {
255
+ const kept = store === false ? undefined : store.get(account);
256
+ record = kept === undefined ? undefined : JSON.parse(kept);
257
+ }
258
+ catch {
259
+ record = undefined;
260
+ }
261
+ // Nothing kept means it asks every time, so every start is a first.
262
+ const due = store === false || (record?.["hello"] === "due" && record["token"] === joined.token);
263
+ if (!due)
264
+ return false;
265
+ await options.say(spaceId);
266
+ if (store !== false)
267
+ store.set(account, JSON.stringify({ ...record, hello: "said" }));
268
+ return true;
269
+ }
270
+ /**
271
+ * Whether a value was filled in, rather than left as the placeholder a command was copied with
272
+ * (`API_SERVER_KEY=…`): a harness checks what it needs before it takes up a link, which works once.
273
+ */
274
+ export function filledIn(value) {
275
+ if (value === undefined)
276
+ return false;
277
+ const given = value.trim();
278
+ return given !== "" && !/^(?:…|\.\.\.|<[^>]*>)$/.test(given);
279
+ }
280
+ /** Forget what a harness was handed, so the next `join()` asks again: a key withdrawn, say. */
281
+ export function forgetJoin(options) {
282
+ // biome-ignore lint/style/noProcessEnv: as `join()`.
283
+ const env = options.env ?? process.env;
284
+ const store = options.keep ?? openKeyStore(env);
285
+ if (store === false)
286
+ return;
287
+ store.remove(accountOf(findServer(options, env).url, options.name));
288
+ }
289
+ //# sourceMappingURL=join.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"join.js","sourceRoot":"","sources":["../src/join.ts"],"names":[],"mappings":"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"}
package/dist/keep.d.ts ADDED
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Where a harness keeps the key it was handed when it joined (ADR 0122), so it asks once: the macOS
3
+ * keychain, the Secret Service where `secret-tool` is installed, or else a file only its user can
4
+ * read, in Chips's own config directory. The CLI's keychain does the same for the harnesses it
5
+ * connects (`cli/src/keychain.ts`); this is that, for a harness on the SDK, which cannot depend on
6
+ * the CLI.
7
+ *
8
+ * What is kept is what `join()` answers — the server's address, the harness's id, its key and its
9
+ * room — under the server and the harness's name, so a second harness of another name on the same
10
+ * machine asks for its own.
11
+ */
12
+ export type Environment = Readonly<Record<string, string | undefined>>;
13
+ /** A place a secret is kept, by account. */
14
+ export interface KeyStore {
15
+ readonly kind: "keychain" | "secret-service" | "file";
16
+ readonly get: (account: string) => string | undefined;
17
+ readonly set: (account: string, secret: string) => void;
18
+ readonly remove: (account: string) => void;
19
+ }
20
+ /**
21
+ * The `add-generic-password` command as it is written to `security -i` (SEC-34): the secret
22
+ * crosses on stdin, which nothing else of this user's can read, rather than on argv, which `ps`
23
+ * shows every process of this user for the moment the tool runs. The CLI's keychain does the same.
24
+ */
25
+ export declare function addPasswordCommand(account: string, secret: string): string;
26
+ /** Chips's config directory: `CHIPS_CONFIG_HOME`, else `$XDG_CONFIG_HOME/chips`, else `~/.config/chips`. */
27
+ export declare function configHome(env: Environment): string;
28
+ /**
29
+ * The best place this machine has. `CHIPS_NO_KEYCHAIN=true` asks for the file, as it does of the
30
+ * CLI: a test, a container, a server with no keyring unlocked.
31
+ */
32
+ export declare function openKeyStore(env: Environment, platform?: string): KeyStore;
33
+ //# sourceMappingURL=keep.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keep.d.ts","sourceRoot":"","sources":["../src/keep.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAOH,MAAM,MAAM,WAAW,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;AAEvE,4CAA4C;AAC5C,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG,gBAAgB,GAAG,MAAM,CAAC;IACtD,QAAQ,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;IACtD,QAAQ,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACxD,QAAQ,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CAC5C;AAWD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAE1E;AAED,4GAA4G;AAC5G,wBAAgB,UAAU,CAAC,GAAG,EAAE,WAAW,GAAG,MAAM,CAQnD;AA8GD;;;GAGG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,WAAW,EAAE,QAAQ,GAAE,MAAyB,GAAG,QAAQ,CAO5F"}
package/dist/keep.js ADDED
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Where a harness keeps the key it was handed when it joined (ADR 0122), so it asks once: the macOS
3
+ * keychain, the Secret Service where `secret-tool` is installed, or else a file only its user can
4
+ * read, in Chips's own config directory. The CLI's keychain does the same for the harnesses it
5
+ * connects (`cli/src/keychain.ts`); this is that, for a harness on the SDK, which cannot depend on
6
+ * the CLI.
7
+ *
8
+ * What is kept is what `join()` answers — the server's address, the harness's id, its key and its
9
+ * room — under the server and the harness's name, so a second harness of another name on the same
10
+ * machine asks for its own.
11
+ */
12
+ import { execFileSync } from "node:child_process";
13
+ import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
14
+ import { homedir } from "node:os";
15
+ import { dirname, join } from "node:path";
16
+ /** The service name every Chips secret is kept under, as the CLI's are. */
17
+ const SERVICE = "chips";
18
+ /** A word as `security -i` reads it: in double quotes, its own quotes and backslashes escaped. */
19
+ function quoted(word) {
20
+ if (/[\r\n]/.test(word))
21
+ throw new Error("A keychain account or secret is one line.");
22
+ return `"${word.replace(/[\\"]/g, "\\$&")}"`;
23
+ }
24
+ /**
25
+ * The `add-generic-password` command as it is written to `security -i` (SEC-34): the secret
26
+ * crosses on stdin, which nothing else of this user's can read, rather than on argv, which `ps`
27
+ * shows every process of this user for the moment the tool runs. The CLI's keychain does the same.
28
+ */
29
+ export function addPasswordCommand(account, secret) {
30
+ return `add-generic-password -U -a ${quoted(account)} -s ${quoted(SERVICE)} -w ${quoted(secret)}\n`;
31
+ }
32
+ /** Chips's config directory: `CHIPS_CONFIG_HOME`, else `$XDG_CONFIG_HOME/chips`, else `~/.config/chips`. */
33
+ export function configHome(env) {
34
+ const own = env["CHIPS_CONFIG_HOME"];
35
+ if (own !== undefined && own !== "")
36
+ return own;
37
+ const xdg = env["XDG_CONFIG_HOME"];
38
+ return join(xdg !== undefined && xdg !== "" ? xdg : join(env["HOME"] ?? homedir(), ".config"), "chips");
39
+ }
40
+ function answers(command, args) {
41
+ try {
42
+ execFileSync(command, [...args], { stdio: "ignore" });
43
+ return true;
44
+ }
45
+ catch {
46
+ return false;
47
+ }
48
+ }
49
+ function keychain() {
50
+ const run = (args) => execFileSync("/usr/bin/security", [...args], {
51
+ encoding: "utf8",
52
+ stdio: ["ignore", "pipe", "ignore"],
53
+ });
54
+ return {
55
+ kind: "keychain",
56
+ get(account) {
57
+ try {
58
+ return run(["find-generic-password", "-a", account, "-s", SERVICE, "-w"]).trim();
59
+ }
60
+ catch {
61
+ return undefined;
62
+ }
63
+ },
64
+ set(account, secret) {
65
+ // On stdin, secret and all (SEC-34); the tool exits non-zero when the command inside fails.
66
+ execFileSync("/usr/bin/security", ["-i"], {
67
+ input: addPasswordCommand(account, secret),
68
+ stdio: ["pipe", "ignore", "ignore"],
69
+ });
70
+ },
71
+ remove(account) {
72
+ try {
73
+ run(["delete-generic-password", "-a", account, "-s", SERVICE]);
74
+ }
75
+ catch {
76
+ // Nothing kept: nothing to remove.
77
+ }
78
+ },
79
+ };
80
+ }
81
+ function secretService() {
82
+ const attributes = (account) => ["service", SERVICE, "account", account];
83
+ return {
84
+ kind: "secret-service",
85
+ get(account) {
86
+ try {
87
+ const kept = execFileSync("secret-tool", ["lookup", ...attributes(account)], {
88
+ encoding: "utf8",
89
+ stdio: ["ignore", "pipe", "ignore"],
90
+ });
91
+ return kept === "" ? undefined : kept;
92
+ }
93
+ catch {
94
+ return undefined;
95
+ }
96
+ },
97
+ set(account, secret) {
98
+ execFileSync("secret-tool", ["store", "--label", `Chips: ${account}`, ...attributes(account)], {
99
+ input: secret,
100
+ stdio: ["pipe", "ignore", "ignore"],
101
+ });
102
+ },
103
+ remove(account) {
104
+ try {
105
+ execFileSync("secret-tool", ["clear", ...attributes(account)], { stdio: "ignore" });
106
+ }
107
+ catch {
108
+ // Nothing kept: nothing to remove.
109
+ }
110
+ },
111
+ };
112
+ }
113
+ /** One JSON map, readable by this user alone, beside the CLI's own `secrets.json`. */
114
+ function file(path) {
115
+ const read = () => {
116
+ if (!existsSync(path))
117
+ return {};
118
+ try {
119
+ return JSON.parse(readFileSync(path, "utf8"));
120
+ }
121
+ catch {
122
+ return {};
123
+ }
124
+ };
125
+ const write = (kept) => {
126
+ if (Object.keys(kept).length === 0) {
127
+ rmSync(path, { force: true });
128
+ return;
129
+ }
130
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
131
+ writeFileSync(path, `${JSON.stringify(kept, null, 2)}\n`, { mode: 0o600 });
132
+ chmodSync(path, 0o600);
133
+ };
134
+ return {
135
+ kind: "file",
136
+ get: (account) => read()[account],
137
+ set(account, secret) {
138
+ write({ ...read(), [account]: secret });
139
+ },
140
+ remove(account) {
141
+ const { [account]: _gone, ...rest } = read();
142
+ write(rest);
143
+ },
144
+ };
145
+ }
146
+ /**
147
+ * The best place this machine has. `CHIPS_NO_KEYCHAIN=true` asks for the file, as it does of the
148
+ * CLI: a test, a container, a server with no keyring unlocked.
149
+ */
150
+ export function openKeyStore(env, platform = process.platform) {
151
+ const plain = file(join(configHome(env), "harness-keys.json"));
152
+ if (env["CHIPS_NO_KEYCHAIN"] === "true")
153
+ return plain;
154
+ if (platform === "darwin" && answers("/usr/bin/security", ["list-keychains"]))
155
+ return keychain();
156
+ if (platform === "linux" && answers("sh", ["-c", "command -v secret-tool"]))
157
+ return secretService();
158
+ return plain;
159
+ }
160
+ //# sourceMappingURL=keep.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keep.js","sourceRoot":"","sources":["../src/keep.ts"],"names":[],"mappings":"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"}
package/dist/lane.d.ts ADDED
@@ -0,0 +1,73 @@
1
+ /**
2
+ * A lane, from the lead's side: what `run.task()` hands back. The task is acknowledged with
3
+ * where the lane lives and which run it is; from then on the lead hears the lane's news as
4
+ * `lane.updated` — waiting, completed with the specialist's summary and what it cost, failed,
5
+ * cancelled — and `result` resolves with the last of those. A task Chips flattened into a step
6
+ * has no lane, and `result` says so at once.
7
+ *
8
+ * A lane between two spaces of one harness — a Space handed down to, a subagent — is routed to
9
+ * the harness itself, and the lead works it: `work()` is the run to post into (M3.5, ADR 0032).
10
+ */
11
+ import type { LaneUpdated } from "@chipschat/protocol";
12
+ import { type Handler } from "./events.js";
13
+ export type LaneState = LaneUpdated["state"];
14
+ /** How a lane ended, as `result` resolves: the last `lane.updated`, or the fact it never opened. */
15
+ export type LaneOutcome = {
16
+ readonly state: "completed" | "failed" | "cancelled";
17
+ readonly summary?: string | undefined;
18
+ readonly costUsd?: number | undefined;
19
+ readonly event: LaneUpdated;
20
+ } | {
21
+ readonly state: "flattened";
22
+ };
23
+ export interface LaneEvents {
24
+ /** Every `lane.updated` for this lane, in order. */
25
+ readonly updated: LaneUpdated;
26
+ }
27
+ /**
28
+ * What the connection gives a lane: its news, and a run for the lead to work it with. Generic
29
+ * over the run, so this module owes the run's nothing: `run.task()` hands back a `Lane<Run>`.
30
+ */
31
+ export interface LaneWire<R> {
32
+ readonly hear: (laneId: string, handler: (event: LaneUpdated) => void) => () => void;
33
+ readonly accept: (runId: string, spaceId: string, laneId: string) => R;
34
+ }
35
+ export declare class Lane<R = unknown> {
36
+ readonly id: string;
37
+ private readonly wire;
38
+ private readonly events;
39
+ private settle;
40
+ private stop;
41
+ private laneState;
42
+ private laneRunId;
43
+ private laneSpaceId;
44
+ private lastSummary;
45
+ private lastCost;
46
+ /** Resolves when the lane ends, or at once when the task was flattened. Never rejects. */
47
+ readonly result: Promise<LaneOutcome>;
48
+ constructor(id: string, wire: LaneWire<R>);
49
+ /** The specialist's run, which the lane is. Known once the task is acknowledged. */
50
+ get runId(): string | undefined;
51
+ /** Where the lane lives. Known once the task is acknowledged. */
52
+ get spaceId(): string | undefined;
53
+ /** Where the lane got to: `opening` until acknowledged, then the specialist's state. */
54
+ get state(): LaneState | "flattened" | "opening";
55
+ get summary(): string | undefined;
56
+ get costUsd(): number | undefined;
57
+ /** True once the lane will not change again. */
58
+ get over(): boolean;
59
+ on<K extends keyof LaneEvents>(name: K, handler: Handler<LaneEvents[K]>): () => void;
60
+ /**
61
+ * The lane's run, for a lead that works the lane itself — a lane between two of its own
62
+ * spaces. Everything sent on it lands in the lane's home, as the lane's own steps and messages.
63
+ */
64
+ work(): R;
65
+ /** The task's acknowledgement: where the lane is, or that there is none. */
66
+ opened(lane: {
67
+ runId: string;
68
+ spaceId: string;
69
+ } | undefined): void;
70
+ private onUpdated;
71
+ private finish;
72
+ }
73
+ //# sourceMappingURL=lane.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lane.d.ts","sourceRoot":"","sources":["../src/lane.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,EAAW,KAAK,OAAO,EAAE,MAAM,aAAa,CAAC;AAEpD,MAAM,MAAM,SAAS,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC;AAE7C,oGAAoG;AACpG,MAAM,MAAM,WAAW,GACnB;IACE,QAAQ,CAAC,KAAK,EAAE,WAAW,GAAG,QAAQ,GAAG,WAAW,CAAC;IACrD,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;CAC7B,GACD;IAAE,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAA;CAAE,CAAC;AAEpC,MAAM,WAAW,UAAU;IACzB,oDAAoD;IACpD,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;CAC/B;AAED;;;GAGG;AACH,MAAM,WAAW,QAAQ,CAAC,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,KAAK,MAAM,IAAI,CAAC;IACrF,QAAQ,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,CAAC,CAAC;CACxE;AAID,qBAAa,IAAI,CAAC,CAAC,GAAG,OAAO;IAazB,QAAQ,CAAC,EAAE,EAAE,MAAM;IACnB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAbvB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA6B;IACpD,OAAO,CAAC,MAAM,CAA+C;IAC7D,OAAO,CAAC,IAAI,CAA2B;IACvC,OAAO,CAAC,SAAS,CAAkD;IACnE,OAAO,CAAC,SAAS,CAAqB;IACtC,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,QAAQ,CAAqB;IACrC,0FAA0F;IAC1F,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC;gBAG3B,EAAE,EAAE,MAAM,EACF,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC;IASpC,oFAAoF;IACpF,IAAI,KAAK,IAAI,MAAM,GAAG,SAAS,CAE9B;IAED,iEAAiE;IACjE,IAAI,OAAO,IAAI,MAAM,GAAG,SAAS,CAEhC;IAED,wFAAwF;IACxF,IAAI,KAAK,IAAI,SAAS,GAAG,WAAW,GAAG,SAAS,CAE/C;IAED,IAAI,OAAO,IAAI,MAAM,GAAG,SAAS,CAEhC;IAED,IAAI,OAAO,IAAI,MAAM,GAAG,SAAS,CAEhC;IAED,gDAAgD;IAChD,IAAI,IAAI,IAAI,OAAO,CAElB;IAED,EAAE,CAAC,CAAC,SAAS,MAAM,UAAU,EAAE,IAAI,EAAE,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI;IAIpF;;;OAGG;IACH,IAAI,IAAI,CAAC;IAWT,4EAA4E;IAC5E,MAAM,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,GAAG,IAAI;IAWlE,OAAO,CAAC,SAAS;IAejB,OAAO,CAAC,MAAM;CAMf"}