@geminixiang/mikan 1.0.0-beta.21 → 1.0.0-beta.23

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 (199) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +12 -8
  3. package/dist/adapters/discord/bot.d.ts.map +1 -1
  4. package/dist/adapters/discord/bot.js +13 -14
  5. package/dist/adapters/discord/bot.js.map +1 -1
  6. package/dist/adapters/intake.d.ts.map +1 -1
  7. package/dist/adapters/intake.js +5 -0
  8. package/dist/adapters/intake.js.map +1 -1
  9. package/dist/adapters/slack/bot.d.ts.map +1 -1
  10. package/dist/adapters/slack/bot.js +13 -2
  11. package/dist/adapters/slack/bot.js.map +1 -1
  12. package/dist/adapters/slack/types.d.ts +2 -0
  13. package/dist/adapters/slack/types.d.ts.map +1 -1
  14. package/dist/adapters/slack/types.js.map +1 -1
  15. package/dist/agent.d.ts.map +1 -1
  16. package/dist/agent.js +34 -24
  17. package/dist/agent.js.map +1 -1
  18. package/dist/cli/boot.d.ts +1 -1
  19. package/dist/cli/boot.d.ts.map +1 -1
  20. package/dist/cli/boot.js +7 -15
  21. package/dist/cli/boot.js.map +1 -1
  22. package/dist/commands/login.js +2 -2
  23. package/dist/commands/login.js.map +1 -1
  24. package/dist/commands/sandbox.d.ts.map +1 -1
  25. package/dist/commands/sandbox.js +4 -7
  26. package/dist/commands/sandbox.js.map +1 -1
  27. package/dist/commands/text.js +1 -1
  28. package/dist/commands/text.js.map +1 -1
  29. package/dist/config.d.ts.map +1 -1
  30. package/dist/config.js +12 -39
  31. package/dist/config.js.map +1 -1
  32. package/dist/execution-resolver.d.ts +6 -8
  33. package/dist/execution-resolver.d.ts.map +1 -1
  34. package/dist/execution-resolver.js +73 -107
  35. package/dist/execution-resolver.js.map +1 -1
  36. package/dist/harness/index.d.ts +1 -1
  37. package/dist/harness/index.d.ts.map +1 -1
  38. package/dist/harness/index.js.map +1 -1
  39. package/dist/harness/runner.d.ts +9 -6
  40. package/dist/harness/runner.d.ts.map +1 -1
  41. package/dist/harness/runner.js +42 -20
  42. package/dist/harness/runner.js.map +1 -1
  43. package/dist/harness/subagent-runner.d.ts +14 -13
  44. package/dist/harness/subagent-runner.d.ts.map +1 -1
  45. package/dist/harness/subagent-runner.js +116 -24
  46. package/dist/harness/subagent-runner.js.map +1 -1
  47. package/dist/harness/types.d.ts +14 -0
  48. package/dist/harness/types.d.ts.map +1 -1
  49. package/dist/harness/types.js.map +1 -1
  50. package/dist/harness/usage.d.ts +6 -0
  51. package/dist/harness/usage.d.ts.map +1 -0
  52. package/dist/harness/usage.js +32 -0
  53. package/dist/harness/usage.js.map +1 -0
  54. package/dist/index.d.ts +3 -2
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +1 -1
  57. package/dist/index.js.map +1 -1
  58. package/dist/main.js +19 -87
  59. package/dist/main.js.map +1 -1
  60. package/dist/runtime/conversation-runtime.d.ts.map +1 -1
  61. package/dist/runtime/conversation-runtime.js +23 -79
  62. package/dist/runtime/conversation-runtime.js.map +1 -1
  63. package/dist/runtime/session-lifecycle.d.ts +19 -0
  64. package/dist/runtime/session-lifecycle.d.ts.map +1 -0
  65. package/dist/runtime/session-lifecycle.js +76 -0
  66. package/dist/runtime/session-lifecycle.js.map +1 -0
  67. package/dist/runtime/types.d.ts +5 -0
  68. package/dist/runtime/types.d.ts.map +1 -1
  69. package/dist/runtime/types.js.map +1 -1
  70. package/dist/sandbox/agent-sandbox.d.ts +18 -0
  71. package/dist/sandbox/agent-sandbox.d.ts.map +1 -0
  72. package/dist/sandbox/agent-sandbox.js +234 -0
  73. package/dist/sandbox/agent-sandbox.js.map +1 -0
  74. package/dist/sandbox/cloudflare.d.ts.map +1 -1
  75. package/dist/sandbox/cloudflare.js +1 -0
  76. package/dist/sandbox/cloudflare.js.map +1 -1
  77. package/dist/sandbox/container.d.ts.map +1 -1
  78. package/dist/sandbox/container.js +1 -0
  79. package/dist/sandbox/container.js.map +1 -1
  80. package/dist/sandbox/host.d.ts.map +1 -1
  81. package/dist/sandbox/host.js +1 -0
  82. package/dist/sandbox/host.js.map +1 -1
  83. package/dist/sandbox/identity.d.ts +11 -16
  84. package/dist/sandbox/identity.d.ts.map +1 -1
  85. package/dist/sandbox/identity.js +32 -26
  86. package/dist/sandbox/identity.js.map +1 -1
  87. package/dist/sandbox/image.d.ts.map +1 -1
  88. package/dist/sandbox/image.js +1 -0
  89. package/dist/sandbox/image.js.map +1 -1
  90. package/dist/sandbox/index.d.ts +7 -7
  91. package/dist/sandbox/index.d.ts.map +1 -1
  92. package/dist/sandbox/index.js +13 -9
  93. package/dist/sandbox/index.js.map +1 -1
  94. package/dist/sandbox/types.d.ts +17 -16
  95. package/dist/sandbox/types.d.ts.map +1 -1
  96. package/dist/sandbox/types.js.map +1 -1
  97. package/dist/sessions/session-key.d.ts +9 -0
  98. package/dist/sessions/session-key.d.ts.map +1 -1
  99. package/dist/sessions/session-key.js +50 -2
  100. package/dist/sessions/session-key.js.map +1 -1
  101. package/dist/sessions/store.d.ts.map +1 -1
  102. package/dist/sessions/store.js +36 -8
  103. package/dist/sessions/store.js.map +1 -1
  104. package/dist/tools/sandbox.d.ts.map +1 -1
  105. package/dist/tools/sandbox.js +4 -5
  106. package/dist/tools/sandbox.js.map +1 -1
  107. package/dist/tools/subagent-slots.d.ts +3 -3
  108. package/dist/tools/subagent-slots.d.ts.map +1 -1
  109. package/dist/tools/subagent-slots.js +25 -10
  110. package/dist/tools/subagent-slots.js.map +1 -1
  111. package/dist/tools/subagent.d.ts +13 -2
  112. package/dist/tools/subagent.d.ts.map +1 -1
  113. package/dist/tools/subagent.js +16 -16
  114. package/dist/tools/subagent.js.map +1 -1
  115. package/dist/types.d.ts +14 -63
  116. package/dist/types.d.ts.map +1 -1
  117. package/dist/types.js.map +1 -1
  118. package/dist/vault/injection.d.ts +4 -14
  119. package/dist/vault/injection.d.ts.map +1 -1
  120. package/dist/vault/injection.js +5 -2
  121. package/dist/vault/injection.js.map +1 -1
  122. package/dist/vault/policy.d.ts +3 -3
  123. package/dist/vault/policy.d.ts.map +1 -1
  124. package/dist/vault/policy.js +6 -4
  125. package/dist/vault/policy.js.map +1 -1
  126. package/dist/vault/types.d.ts +4 -0
  127. package/dist/vault/types.d.ts.map +1 -1
  128. package/dist/vault/types.js.map +1 -1
  129. package/dist/web/admin/portal.js +2 -2
  130. package/dist/web/admin/portal.js.map +1 -1
  131. package/dist/workspace-projection/index.d.ts +5 -0
  132. package/dist/workspace-projection/index.d.ts.map +1 -0
  133. package/dist/workspace-projection/index.js +71 -0
  134. package/dist/workspace-projection/index.js.map +1 -0
  135. package/dist/workspace-projection/types.d.ts +7 -0
  136. package/dist/workspace-projection/types.d.ts.map +1 -0
  137. package/dist/workspace-projection/types.js +2 -0
  138. package/dist/workspace-projection/types.js.map +1 -0
  139. package/package.json +3 -3
  140. package/dist/sandbox/firecracker.d.ts +0 -18
  141. package/dist/sandbox/firecracker.d.ts.map +0 -1
  142. package/dist/sandbox/firecracker.js +0 -221
  143. package/dist/sandbox/firecracker.js.map +0 -1
  144. package/dist/sandbox/gondolin-bootstrap.d.ts +0 -26
  145. package/dist/sandbox/gondolin-bootstrap.d.ts.map +0 -1
  146. package/dist/sandbox/gondolin-bootstrap.js +0 -27
  147. package/dist/sandbox/gondolin-bootstrap.js.map +0 -1
  148. package/dist/sandbox/gondolin-contract.d.ts +0 -125
  149. package/dist/sandbox/gondolin-contract.d.ts.map +0 -1
  150. package/dist/sandbox/gondolin-contract.js +0 -155
  151. package/dist/sandbox/gondolin-contract.js.map +0 -1
  152. package/dist/sandbox/gondolin-fleet.d.ts +0 -104
  153. package/dist/sandbox/gondolin-fleet.d.ts.map +0 -1
  154. package/dist/sandbox/gondolin-fleet.js +0 -330
  155. package/dist/sandbox/gondolin-fleet.js.map +0 -1
  156. package/dist/sandbox/gondolin-gateway.d.ts +0 -86
  157. package/dist/sandbox/gondolin-gateway.d.ts.map +0 -1
  158. package/dist/sandbox/gondolin-gateway.js +0 -407
  159. package/dist/sandbox/gondolin-gateway.js.map +0 -1
  160. package/dist/sandbox/gondolin-inventory.d.ts +0 -75
  161. package/dist/sandbox/gondolin-inventory.d.ts.map +0 -1
  162. package/dist/sandbox/gondolin-inventory.js +0 -257
  163. package/dist/sandbox/gondolin-inventory.js.map +0 -1
  164. package/dist/sandbox/gondolin-join.d.ts +0 -62
  165. package/dist/sandbox/gondolin-join.d.ts.map +0 -1
  166. package/dist/sandbox/gondolin-join.js +0 -261
  167. package/dist/sandbox/gondolin-join.js.map +0 -1
  168. package/dist/sandbox/gondolin-nfs-advisory.d.ts +0 -15
  169. package/dist/sandbox/gondolin-nfs-advisory.d.ts.map +0 -1
  170. package/dist/sandbox/gondolin-nfs-advisory.js +0 -60
  171. package/dist/sandbox/gondolin-nfs-advisory.js.map +0 -1
  172. package/dist/sandbox/gondolin-placement.d.ts +0 -30
  173. package/dist/sandbox/gondolin-placement.d.ts.map +0 -1
  174. package/dist/sandbox/gondolin-placement.js +0 -77
  175. package/dist/sandbox/gondolin-placement.js.map +0 -1
  176. package/dist/sandbox/gondolin-recovery.d.ts +0 -33
  177. package/dist/sandbox/gondolin-recovery.d.ts.map +0 -1
  178. package/dist/sandbox/gondolin-recovery.js +0 -39
  179. package/dist/sandbox/gondolin-recovery.js.map +0 -1
  180. package/dist/sandbox/gondolin-remote.d.ts +0 -70
  181. package/dist/sandbox/gondolin-remote.d.ts.map +0 -1
  182. package/dist/sandbox/gondolin-remote.js +0 -325
  183. package/dist/sandbox/gondolin-remote.js.map +0 -1
  184. package/dist/sandbox/gondolin-worker-client.d.ts +0 -135
  185. package/dist/sandbox/gondolin-worker-client.d.ts.map +0 -1
  186. package/dist/sandbox/gondolin-worker-client.js +0 -323
  187. package/dist/sandbox/gondolin-worker-client.js.map +0 -1
  188. package/dist/sandbox/gondolin-worker-main.d.ts +0 -2
  189. package/dist/sandbox/gondolin-worker-main.d.ts.map +0 -1
  190. package/dist/sandbox/gondolin-worker-main.js +0 -22
  191. package/dist/sandbox/gondolin-worker-main.js.map +0 -1
  192. package/dist/sandbox/gondolin-worker.d.ts +0 -17
  193. package/dist/sandbox/gondolin-worker.d.ts.map +0 -1
  194. package/dist/sandbox/gondolin-worker.js +0 -201
  195. package/dist/sandbox/gondolin-worker.js.map +0 -1
  196. package/dist/sandbox/gondolin.d.ts +0 -53
  197. package/dist/sandbox/gondolin.d.ts.map +0 -1
  198. package/dist/sandbox/gondolin.js +0 -429
  199. package/dist/sandbox/gondolin.js.map +0 -1
