@fungi.computer/oyster 0.2.0 → 0.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 (291) hide show
  1. package/README.md +396 -72
  2. package/dist/agents.d.ts +3 -2
  3. package/dist/agents.js +4 -2
  4. package/dist/app-builds.d.ts +81 -8
  5. package/dist/app-builds.js +213 -60
  6. package/dist/app-storage.d.ts +3 -3
  7. package/dist/approval-capability.d.ts +5 -0
  8. package/dist/approval-capability.js +46 -0
  9. package/dist/approval-home.d.ts +51 -0
  10. package/dist/approval-home.js +226 -0
  11. package/dist/apps/catalog.d.ts +335 -0
  12. package/dist/apps/catalog.js +46 -0
  13. package/dist/apps/gate.d.ts +20 -6
  14. package/dist/apps/gate.js +42 -16
  15. package/dist/apps/index.d.ts +3 -1
  16. package/dist/apps/index.js +3 -1
  17. package/dist/apps/installation.d.ts +132 -51
  18. package/dist/apps/installation.js +176 -70
  19. package/dist/apps/permissions.d.ts +247 -0
  20. package/dist/apps/permissions.js +255 -0
  21. package/dist/boot-capability-route.d.ts +15 -0
  22. package/dist/boot-capability-route.js +196 -0
  23. package/dist/boot-capability.d.ts +103 -0
  24. package/dist/boot-capability.js +141 -0
  25. package/dist/boot-construction.d.ts +12 -0
  26. package/dist/boot-construction.js +175 -0
  27. package/dist/boot-gate.d.ts +267 -0
  28. package/dist/boot-gate.js +236 -0
  29. package/dist/boot-host-script.d.ts +2 -0
  30. package/dist/boot-host-script.js +1 -0
  31. package/dist/boot-mount.d.ts +21 -0
  32. package/dist/boot-mount.js +65 -0
  33. package/dist/browser/boot.d.ts +73 -0
  34. package/dist/browser/boot.js +502 -0
  35. package/dist/browser/browser.d.ts +104 -0
  36. package/dist/browser/browser.js +374 -0
  37. package/dist/browser/capability-source.d.ts +76 -0
  38. package/dist/browser/capability-source.js +338 -0
  39. package/dist/browser/cdp-client.d.ts +56 -0
  40. package/dist/browser/cdp-client.js +184 -0
  41. package/dist/browser/errors.d.ts +79 -0
  42. package/dist/browser/errors.js +32 -0
  43. package/dist/browser/index.d.ts +16 -0
  44. package/dist/browser/index.js +14 -0
  45. package/dist/browser/lease-registry.d.ts +61 -0
  46. package/dist/browser/lease-registry.js +101 -0
  47. package/dist/browser/operations.d.ts +225 -0
  48. package/dist/browser/operations.js +137 -0
  49. package/dist/browser/page.d.ts +25 -0
  50. package/dist/browser/page.js +172 -0
  51. package/dist/browser/quick-reads.d.ts +29 -0
  52. package/dist/browser/quick-reads.js +160 -0
  53. package/dist/browser/sessions.d.ts +52 -0
  54. package/dist/browser/sessions.js +118 -0
  55. package/dist/browser/url-policy.d.ts +18 -0
  56. package/dist/browser/url-policy.js +58 -0
  57. package/dist/build/app-imports.d.ts +12 -0
  58. package/dist/build/app-imports.js +105 -0
  59. package/dist/build/build-policy.d.ts +5 -3
  60. package/dist/build/build-policy.js +5 -3
  61. package/dist/build/candidate-builder.d.ts +1 -1
  62. package/dist/build/candidate-cli.mjs +8375 -334
  63. package/dist/build/local-build.d.ts +2 -0
  64. package/dist/build/local-build.js +28 -6
  65. package/dist/build/snapshot-package-store.d.ts +1 -1
  66. package/dist/build/snapshot-package-store.js +7 -2
  67. package/dist/build/workspace-build.d.ts +92 -5
  68. package/dist/build/workspace-build.js +93 -19
  69. package/dist/build.d.ts +1 -1
  70. package/dist/build.js +1 -1
  71. package/dist/catalog-frame.js +1 -1
  72. package/dist/cloudflare.d.ts +13 -3
  73. package/dist/cloudflare.js +102 -4
  74. package/dist/frame-binding-wire-script.js +1 -1
  75. package/dist/frame-binding-wire.js +4 -2
  76. package/dist/git-auth.d.ts +0 -2
  77. package/dist/git-auth.js +0 -2
  78. package/dist/git.d.ts +0 -1
  79. package/dist/git.js +0 -2
  80. package/dist/host.d.ts +39 -13
  81. package/dist/host.js +268 -142
  82. package/dist/index.d.ts +25 -15
  83. package/dist/index.js +39 -16
  84. package/dist/install-release-cli.d.ts +2 -0
  85. package/dist/install-release-cli.js +46 -0
  86. package/dist/installation-client.d.ts +43 -0
  87. package/dist/installation-client.js +178 -0
  88. package/dist/installation-execution.d.ts +11 -0
  89. package/dist/installation-execution.js +184 -0
  90. package/dist/installation-operations.d.ts +1437 -0
  91. package/dist/installation-operations.js +664 -0
  92. package/dist/installation-store-generation.d.ts +3 -0
  93. package/dist/installation-store-generation.js +16 -0
  94. package/dist/instance-id.d.ts +10 -0
  95. package/dist/instance-id.js +10 -0
  96. package/dist/instance-refusal.d.ts +11 -0
  97. package/dist/instance-refusal.js +21 -0
  98. package/dist/internal/delivery.js +4 -2
  99. package/dist/internal/host-gate.js +7 -1
  100. package/dist/internal/registry-installations.js +4 -4
  101. package/dist/keys.d.ts +5 -1
  102. package/dist/keys.js +10 -2
  103. package/dist/machine-link/authority.d.ts +89 -0
  104. package/dist/machine-link/authority.js +37 -0
  105. package/dist/machine-link/client.d.ts +66 -0
  106. package/dist/machine-link/client.js +94 -0
  107. package/dist/machine-link/delivery.d.ts +18 -0
  108. package/dist/machine-link/delivery.js +55 -0
  109. package/dist/machine-link/enrollment.d.ts +111 -0
  110. package/dist/machine-link/enrollment.js +286 -0
  111. package/dist/machine-link/fences.d.ts +37 -0
  112. package/dist/machine-link/fences.js +166 -0
  113. package/dist/machine-link/home.d.ts +20 -0
  114. package/dist/machine-link/home.js +24 -0
  115. package/dist/machine-link/host.d.ts +43 -0
  116. package/dist/machine-link/host.js +189 -0
  117. package/dist/machine-link/index.d.ts +15 -0
  118. package/dist/machine-link/index.js +15 -0
  119. package/dist/machine-link/link.d.ts +48 -0
  120. package/dist/machine-link/link.js +223 -0
  121. package/dist/machine-link/presence.d.ts +10 -0
  122. package/dist/machine-link/presence.js +25 -0
  123. package/dist/machine-link/workers.d.ts +6 -0
  124. package/dist/machine-link/workers.js +6 -0
  125. package/dist/mcp-owner.d.ts +2 -2
  126. package/dist/mcp-owner.js +41 -33
  127. package/dist/mcp.d.ts +15 -3
  128. package/dist/mcp.js +146 -25
  129. package/dist/older-oyster-store-refusal.d.ts +4 -0
  130. package/dist/older-oyster-store-refusal.js +18 -0
  131. package/dist/older-oyster-store.d.ts +21 -0
  132. package/dist/older-oyster-store.js +43 -0
  133. package/dist/pack.d.ts +14 -3
  134. package/dist/pack.js +44 -19
  135. package/dist/page/app-access.d.ts +7 -0
  136. package/dist/page/app-access.js +75 -0
  137. package/dist/page/boot-host.js +57 -0
  138. package/dist/page/capabilities.d.ts +3 -3
  139. package/dist/page/capabilities.js +16 -12
  140. package/dist/page/computer-handles.d.ts +3 -3
  141. package/dist/page/computer-target.d.ts +3 -0
  142. package/dist/page/computer-target.js +9 -0
  143. package/dist/page/mount.d.ts +23 -3
  144. package/dist/page/mount.js +152 -16
  145. package/dist/page-assets.d.ts +22 -0
  146. package/dist/page-assets.js +28 -0
  147. package/dist/page-script.d.ts +3 -0
  148. package/dist/page-script.js +5 -4
  149. package/dist/release.d.ts +13 -6
  150. package/dist/release.js +10 -7
  151. package/dist/root-data.d.ts +16 -6
  152. package/dist/root-data.js +2 -2
  153. package/dist/source/index.d.ts +1 -1
  154. package/dist/source/team-source.d.ts +20 -8
  155. package/dist/source/team-source.js +39 -10
  156. package/dist/source/template-seed.d.ts +2 -2
  157. package/dist/standalone/agent-mail.d.ts +24 -0
  158. package/dist/standalone/agent-mail.js +62 -0
  159. package/dist/standalone/agent-store.d.ts +9 -0
  160. package/dist/standalone/agent-store.js +30 -0
  161. package/dist/standalone/agents.d.ts +68 -9
  162. package/dist/standalone/agents.js +428 -56
  163. package/dist/standalone/app-builds-binding.d.ts +20 -2
  164. package/dist/standalone/app-builds-binding.js +147 -23
  165. package/dist/standalone/app-set.d.ts +5 -2
  166. package/dist/standalone/app-set.js +186 -144
  167. package/dist/standalone/browser.d.ts +11 -0
  168. package/dist/standalone/browser.js +72 -0
  169. package/dist/standalone/builds.d.ts +13 -4
  170. package/dist/standalone/builds.js +28 -44
  171. package/dist/standalone/computer-egress.d.ts +2 -0
  172. package/dist/standalone/computer-egress.js +13 -4
  173. package/dist/standalone/computer-fleet.d.ts +43 -0
  174. package/dist/standalone/computer-fleet.js +200 -0
  175. package/dist/standalone/computer.d.ts +44 -9
  176. package/dist/standalone/computer.js +129 -22
  177. package/dist/standalone/install-apps-cli.js +43 -6
  178. package/dist/standalone/instance.d.ts +2 -2
  179. package/dist/standalone/instance.js +8 -11
  180. package/dist/standalone/machine-link.d.ts +354 -0
  181. package/dist/standalone/machine-link.js +368 -0
  182. package/dist/standalone/media.d.ts +2 -2
  183. package/dist/standalone/media.js +19 -16
  184. package/dist/standalone/preview.d.ts +2 -2
  185. package/dist/standalone/preview.js +37 -10
  186. package/dist/standalone/registry.d.ts +151 -9
  187. package/dist/standalone/registry.js +305 -8
  188. package/dist/standalone/worker.d.ts +19 -5
  189. package/dist/standalone/worker.js +90 -54
  190. package/dist/standalone/workflow-binding.d.ts +18 -22
  191. package/dist/standalone/workflow-binding.js +18 -18
  192. package/dist/standalone/workflow-control.d.ts +4 -0
  193. package/dist/standalone/workflow-control.js +40 -0
  194. package/dist/standalone/workflow-events.d.ts +2 -2
  195. package/dist/standalone/workflow-events.js +9 -10
  196. package/dist/standalone/workflow-git-capture.js +4 -3
  197. package/dist/standalone/workflow-host.d.ts +162 -161
  198. package/dist/standalone/workflow-host.js +101 -21
  199. package/dist/standalone/workflow-node-capture.js +11 -4
  200. package/dist/standalone/workflow-schedules.d.ts +5 -2
  201. package/dist/standalone/workflow-schedules.js +9 -10
  202. package/dist/standalone/workflow-trigger-host.d.ts +65 -5
  203. package/dist/standalone/workflow-trigger-host.js +2 -2
  204. package/dist/standalone/workflow-webhooks.d.ts +22 -0
  205. package/dist/standalone/workflow-webhooks.js +39 -0
  206. package/dist/standalone/workflows.d.ts +140 -43
  207. package/dist/standalone/workflows.js +198 -39
  208. package/dist/storage-frame.d.ts +6 -6
  209. package/dist/testing/capabilities.js +14 -3
  210. package/dist/testing/installation.js +26 -2
  211. package/dist/testing/worker.js +1 -1
  212. package/dist/testing/workers-runtime.d.ts +8 -2
  213. package/dist/testing/workers-runtime.js +20 -2
  214. package/dist/webhook-abuse.d.ts +25 -0
  215. package/dist/webhook-abuse.js +54 -0
  216. package/dist/webhook-custody.d.ts +144 -0
  217. package/dist/webhook-custody.js +222 -0
  218. package/dist/webhook-ingress.d.ts +36 -0
  219. package/dist/webhook-ingress.js +146 -0
  220. package/dist/webhooks.d.ts +111 -0
  221. package/dist/webhooks.js +189 -0
  222. package/dist/workers-git.d.ts +12 -17
  223. package/dist/workers-git.js +64 -52
  224. package/dist/workers.d.ts +226 -13
  225. package/dist/workers.js +363 -51
  226. package/dist/workflow-agent-authority.d.ts +8 -0
  227. package/dist/workflow-agent-authority.js +42 -0
  228. package/dist/workflows.d.ts +66 -0
  229. package/dist/workflows.js +97 -0
  230. package/docs/api/README.md +63 -37
  231. package/docs/api/agents.md +17 -0
  232. package/docs/api/app-storage.md +1 -1
  233. package/docs/api/apps.md +1394 -543
  234. package/docs/api/browser/boot.md +132 -0
  235. package/docs/api/browser.md +1440 -0
  236. package/docs/api/build-contract.md +38 -25
  237. package/docs/api/build.md +263 -84
  238. package/docs/api/cloudflare.md +93 -7
  239. package/docs/api/git.md +3 -13
  240. package/docs/api/host.md +944 -118
  241. package/docs/api/index.md +320 -18
  242. package/docs/api/keys.md +24 -1
  243. package/docs/api/mcp.md +59 -12
  244. package/docs/api/modules.md +33 -40
  245. package/docs/api/pack.md +57 -8
  246. package/docs/api/page/mount.md +92 -4
  247. package/docs/api/workers-git.md +103 -24
  248. package/docs/api/workers.md +1594 -244
  249. package/docs/site/agent-quickstart.mdx +28 -14
  250. package/examples/standalone/worker.ts +3 -1
  251. package/examples/standalone/wrangler.jsonc +2 -0
  252. package/package.json +73 -71
  253. package/dist/installation-consent.d.ts +0 -81
  254. package/dist/installation-consent.js +0 -84
  255. package/dist/shell/app-directory.d.ts +0 -542
  256. package/dist/shell/app-directory.js +0 -196
  257. package/dist/shell/browser-actions.d.ts +0 -7
  258. package/dist/shell/browser-actions.js +0 -56
  259. package/dist/shell/command-host.d.ts +0 -112
  260. package/dist/shell/command-host.js +0 -502
  261. package/dist/shell/commands.d.ts +0 -25
  262. package/dist/shell/commands.js +0 -78
  263. package/dist/shell/index.d.ts +0 -8
  264. package/dist/shell/index.js +0 -8
  265. package/dist/shell/keys.d.ts +0 -41
  266. package/dist/shell/keys.js +0 -134
  267. package/dist/shell/shell-module.d.ts +0 -55
  268. package/dist/shell/shell-module.js +0 -86
  269. package/dist/shell/window-manager.d.ts +0 -128
  270. package/dist/shell/window-manager.js +0 -730
  271. package/dist/shell/workbench-contract.d.ts +0 -68
  272. package/dist/shell/workbench-directory.d.ts +0 -332
  273. package/dist/shell/workbench-directory.js +0 -35
  274. package/dist/shell/workbench-host-types.d.ts +0 -96
  275. package/dist/shell/workbench-host-types.js +0 -1
  276. package/dist/shell/workbench-host.d.ts +0 -34
  277. package/dist/shell/workbench-host.js +0 -352
  278. package/dist/shell/workbench-model.d.ts +0 -194
  279. package/dist/shell/workbench-model.js +0 -53
  280. package/dist/shell/workbench.d.ts +0 -4
  281. package/dist/shell/workbench.js +0 -2
  282. package/docs/api/shell/app-directory.md +0 -599
  283. package/docs/api/shell/command-host.md +0 -224
  284. package/docs/api/shell/commands.md +0 -197
  285. package/docs/api/shell/keys.md +0 -183
  286. package/docs/api/shell/shell-module.md +0 -204
  287. package/docs/api/shell/window-manager.md +0 -267
  288. package/docs/api/shell/workbench-directory.md +0 -119
  289. package/docs/api/shell/workbench.md +0 -205
  290. package/docs/api/shell.md +0 -1915
  291. /package/dist/{shell/workbench-contract.js → page/boot-host.d.ts} +0 -0
