@kodycodes/cli 3.2.0 → 3.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/dist/{auth-GVBMBBDV.js → auth-7CZOBANL.js} +6 -5
  2. package/dist/bin.js +10 -9
  3. package/dist/bin.js.map +1 -1
  4. package/dist/{chunk-NRLXYJKW.js → chunk-3DASQK6N.js} +3 -3
  5. package/dist/chunk-AED3YBAI.js +355 -0
  6. package/dist/chunk-AED3YBAI.js.map +6 -0
  7. package/dist/{chunk-VEYAO5OE.js → chunk-CHLT3KZU.js} +6 -6
  8. package/dist/{chunk-OIXNZ63K.js → chunk-DMA6RYFC.js} +13 -13
  9. package/dist/{chunk-OWIJPBAX.js → chunk-KJV3IWUL.js} +6 -4
  10. package/dist/chunk-KJV3IWUL.js.map +6 -0
  11. package/dist/{chunk-ZK52DY36.js → chunk-MU6NWE3A.js} +6 -6
  12. package/dist/{chunk-FMBLQ7J2.js → chunk-Q7D7J7EQ.js} +84 -45
  13. package/dist/chunk-Q7D7J7EQ.js.map +6 -0
  14. package/dist/{chunk-2GSKFEWW.js → chunk-QYKQTFEU.js} +101 -5
  15. package/dist/chunk-QYKQTFEU.js.map +6 -0
  16. package/dist/{chunk-SAK2XRKX.js → chunk-RETDMHIH.js} +3 -3
  17. package/dist/chunk-S3KLKZ4C.js +124 -0
  18. package/dist/chunk-S3KLKZ4C.js.map +6 -0
  19. package/dist/{chunk-LISUPAAK.js → chunk-TZKZOMEA.js} +5 -117
  20. package/dist/chunk-TZKZOMEA.js.map +6 -0
  21. package/dist/{chunk-H4BN6EPW.js → chunk-WUIKH7FJ.js} +3 -3
  22. package/dist/cli.d.ts +3 -1
  23. package/dist/cli.js +11 -10
  24. package/dist/commands-RPUZAOAB.js +806 -0
  25. package/dist/commands-RPUZAOAB.js.map +6 -0
  26. package/dist/{dev-command-NUN5HHRC.js → dev-command-M7TWT36J.js} +28 -27
  27. package/dist/dev-command-M7TWT36J.js.map +6 -0
  28. package/dist/execution-context/client.d.ts +89 -0
  29. package/dist/execution-context/commands.d.ts +26 -0
  30. package/dist/execution-context/daemon.d.ts +47 -0
  31. package/dist/execution-context/keystore.d.ts +58 -0
  32. package/dist/execution-context/run-executor.d.ts +20 -0
  33. package/dist/{git-credential-XTJ5JZ6V.js → git-credential-4SOTXCMS.js} +8 -7
  34. package/dist/git-credential-4SOTXCMS.js.map +6 -0
  35. package/dist/{local-execute-5J53ZM5Z.js → local-execute-3H2ROHTP.js} +14 -13
  36. package/dist/local-execute-3H2ROHTP.js.map +6 -0
  37. package/dist/{local-package-graph-7TEPDAGV.js → local-package-graph-657CN4KN.js} +4 -3
  38. package/dist/{local-package-run-OPC7VXO3.js → local-package-run-2FZDLAAR.js} +10 -9
  39. package/dist/{mock-dep-resolve-RRR74F7X.js → mock-dep-resolve-EHVSQMXM.js} +8 -7
  40. package/dist/mock-dep-resolve-EHVSQMXM.js.map +6 -0
  41. package/dist/{mock-local-deps-EUL62AHI.js → mock-local-deps-6D73XVTZ.js} +6 -5
  42. package/dist/{open-api-client-K2BEQBO3.js → open-api-client-KA56PUDJ.js} +4 -3
  43. package/dist/{remote-execute-BRXK2NIJ.js → remote-execute-ZRDVUTOB.js} +5 -4
  44. package/dist/remote-execute-ZRDVUTOB.js.map +6 -0
  45. package/dist/run-executor-VXPFKJ3K.js +27 -0
  46. package/dist/run-executor-VXPFKJ3K.js.map +6 -0
  47. package/dist/{setup-git-DCMWZYIG.js → setup-git-EGQWSXZK.js} +5 -4
  48. package/package.json +1 -1
  49. package/dist/chunk-2GSKFEWW.js.map +0 -6
  50. package/dist/chunk-FMBLQ7J2.js.map +0 -6
  51. package/dist/chunk-LISUPAAK.js.map +0 -6
  52. package/dist/chunk-OWIJPBAX.js.map +0 -6
  53. package/dist/dev-command-NUN5HHRC.js.map +0 -6
  54. package/dist/git-credential-XTJ5JZ6V.js.map +0 -6
  55. package/dist/local-execute-5J53ZM5Z.js.map +0 -6
  56. package/dist/mock-dep-resolve-RRR74F7X.js.map +0 -6
  57. package/dist/remote-execute-BRXK2NIJ.js.map +0 -6
  58. /package/dist/{auth-GVBMBBDV.js.map → auth-7CZOBANL.js.map} +0 -0
  59. /package/dist/{chunk-NRLXYJKW.js.map → chunk-3DASQK6N.js.map} +0 -0
  60. /package/dist/{chunk-VEYAO5OE.js.map → chunk-CHLT3KZU.js.map} +0 -0
  61. /package/dist/{chunk-OIXNZ63K.js.map → chunk-DMA6RYFC.js.map} +0 -0
  62. /package/dist/{chunk-ZK52DY36.js.map → chunk-MU6NWE3A.js.map} +0 -0
  63. /package/dist/{chunk-SAK2XRKX.js.map → chunk-RETDMHIH.js.map} +0 -0
  64. /package/dist/{chunk-H4BN6EPW.js.map → chunk-WUIKH7FJ.js.map} +0 -0
  65. /package/dist/{local-package-graph-7TEPDAGV.js.map → local-package-graph-657CN4KN.js.map} +0 -0
  66. /package/dist/{local-package-run-OPC7VXO3.js.map → local-package-run-2FZDLAAR.js.map} +0 -0
  67. /package/dist/{mock-local-deps-EUL62AHI.js.map → mock-local-deps-6D73XVTZ.js.map} +0 -0
  68. /package/dist/{open-api-client-K2BEQBO3.js.map → open-api-client-KA56PUDJ.js.map} +0 -0
  69. /package/dist/{setup-git-DCMWZYIG.js.map → setup-git-EGQWSXZK.js.map} +0 -0
