@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,155 +0,0 @@
1
- import { Type } from "@sinclair/typebox";
2
- import { isRecord } from "../utils/file-guards.js";
3
- /**
4
- * Single home for every wire shape that crosses the mikan ⇄ gondolin-worker
5
- * boundary. The Go daemon mirrors these shapes by hand
6
- * (worker/internal/runtime/supervisor.go, worker/internal/api/server.go,
7
- * worker/internal/lease/lease.go); the shared fixtures under
8
- * `worker/contract-fixtures/` pin both sides — the TS test
9
- * (test/gondolin-contract.test.ts) checks each fixture against the schemas
10
- * here, and the Go contract tests round-trip the same files through the Go
11
- * structs. Add a field on one side only and one of those tests fails.
12
- *
13
- * Secrets never travel through these shapes: vault env rides per-exec over
14
- * the session IPC socket, and credential files are shipped by content only
15
- * on the authenticated ensure request.
16
- */
17
- const GondolinMountSchema = Type.Object({
18
- source: Type.String(),
19
- target: Type.String(),
20
- });
21
- /**
22
- * Everything a worker process needs to host one Gondolin VM. Serialized as a
23
- * single JSON argv entry by both spawners (the local TS worker client and
24
- * the Go daemon's supervisor).
25
- */
26
- export const GondolinWorkerConfigSchema = Type.Object({
27
- /** mikan session key the runtime belongs to. */
28
- instanceId: Type.String(),
29
- /** Resolved guest image asset directory. */
30
- image: Type.Optional(Type.String()),
31
- /** Image selector resolved inside the worker (remote workers, where the
32
- * spawning host has no image store). Ignored when `image` is set. */
33
- imageSelector: Type.Optional(Type.String()),
34
- mounts: Type.Array(GondolinMountSchema),
35
- cpus: Type.Optional(Type.Number()),
36
- memory: Type.Optional(Type.String()),
37
- /** Desired-runtime fingerprint recorded for adoption checks. */
38
- fingerprint: Type.String(),
39
- /** Runtime inventory directory (shared with the spawning mikan). */
40
- inventoryDir: Type.String(),
41
- /** Self-stop when the mikan heartbeat file is older than this. 0 disables. */
42
- heartbeatStaleMs: Type.Number(),
43
- });
44
- /** First stdout line of a worker, consumed by both spawners. */
45
- export const GondolinWorkerHandshakeSchema = Type.Object({
46
- ready: Type.Boolean(),
47
- error: Type.Optional(Type.String()),
48
- sessionId: Type.Optional(Type.String()),
49
- socketPath: Type.Optional(Type.String()),
50
- workerPid: Type.Optional(Type.Number()),
51
- runnerPid: Type.Optional(Type.Union([Type.Number(), Type.Null()])),
52
- });
53
- /**
54
- * One `<sessionId>.json` inventory record, written by the worker process and
55
- * read back by the TS inventory and the Go daemon's rediscovery.
56
- */
57
- export const GondolinRuntimeRecordSchema = Type.Object({
58
- /** Gondolin session id (`vm.id`); also names the record file. */
59
- sessionId: Type.String(),
60
- /** mikan session key (vault key) the runtime was created for. */
61
- instanceId: Type.String(),
62
- /** Process that owns the runtime (the worker process). */
63
- ownerPid: Type.Number(),
64
- /** Host pid of the active VM runner process (QEMU/krun), if started. */
65
- runnerPid: Type.Union([Type.Number(), Type.Null()]),
66
- createdAt: Type.String(),
67
- /** Session IPC socket for adopting the runtime from a new mikan process. */
68
- socketPath: Type.Optional(Type.String()),
69
- /** Desired-runtime fingerprint the VM was created from. */
70
- fingerprint: Type.Optional(Type.String()),
71
- });
72
- export function isGondolinRuntimeRecord(value) {
73
- return (isRecord(value) &&
74
- typeof value.sessionId === "string" &&
75
- value.sessionId.length > 0 &&
76
- typeof value.instanceId === "string" &&
77
- typeof value.ownerPid === "number" &&
78
- (value.runnerPid === null || typeof value.runnerPid === "number") &&
79
- typeof value.createdAt === "string" &&
80
- (value.socketPath === undefined || typeof value.socketPath === "string") &&
81
- (value.fingerprint === undefined || typeof value.fingerprint === "string"));
82
- }
83
- /** A vault credential shipped by content on the ensure request (never a shared-fs mount). */
84
- const GondolinCredentialFileSchema = Type.Object({
85
- target: Type.String(),
86
- contentBase64: Type.String(),
87
- });
88
- /** POST /v1/runtimes body sent to the remote worker daemon. */
89
- export const GondolinEnsureRuntimeRequestSchema = Type.Object({
90
- instanceId: Type.String(),
91
- imageSelector: Type.String(),
92
- mounts: Type.Array(GondolinMountSchema),
93
- credentialFiles: Type.Array(GondolinCredentialFileSchema),
94
- /** Fractional cgroup CPU limit as a string; "" when unset. */
95
- cpus: Type.String(),
96
- memory: Type.String(),
97
- fingerprint: Type.String(),
98
- /** Accepted by the daemon; the TS client currently leaves it unset (0). */
99
- heartbeatStaleMs: Type.Optional(Type.Number()),
100
- });
101
- /**
102
- * One runtime as reported by the daemon (ensure response, GET, and the
103
- * elements of the list response). The daemon's socketPath never crosses the
104
- * wire — sessions reach the runtime through the tunnel, not the socket.
105
- */
106
- export const GondolinRemoteRuntimeSchema = Type.Object({
107
- sessionId: Type.String(),
108
- instanceId: Type.String(),
109
- fingerprint: Type.String(),
110
- workerPid: Type.Number(),
111
- runnerPid: Type.Number(),
112
- epoch: Type.Number(),
113
- adopted: Type.Boolean(),
114
- });
115
- /** POST /v1/leases response: a fenced, epoch-monotonic per-instance lease. */
116
- export const GondolinLeaseGrantSchema = Type.Object({
117
- id: Type.String(),
118
- instanceId: Type.String(),
119
- epoch: Type.Number(),
120
- /** RFC 3339 timestamp (Go time.Time marshalling). */
121
- expiresAt: Type.String(),
122
- });
123
- /** Parses the daemon-to-client session framing: u8 type + u32be length + payload. */
124
- export function createSessionFrameParser(callbacks) {
125
- let buffer = Buffer.alloc(0);
126
- return (chunk) => {
127
- buffer = Buffer.concat([buffer, chunk]);
128
- while (buffer.length >= 5) {
129
- const type = buffer.readUInt8(0);
130
- const length = buffer.readUInt32BE(1);
131
- if (buffer.length < 5 + length)
132
- return;
133
- const payload = buffer.subarray(5, 5 + length);
134
- buffer = buffer.subarray(5 + length);
135
- if (type === 1) {
136
- callbacks.onBinary(Buffer.from(payload));
137
- continue;
138
- }
139
- try {
140
- callbacks.onJson(JSON.parse(payload.toString("utf8")));
141
- }
142
- catch {
143
- // ignore malformed frames
144
- }
145
- }
146
- };
147
- }
148
- /** Encodes a client-to-daemon session message: u32be length + JSON. */
149
- export function encodeSessionMessage(message) {
150
- const payload = Buffer.from(JSON.stringify(message));
151
- const header = Buffer.alloc(4);
152
- header.writeUInt32BE(payload.length, 0);
153
- return Buffer.concat([header, payload]);
154
- }
155
- //# sourceMappingURL=gondolin-contract.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"gondolin-contract.js","sourceRoot":"","sources":["../../src/sandbox/gondolin-contract.ts"],"names":[],"mappings":"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","sourcesContent":["import { Type, type Static } from \"@sinclair/typebox\";\nimport { isRecord } from \"../utils/file-guards.js\";\n\n/**\n * Single home for every wire shape that crosses the mikan ⇄ gondolin-worker\n * boundary. The Go daemon mirrors these shapes by hand\n * (worker/internal/runtime/supervisor.go, worker/internal/api/server.go,\n * worker/internal/lease/lease.go); the shared fixtures under\n * `worker/contract-fixtures/` pin both sides — the TS test\n * (test/gondolin-contract.test.ts) checks each fixture against the schemas\n * here, and the Go contract tests round-trip the same files through the Go\n * structs. Add a field on one side only and one of those tests fails.\n *\n * Secrets never travel through these shapes: vault env rides per-exec over\n * the session IPC socket, and credential files are shipped by content only\n * on the authenticated ensure request.\n */\n\nconst GondolinMountSchema = Type.Object({\n source: Type.String(),\n target: Type.String(),\n});\n\n/**\n * Everything a worker process needs to host one Gondolin VM. Serialized as a\n * single JSON argv entry by both spawners (the local TS worker client and\n * the Go daemon's supervisor).\n */\nexport const GondolinWorkerConfigSchema = Type.Object({\n /** mikan session key the runtime belongs to. */\n instanceId: Type.String(),\n /** Resolved guest image asset directory. */\n image: Type.Optional(Type.String()),\n /** Image selector resolved inside the worker (remote workers, where the\n * spawning host has no image store). Ignored when `image` is set. */\n imageSelector: Type.Optional(Type.String()),\n mounts: Type.Array(GondolinMountSchema),\n cpus: Type.Optional(Type.Number()),\n memory: Type.Optional(Type.String()),\n /** Desired-runtime fingerprint recorded for adoption checks. */\n fingerprint: Type.String(),\n /** Runtime inventory directory (shared with the spawning mikan). */\n inventoryDir: Type.String(),\n /** Self-stop when the mikan heartbeat file is older than this. 0 disables. */\n heartbeatStaleMs: Type.Number(),\n});\n\nexport type GondolinWorkerConfig = Static<typeof GondolinWorkerConfigSchema>;\n\n/** First stdout line of a worker, consumed by both spawners. */\nexport const GondolinWorkerHandshakeSchema = Type.Object({\n ready: Type.Boolean(),\n error: Type.Optional(Type.String()),\n sessionId: Type.Optional(Type.String()),\n socketPath: Type.Optional(Type.String()),\n workerPid: Type.Optional(Type.Number()),\n runnerPid: Type.Optional(Type.Union([Type.Number(), Type.Null()])),\n});\n\nexport type GondolinWorkerHandshake = Static<typeof GondolinWorkerHandshakeSchema>;\n\n/**\n * One `<sessionId>.json` inventory record, written by the worker process and\n * read back by the TS inventory and the Go daemon's rediscovery.\n */\nexport const GondolinRuntimeRecordSchema = Type.Object({\n /** Gondolin session id (`vm.id`); also names the record file. */\n sessionId: Type.String(),\n /** mikan session key (vault key) the runtime was created for. */\n instanceId: Type.String(),\n /** Process that owns the runtime (the worker process). */\n ownerPid: Type.Number(),\n /** Host pid of the active VM runner process (QEMU/krun), if started. */\n runnerPid: Type.Union([Type.Number(), Type.Null()]),\n createdAt: Type.String(),\n /** Session IPC socket for adopting the runtime from a new mikan process. */\n socketPath: Type.Optional(Type.String()),\n /** Desired-runtime fingerprint the VM was created from. */\n fingerprint: Type.Optional(Type.String()),\n});\n\nexport type GondolinRuntimeRecord = Static<typeof GondolinRuntimeRecordSchema>;\n\nexport function isGondolinRuntimeRecord(value: unknown): value is GondolinRuntimeRecord {\n return (\n isRecord(value) &&\n typeof value.sessionId === \"string\" &&\n value.sessionId.length > 0 &&\n typeof value.instanceId === \"string\" &&\n typeof value.ownerPid === \"number\" &&\n (value.runnerPid === null || typeof value.runnerPid === \"number\") &&\n typeof value.createdAt === \"string\" &&\n (value.socketPath === undefined || typeof value.socketPath === \"string\") &&\n (value.fingerprint === undefined || typeof value.fingerprint === \"string\")\n );\n}\n\n/** A vault credential shipped by content on the ensure request (never a shared-fs mount). */\nconst GondolinCredentialFileSchema = Type.Object({\n target: Type.String(),\n contentBase64: Type.String(),\n});\n\nexport type GondolinCredentialFile = Static<typeof GondolinCredentialFileSchema>;\n\n/** POST /v1/runtimes body sent to the remote worker daemon. */\nexport const GondolinEnsureRuntimeRequestSchema = Type.Object({\n instanceId: Type.String(),\n imageSelector: Type.String(),\n mounts: Type.Array(GondolinMountSchema),\n credentialFiles: Type.Array(GondolinCredentialFileSchema),\n /** Fractional cgroup CPU limit as a string; \"\" when unset. */\n cpus: Type.String(),\n memory: Type.String(),\n fingerprint: Type.String(),\n /** Accepted by the daemon; the TS client currently leaves it unset (0). */\n heartbeatStaleMs: Type.Optional(Type.Number()),\n});\n\nexport type GondolinEnsureRuntimeRequest = Static<typeof GondolinEnsureRuntimeRequestSchema>;\n\n/**\n * One runtime as reported by the daemon (ensure response, GET, and the\n * elements of the list response). The daemon's socketPath never crosses the\n * wire — sessions reach the runtime through the tunnel, not the socket.\n */\nexport const GondolinRemoteRuntimeSchema = Type.Object({\n sessionId: Type.String(),\n instanceId: Type.String(),\n fingerprint: Type.String(),\n workerPid: Type.Number(),\n runnerPid: Type.Number(),\n epoch: Type.Number(),\n adopted: Type.Boolean(),\n});\n\nexport type GondolinRemoteRuntime = Static<typeof GondolinRemoteRuntimeSchema>;\n\n/** POST /v1/leases response: a fenced, epoch-monotonic per-instance lease. */\nexport const GondolinLeaseGrantSchema = Type.Object({\n id: Type.String(),\n instanceId: Type.String(),\n epoch: Type.Number(),\n /** RFC 3339 timestamp (Go time.Time marshalling). */\n expiresAt: Type.String(),\n});\n\n// ── session tunnel framing ────────────────────────────────────────────────────\n// The session protocol itself is TS⇄TS (worker session server ⇄ client); the\n// Go daemon splices it byte-for-byte through the upgraded tunnel. The framing\n// still lives here because it is part of the boundary a transport must carry.\n\n/** Structural shape of the JSON messages on the session protocol. */\nexport interface SessionFrameCallbacks {\n onJson: (message: {\n type: string;\n id?: number;\n exit_code?: number | null;\n code?: string;\n message?: string;\n }) => void;\n onBinary: (frame: Buffer) => void;\n}\n\n/** Parses the daemon-to-client session framing: u8 type + u32be length + payload. */\nexport function createSessionFrameParser(\n callbacks: SessionFrameCallbacks,\n): (chunk: Buffer) => void {\n let buffer = Buffer.alloc(0);\n return (chunk: Buffer) => {\n buffer = Buffer.concat([buffer, chunk]);\n while (buffer.length >= 5) {\n const type = buffer.readUInt8(0);\n const length = buffer.readUInt32BE(1);\n if (buffer.length < 5 + length) return;\n const payload = buffer.subarray(5, 5 + length);\n buffer = buffer.subarray(5 + length);\n if (type === 1) {\n callbacks.onBinary(Buffer.from(payload));\n continue;\n }\n try {\n callbacks.onJson(JSON.parse(payload.toString(\"utf8\")));\n } catch {\n // ignore malformed frames\n }\n }\n };\n}\n\n/** Encodes a client-to-daemon session message: u32be length + JSON. */\nexport function encodeSessionMessage(message: object): Buffer {\n const payload = Buffer.from(JSON.stringify(message));\n const header = Buffer.alloc(4);\n header.writeUInt32BE(payload.length, 0);\n return Buffer.concat([header, payload]);\n}\n"]}
@@ -1,104 +0,0 @@
1
- import type { GondolinRemoteSettings, GondolinRemoteWorkerSettings } from "../types.js";
2
- import { type GondolinPlacementStore } from "./gondolin-placement.js";
3
- import { type GondolinRemoteOverrides } from "./gondolin-remote.js";
4
- import type { GondolinRuntimeHandle, GondolinRuntimeSpec, GondolinRuntimeTransport } from "./gondolin-worker-client.js";
5
- import type { ExecResult } from "./types.js";
6
- /** The connection surface the fleet needs from each worker. */
7
- export interface GondolinWorkerConnection {
8
- health(): Promise<Record<string, unknown>>;
9
- ensure(instanceId: string, spec: GondolinRuntimeSpec): Promise<GondolinRuntimeHandle>;
10
- stop(handle: Pick<GondolinRuntimeHandle, "workerPid" | "sessionId" | "instanceId">): Promise<void>;
11
- exec(handle: GondolinRuntimeHandle, command: string, options?: {
12
- env?: Record<string, string>;
13
- signal?: AbortSignal;
14
- }): Promise<ExecResult>;
15
- isRuntimeAlive(handle: GondolinRuntimeHandle): Promise<boolean>;
16
- listRuntimes(): Promise<Array<{
17
- sessionId: string;
18
- instanceId: string;
19
- }>>;
20
- dispose(): void;
21
- }
22
- interface GondolinFleetOverrides {
23
- createConnection?: (worker: GondolinRemoteWorkerSettings, connectionOverrides: GondolinRemoteOverrides) => GondolinWorkerConnection;
24
- placements?: GondolinPlacementStore;
25
- connectionOverrides?: GondolinRemoteOverrides;
26
- now?: () => number;
27
- queuePollMs?: number;
28
- sleep?: (ms: number) => Promise<void>;
29
- }
30
- /**
31
- * Places each conversation's runtime on one worker of the fleet and keeps it
32
- * there (sticky placement). Failover to another worker happens only when the
33
- * placed worker is unreachable AND its lease fencing watermark has passed —
34
- * the point after which the old worker's janitor has provably stopped the
35
- * runtime, so shared storage never sees two writers.
36
- */
37
- declare class GondolinFleetClient implements GondolinRuntimeTransport {
38
- private settings?;
39
- private workers;
40
- private connectionOverrides;
41
- private placements;
42
- private leaseTtlMs;
43
- private queueWaitMs;
44
- private queuePollMs;
45
- private now;
46
- private sleep;
47
- configure(settings?: GondolinRemoteSettings, overrides?: GondolinFleetOverrides): void;
48
- isConfigured(): boolean;
49
- /** Names of statically-configured workers with no workspaceRoot (startup guard). */
50
- workersMissingWorkspaceRoot(): string[];
51
- /**
52
- * Attach a dial-home worker at runtime (the gateway calls this when a
53
- * worker registers). The factory receives the fleet's connection overrides
54
- * plus the lease-watermark callback, mirroring configure()'s wiring.
55
- */
56
- attachWorker(settings: GondolinRemoteWorkerSettings, createConnection: (overrides: GondolinRemoteOverrides) => GondolinWorkerConnection): void;
57
- /**
58
- * Record a worker's shared-workspace health (the gateway pushes this from
59
- * register and ping frames). A degraded worker keeps its runtimes and its
60
- * connection, but new placements skip it and runtime operations fail fast
61
- * with the probe's diagnosis instead of hanging on a dead mount.
62
- */
63
- setWorkspaceHealth(name: string, error?: string): void;
64
- /**
65
- * Detach a dial-home worker (disconnect). Placements survive: their lease
66
- * watermarks keep fencing failover until expiry, exactly as for an
67
- * unreachable static worker.
68
- */
69
- detachWorker(name: string): void;
70
- imageSelector(): string | undefined;
71
- /** At least one worker must answer for the sandbox mode to come up. */
72
- health(): Promise<Array<{
73
- worker: string;
74
- health?: Record<string, unknown>;
75
- }>>;
76
- ensure(instanceId: string, spec: GondolinRuntimeSpec): Promise<GondolinRuntimeHandle>;
77
- stop(handle: Pick<GondolinRuntimeHandle, "workerPid" | "sessionId" | "instanceId" | "workerName">): Promise<void>;
78
- exec(handle: GondolinRuntimeHandle, command: string, options?: {
79
- env?: Record<string, string>;
80
- signal?: AbortSignal;
81
- }): Promise<ExecResult>;
82
- isRuntimeAlive(handle: GondolinRuntimeHandle): Promise<boolean>;
83
- /**
84
- * Fleet reconciliation: a runtime whose conversation is placed on a
85
- * different worker is a stray from a superseded placement — stop it. A
86
- * runtime with no placement at all (crash between daemon ensure and the
87
- * placement write) is adopted instead of killed.
88
- */
89
- reconcile(): Promise<void>;
90
- private ensureOn;
91
- /**
92
- * Fail fast — with the probe's diagnosis — instead of dispatching work that
93
- * would hang on a dead shared-workspace mount. Deliberately NOT a
94
- * GondolinWorkerUnreachableError: a broken mount must not trigger failover,
95
- * because the workspace is shared and likely broken for every worker.
96
- */
97
- private assertWorkspaceUsable;
98
- private assertFailoverAllowed;
99
- private selectWorker;
100
- private workerFor;
101
- }
102
- export declare const gondolinFleet: GondolinFleetClient;
103
- export {};
104
- //# sourceMappingURL=gondolin-fleet.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"gondolin-fleet.d.ts","sourceRoot":"","sources":["../../src/sandbox/gondolin-fleet.ts"],"names":[],"mappings":"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"}
@@ -1,330 +0,0 @@
1
- import * as log from "../log.js";
2
- import { SandboxError } from "./errors.js";
3
- import { gondolinPlacements } from "./gondolin-placement.js";
4
- import { GondolinRemoteConnection, GondolinWorkerUnreachableError, } from "./gondolin-remote.js";
5
- import { GondolinRuntimeGoneError } from "./gondolin-recovery.js";
6
- const LEASE_TTL_SECONDS = 300;
7
- const QUEUE_WAIT_SECONDS = 60;
8
- const QUEUE_POLL_MS = 2000;
9
- function normalizeWorkers(settings) {
10
- const inline = settings.workers ?? [{ url: settings.url }];
11
- return inline.map((worker) => ({
12
- ...worker,
13
- name: worker.name ?? worker.url,
14
- caFile: worker.caFile ?? settings.caFile,
15
- certFile: worker.certFile ?? settings.certFile,
16
- keyFile: worker.keyFile ?? settings.keyFile,
17
- workspaceRoot: worker.workspaceRoot ?? settings.workspaceRoot,
18
- maxRuntimes: worker.maxRuntimes ?? settings.maxRuntimes,
19
- }));
20
- }
21
- /**
22
- * Places each conversation's runtime on one worker of the fleet and keeps it
23
- * there (sticky placement). Failover to another worker happens only when the
24
- * placed worker is unreachable AND its lease fencing watermark has passed —
25
- * the point after which the old worker's janitor has provably stopped the
26
- * runtime, so shared storage never sees two writers.
27
- */
28
- class GondolinFleetClient {
29
- constructor() {
30
- this.workers = new Map();
31
- this.connectionOverrides = {};
32
- this.placements = gondolinPlacements;
33
- this.leaseTtlMs = LEASE_TTL_SECONDS * 1000;
34
- this.queueWaitMs = QUEUE_WAIT_SECONDS * 1000;
35
- this.queuePollMs = QUEUE_POLL_MS;
36
- this.now = Date.now;
37
- this.sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
38
- }
39
- configure(settings, overrides) {
40
- for (const worker of this.workers.values())
41
- worker.connection.dispose();
42
- this.workers.clear();
43
- this.settings = settings;
44
- this.placements = overrides?.placements ?? gondolinPlacements;
45
- this.now = overrides?.now ?? Date.now;
46
- this.sleep = overrides?.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
47
- this.queuePollMs = overrides?.queuePollMs ?? QUEUE_POLL_MS;
48
- this.connectionOverrides = overrides?.connectionOverrides ?? {};
49
- this.leaseTtlMs = (overrides?.connectionOverrides?.leaseTtlSeconds ?? LEASE_TTL_SECONDS) * 1000;
50
- this.queueWaitMs = (settings?.queueWaitSeconds ?? QUEUE_WAIT_SECONDS) * 1000;
51
- if (!settings)
52
- return;
53
- if (!settings.workers?.length && !settings.url) {
54
- return; // not configured; validate() reports the actionable error
55
- }
56
- const createConnection = overrides?.createConnection ??
57
- ((worker, connectionOverrides) => new GondolinRemoteConnection(worker, connectionOverrides));
58
- for (const worker of normalizeWorkers(settings)) {
59
- const name = worker.name;
60
- const connection = createConnection(worker, {
61
- ...overrides?.connectionOverrides,
62
- onLeaseActivity: (instanceId, expiresAtMs) => {
63
- if (this.placements.get(instanceId)?.worker === name) {
64
- this.placements.touch(instanceId, expiresAtMs);
65
- }
66
- },
67
- });
68
- this.workers.set(name, { name, settings: worker, connection });
69
- }
70
- }
71
- isConfigured() {
72
- return this.workers.size > 0;
73
- }
74
- /** Names of statically-configured workers with no workspaceRoot (startup guard). */
75
- workersMissingWorkspaceRoot() {
76
- return Array.from(this.workers.values())
77
- .filter((worker) => !worker.settings.workspaceRoot)
78
- .map((worker) => worker.name);
79
- }
80
- /**
81
- * Attach a dial-home worker at runtime (the gateway calls this when a
82
- * worker registers). The factory receives the fleet's connection overrides
83
- * plus the lease-watermark callback, mirroring configure()'s wiring.
84
- */
85
- attachWorker(settings, createConnection) {
86
- const name = settings.name ?? settings.url;
87
- this.workers.get(name)?.connection.dispose();
88
- const connection = createConnection({
89
- ...this.connectionOverrides,
90
- onLeaseActivity: (instanceId, expiresAtMs) => {
91
- if (this.placements.get(instanceId)?.worker === name) {
92
- this.placements.touch(instanceId, expiresAtMs);
93
- }
94
- },
95
- });
96
- this.workers.set(name, { name, settings: { ...settings, name }, connection });
97
- }
98
- /**
99
- * Record a worker's shared-workspace health (the gateway pushes this from
100
- * register and ping frames). A degraded worker keeps its runtimes and its
101
- * connection, but new placements skip it and runtime operations fail fast
102
- * with the probe's diagnosis instead of hanging on a dead mount.
103
- */
104
- setWorkspaceHealth(name, error) {
105
- const worker = this.workers.get(name);
106
- if (!worker)
107
- return;
108
- worker.workspaceError = error || undefined;
109
- }
110
- /**
111
- * Detach a dial-home worker (disconnect). Placements survive: their lease
112
- * watermarks keep fencing failover until expiry, exactly as for an
113
- * unreachable static worker.
114
- */
115
- detachWorker(name) {
116
- const worker = this.workers.get(name);
117
- if (!worker)
118
- return;
119
- worker.connection.dispose();
120
- this.workers.delete(name);
121
- }
122
- imageSelector() {
123
- return this.settings?.imageSelector;
124
- }
125
- /** At least one worker must answer for the sandbox mode to come up. */
126
- async health() {
127
- const report = await Promise.all(Array.from(this.workers.values()).map(async (worker) => {
128
- try {
129
- return { worker: worker.name, health: await worker.connection.health() };
130
- }
131
- catch {
132
- return { worker: worker.name };
133
- }
134
- }));
135
- if (report.length > 0 && !report.some((entry) => entry.health)) {
136
- throw new SandboxError("Error: no gondolin remote worker is reachable");
137
- }
138
- return report;
139
- }
140
- async ensure(instanceId, spec) {
141
- const placed = this.placements.get(instanceId);
142
- if (placed) {
143
- const worker = this.workers.get(placed.worker);
144
- if (worker) {
145
- try {
146
- return await this.ensureOn(worker, instanceId, spec);
147
- }
148
- catch (err) {
149
- if (!(err instanceof GondolinWorkerUnreachableError))
150
- throw err;
151
- this.assertFailoverAllowed(instanceId, placed.worker, placed.leaseExpiresAt, err);
152
- }
153
- }
154
- else {
155
- this.assertFailoverAllowed(instanceId, placed.worker, placed.leaseExpiresAt, new GondolinWorkerUnreachableError("worker is no longer configured"));
156
- }
157
- log.logWarning(`Gondolin worker '${placed.worker}' lost runtime for '${instanceId}'; failing over`);
158
- }
159
- const worker = await this.selectWorker();
160
- return this.ensureOn(worker, instanceId, spec);
161
- }
162
- async stop(handle) {
163
- const worker = this.workerFor(handle.workerName);
164
- if (!worker) {
165
- // the worker left the fleet config; its janitor stops the runtime when
166
- // the lease expires, and the record keeps fencing until then
167
- return;
168
- }
169
- try {
170
- await worker.connection.stop(handle);
171
- this.placements.delete(handle.instanceId);
172
- }
173
- catch (err) {
174
- if (err instanceof GondolinWorkerUnreachableError) {
175
- // cannot confirm the stop; keep the placement so its watermark still
176
- // fences any failover, and let the daemon's expiry janitor finish it
177
- log.logWarning(`Gondolin worker '${worker.name}' unreachable while stopping '${handle.instanceId}'; leaving it to lease expiry`);
178
- return;
179
- }
180
- throw err;
181
- }
182
- }
183
- async exec(handle, command, options = {}) {
184
- const worker = this.workerFor(handle.workerName);
185
- if (!worker)
186
- throw new GondolinRuntimeGoneError("worker is no longer configured");
187
- this.assertWorkspaceUsable(worker);
188
- log.logInfo(`Gondolin execution '${handle.instanceId}' routed to worker '${worker.name}'`);
189
- return worker.connection.exec(handle, command, options);
190
- }
191
- async isRuntimeAlive(handle) {
192
- const worker = this.workerFor(handle.workerName);
193
- if (!worker)
194
- return false;
195
- return worker.connection.isRuntimeAlive(handle);
196
- }
197
- /**
198
- * Fleet reconciliation: a runtime whose conversation is placed on a
199
- * different worker is a stray from a superseded placement — stop it. A
200
- * runtime with no placement at all (crash between daemon ensure and the
201
- * placement write) is adopted instead of killed.
202
- */
203
- async reconcile() {
204
- for (const worker of this.workers.values()) {
205
- let runtimes;
206
- try {
207
- runtimes = await worker.connection.listRuntimes();
208
- }
209
- catch {
210
- continue; // unreachable workers reconcile on a later sweep
211
- }
212
- for (const runtime of runtimes) {
213
- const record = this.placements.get(runtime.instanceId);
214
- if (!record) {
215
- log.logInfo(`Adopting unplaced Gondolin runtime '${runtime.instanceId}' on worker '${worker.name}'`);
216
- this.placements.set(runtime.instanceId, worker.name, this.now() + this.leaseTtlMs);
217
- continue;
218
- }
219
- if (record.worker === worker.name)
220
- continue;
221
- log.logInfo(`Stopping stray Gondolin runtime '${runtime.instanceId}' on worker '${worker.name}' (placed on '${record.worker}')`);
222
- try {
223
- await worker.connection.stop({
224
- sessionId: runtime.sessionId,
225
- instanceId: runtime.instanceId,
226
- workerPid: 0,
227
- });
228
- }
229
- catch (err) {
230
- log.logWarning(`Failed to stop stray runtime '${runtime.instanceId}' on '${worker.name}'`, err instanceof Error ? err.message : String(err));
231
- }
232
- }
233
- }
234
- }
235
- async ensureOn(worker, instanceId, spec) {
236
- this.assertWorkspaceUsable(worker);
237
- const handle = await worker.connection.ensure(instanceId, spec);
238
- this.placements.set(instanceId, worker.name, this.now() + this.leaseTtlMs);
239
- return { ...handle, workerName: worker.name };
240
- }
241
- /**
242
- * Fail fast — with the probe's diagnosis — instead of dispatching work that
243
- * would hang on a dead shared-workspace mount. Deliberately NOT a
244
- * GondolinWorkerUnreachableError: a broken mount must not trigger failover,
245
- * because the workspace is shared and likely broken for every worker.
246
- */
247
- assertWorkspaceUsable(worker) {
248
- if (!worker.workspaceError)
249
- return;
250
- throw new SandboxError(`Error: Gondolin worker '${worker.name}' has an unusable shared workspace: ` +
251
- `${worker.workspaceError} (fix the mount on the worker; health refreshes with its next heartbeat)`);
252
- }
253
- assertFailoverAllowed(instanceId, workerName, leaseExpiresAt, cause) {
254
- const remainingMs = leaseExpiresAt - this.now();
255
- if (remainingMs <= 0)
256
- return;
257
- // Failing over earlier could put a second writable VM on the same shared
258
- // workspace: the old worker may be alive behind a partition, and its
259
- // daemon only stops the runtime once the lease expires.
260
- throw new SandboxError(`Error: Gondolin worker '${workerName}' is unreachable (${cause.message}); ` +
261
- `retrying '${instanceId}' elsewhere in ${Math.ceil(remainingMs / 1000)}s once its lease has expired`);
262
- }
263
- async selectWorker() {
264
- const deadline = this.now() + this.queueWaitMs;
265
- for (;;) {
266
- let best;
267
- let reachable = 0;
268
- let draining = 0;
269
- let full = 0;
270
- const degraded = [];
271
- for (const worker of this.workers.values()) {
272
- if (worker.settings.draining) {
273
- draining += 1;
274
- continue;
275
- }
276
- if (worker.workspaceError) {
277
- degraded.push(`'${worker.name}': ${worker.workspaceError}`);
278
- continue;
279
- }
280
- let active;
281
- let workspaceError;
282
- try {
283
- const health = await worker.connection.health();
284
- active = typeof health.activeRuntimes === "number" ? health.activeRuntimes : 0;
285
- workspaceError =
286
- typeof health.workspaceError === "string" && health.workspaceError
287
- ? health.workspaceError
288
- : undefined;
289
- }
290
- catch {
291
- continue;
292
- }
293
- reachable += 1;
294
- if (workspaceError) {
295
- // static workers report probe results via /v1/health only
296
- degraded.push(`'${worker.name}': ${workspaceError}`);
297
- continue;
298
- }
299
- const cap = worker.settings.maxRuntimes;
300
- if (cap !== undefined && active >= cap) {
301
- full += 1;
302
- continue;
303
- }
304
- const load = cap !== undefined ? active / cap : active / Number.MAX_SAFE_INTEGER;
305
- if (!best || load < best.load)
306
- best = { worker, load };
307
- }
308
- if (best)
309
- return best.worker;
310
- if (reachable === 0 && draining === 0 && degraded.length === 0) {
311
- throw new SandboxError("Error: no gondolin remote worker is reachable");
312
- }
313
- if (full === 0) {
314
- const detail = degraded.length > 0 ? ` — degraded: ${degraded.join("; ")}` : "";
315
- throw new SandboxError(`Error: every gondolin remote worker is draining, degraded, or unreachable${detail}`);
316
- }
317
- if (this.now() >= deadline) {
318
- throw new SandboxError(`Error: all gondolin remote workers are at capacity (waited ${Math.round(this.queueWaitMs / 1000)}s)`);
319
- }
320
- await this.sleep(this.queuePollMs);
321
- }
322
- }
323
- workerFor(workerName) {
324
- if (!workerName)
325
- return undefined;
326
- return this.workers.get(workerName);
327
- }
328
- }
329
- export const gondolinFleet = new GondolinFleetClient();
330
- //# sourceMappingURL=gondolin-fleet.js.map