@@ -1,75 +0,0 @@
1
- /**
2
- * Durable record of one Gondolin runtime. Written beside the state dir (never
3
- * inside the workspace) by whichever process owns the VM — the worker process
4
- * hosting it — so a crash that skips every exit hook still leaves enough
5
- * behind to adopt the runtime back or stop its orphaned VM runner.
6
- */
7
- export type { GondolinRuntimeRecord } from "./gondolin-contract.js";
8
- import { type GondolinRuntimeRecord } from "./gondolin-contract.js";
9
- interface GondolinInventoryOverrides {
10
- execFile?: (cmd: string, args: string[]) => Promise<string>;
11
- kill?: (pid: number, signal: NodeJS.Signals | 0) => void;
12
- gcSessions?: () => Promise<number>;
13
- killWaitMs?: number;
14
- killPollIntervalMs?: number;
15
- }
16
- declare class GondolinRuntimeInventory {
17
- private dir?;
18
- private execFileImpl;
19
- private killImpl;
20
- private gcSessionsImpl;
21
- private killWaitMs;
22
- private killPollIntervalMs;
23
- private readonly liveRecords;
24
- configure(dir?: string, overrides?: GondolinInventoryOverrides): void;
25
- record(entry: {
26
- sessionId: string;
27
- instanceId: string;
28
- runnerPid: number | null;
29
- socketPath?: string;
30
- fingerprint?: string;
31
- }): void;
32
- /** Re-persist a runtime whose runner pid changed (late boot, runner restart). */
33
- refresh(sessionId: string, runnerPid: number | null): void;
34
- release(sessionId: string): void;
35
- /**
36
- * Newest worker-owned record for a session key whose owner is still alive —
37
- * a candidate for adoption instead of spawning a fresh worker. Records left
38
- * by dead owners are reaped on the spot.
39
- */
40
- findAdoptable(instanceId: string): Promise<GondolinRuntimeRecord | undefined>;
41
- /** Reap one session's record if its owner is gone (e.g. after force-killing a worker). */
42
- reapSession(sessionId: string): Promise<void>;
43
- /** Worker-owned records with live owners (runtimes surviving from a previous mikan). */
44
- listWorkerRecords(): GondolinRuntimeRecord[];
45
- /**
46
- * mikan liveness signal for workers: a worker whose heartbeat goes stale
47
- * knows every mikan that could adopt it is gone and shuts itself down.
48
- */
49
- touchHeartbeat(): void;
50
- heartbeatPath(): string | undefined;
51
- directory(): string | undefined;
52
- /**
53
- * Startup reconciliation: stop VM runners orphaned by a previous owner
54
- * process that died without cleanup, drop stale records, and let Gondolin
55
- * collect its own stale session registry entries. Runtimes whose owner is
56
- * still alive (workers surviving a mikan restart) are left for adoption.
57
- * Idempotent — a second run finds an empty inventory.
58
- */
59
- reconcile(): Promise<void>;
60
- /**
61
- * Reap one dead-owner record: stop its orphaned runner if it is still the
62
- * process we launched, then drop the record (kept only while the runner
63
- * refuses to die, so the next reconcile retries).
64
- */
65
- reapRecord(record: GondolinRuntimeRecord, path?: string): Promise<"killed" | "gone" | "reused" | "alive">;
66
- private readRecords;
67
- private stopOrphan;
68
- private signal;
69
- isPidAlive(pid: number): boolean;
70
- private waitForExit;
71
- private write;
72
- private recordPath;
73
- }
74
- export declare const gondolinInventory: GondolinRuntimeInventory;
75
- //# sourceMappingURL=gondolin-inventory.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"gondolin-inventory.d.ts","sourceRoot":"","sources":["../../src/sandbox/gondolin-inventory.ts"],"names":[],"mappings":"AAMA;;;;;GAKG;AAGH,YAAY,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AACpE,OAAO,EAA2B,KAAK,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAE7F,UAAU,0BAA0B;IAClC,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5D,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,CAAC,KAAK,IAAI,CAAC;IACzD,UAAU,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IACnC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAOD,cAAM,wBAAwB;IAC5B,OAAO,CAAC,GAAG,CAAC,CAAS;IACrB,OAAO,CAAC,YAAY,CAAc;IAClC,OAAO,CAAC,QAAQ,CACY;IAC5B,OAAO,CAAC,cAAc,CAAqB;IAC3C,OAAO,CAAC,UAAU,CAAQ;IAC1B,OAAO,CAAC,kBAAkB,CAAO;IACjC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA4C;IAExE,SAAS,CAAC,GAAG,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,0BAA0B,GAAG,IAAI,CAQpE;IAED,MAAM,CAAC,KAAK,EAAE;QACZ,SAAS,EAAE,MAAM,CAAC;QAClB,UAAU,EAAE,MAAM,CAAC;QACnB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;QACzB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,GAAG,IAAI,CASP;IAED,iFAAiF;IACjF,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAKzD;IAED,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAW/B;IAED;;;;OAIG;IACG,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,GAAG,SAAS,CAAC,CAWlF;IAED,0FAA0F;IACpF,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAMlD;IAED,wFAAwF;IACxF,iBAAiB,IAAI,qBAAqB,EAAE,CAI3C;IAED;;;OAGG;IACH,cAAc,IAAI,IAAI,CAWrB;IAED,aAAa,IAAI,MAAM,GAAG,SAAS,CAElC;IAED,SAAS,IAAI,MAAM,GAAG,SAAS,CAE9B;IAED;;;;;;OAMG;IACG,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,CA8B/B;IAED;;;;OAIG;IACG,UAAU,CACd,MAAM,EAAE,qBAAqB,EAC7B,IAAI,SAAoC,GACvC,OAAO,CAAC,QAAQ,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAC,CAUjD;IAED,OAAO,CAAC,WAAW;YA4BL,UAAU;IA6BxB,OAAO,CAAC,MAAM;IAQd,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAO/B;YAEa,WAAW;IASzB,OAAO,CAAC,KAAK;IAab,OAAO,CAAC,UAAU;CAGnB;AAED,eAAO,MAAM,iBAAiB,0BAAiC,CAAC"}
@@ -1,257 +0,0 @@
1
- import { readdirSync, rmSync, writeFileSync } from "node:fs";
2
- import { join } from "node:path";
3
- import * as log from "../log.js";
4
- import { ensureDirExists, readJsonFileIfExists } from "../utils/file-guards.js";
5
- import { execSimple } from "./utils.js";
6
- import { isGondolinRuntimeRecord } from "./gondolin-contract.js";
7
- async function defaultGcSessions() {
8
- const { gcSessions } = await import("@earendil-works/gondolin");
9
- return gcSessions();
10
- }
11
- class GondolinRuntimeInventory {
12
- constructor() {
13
- this.execFileImpl = execSimple;
14
- this.killImpl = (pid, signal) => process.kill(pid, signal);
15
- this.gcSessionsImpl = defaultGcSessions;
16
- this.killWaitMs = 2000;
17
- this.killPollIntervalMs = 100;
18
- this.liveRecords = new Map();
19
- }
20
- configure(dir, overrides) {
21
- this.dir = dir;
22
- this.execFileImpl = overrides?.execFile ?? execSimple;
23
- this.killImpl = overrides?.kill ?? ((pid, signal) => process.kill(pid, signal));
24
- this.gcSessionsImpl = overrides?.gcSessions ?? defaultGcSessions;
25
- this.killWaitMs = overrides?.killWaitMs ?? 2000;
26
- this.killPollIntervalMs = overrides?.killPollIntervalMs ?? 100;
27
- this.liveRecords.clear();
28
- }
29
- record(entry) {
30
- if (!this.dir)
31
- return;
32
- const record = {
33
- ...entry,
34
- ownerPid: process.pid,
35
- createdAt: new Date().toISOString(),
36
- };
37
- this.liveRecords.set(entry.sessionId, record);
38
- this.write(record);
39
- }
40
- /** Re-persist a runtime whose runner pid changed (late boot, runner restart). */
41
- refresh(sessionId, runnerPid) {
42
- const record = this.liveRecords.get(sessionId);
43
- if (!record || record.runnerPid === runnerPid)
44
- return;
45
- record.runnerPid = runnerPid;
46
- this.write(record);
47
- }
48
- release(sessionId) {
49
- this.liveRecords.delete(sessionId);
50
- if (!this.dir)
51
- return;
52
- try {
53
- rmSync(this.recordPath(sessionId), { force: true });
54
- }
55
- catch (err) {
56
- log.logWarning(`Failed to release Gondolin runtime record '${sessionId}'`, err instanceof Error ? err.message : String(err));
57
- }
58
- }
59
- /**
60
- * Newest worker-owned record for a session key whose owner is still alive —
61
- * a candidate for adoption instead of spawning a fresh worker. Records left
62
- * by dead owners are reaped on the spot.
63
- */
64
- async findAdoptable(instanceId) {
65
- let newest;
66
- for (const { record, path } of this.readRecords()) {
67
- if (record.instanceId !== instanceId || !record.socketPath)
68
- continue;
69
- if (!this.isPidAlive(record.ownerPid)) {
70
- await this.reapRecord(record, path);
71
- continue;
72
- }
73
- if (!newest || record.createdAt > newest.createdAt)
74
- newest = record;
75
- }
76
- return newest;
77
- }
78
- /** Reap one session's record if its owner is gone (e.g. after force-killing a worker). */
79
- async reapSession(sessionId) {
80
- for (const { record, path } of this.readRecords()) {
81
- if (record.sessionId !== sessionId)
82
- continue;
83
- if (!this.isPidAlive(record.ownerPid))
84
- await this.reapRecord(record, path);
85
- return;
86
- }
87
- }
88
- /** Worker-owned records with live owners (runtimes surviving from a previous mikan). */
89
- listWorkerRecords() {
90
- return this.readRecords()
91
- .map(({ record }) => record)
92
- .filter((record) => record.socketPath !== undefined && this.isPidAlive(record.ownerPid));
93
- }
94
- /**
95
- * mikan liveness signal for workers: a worker whose heartbeat goes stale
96
- * knows every mikan that could adopt it is gone and shuts itself down.
97
- */
98
- touchHeartbeat() {
99
- if (!this.dir)
100
- return;
101
- try {
102
- ensureDirExists(this.dir);
103
- writeFileSync(this.heartbeatPath(), new Date().toISOString() + "\n");
104
- }
105
- catch (err) {
106
- log.logWarning("Failed to touch Gondolin heartbeat", err instanceof Error ? err.message : String(err));
107
- }
108
- }
109
- heartbeatPath() {
110
- return this.dir ? join(this.dir, "heartbeat") : undefined;
111
- }
112
- directory() {
113
- return this.dir;
114
- }
115
- /**
116
- * Startup reconciliation: stop VM runners orphaned by a previous owner
117
- * process that died without cleanup, drop stale records, and let Gondolin
118
- * collect its own stale session registry entries. Runtimes whose owner is
119
- * still alive (workers surviving a mikan restart) are left for adoption.
120
- * Idempotent — a second run finds an empty inventory.
121
- */
122
- async reconcile() {
123
- if (!this.dir)
124
- return;
125
- ensureDirExists(this.dir);
126
- let killed = 0;
127
- let stale = 0;
128
- let skipped = 0;
129
- for (const { record, path } of this.readRecords((badPath) => {
130
- rmSync(badPath, { force: true });
131
- stale += 1;
132
- })) {
133
- if (record.ownerPid === process.pid)
134
- continue;
135
- if (this.isPidAlive(record.ownerPid)) {
136
- skipped += 1;
137
- continue;
138
- }
139
- const outcome = await this.reapRecord(record, path);
140
- if (outcome === "killed")
141
- killed += 1;
142
- else if (outcome !== "alive")
143
- stale += 1;
144
- }
145
- try {
146
- await this.gcSessionsImpl();
147
- }
148
- catch (err) {
149
- log.logWarning("Failed to collect Gondolin session registry", err instanceof Error ? err.message : String(err));
150
- }
151
- log.logInfo(`Reconciled Gondolin runtimes (killed=${killed}, stale=${stale}, liveOwner=${skipped})`);
152
- }
153
- /**
154
- * Reap one dead-owner record: stop its orphaned runner if it is still the
155
- * process we launched, then drop the record (kept only while the runner
156
- * refuses to die, so the next reconcile retries).
157
- */
158
- async reapRecord(record, path = this.recordPath(record.sessionId)) {
159
- const outcome = record.runnerPid === null ? "gone" : await this.stopOrphan(record);
160
- if (outcome === "alive") {
161
- log.logWarning(`Orphaned Gondolin runner ${record.runnerPid} survived SIGKILL; keeping record '${record.sessionId}'`);
162
- return outcome;
163
- }
164
- rmSync(path, { force: true });
165
- return outcome;
166
- }
167
- readRecords(onMalformed) {
168
- if (!this.dir)
169
- return [];
170
- ensureDirExists(this.dir);
171
- const entries = [];
172
- for (const file of readdirSync(this.dir)) {
173
- if (!file.endsWith(".json"))
174
- continue;
175
- const path = join(this.dir, file);
176
- let record;
177
- try {
178
- record = readJsonFileIfExists(path, isGondolinRuntimeRecord, (detail) => `Malformed Gondolin runtime record at ${path}: ${detail}`);
179
- }
180
- catch (err) {
181
- log.logWarning("Dropping malformed Gondolin runtime record", err instanceof Error ? err.message : String(err));
182
- }
183
- if (record)
184
- entries.push({ record, path });
185
- else
186
- onMalformed?.(path);
187
- }
188
- return entries;
189
- }
190
- async stopOrphan(record) {
191
- const pid = record.runnerPid;
192
- if (!this.isPidAlive(pid))
193
- return "gone";
194
- // Guard against pid reuse: only kill processes that still look like a
195
- // Gondolin runner (its guest assets and overlay disk live under paths
196
- // containing "gondolin").
197
- let command;
198
- try {
199
- command = await this.execFileImpl("ps", ["-p", String(pid), "-o", "command="]);
200
- }
201
- catch {
202
- return "gone";
203
- }
204
- if (!command.toLowerCase().includes("gondolin")) {
205
- log.logWarning(`Pid ${pid} from Gondolin runtime record '${record.sessionId}' no longer looks like a Gondolin runner; skipping kill`);
206
- return "reused";
207
- }
208
- log.logInfo(`Stopping orphaned Gondolin runner ${pid} (session '${record.sessionId}', instance '${record.instanceId}')`);
209
- this.signal(pid, "SIGTERM");
210
- if (await this.waitForExit(pid))
211
- return "killed";
212
- this.signal(pid, "SIGKILL");
213
- return (await this.waitForExit(pid)) ? "killed" : "alive";
214
- }
215
- signal(pid, signal) {
216
- try {
217
- this.killImpl(pid, signal);
218
- }
219
- catch {
220
- // process already gone
221
- }
222
- }
223
- isPidAlive(pid) {
224
- try {
225
- this.killImpl(pid, 0);
226
- return true;
227
- }
228
- catch {
229
- return false;
230
- }
231
- }
232
- async waitForExit(pid) {
233
- const deadline = Date.now() + this.killWaitMs;
234
- while (this.isPidAlive(pid)) {
235
- if (Date.now() >= deadline)
236
- return false;
237
- await new Promise((resolve) => setTimeout(resolve, this.killPollIntervalMs));
238
- }
239
- return true;
240
- }
241
- write(record) {
242
- if (!this.dir)
243
- return;
244
- try {
245
- ensureDirExists(this.dir);
246
- writeFileSync(this.recordPath(record.sessionId), JSON.stringify(record, null, 2) + "\n");
247
- }
248
- catch (err) {
249
- log.logWarning(`Failed to persist Gondolin runtime record '${record.sessionId}'`, err instanceof Error ? err.message : String(err));
250
- }
251
- }
252
- recordPath(sessionId) {
253
- return join(this.dir, `${sessionId}.json`);
254
- }
255
- }
256
- export const gondolinInventory = new GondolinRuntimeInventory();
257
- //# sourceMappingURL=gondolin-inventory.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"gondolin-inventory.js","sourceRoot":"","sources":["../../src/sandbox/gondolin-inventory.ts"],"names":[],"mappings":"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","sourcesContent":["import { readdirSync, rmSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport * as log from \"../log.js\";\nimport { ensureDirExists, readJsonFileIfExists } from \"../utils/file-guards.js\";\nimport { execSimple } from \"./utils.js\";\n\n/**\n * Durable record of one Gondolin runtime. Written beside the state dir (never\n * inside the workspace) by whichever process owns the VM — the worker process\n * hosting it — so a crash that skips every exit hook still leaves enough\n * behind to adopt the runtime back or stop its orphaned VM runner.\n */\n// The record is a wire contract shape shared with the Go daemon's\n// rediscovery; its single home is gondolin-contract.ts.\nexport type { GondolinRuntimeRecord } from \"./gondolin-contract.js\";\nimport { isGondolinRuntimeRecord, type GondolinRuntimeRecord } from \"./gondolin-contract.js\";\n\ninterface GondolinInventoryOverrides {\n execFile?: (cmd: string, args: string[]) => Promise<string>;\n kill?: (pid: number, signal: NodeJS.Signals | 0) => void;\n gcSessions?: () => Promise<number>;\n killWaitMs?: number;\n killPollIntervalMs?: number;\n}\n\nasync function defaultGcSessions(): Promise<number> {\n const { gcSessions } = await import(\"@earendil-works/gondolin\");\n return gcSessions();\n}\n\nclass GondolinRuntimeInventory {\n private dir?: string;\n private execFileImpl = execSimple;\n private killImpl: (pid: number, signal: NodeJS.Signals | 0) => void = (pid, signal) =>\n process.kill(pid, signal);\n private gcSessionsImpl = defaultGcSessions;\n private killWaitMs = 2000;\n private killPollIntervalMs = 100;\n private readonly liveRecords = new Map<string, GondolinRuntimeRecord>();\n\n configure(dir?: string, overrides?: GondolinInventoryOverrides): void {\n this.dir = dir;\n this.execFileImpl = overrides?.execFile ?? execSimple;\n this.killImpl = overrides?.kill ?? ((pid, signal) => process.kill(pid, signal));\n this.gcSessionsImpl = overrides?.gcSessions ?? defaultGcSessions;\n this.killWaitMs = overrides?.killWaitMs ?? 2000;\n this.killPollIntervalMs = overrides?.killPollIntervalMs ?? 100;\n this.liveRecords.clear();\n }\n\n record(entry: {\n sessionId: string;\n instanceId: string;\n runnerPid: number | null;\n socketPath?: string;\n fingerprint?: string;\n }): void {\n if (!this.dir) return;\n const record: GondolinRuntimeRecord = {\n ...entry,\n ownerPid: process.pid,\n createdAt: new Date().toISOString(),\n };\n this.liveRecords.set(entry.sessionId, record);\n this.write(record);\n }\n\n /** Re-persist a runtime whose runner pid changed (late boot, runner restart). */\n refresh(sessionId: string, runnerPid: number | null): void {\n const record = this.liveRecords.get(sessionId);\n if (!record || record.runnerPid === runnerPid) return;\n record.runnerPid = runnerPid;\n this.write(record);\n }\n\n release(sessionId: string): void {\n this.liveRecords.delete(sessionId);\n if (!this.dir) return;\n try {\n rmSync(this.recordPath(sessionId), { force: true });\n } catch (err) {\n log.logWarning(\n `Failed to release Gondolin runtime record '${sessionId}'`,\n err instanceof Error ? err.message : String(err),\n );\n }\n }\n\n /**\n * Newest worker-owned record for a session key whose owner is still alive —\n * a candidate for adoption instead of spawning a fresh worker. Records left\n * by dead owners are reaped on the spot.\n */\n async findAdoptable(instanceId: string): Promise<GondolinRuntimeRecord | undefined> {\n let newest: GondolinRuntimeRecord | undefined;\n for (const { record, path } of this.readRecords()) {\n if (record.instanceId !== instanceId || !record.socketPath) continue;\n if (!this.isPidAlive(record.ownerPid)) {\n await this.reapRecord(record, path);\n continue;\n }\n if (!newest || record.createdAt > newest.createdAt) newest = record;\n }\n return newest;\n }\n\n /** Reap one session's record if its owner is gone (e.g. after force-killing a worker). */\n async reapSession(sessionId: string): Promise<void> {\n for (const { record, path } of this.readRecords()) {\n if (record.sessionId !== sessionId) continue;\n if (!this.isPidAlive(record.ownerPid)) await this.reapRecord(record, path);\n return;\n }\n }\n\n /** Worker-owned records with live owners (runtimes surviving from a previous mikan). */\n listWorkerRecords(): GondolinRuntimeRecord[] {\n return this.readRecords()\n .map(({ record }) => record)\n .filter((record) => record.socketPath !== undefined && this.isPidAlive(record.ownerPid));\n }\n\n /**\n * mikan liveness signal for workers: a worker whose heartbeat goes stale\n * knows every mikan that could adopt it is gone and shuts itself down.\n */\n touchHeartbeat(): void {\n if (!this.dir) return;\n try {\n ensureDirExists(this.dir);\n writeFileSync(this.heartbeatPath() as string, new Date().toISOString() + \"\\n\");\n } catch (err) {\n log.logWarning(\n \"Failed to touch Gondolin heartbeat\",\n err instanceof Error ? err.message : String(err),\n );\n }\n }\n\n heartbeatPath(): string | undefined {\n return this.dir ? join(this.dir, \"heartbeat\") : undefined;\n }\n\n directory(): string | undefined {\n return this.dir;\n }\n\n /**\n * Startup reconciliation: stop VM runners orphaned by a previous owner\n * process that died without cleanup, drop stale records, and let Gondolin\n * collect its own stale session registry entries. Runtimes whose owner is\n * still alive (workers surviving a mikan restart) are left for adoption.\n * Idempotent — a second run finds an empty inventory.\n */\n async reconcile(): Promise<void> {\n if (!this.dir) return;\n ensureDirExists(this.dir);\n let killed = 0;\n let stale = 0;\n let skipped = 0;\n for (const { record, path } of this.readRecords((badPath) => {\n rmSync(badPath, { force: true });\n stale += 1;\n })) {\n if (record.ownerPid === process.pid) continue;\n if (this.isPidAlive(record.ownerPid)) {\n skipped += 1;\n continue;\n }\n const outcome = await this.reapRecord(record, path);\n if (outcome === \"killed\") killed += 1;\n else if (outcome !== \"alive\") stale += 1;\n }\n try {\n await this.gcSessionsImpl();\n } catch (err) {\n log.logWarning(\n \"Failed to collect Gondolin session registry\",\n err instanceof Error ? err.message : String(err),\n );\n }\n log.logInfo(\n `Reconciled Gondolin runtimes (killed=${killed}, stale=${stale}, liveOwner=${skipped})`,\n );\n }\n\n /**\n * Reap one dead-owner record: stop its orphaned runner if it is still the\n * process we launched, then drop the record (kept only while the runner\n * refuses to die, so the next reconcile retries).\n */\n async reapRecord(\n record: GondolinRuntimeRecord,\n path = this.recordPath(record.sessionId),\n ): Promise<\"killed\" | \"gone\" | \"reused\" | \"alive\"> {\n const outcome = record.runnerPid === null ? \"gone\" : await this.stopOrphan(record);\n if (outcome === \"alive\") {\n log.logWarning(\n `Orphaned Gondolin runner ${record.runnerPid} survived SIGKILL; keeping record '${record.sessionId}'`,\n );\n return outcome;\n }\n rmSync(path, { force: true });\n return outcome;\n }\n\n private readRecords(\n onMalformed?: (path: string) => void,\n ): Array<{ record: GondolinRuntimeRecord; path: string }> {\n if (!this.dir) return [];\n ensureDirExists(this.dir);\n const entries: Array<{ record: GondolinRuntimeRecord; path: string }> = [];\n for (const file of readdirSync(this.dir)) {\n if (!file.endsWith(\".json\")) continue;\n const path = join(this.dir, file);\n let record: GondolinRuntimeRecord | undefined;\n try {\n record = readJsonFileIfExists(\n path,\n isGondolinRuntimeRecord,\n (detail) => `Malformed Gondolin runtime record at ${path}: ${detail}`,\n );\n } catch (err) {\n log.logWarning(\n \"Dropping malformed Gondolin runtime record\",\n err instanceof Error ? err.message : String(err),\n );\n }\n if (record) entries.push({ record, path });\n else onMalformed?.(path);\n }\n return entries;\n }\n\n private async stopOrphan(\n record: GondolinRuntimeRecord,\n ): Promise<\"killed\" | \"gone\" | \"reused\" | \"alive\"> {\n const pid = record.runnerPid as number;\n if (!this.isPidAlive(pid)) return \"gone\";\n // Guard against pid reuse: only kill processes that still look like a\n // Gondolin runner (its guest assets and overlay disk live under paths\n // containing \"gondolin\").\n let command: string;\n try {\n command = await this.execFileImpl(\"ps\", [\"-p\", String(pid), \"-o\", \"command=\"]);\n } catch {\n return \"gone\";\n }\n if (!command.toLowerCase().includes(\"gondolin\")) {\n log.logWarning(\n `Pid ${pid} from Gondolin runtime record '${record.sessionId}' no longer looks like a Gondolin runner; skipping kill`,\n );\n return \"reused\";\n }\n log.logInfo(\n `Stopping orphaned Gondolin runner ${pid} (session '${record.sessionId}', instance '${record.instanceId}')`,\n );\n this.signal(pid, \"SIGTERM\");\n if (await this.waitForExit(pid)) return \"killed\";\n this.signal(pid, \"SIGKILL\");\n return (await this.waitForExit(pid)) ? \"killed\" : \"alive\";\n }\n\n private signal(pid: number, signal: NodeJS.Signals): void {\n try {\n this.killImpl(pid, signal);\n } catch {\n // process already gone\n }\n }\n\n isPidAlive(pid: number): boolean {\n try {\n this.killImpl(pid, 0);\n return true;\n } catch {\n return false;\n }\n }\n\n private async waitForExit(pid: number): Promise<boolean> {\n const deadline = Date.now() + this.killWaitMs;\n while (this.isPidAlive(pid)) {\n if (Date.now() >= deadline) return false;\n await new Promise((resolve) => setTimeout(resolve, this.killPollIntervalMs));\n }\n return true;\n }\n\n private write(record: GondolinRuntimeRecord): void {\n if (!this.dir) return;\n try {\n ensureDirExists(this.dir);\n writeFileSync(this.recordPath(record.sessionId), JSON.stringify(record, null, 2) + \"\\n\");\n } catch (err) {\n log.logWarning(\n `Failed to persist Gondolin runtime record '${record.sessionId}'`,\n err instanceof Error ? err.message : String(err),\n );\n }\n }\n\n private recordPath(sessionId: string): string {\n return join(this.dir as string, `${sessionId}.json`);\n }\n}\n\nexport const gondolinInventory = new GondolinRuntimeInventory();\n"]}
@@ -1,62 +0,0 @@
1
- /**
2
- * Certificate provisioning and join-token authority for dial-home workers.
3
- *
4
- * Owns a private CA under the state dir (`gondolin-gateway/`), auto-issues the
5
- * gateway's own server certificate from it, and exchanges one-time join
6
- * tokens for CA-signed worker client certificates — the kubeadm-style
7
- * bootstrap that replaces hand-provisioned mTLS material. All X.509 issuance
8
- * shells out to openssl (Node can parse but not issue certificates).
9
- */
10
- declare class GondolinJoinService {
11
- private dir?;
12
- private execImpl;
13
- private now;
14
- private tokenTtlSeconds;
15
- configure(dir?: string, overrides?: {
16
- execFile?: (cmd: string, args: string[]) => Promise<unknown>;
17
- now?: () => number;
18
- tokenTtlSeconds?: number;
19
- }): void;
20
- isConfigured(): boolean;
21
- paths(): {
22
- caFile: string;
23
- caKeyFile: string;
24
- serverCertFile: string;
25
- serverKeyFile: string;
26
- };
27
- /** Create the worker CA on first use; idempotent afterwards. */
28
- ensureCa(): Promise<void>;
29
- /**
30
- * Issue the gateway's server certificate from the worker CA so joining
31
- * workers can verify the host against the pinned CA. Reissued when the SAN
32
- * list changes.
33
- */
34
- ensureServerCert(hostnames: string[]): Promise<{
35
- certFile: string;
36
- keyFile: string;
37
- }>;
38
- /** SHA-256 of the CA certificate DER — the join command's --ca-pin. */
39
- caFingerprint(): string;
40
- /** Mint a single-use join token; only its hash is stored. */
41
- mintToken(): Promise<{
42
- token: string;
43
- fingerprint: string;
44
- expiresAt: number;
45
- }>;
46
- /** Validate and burn a join token (single use, TTL-bounded). */
47
- consumeToken(token: string): boolean;
48
- /** Sign a joining worker's CSR with the worker CA. */
49
- signCsr(csrPem: string): Promise<{
50
- certPem: string;
51
- caPem: string;
52
- }>;
53
- private subjectAltNames;
54
- private openssl;
55
- private tokensPath;
56
- private readTokens;
57
- private writeTokens;
58
- private requireDir;
59
- }
60
- export declare const gondolinJoin: GondolinJoinService;
61
- export {};
62
- //# sourceMappingURL=gondolin-join.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"gondolin-join.d.ts","sourceRoot":"","sources":["../../src/sandbox/gondolin-join.ts"],"names":[],"mappings":"AAyCA;;;;;;;;GAQG;AACH,cAAM,mBAAmB;IACvB,OAAO,CAAC,GAAG,CAAC,CAAS;IACrB,OAAO,CAAC,QAAQ,CAAoE;IACpF,OAAO,CAAC,GAAG,CAA0B;IACrC,OAAO,CAAC,eAAe,CAAqB;IAE5C,SAAS,CACP,GAAG,CAAC,EAAE,MAAM,EACZ,SAAS,CAAC,EAAE;QACV,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;QAC7D,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;QACnB,eAAe,CAAC,EAAE,MAAM,CAAC;KAC1B,GACA,IAAI,CAKN;IAED,YAAY,IAAI,OAAO,CAEtB;IAED,KAAK,IAAI;QACP,MAAM,EAAE,MAAM,CAAC;QACf,SAAS,EAAE,MAAM,CAAC;QAClB,cAAc,EAAE,MAAM,CAAC;QACvB,aAAa,EAAE,MAAM,CAAC;KACvB,CAQA;IAED,gEAAgE;IAC1D,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAsB9B;IAED;;;;OAIG;IACG,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CA0D1F;IAED,uEAAuE;IACvE,aAAa,IAAI,MAAM,CAGtB;IAED,6DAA6D;IACvD,SAAS,IAAI,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CASpF;IAED,gEAAgE;IAChE,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAcnC;IAED,sDAAsD;IAChD,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CA+BzE;IAED,OAAO,CAAC,eAAe;YAST,OAAO;IAWrB,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,UAAU;IAkBlB,OAAO,CAAC,WAAW;IAOnB,OAAO,CAAC,UAAU;CAInB;AAED,eAAO,MAAM,YAAY,qBAA4B,CAAC"}