@@ -0,0 +1,806 @@
1
+ import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
2
+ import {
3
+ classifyFrameText,
4
+ decodeEnvelopePayload,
5
+ ecpContextKeyId,
6
+ ecpPingFrame,
7
+ ecpProtocolVersion,
8
+ ecpRunHeader,
9
+ ecpSignatureHeader,
10
+ ed25519JwkThumbprint,
11
+ formatEcpSignatureHeader,
12
+ importEd25519PrivateKey,
13
+ importEd25519PublicKey,
14
+ publishedKeyJwk,
15
+ signEcpHttpRequest,
16
+ signFrame,
17
+ verifyFrame,
18
+ verifyServerKeySet
19
+ } from "./chunk-AED3YBAI.js";
20
+ import {
21
+ apiTokenFileStorePath
22
+ } from "./chunk-S3KLKZ4C.js";
23
+ import {
24
+ secretServiceKeyringOptions
25
+ } from "./chunk-CMIH7XDH.js";
26
+ import {
27
+ keyringService
28
+ } from "./chunk-V7P2QDVU.js";
29
+ import "./chunk-UW6AWLXX.js";
30
+
31
+ // src/execution-context/commands.ts
32
+ import { readdirSync, readFileSync as readFileSync2 } from "node:fs";
33
+ import { cpus, platform, totalmem } from "node:os";
34
+
35
+ // src/execution-context/client.ts
36
+ var executionContextsPath = "/v1/execution-contexts";
37
+ var reloginMessage = "Your Kody login predates execution context permissions, so it cannot register a machine. Run `kody login` again, then retry `kody context register`.";
38
+ var ExecutionContextApiError = class extends Error {
39
+ constructor(message, status, code) {
40
+ super(message);
41
+ this.status = status;
42
+ this.code = code;
43
+ }
44
+ status;
45
+ code;
46
+ };
47
+ async function readError(response) {
48
+ const text = await response.text();
49
+ let code = null;
50
+ let message = text || `HTTP ${response.status}`;
51
+ try {
52
+ const body = JSON.parse(text);
53
+ if (typeof body.error === "string") {
54
+ code = body.error;
55
+ message = body.message ?? body.error;
56
+ } else if (body.error) {
57
+ code = body.error.code ?? null;
58
+ message = body.error.message ?? message;
59
+ }
60
+ } catch {
61
+ }
62
+ return new ExecutionContextApiError(message, response.status, code);
63
+ }
64
+ async function signedFetch(input) {
65
+ const body = input.body ?? "";
66
+ const signature = await signEcpHttpRequest({
67
+ ec: input.ecid,
68
+ method: input.method,
69
+ pathAndQuery: input.pathAndQuery,
70
+ body,
71
+ privateKey: input.privateKey
72
+ });
73
+ return (input.fetchFn ?? fetch)(new URL(input.pathAndQuery, input.apiUrl), {
74
+ method: input.method,
75
+ headers: {
76
+ ...input.headers,
77
+ [ecpSignatureHeader]: formatEcpSignatureHeader(signature),
78
+ ...input.runId ? { [ecpRunHeader]: input.runId } : {}
79
+ },
80
+ ...input.method === "GET" || input.method === "HEAD" ? {} : { body: typeof body === "string" ? body : Buffer.from(body) }
81
+ });
82
+ }
83
+ async function generateContextKey() {
84
+ const pair = await crypto.subtle.generateKey({ name: "Ed25519" }, true, [
85
+ "sign",
86
+ "verify"
87
+ ]);
88
+ const privateJwk = await crypto.subtle.exportKey(
89
+ "jwk",
90
+ pair.privateKey
91
+ );
92
+ const publicKeyJwk = {
93
+ kty: "OKP",
94
+ crv: "Ed25519",
95
+ x: String(privateJwk.x)
96
+ };
97
+ const privateKeyJwk = {
98
+ ...publicKeyJwk,
99
+ d: String(privateJwk.d)
100
+ };
101
+ return { publicKeyJwk, privateKeyJwk };
102
+ }
103
+ function importContextKey(privateJwk) {
104
+ return importEd25519PrivateKey(privateJwk);
105
+ }
106
+ async function registerContext(input) {
107
+ const fetchFn = input.fetchFn ?? fetch;
108
+ const query = input.org ? `?org=${encodeURIComponent(input.org)}` : "";
109
+ const auth = { authorization: `Bearer ${input.token}` };
110
+ const challengeResponse = await fetchFn(
111
+ new URL(
112
+ `${executionContextsPath}/registration-challenge${query}`,
113
+ input.apiUrl
114
+ ),
115
+ { method: "POST", headers: auth }
116
+ );
117
+ if (!challengeResponse.ok) throw await registrationError(challengeResponse);
118
+ const { challenge } = await challengeResponse.json();
119
+ const body = JSON.stringify({
120
+ name: input.name,
121
+ publicKeyJwk: input.publicKeyJwk,
122
+ challenge,
123
+ capabilities: input.capabilities,
124
+ client: input.client
125
+ });
126
+ const response = await signedFetch({
127
+ apiUrl: input.apiUrl,
128
+ ecid: input.thumbprint,
129
+ privateKey: input.privateKey,
130
+ method: "POST",
131
+ pathAndQuery: `${executionContextsPath}${query}`,
132
+ body,
133
+ headers: { ...auth, "content-type": "application/json" },
134
+ fetchFn
135
+ });
136
+ if (!response.ok) throw await registrationError(response);
137
+ return await response.json();
138
+ }
139
+ async function registrationError(response) {
140
+ const error = await readError(response);
141
+ if (error.code === "credential_scope" || error.message.includes("predates execution context permissions")) {
142
+ return new ExecutionContextApiError(
143
+ reloginMessage,
144
+ error.status,
145
+ error.code
146
+ );
147
+ }
148
+ return error;
149
+ }
150
+ async function requestConnectTicket(input) {
151
+ const response = await signedFetch({
152
+ ...input,
153
+ method: "POST",
154
+ pathAndQuery: `${executionContextsPath}/connect-ticket`,
155
+ body: "{}",
156
+ headers: { "content-type": "application/json" }
157
+ });
158
+ if (!response.ok) throw await readError(response);
159
+ return await response.json();
160
+ }
161
+ async function fetchServerKeys(input) {
162
+ const response = await (input.fetchFn ?? fetch)(
163
+ new URL(`${executionContextsPath}/server-keys`, input.apiUrl)
164
+ );
165
+ if (!response.ok) throw await readError(response);
166
+ const set = await verifyServerKeySet({ document: await response.json() });
167
+ if (!set) {
168
+ throw new Error(
169
+ "Kody server keys failed root signature verification. Refusing to connect."
170
+ );
171
+ }
172
+ const now = input.now ?? Date.now();
173
+ return set.keys.filter((key) => key.notAfter > now);
174
+ }
175
+ async function apiJson(input) {
176
+ const url = new URL(input.path, input.apiUrl);
177
+ if (input.org) url.searchParams.set("org", input.org);
178
+ const response = await (input.fetchFn ?? fetch)(url, {
179
+ method: input.method,
180
+ headers: {
181
+ authorization: `Bearer ${input.token}`,
182
+ ...input.body === void 0 ? {} : { "content-type": "application/json" }
183
+ },
184
+ ...input.body === void 0 ? {} : { body: JSON.stringify(input.body) }
185
+ });
186
+ if (!response.ok) throw await readError(response);
187
+ return await response.json();
188
+ }
189
+
190
+ // src/execution-context/daemon.ts
191
+ function runScopedFetch(input) {
192
+ const apiOrigin = new URL(input.apiUrl).origin;
193
+ const base = input.fetchFn ?? fetch;
194
+ return async (resource, init) => {
195
+ const request = new Request(resource, init);
196
+ const url = new URL(request.url);
197
+ if (url.origin !== apiOrigin) return base(request);
198
+ const body = new Uint8Array(await request.arrayBuffer());
199
+ const headers = {};
200
+ request.headers.forEach((value, name) => {
201
+ if (name !== "authorization") headers[name] = value;
202
+ });
203
+ return signedFetch({
204
+ apiUrl: input.apiUrl,
205
+ ecid: input.ecid,
206
+ privateKey: input.privateKey,
207
+ method: request.method,
208
+ pathAndQuery: `${url.pathname}${url.search}`,
209
+ body,
210
+ runId: input.runId,
211
+ headers,
212
+ fetchFn: base
213
+ });
214
+ };
215
+ }
216
+ async function runDaemonConnection(input) {
217
+ const Socket = input.WebSocketImpl ?? globalThis.WebSocket;
218
+ if (!Socket) {
219
+ throw new Error(
220
+ "This Node.js has no global WebSocket. Use Node 22 or newer."
221
+ );
222
+ }
223
+ const { ticket } = await requestConnectTicket(input);
224
+ const url = new URL(`${executionContextsPath}/connect`, input.apiUrl);
225
+ url.protocol = url.protocol === "http:" ? "ws:" : "wss:";
226
+ url.searchParams.set("ticket", ticket);
227
+ const ws = new Socket(url.toString());
228
+ const serverKeys = new Map(input.serverKeys.map((key) => [key.kid, key]));
229
+ let sid = "";
230
+ let inSeq = 0;
231
+ let outSeq = 0;
232
+ const runs = /* @__PURE__ */ new Map();
233
+ const send = async (t, body, re) => {
234
+ outSeq += 1;
235
+ const payload = {
236
+ v: 1,
237
+ t,
238
+ ec: input.ecid,
239
+ sid,
240
+ seq: outSeq,
241
+ ts: Date.now(),
242
+ ...re === void 0 ? {} : { re },
243
+ body
244
+ };
245
+ ws.send(
246
+ await signFrame({
247
+ privateKey: input.privateKey,
248
+ kid: ecpContextKeyId,
249
+ payload
250
+ })
251
+ );
252
+ };
253
+ const status = () => ({
254
+ running: runs.size,
255
+ maxConcurrent: input.maxConcurrent,
256
+ acceptingRuns: runs.size < input.maxConcurrent
257
+ });
258
+ const startRun = async (run) => {
259
+ const startedAt = Date.now();
260
+ const timer = setTimeout(
261
+ () => run.abort.abort(),
262
+ Math.max(0, run.request.deadline - Date.now())
263
+ );
264
+ let result;
265
+ try {
266
+ result = await input.execute({
267
+ request: run.request,
268
+ signal: run.abort.signal,
269
+ fetchFn: runScopedFetch({
270
+ apiUrl: input.apiUrl,
271
+ ecid: input.ecid,
272
+ runId: run.request.runId,
273
+ privateKey: input.privateKey,
274
+ ...input.fetchFn ? { fetchFn: input.fetchFn } : {}
275
+ })
276
+ });
277
+ } catch (error) {
278
+ result = {
279
+ isError: true,
280
+ content: [
281
+ {
282
+ type: "text",
283
+ text: error instanceof Error ? error.message : String(error)
284
+ }
285
+ ]
286
+ };
287
+ } finally {
288
+ clearTimeout(timer);
289
+ }
290
+ const endedAt = Date.now();
291
+ const resultBytes = new TextEncoder().encode(
292
+ JSON.stringify(result)
293
+ ).byteLength;
294
+ runs.delete(run.request.runId);
295
+ await send("run.result", {
296
+ runId: run.request.runId,
297
+ ...result,
298
+ startedAt,
299
+ endedAt,
300
+ resultBytes
301
+ });
302
+ await send("status", status());
303
+ };
304
+ const handle = async (frame) => {
305
+ const body = frame.body;
306
+ const runId = typeof body.runId === "string" ? body.runId : "";
307
+ switch (frame.t) {
308
+ case "welcome":
309
+ await send("hello", {
310
+ protocol: { min: ecpProtocolVersion, max: ecpProtocolVersion },
311
+ client: input.client,
312
+ capabilities: input.capabilities,
313
+ grantVersion: body.grantVersion,
314
+ status: status(),
315
+ pendingResults: []
316
+ });
317
+ input.log("Connected. Waiting for runs.");
318
+ return;
319
+ case "run.request": {
320
+ const request = body;
321
+ if (runs.size >= input.maxConcurrent) {
322
+ await send("run.reject", { runId, code: "busy" }, frame.seq);
323
+ return;
324
+ }
325
+ if (Date.now() > request.acceptBy || request.apiUrl !== input.apiUrl) {
326
+ await send(
327
+ "run.reject",
328
+ {
329
+ runId,
330
+ code: request.apiUrl !== input.apiUrl ? "grant_mismatch" : "deadline_passed"
331
+ },
332
+ frame.seq
333
+ );
334
+ return;
335
+ }
336
+ runs.set(runId, {
337
+ request,
338
+ abort: new AbortController(),
339
+ committed: false
340
+ });
341
+ await send("run.accept", { runId }, frame.seq);
342
+ return;
343
+ }
344
+ case "run.commit": {
345
+ const run = runs.get(runId);
346
+ if (!run || run.committed) return;
347
+ run.committed = true;
348
+ input.log(
349
+ `Running ${runId} for ${run.request.requestedBy.handle ?? run.request.requestedBy.userId}.`
350
+ );
351
+ void startRun(run);
352
+ return;
353
+ }
354
+ case "run.abort":
355
+ case "run.cancel": {
356
+ const run = runs.get(runId);
357
+ run?.abort.abort();
358
+ if (!run?.committed) runs.delete(runId);
359
+ if (frame.t === "run.cancel")
360
+ await send("run.cancel.ack", { runId }, frame.seq);
361
+ return;
362
+ }
363
+ case "grant.update":
364
+ await send("grant.ack", { grantVersion: body.grantVersion }, frame.seq);
365
+ return;
366
+ case "revoke":
367
+ input.log(
368
+ `This context was revoked (${String(body.reason ?? "revoked")}).`
369
+ );
370
+ ws.close(1e3, "revoked");
371
+ return;
372
+ case "error":
373
+ input.log(`Kody: ${String(body.code)} ${String(body.message ?? "")}`);
374
+ return;
375
+ default:
376
+ return;
377
+ }
378
+ };
379
+ return await new Promise((resolve) => {
380
+ const ping = setInterval(() => {
381
+ if (ws.readyState === ws.OPEN) ws.send(ecpPingFrame);
382
+ }, 3e4);
383
+ input.signal?.addEventListener("abort", () => ws.close(1e3, "shutdown"));
384
+ ws.addEventListener("message", (event) => {
385
+ const text = typeof event.data === "string" ? event.data : "";
386
+ void (async () => {
387
+ const classified = classifyFrameText(text);
388
+ if (classified.kind !== "frame") return;
389
+ const key = serverKeys.get(classified.envelope.k);
390
+ if (!key) {
391
+ ws.close(1008, "unknown server key");
392
+ return;
393
+ }
394
+ if (!sid) {
395
+ sid = peekSid(classified.envelope.p);
396
+ }
397
+ const verified = await verifyFrame({
398
+ envelope: classified.envelope,
399
+ publicKey: await importEd25519PublicKey(publishedKeyJwk(key)),
400
+ expect: { ec: input.ecid, sid, lastSeq: inSeq },
401
+ now: Date.now()
402
+ });
403
+ if (!verified.ok) {
404
+ ws.close(1008, verified.code);
405
+ return;
406
+ }
407
+ inSeq = verified.frame.seq;
408
+ await handle(verified.frame);
409
+ })().catch((error) => input.log(`Frame error: ${String(error)}`));
410
+ });
411
+ ws.addEventListener("close", (event) => {
412
+ clearInterval(ping);
413
+ for (const run of runs.values()) run.abort.abort();
414
+ resolve({ code: event.code, reason: event.reason });
415
+ });
416
+ });
417
+ }
418
+ function peekSid(p) {
419
+ try {
420
+ const decoded = decodeEnvelopePayload(p);
421
+ return decoded?.t === "welcome" && typeof decoded.body?.sid === "string" ? decoded.body.sid : "";
422
+ } catch {
423
+ return "";
424
+ }
425
+ }
426
+ async function runDaemon(input) {
427
+ let attempt = 0;
428
+ while (!input.signal?.aborted) {
429
+ const startedAt = Date.now();
430
+ try {
431
+ const closed = await runDaemonConnection(input);
432
+ if (closed.reason === "revoked") return;
433
+ input.log(`Disconnected (${closed.code}). Reconnecting.`);
434
+ } catch (error) {
435
+ input.log(
436
+ `Connect failed: ${error instanceof Error ? error.message : String(error)}`
437
+ );
438
+ }
439
+ if (Date.now() - startedAt > 6e4) attempt = 0;
440
+ attempt += 1;
441
+ const delay = Math.min(6e4, 1e3 * 2 ** Math.min(attempt, 6));
442
+ await new Promise(
443
+ (resolve) => setTimeout(resolve, delay * (0.5 + Math.random() / 2))
444
+ );
445
+ }
446
+ }
447
+
448
+ // src/execution-context/keystore.ts
449
+ import {
450
+ createCipheriv,
451
+ createDecipheriv,
452
+ randomBytes,
453
+ scryptSync
454
+ } from "node:crypto";
455
+ import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
456
+ import { dirname, join } from "node:path";
457
+ import { Entry } from "@napi-rs/keyring";
458
+ var contextKeyPassphraseEnvVar = "KODY_CONTEXT_KEY_PASSPHRASE";
459
+ function contextsDir(apiUrl) {
460
+ return join(dirname(apiTokenFileStorePath(apiUrl)), "contexts");
461
+ }
462
+ function contextDir(apiUrl, ecid) {
463
+ if (!/^ec_[A-Za-z0-9_-]+$/.test(ecid)) {
464
+ throw new Error(`Not an execution context id: ${ecid}`);
465
+ }
466
+ return join(contextsDir(apiUrl), ecid);
467
+ }
468
+ function resolveWrappingKey(input) {
469
+ const env = input.env ?? process.env;
470
+ const passphrase = env[contextKeyPassphraseEnvVar];
471
+ if (passphrase) {
472
+ return {
473
+ kind: "passphrase",
474
+ key: (salt) => scryptSync(passphrase, salt ?? Buffer.alloc(16), 32, {
475
+ N: 1 << 15,
476
+ r: 8,
477
+ p: 1,
478
+ maxmem: 64 * 1024 * 1024
479
+ })
480
+ };
481
+ }
482
+ const account = `execution-context:${input.ecid}`;
483
+ let entry;
484
+ try {
485
+ entry = input.keyringFactory ? input.keyringFactory(account) : new Entry(keyringService, account, secretServiceKeyringOptions);
486
+ } catch {
487
+ throw noWrappingKeyError();
488
+ }
489
+ return {
490
+ kind: "keyring",
491
+ key: () => {
492
+ let stored = null;
493
+ try {
494
+ stored = entry.getPassword();
495
+ } catch {
496
+ stored = null;
497
+ }
498
+ if (stored) return Buffer.from(stored, "base64url");
499
+ if (!input.create) {
500
+ throw new Error(
501
+ "The key for this execution context is missing from the OS keyring. Re-register the machine with `kody context register`."
502
+ );
503
+ }
504
+ const fresh = randomBytes(32);
505
+ try {
506
+ entry.setPassword(fresh.toString("base64url"));
507
+ } catch {
508
+ throw noWrappingKeyError();
509
+ }
510
+ return fresh;
511
+ }
512
+ };
513
+ }
514
+ function noWrappingKeyError() {
515
+ return new Error(
516
+ `No OS keyring is available to protect this machine's execution context key. Set ${contextKeyPassphraseEnvVar} to a strong passphrase and retry.`
517
+ );
518
+ }
519
+ function encryptPrivateKey(privateJwk, wrapping) {
520
+ const salt = wrapping.kind === "passphrase" ? randomBytes(16) : null;
521
+ const key = wrapping.key(salt);
522
+ const iv = randomBytes(12);
523
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
524
+ const data = Buffer.concat([
525
+ cipher.update(JSON.stringify(privateJwk), "utf8"),
526
+ cipher.final()
527
+ ]);
528
+ return {
529
+ version: 1,
530
+ wrap: wrapping.kind,
531
+ ...salt ? { salt: salt.toString("base64url") } : {},
532
+ iv: iv.toString("base64url"),
533
+ tag: cipher.getAuthTag().toString("base64url"),
534
+ data: data.toString("base64url")
535
+ };
536
+ }
537
+ function decryptPrivateKey(file, wrapping) {
538
+ if (file.wrap !== wrapping.kind) {
539
+ throw new Error(
540
+ file.wrap === "passphrase" ? `This context key is protected by a passphrase. Set ${contextKeyPassphraseEnvVar}.` : `This context key is protected by the OS keyring. Unset ${contextKeyPassphraseEnvVar}.`
541
+ );
542
+ }
543
+ const key = wrapping.key(
544
+ file.salt ? Buffer.from(file.salt, "base64url") : null
545
+ );
546
+ const decipher = createDecipheriv(
547
+ "aes-256-gcm",
548
+ key,
549
+ Buffer.from(file.iv, "base64url")
550
+ );
551
+ decipher.setAuthTag(Buffer.from(file.tag, "base64url"));
552
+ try {
553
+ const plain = Buffer.concat([
554
+ decipher.update(Buffer.from(file.data, "base64url")),
555
+ decipher.final()
556
+ ]);
557
+ return JSON.parse(plain.toString("utf8"));
558
+ } catch {
559
+ throw new Error(
560
+ "Could not decrypt the execution context key. The passphrase or keyring entry does not match."
561
+ );
562
+ }
563
+ }
564
+ function saveContext(input) {
565
+ const dir = contextDir(input.context.apiUrl, input.context.ecid);
566
+ mkdirSync(dir, { recursive: true, mode: 448 });
567
+ const keyPath = join(dir, "key.enc");
568
+ writeFileSync(
569
+ keyPath,
570
+ JSON.stringify(encryptPrivateKey(input.privateJwk, input.wrapping)),
571
+ { mode: 384 }
572
+ );
573
+ chmodSync(keyPath, 384);
574
+ writeFileSync(
575
+ join(dir, "context.json"),
576
+ `${JSON.stringify(input.context, null, 2)}
577
+ `,
578
+ { mode: 384 }
579
+ );
580
+ }
581
+ function loadContext(input) {
582
+ const dir = contextDir(input.apiUrl, input.ecid);
583
+ const context = JSON.parse(
584
+ readFileSync(join(dir, "context.json"), "utf8")
585
+ );
586
+ const file = JSON.parse(readFileSync(join(dir, "key.enc"), "utf8"));
587
+ return { context, privateJwk: decryptPrivateKey(file, input.wrapping) };
588
+ }
589
+
590
+ // src/execution-context/commands.ts
591
+ function localCapabilities() {
592
+ return {
593
+ runtime: "workerd",
594
+ node: process.versions.node,
595
+ os: platform(),
596
+ cpuCount: cpus().length,
597
+ memoryMb: Math.round(totalmem() / (1024 * 1024)),
598
+ maxConcurrent: 1,
599
+ features: []
600
+ };
601
+ }
602
+ function storedContextIds(apiUrl) {
603
+ try {
604
+ return readdirSync(contextsDir(apiUrl)).filter(
605
+ (entry) => entry.startsWith("ec_")
606
+ );
607
+ } catch {
608
+ return [];
609
+ }
610
+ }
611
+ function out(input, value, text) {
612
+ input.write(
613
+ input.values.json ? `${JSON.stringify(value, null, 2)}
614
+ ` : `${text}
615
+ `
616
+ );
617
+ }
618
+ async function runExecutionContextCommand(input) {
619
+ const [sub, ...rest] = input.positionals;
620
+ const base = {
621
+ apiUrl: input.apiUrl,
622
+ ...input.org ? { org: input.org } : {},
623
+ ...input.fetchFn ? { fetchFn: input.fetchFn } : {}
624
+ };
625
+ if (input.command === "context" && sub === "register") {
626
+ const name = input.values.name ?? rest[0];
627
+ if (!name) throw new Error("Pass --name <name> for this machine.");
628
+ const { publicKeyJwk, privateKeyJwk } = await generateContextKey();
629
+ const privateKey = await importContextKey(privateKeyJwk);
630
+ const thumbprint = await ed25519JwkThumbprint(publicKeyJwk);
631
+ const registered = await registerContext({
632
+ ...base,
633
+ token: await input.token(),
634
+ name,
635
+ publicKeyJwk,
636
+ privateKey,
637
+ thumbprint,
638
+ capabilities: localCapabilities(),
639
+ client: { name: "kody-cli", version: input.clientVersion }
640
+ });
641
+ saveContext({
642
+ context: {
643
+ version: 1,
644
+ ecid: registered.ecid,
645
+ name: registered.name,
646
+ apiUrl: input.apiUrl,
647
+ orgSlug: registered.owner.orgSlug,
648
+ publicKeyJwk
649
+ },
650
+ privateJwk: privateKeyJwk,
651
+ wrapping: resolveWrappingKey({ ecid: registered.ecid, create: true })
652
+ });
653
+ const steps = (registered.nextSteps ?? []).map(
654
+ (step) => ` ${step.text ?? ""}${step.cli ? `
655
+ ${step.cli}` : ""}`
656
+ ).join("\n");
657
+ out(
658
+ input,
659
+ registered,
660
+ [
661
+ registered.notice ?? `Registered ${registered.name} (${registered.ecid}).`,
662
+ steps ? `Next steps:
663
+ ${steps}` : "",
664
+ "Start it with: kody daemon"
665
+ ].filter(Boolean).join("\n")
666
+ );
667
+ return 0;
668
+ }
669
+ if (input.command === "context" && (sub === "list" || sub === void 0)) {
670
+ const body = await apiJson({
671
+ ...base,
672
+ token: await input.token(),
673
+ method: "GET",
674
+ path: "/v1/execution-contexts"
675
+ });
676
+ const contexts = body.contexts ?? [];
677
+ out(
678
+ input,
679
+ body,
680
+ contexts.length === 0 ? "No execution contexts yet. Register this machine with `kody context register --name <name>`." : contexts.map(
681
+ (c) => `${c.name} (${c.id}) ${c.status}${c.usable ? "" : `, not usable: ${c.unusableReason}`}`
682
+ ).join("\n")
683
+ );
684
+ return 0;
685
+ }
686
+ if (input.command === "chain") {
687
+ const token = await input.token();
688
+ const read = await apiJson({
689
+ ...base,
690
+ token,
691
+ method: "GET",
692
+ path: "/v1/execution-chain"
693
+ });
694
+ const current = chainOf(read);
695
+ if (sub === "show" || sub === void 0) {
696
+ out(input, read, describeChain(current));
697
+ return 0;
698
+ }
699
+ if (sub !== "add" && sub !== "remove") {
700
+ throw new Error("Usage: kody chain show | add <name> | remove <name>");
701
+ }
702
+ const ref = rest[0];
703
+ if (!ref) throw new Error(`Pass the context name: kody chain ${sub} <name>`);
704
+ const id = await resolveContextId({
705
+ ...base,
706
+ token,
707
+ ref
708
+ });
709
+ const entries = (current.entries ?? []).map(
710
+ (entry) => entry.kind === "cloud" ? { kind: "cloud" } : { kind: "context", id: entry.id }
711
+ ).filter((entry) => !(entry.kind === "context" && entry.id === id));
712
+ if (sub === "add") {
713
+ const cloudAt = entries.findIndex((entry) => entry.kind === "cloud");
714
+ entries.splice(cloudAt === -1 ? entries.length : cloudAt, 0, {
715
+ kind: "context",
716
+ id
717
+ });
718
+ }
719
+ const updated = await apiJson({
720
+ ...base,
721
+ token,
722
+ method: "PUT",
723
+ path: "/v1/execution-chain",
724
+ body: {
725
+ entries,
726
+ strict: !entries.some((entry) => entry.kind === "cloud"),
727
+ ifMatch: current.updatedAt ?? null
728
+ }
729
+ });
730
+ out(input, updated, describeChain(chainOf(updated)));
731
+ return 0;
732
+ }
733
+ if (input.command === "daemon") {
734
+ const ids = input.values.context ? [input.values.context] : storedContextIds(input.apiUrl);
735
+ const ecid = ids[0];
736
+ if (!ecid) {
737
+ throw new Error(
738
+ "This machine has no registered execution context. Run `kody context register --name <name>` first."
739
+ );
740
+ }
741
+ const { context, privateJwk } = loadContext({
742
+ apiUrl: input.apiUrl,
743
+ ecid,
744
+ wrapping: resolveWrappingKey({ ecid, create: false })
745
+ });
746
+ const privateKey = await importContextKey(privateJwk);
747
+ const serverKeys = await fetchServerKeys({ apiUrl: input.apiUrl });
748
+ const { runContextRun } = await import("./run-executor-VXPFKJ3K.js");
749
+ const abort = new AbortController();
750
+ process.once("SIGINT", () => abort.abort());
751
+ process.once("SIGTERM", () => abort.abort());
752
+ input.write(
753
+ `Starting ${context.name} (${context.ecid}) against ${input.apiUrl}.
754
+ `
755
+ );
756
+ await runDaemon({
757
+ apiUrl: input.apiUrl,
758
+ ecid: context.ecid,
759
+ privateKey,
760
+ serverKeys,
761
+ maxConcurrent: 1,
762
+ client: { name: "kody-cli", version: input.clientVersion },
763
+ capabilities: localCapabilities(),
764
+ execute: (run) => runContextRun({ ...run, apiUrl: input.apiUrl }),
765
+ log: (line) => input.write(`${line}
766
+ `),
767
+ signal: abort.signal
768
+ });
769
+ return 0;
770
+ }
771
+ throw new Error(
772
+ "Usage: kody context register --name <name> [--org <slug>] | kody context list | kody chain show | add <name> | remove <name> | kody daemon [--context <ec id>]"
773
+ );
774
+ }
775
+ async function resolveContextId(input) {
776
+ if (input.ref.startsWith("ec_")) return input.ref;
777
+ const body = await apiJson({
778
+ ...input,
779
+ method: "GET",
780
+ path: "/v1/execution-contexts"
781
+ });
782
+ const match = (body.contexts ?? []).find((c) => c.name === input.ref);
783
+ if (!match)
784
+ throw new Error(`No execution context named ${input.ref} in this org.`);
785
+ return match.id;
786
+ }
787
+ function chainOf(body) {
788
+ return body.chain ?? {};
789
+ }
790
+ function describeChain(chain) {
791
+ const entries = chain.entries ?? [];
792
+ const lines = entries.map(
793
+ (entry, index) => entry.kind === "cloud" ? `${index + 1}. Kody cloud (always last)` : `${index + 1}. ${entry.id}${entry.usable === false ? ` (skipped: ${entry.unusableReason})` : ""}`
794
+ );
795
+ if (chain.strict) lines.push("Strict: Kody cloud is not in this chain.");
796
+ return lines.join("\n") || "Empty chain.";
797
+ }
798
+ function readCodeFile(path) {
799
+ return readFileSync2(path, "utf8");
800
+ }
801
+ export {
802
+ localCapabilities,
803
+ readCodeFile,
804
+ runExecutionContextCommand
805
+ };
806
+ //# sourceMappingURL=commands-RPUZAOAB.js.map