@@ -0,0 +1,189 @@
1
+ import { Hono } from "hono";
2
+ import { z } from "zod";
3
+ import { correlationKey } from "@fungi.computer/cairn";
4
+ import { createMachineLinkPostChannel, MachineLinkPostUnavailable, machineLinkAudience, } from "@fungi.computer/seam/machine-link-protocol";
5
+ import { importSeamVerifiers, seamHeader, verifySeamRequest, } from "@fungi.computer/seam";
6
+ import { createLocalBackendReplayStore } from "@fungi.computer/computer";
7
+ import { createMachineLinkSockets } from "./presence.js";
8
+ const attachmentSchema = z.strictObject({
9
+ deviceId: z.string().min(1).max(256),
10
+ fenceEpoch: z.int().nonnegative(),
11
+ });
12
+ const maximumInFlight = 32;
13
+ /**
14
+ * Native device socket I/O. Pins/member authority and mail admission are supplied
15
+ * by their existing owners. Native attachments survive hibernation; pending
16
+ * Post I/O does not claim durable custody and is retried by Carrier.
17
+ */
18
+ export class MachineLinkHost {
19
+ context;
20
+ options;
21
+ channels = new Map();
22
+ replay;
23
+ routes;
24
+ constructor(context, owner, options) {
25
+ this.context = context;
26
+ this.options = options;
27
+ this.replay = createLocalBackendReplayStore(owner);
28
+ context.setWebSocketAutoResponse(new WebSocketRequestResponsePair("ping", "pong"));
29
+ this.routes = new Hono().get("/api/teams/:teamId/local/mesh/:deviceId/connect", async (context) => {
30
+ if (context.req.param("teamId") !== options.teamId)
31
+ return context.body(null, 403);
32
+ const request = context.req.raw;
33
+ if (request.headers.get("Upgrade")?.toLowerCase() !== "websocket")
34
+ return context.body(null, 426);
35
+ const deviceId = context.req.param("deviceId");
36
+ try {
37
+ const device = await options.authorize(deviceId);
38
+ const keyId = await verifySeamRequest(await importSeamVerifiers({ [deviceId]: device.publicKey }), machineLinkAudience(options.teamId, deviceId), request.method, request.url, request.headers.get(seamHeader), null, this.replay);
39
+ if (keyId !== deviceId)
40
+ return context.body(null, 403);
41
+ const pair = new WebSocketPair();
42
+ pair[1].serializeAttachment({
43
+ deviceId,
44
+ fenceEpoch: device.fenceEpoch,
45
+ });
46
+ createMachineLinkSockets(this.context, deviceId).accept(pair[1]);
47
+ await this.publishDirectory(pair[1], deviceId);
48
+ return new Response(null, { status: 101, webSocket: pair[0] });
49
+ }
50
+ catch (cause) {
51
+ this.options.report("machine-link.dial", cause);
52
+ return context.body(null, 403);
53
+ }
54
+ });
55
+ }
56
+ fetch(request) {
57
+ return Promise.resolve(this.routes.fetch(request));
58
+ }
59
+ async publishDirectory(socket, deviceId) {
60
+ // Ordered before the addressed operation, but its metadata ACK is not a
61
+ // prerequisite for work admission (including the deliberately lost ACK).
62
+ const agents = await this.options.directory();
63
+ this.context.waitUntil(this.channel(socket, deviceId)
64
+ .deliver({
65
+ key: `directory:${correlationKey()}`,
66
+ kind: "machineDirectory",
67
+ address: { teamId: this.options.teamId, deviceId },
68
+ event: {
69
+ agents: agents.map(({ agentId, name }) => ({ agentId, name })),
70
+ },
71
+ })
72
+ .then(() => undefined, (cause) => {
73
+ this.options.report("machine-link.directory", cause);
74
+ socket.close(1011, "directory unavailable");
75
+ }));
76
+ }
77
+ channel(socket, deviceId) {
78
+ let channel = this.channels.get(socket);
79
+ if (channel === undefined) {
80
+ // Native stream ordering survives asynchronous authority pulls. Bound
81
+ // writes to the combined send/receive budgets independently of receipt
82
+ // timeouts; this is not a custody or retry queue.
83
+ const writer = new WritableStream({
84
+ write: async (frame) => {
85
+ await this.authorizeSocket(socket, deviceId);
86
+ socket.send(frame);
87
+ },
88
+ }, new CountQueuingStrategy({ highWaterMark: 2 * maximumInFlight })).getWriter();
89
+ channel = createMachineLinkPostChannel({
90
+ limits: {
91
+ maxInFlight: maximumInFlight,
92
+ maxFrameBytes: 512 * 1024,
93
+ acknowledgmentMs: 10_000,
94
+ },
95
+ send: (frame) => {
96
+ // A receipt timeout releases Post custody, not a still-blocked
97
+ // native write. Enforce the stream's bound before enqueueing again.
98
+ if (writer.desiredSize === null || writer.desiredSize <= 0)
99
+ throw new MachineLinkPostUnavailable({ reason: "busy" });
100
+ return writer.write(frame);
101
+ },
102
+ receive: async (envelope) => {
103
+ const device = await this.authorizeSocket(socket, deviceId);
104
+ return await this.options.receive(device, envelope);
105
+ },
106
+ });
107
+ this.channels.set(socket, channel);
108
+ }
109
+ return channel;
110
+ }
111
+ async deliver(deviceId, envelope) {
112
+ await this.options.authorize(deviceId);
113
+ const links = createMachineLinkSockets(this.context, deviceId);
114
+ const socket = this.context
115
+ .getWebSockets(`machine-link:${deviceId}`)
116
+ .find((candidate) => links.isCurrent(candidate));
117
+ if (socket === undefined)
118
+ throw new MachineLinkPostUnavailable({ reason: "closed" });
119
+ await this.publishDirectory(socket, deviceId);
120
+ return await this.channel(socket, deviceId).deliver(envelope);
121
+ }
122
+ async message(socket, message) {
123
+ try {
124
+ const { deviceId } = attachmentSchema.parse(socket.deserializeAttachment());
125
+ if (!createMachineLinkSockets(this.context, deviceId).isCurrent(socket)) {
126
+ socket.close(4001, "replaced");
127
+ return;
128
+ }
129
+ await this.authorizeSocket(socket, deviceId);
130
+ const text = z.string().safeParse(message);
131
+ if (!text.success) {
132
+ socket.close(1003, "text required");
133
+ return;
134
+ }
135
+ await this.channel(socket, deviceId).receive(text.data);
136
+ }
137
+ catch (cause) {
138
+ this.#fail(socket, cause);
139
+ }
140
+ }
141
+ close(socket) {
142
+ this.channels.get(socket)?.close();
143
+ this.channels.delete(socket);
144
+ }
145
+ // A message that cannot be admitted or received ends this link: the host
146
+ // records the cause and the device sees 1011, then reconnects with a fresh dial.
147
+ #fail(socket, cause) {
148
+ this.options.report("machine-link.message", cause);
149
+ this.close(socket);
150
+ socket.close(1011, "machine link unavailable");
151
+ }
152
+ async authorizeSocket(socket, deviceId) {
153
+ const attachment = attachmentSchema.parse(socket.deserializeAttachment());
154
+ const device = await this.options
155
+ .authorize(deviceId)
156
+ .catch((cause) => {
157
+ this.close(socket);
158
+ socket.close(4003, "authority unavailable");
159
+ throw cause;
160
+ });
161
+ if (attachment.deviceId !== deviceId ||
162
+ attachment.fenceEpoch !== device.fenceEpoch ||
163
+ !createMachineLinkSockets(this.context, deviceId).isCurrent(socket)) {
164
+ this.close(socket);
165
+ socket.close(4003, "authority changed");
166
+ throw new MachineLinkPostUnavailable({ reason: "closed" });
167
+ }
168
+ return device;
169
+ }
170
+ /** A delayed old fence cannot retire a fresh connection at the current epoch. */
171
+ fence(fenceEpoch, live) {
172
+ for (const socket of this.context.getWebSockets()) {
173
+ const pin = attachmentSchema.safeParse(socket.deserializeAttachment());
174
+ if (!pin.success ||
175
+ pin.data.fenceEpoch < fenceEpoch ||
176
+ (!live && pin.data.fenceEpoch === fenceEpoch)) {
177
+ this.close(socket);
178
+ socket.close(4003, "authority changed");
179
+ }
180
+ }
181
+ }
182
+ /** Authorized physical erasure retires even hibernated sockets, at any epoch. */
183
+ retireForErasure() {
184
+ for (const socket of this.context.getWebSockets()) {
185
+ this.close(socket);
186
+ socket.close(4003, "erased");
187
+ }
188
+ }
189
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * One device's outbound machine link, shared by every Oyster host. The host
3
+ * supplies live authority; this entry owns enrollment, the fence outbox and
4
+ * the shared link types, and runs on any host. Device sockets, signed-dial
5
+ * replay and Post custody need the Workers runtime and live in
6
+ * `@fungi.computer/oyster/machine-link/workers`. The wire protocol stays in
7
+ * `@fungi.computer/seam/machine-link-protocol`.
8
+ */
9
+ export * from "./authority.js";
10
+ export * from "./enrollment.js";
11
+ export * from "./fences.js";
12
+ export * from "./home.js";
13
+ export * from "./presence.js";
14
+ export * from "./client.js";
15
+ export * from "./delivery.js";
@@ -0,0 +1,15 @@
1
+ /**
2
+ * One device's outbound machine link, shared by every Oyster host. The host
3
+ * supplies live authority; this entry owns enrollment, the fence outbox and
4
+ * the shared link types, and runs on any host. Device sockets, signed-dial
5
+ * replay and Post custody need the Workers runtime and live in
6
+ * `@fungi.computer/oyster/machine-link/workers`. The wire protocol stays in
7
+ * `@fungi.computer/seam/machine-link-protocol`.
8
+ */
9
+ export * from "./authority.js";
10
+ export * from "./enrollment.js";
11
+ export * from "./fences.js";
12
+ export * from "./home.js";
13
+ export * from "./presence.js";
14
+ export * from "./client.js";
15
+ export * from "./delivery.js";
@@ -0,0 +1,48 @@
1
+ import * as Schema from "effect/Schema";
2
+ import { type MachineAgentOperation } from "@fungi.computer/seam/machine-link-protocol";
3
+ import type { PostDeliveryResult, PostEnvelope } from "@fungi.computer/carrier/post";
4
+ import type { WatchdogSqliteOwner } from "@fungi.computer/watchdog";
5
+ import { type MachineLinkAuthority, type MachineLinkCaller, type MachineLinkFenceFact, type MachineLinkFenceResult, type MachineLinkIdentity } from "./authority.js";
6
+ /** The Durable Object state a machine link needs from its host class. */
7
+ export type MachineLinkState = Pick<DurableObjectState, "getWebSockets" | "acceptWebSocket" | "setWebSocketAutoResponse" | "waitUntil"> & Readonly<{
8
+ storage: WatchdogSqliteOwner & Pick<DurableObjectStorage, "get" | "put">;
9
+ }>;
10
+ /** The host's own storage erasure, so readback matches its other owners. */
11
+ export type MachineLinkStorageErasure<Readback> = Readonly<{
12
+ erased(): Promise<boolean>;
13
+ erase(): Promise<Readback>;
14
+ read(): Promise<Readback>;
15
+ }>;
16
+ /**
17
+ * One device's link inside its own Durable Object. The host's DO class
18
+ * delegates every entry point here. Identity is pinned on the first live
19
+ * answer and every act rereads the host's authority; nothing is written
20
+ * before the host says the device is live.
21
+ */
22
+ export declare function createMachineLink<Readback>(state: MachineLinkState, options: Readonly<{
23
+ authority: Omit<MachineLinkAuthority, "deliverControlFact">;
24
+ /** Whether this DO is the canonical owner for the identity. */
25
+ addressed: (identity: MachineLinkIdentity) => boolean;
26
+ erasure: MachineLinkStorageErasure<Readback>;
27
+ report: (operation: string, cause: unknown) => void;
28
+ }>): {
29
+ fetch: (request: Request) => Promise<Response>;
30
+ /** Mail addressed to this device's Agent, rechecked against its home. */
31
+ deliver(input: MachineLinkIdentity, caller: MachineLinkCaller, agentId: string, envelope: PostEnvelope): Promise<PostDeliveryResult>;
32
+ /** One finite Agent operation on this device, returning its receipt. */
33
+ operation(input: MachineLinkIdentity, caller: MachineLinkCaller, agentId: string, raw: MachineAgentOperation): Promise<Schema.Json>;
34
+ /**
35
+ * Apply one committed fence fact pushed by the host's producer. The fact
36
+ * is the push; the link does not read authority back here. Sockets at an
37
+ * older epoch close, and a not-live fact closes the current one too. The
38
+ * per-frame live read remains the guarantee.
39
+ */
40
+ applyFence(raw: MachineLinkFenceFact): MachineLinkFenceResult;
41
+ preflightErasure: (teamId: string, deviceId: string) => Promise<void>;
42
+ eraseForTeam(teamId: string, deviceId: string): Promise<Readback>;
43
+ readErasure: () => Promise<Readback>;
44
+ webSocketMessage(socket: WebSocket, message: string | ArrayBuffer): Promise<void>;
45
+ webSocketClose(socket: WebSocket, code: number, reason: string): void;
46
+ webSocketError(socket: WebSocket): void;
47
+ };
48
+ export type MachineLink = ReturnType<typeof createMachineLink>;
@@ -0,0 +1,223 @@
1
+ import { createHash } from "node:crypto";
2
+ import { Hono } from "hono";
3
+ import * as Schema from "effect/Schema";
4
+ import { BirdDogMailSchema } from "@fungi.computer/bird-dog/effect";
5
+ import { agentMailAddressSchema, machineAgentOperationSchema, MachineLinkPostUnavailable, } from "@fungi.computer/seam/machine-link-protocol";
6
+ import { machineLinkFenceFactSchema, machineLinkIdentitySchema, machineLinkName, MachineLinkRefused, } from "./authority.js";
7
+ import { MachineLinkHost } from "./host.js";
8
+ const pinKey = "machine-link:identity";
9
+ const maximumMailBytes = 256 * 1024;
10
+ /**
11
+ * One device's link inside its own Durable Object. The host's DO class
12
+ * delegates every entry point here. Identity is pinned on the first live
13
+ * answer and every act rereads the host's authority; nothing is written
14
+ * before the host says the device is live.
15
+ */
16
+ export function createMachineLink(state, options) {
17
+ const { authority } = options;
18
+ let host;
19
+ const match = (identity) => {
20
+ if (!options.addressed(identity))
21
+ throw new MachineLinkRefused("actor_not_allowed");
22
+ };
23
+ const savedIdentity = async () => {
24
+ const raw = await state.storage.get(pinKey);
25
+ return raw === undefined ? null : machineLinkIdentitySchema.parse(raw);
26
+ };
27
+ const current = async (input) => {
28
+ const proposed = machineLinkIdentitySchema.parse(input);
29
+ const saved = await savedIdentity();
30
+ if (saved !== null && machineLinkName(saved) !== machineLinkName(proposed))
31
+ throw new MachineLinkRefused("actor_not_allowed");
32
+ const identity = saved ?? proposed;
33
+ match(identity);
34
+ if (await options.erasure.erased())
35
+ throw new MachineLinkPostUnavailable({ reason: "closed" });
36
+ // Pinned address only. Neither a forwarding header nor caller-provided key
37
+ // can lend authority to this link.
38
+ const facts = await authority.admitDevice(identity);
39
+ if (!facts.live)
40
+ throw new MachineLinkPostUnavailable({ reason: "closed" });
41
+ match(facts);
42
+ if (facts.teamId !== identity.teamId ||
43
+ facts.deviceId !== identity.deviceId)
44
+ throw new MachineLinkRefused("actor_not_allowed");
45
+ // First product write is after the host's independent live decision.
46
+ if (saved === null)
47
+ await state.storage.put(pinKey, identity);
48
+ return facts;
49
+ };
50
+ const mail = (envelope) => {
51
+ const address = Schema.decodeUnknownSync(agentMailAddressSchema, {
52
+ onExcessProperty: "error",
53
+ })(envelope.address);
54
+ const body = Schema.decodeUnknownSync(BirdDogMailSchema, {
55
+ onExcessProperty: "error",
56
+ })(envelope.event);
57
+ return { address, mail: body };
58
+ };
59
+ /**
60
+ * The link's own sender check, whatever the host answered: the facts must
61
+ * name the caller and actor it saw, a hosted caller must be hosted, and a
62
+ * device caller must be an Agent homed on that live device, same member.
63
+ */
64
+ const checkedTarget = async (identity, caller, agentId, requesterAgentId) => {
65
+ const facts = await authority.resolveTarget(identity, caller, agentId, requesterAgentId);
66
+ if (facts.teamId !== identity.teamId ||
67
+ facts.callerAgentId !== caller.agentId ||
68
+ facts.targetAgentId !== agentId ||
69
+ facts.actorAgentId !== (requesterAgentId ?? caller.agentId))
70
+ throw new MachineLinkRefused("actor_not_allowed");
71
+ if (caller.kind === "hosted") {
72
+ if (facts.callerHome.kind !== "hosted")
73
+ throw new MachineLinkRefused("actor_not_allowed");
74
+ return facts;
75
+ }
76
+ const source = await authority.admitDevice({
77
+ teamId: identity.teamId,
78
+ deviceId: caller.deviceId,
79
+ });
80
+ if (!source.live ||
81
+ source.teamId !== identity.teamId ||
82
+ source.deviceId !== caller.deviceId ||
83
+ facts.callerHome.kind !== "machine" ||
84
+ facts.callerHome.deviceId !== source.deviceId)
85
+ throw new MachineLinkRefused("actor_not_allowed");
86
+ return facts;
87
+ };
88
+ const receive = async (identity, envelope) => {
89
+ await current(identity);
90
+ if (envelope.kind !== "agentMail")
91
+ return "refused";
92
+ const { address, mail: body } = mail(envelope);
93
+ const caller = {
94
+ kind: "device",
95
+ deviceId: identity.deviceId,
96
+ agentId: body.from.agentId,
97
+ };
98
+ const target = await checkedTarget(identity, caller, address.agentId, null);
99
+ if (envelope.key !== body.key ||
100
+ address.scopeId !== target.teamId ||
101
+ address.agentId !== body.to.agentId ||
102
+ new TextEncoder().encode(JSON.stringify(body)).byteLength >
103
+ maximumMailBytes)
104
+ return "refused";
105
+ return await authority.deliverHome(identity, caller, target, envelope);
106
+ };
107
+ const hostFor = (input) => {
108
+ const identity = { teamId: input.teamId, deviceId: input.deviceId };
109
+ return (host ??= new MachineLinkHost(state, state.storage, {
110
+ teamId: identity.teamId,
111
+ authorize: async (deviceId) => {
112
+ if (deviceId !== identity.deviceId)
113
+ throw new MachineLinkRefused("actor_not_allowed");
114
+ return await current(identity);
115
+ },
116
+ directory: () => authority.directory(identity),
117
+ receive: (_device, envelope) => receive(identity, envelope),
118
+ report: options.report,
119
+ }));
120
+ };
121
+ const routes = new Hono().get("/api/teams/:teamId/local/mesh/:deviceId/connect", async (context) => {
122
+ const identity = machineLinkIdentitySchema.parse(context.req.param());
123
+ try {
124
+ await current(identity);
125
+ return await hostFor(identity).fetch(context.req.raw);
126
+ }
127
+ catch {
128
+ return context.body(null, 403);
129
+ }
130
+ });
131
+ const preflightErasure = async (teamId, deviceId) => {
132
+ const identity = machineLinkIdentitySchema.parse({ teamId, deviceId });
133
+ match(identity);
134
+ if (await options.erasure.erased())
135
+ return;
136
+ if (!(await authority.canErase(identity)))
137
+ throw new MachineLinkRefused("actor_not_allowed");
138
+ };
139
+ return {
140
+ fetch: (request) => Promise.resolve(routes.fetch(request)),
141
+ /** Mail addressed to this device's Agent, rechecked against its home. */
142
+ async deliver(input, caller, agentId, envelope) {
143
+ const device = await current(input);
144
+ if (envelope.kind !== "agentMail")
145
+ return "refused";
146
+ const { address, mail: body } = mail(envelope);
147
+ const facts = await checkedTarget(device, caller, agentId, null);
148
+ if (facts.targetHome.kind !== "machine" ||
149
+ facts.targetHome.deviceId !== device.deviceId ||
150
+ envelope.key !== body.key ||
151
+ address.scopeId !== facts.teamId ||
152
+ address.agentId !== facts.targetAgentId ||
153
+ body.to.agentId !== facts.targetAgentId ||
154
+ body.from.agentId !== facts.callerAgentId ||
155
+ new TextEncoder().encode(JSON.stringify(body)).byteLength >
156
+ maximumMailBytes)
157
+ return "refused";
158
+ return await hostFor(device).deliver(device.deviceId, envelope);
159
+ },
160
+ /** One finite Agent operation on this device, returning its receipt. */
161
+ async operation(input, caller, agentId, raw) {
162
+ const device = await current(input);
163
+ const operation = Schema.decodeUnknownSync(machineAgentOperationSchema, {
164
+ onExcessProperty: "error",
165
+ })(raw);
166
+ const facts = await checkedTarget(device, caller, agentId, operation.type === "settlement" ? operation.requesterAgentId : null);
167
+ if (facts.targetHome.kind !== "machine" ||
168
+ facts.targetHome.deviceId !== device.deviceId)
169
+ throw new MachineLinkRefused("actor_not_allowed");
170
+ const key = createHash("sha256")
171
+ .update(JSON.stringify([facts.callerAgentId, facts.targetAgentId, operation]))
172
+ .digest("hex");
173
+ const result = await hostFor(device).deliver(device.deviceId, {
174
+ key,
175
+ kind: "machineAgent",
176
+ address: { scopeId: facts.teamId, agentId: facts.targetAgentId },
177
+ event: { actorAgentId: facts.actorAgentId, operation },
178
+ });
179
+ if (result === "accepted" ||
180
+ result === "duplicate" ||
181
+ result === "refused" ||
182
+ !("receipt" in result) ||
183
+ result.status === "refused")
184
+ throw new MachineLinkRefused("unavailable");
185
+ return result.receipt;
186
+ },
187
+ /**
188
+ * Apply one committed fence fact pushed by the host's producer. The fact
189
+ * is the push; the link does not read authority back here. Sockets at an
190
+ * older epoch close, and a not-live fact closes the current one too. The
191
+ * per-frame live read remains the guarantee.
192
+ */
193
+ applyFence(raw) {
194
+ const fact = machineLinkFenceFactSchema.parse(raw);
195
+ match(fact);
196
+ hostFor(fact).fence(fact.fenceEpoch, fact.live);
197
+ return { kind: "confirmed", fenceEpoch: fact.fenceEpoch };
198
+ },
199
+ preflightErasure,
200
+ async eraseForTeam(teamId, deviceId) {
201
+ await preflightErasure(teamId, deviceId);
202
+ hostFor({ teamId, deviceId }).retireForErasure();
203
+ return await options.erasure.erase();
204
+ },
205
+ readErasure: () => options.erasure.read(),
206
+ async webSocketMessage(socket, message) {
207
+ const identity = await savedIdentity();
208
+ if (identity === null) {
209
+ socket.close(4003, "identity unavailable");
210
+ return;
211
+ }
212
+ await hostFor(identity).message(socket, message);
213
+ },
214
+ webSocketClose(socket, code, reason) {
215
+ host?.close(socket);
216
+ socket.close(code === 1005 || code === 1006 ? 1000 : code, reason);
217
+ },
218
+ webSocketError(socket) {
219
+ host?.close(socket);
220
+ socket.close(1011, "machine link error");
221
+ },
222
+ };
223
+ }
@@ -0,0 +1,10 @@
1
+ /** Native socket ownership for one machine; the caller authenticates each dial. */
2
+ export declare function createMachineLinkSockets(context: Pick<DurableObjectState, "acceptWebSocket" | "getWebSockets">, deviceId: string): {
3
+ accept: (socket: WebSocket) => void;
4
+ isCurrent: (socket: WebSocket) => boolean;
5
+ };
6
+ /**
7
+ * Classify the accepted socket using its platform auto-response timestamp.
8
+ * Presence is a liveness hint, never Team membership or execution authority.
9
+ */
10
+ export declare function isMachineLinkOnline(hasCurrentSocket: boolean, lastAutoPongAt: Date | null, now: number): boolean;
@@ -0,0 +1,25 @@
1
+ /** Native socket ownership for one machine; the caller authenticates each dial. */
2
+ export function createMachineLinkSockets(context, deviceId) {
3
+ const machineLinkTag = `machine-link:${deviceId}`;
4
+ return {
5
+ accept: (socket) => {
6
+ context.acceptWebSocket(socket, [machineLinkTag]);
7
+ for (const previous of context.getWebSockets(machineLinkTag)) {
8
+ if (previous !== socket && previous.readyState === WebSocket.OPEN)
9
+ previous.close(4001, "replaced");
10
+ }
11
+ },
12
+ isCurrent: (socket) => socket.readyState === WebSocket.OPEN &&
13
+ context.getWebSockets(machineLinkTag).includes(socket),
14
+ };
15
+ }
16
+ /**
17
+ * Classify the accepted socket using its platform auto-response timestamp.
18
+ * Presence is a liveness hint, never Team membership or execution authority.
19
+ */
20
+ export function isMachineLinkOnline(hasCurrentSocket, lastAutoPongAt, now) {
21
+ if (!hasCurrentSocket || lastAutoPongAt === null)
22
+ return false;
23
+ const age = now - lastAutoPongAt.getTime();
24
+ return age >= 0 && age < 90_000;
25
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The Workers half of the machine link: one device's native sockets,
3
+ * signed-dial replay ledger and Post custody inside its own Durable Object.
4
+ */
5
+ export * from "./host.js";
6
+ export * from "./link.js";
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The Workers half of the machine link: one device's native sockets,
3
+ * signed-dial replay ledger and Post custody inside its own Durable Object.
4
+ */
5
+ export * from "./host.js";
6
+ export * from "./link.js";
@@ -17,11 +17,11 @@ export type OysterMcpTenantPin = Readonly<{
17
17
  */
18
18
  export declare class OysterMcpOwner<Env extends OysterMcpOwnerEnv = OysterMcpOwnerEnv> extends DurableObject<Env> {
19
19
  #private;
20
- constructor(ctx: DurableObjectState, env: Env, createHost: (env: Env, scope: TenantScope) => OysterHost);
20
+ constructor(ctx: DurableObjectState, env: Env, createHost: (env: Env, scope: TenantScope) => OysterHost | Promise<OysterHost>);
21
21
  /** Trusted host RPC: the namespace and pin are never selected by guest headers. */
22
22
  serve(request: Request, tenant: OysterMcpTenantPin): Promise<Response>;
23
23
  }
24
24
  /** Bind the owner to the deployment's existing host factory and options. */
25
- export declare function createOysterMcpOwner<Env extends OysterMcpOwnerEnv>(host: (env: Env, scope: TenantScope) => OysterHost): new (ctx: DurableObjectState, env: Env) => OysterMcpOwner<Env>;
25
+ export declare function createOysterMcpOwner<Env extends OysterMcpOwnerEnv>(host: (env: Env, scope: TenantScope) => OysterHost | Promise<OysterHost>): new (ctx: DurableObjectState, env: Env) => OysterMcpOwner<Env>;
26
26
  /** Frontends own no SDK state and cannot close another isolate's sessions. */
27
27
  export declare function remoteOysterMcp(binding: DurableObjectNamespace<OysterMcpOwner> | undefined, scope: TenantScope): OysterMcp;