@fungi.computer/oyster 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (292) hide show
  1. package/README.md +376 -72
  2. package/dist/agent-avatars.d.ts +103 -0
  3. package/dist/agent-avatars.js +66 -0
  4. package/dist/agents.d.ts +15 -4
  5. package/dist/agents.js +43 -4
  6. package/dist/app-builds.d.ts +81 -8
  7. package/dist/app-builds.js +210 -60
  8. package/dist/app-storage.d.ts +3 -3
  9. package/dist/approval-capability.d.ts +5 -0
  10. package/dist/approval-capability.js +46 -0
  11. package/dist/approval-home.d.ts +51 -0
  12. package/dist/approval-home.js +226 -0
  13. package/dist/apps/catalog.d.ts +335 -0
  14. package/dist/apps/catalog.js +46 -0
  15. package/dist/apps/gate.d.ts +26 -12
  16. package/dist/apps/gate.js +48 -22
  17. package/dist/apps/index.d.ts +3 -1
  18. package/dist/apps/index.js +3 -1
  19. package/dist/apps/installation.d.ts +147 -66
  20. package/dist/apps/installation.js +193 -86
  21. package/dist/apps/permissions.d.ts +247 -0
  22. package/dist/apps/permissions.js +255 -0
  23. package/dist/boot-capability-route.d.ts +15 -0
  24. package/dist/boot-capability-route.js +196 -0
  25. package/dist/boot-capability.d.ts +103 -0
  26. package/dist/boot-capability.js +141 -0
  27. package/dist/boot-construction.d.ts +12 -0
  28. package/dist/boot-construction.js +175 -0
  29. package/dist/boot-gate.d.ts +267 -0
  30. package/dist/boot-gate.js +236 -0
  31. package/dist/boot-host-script.d.ts +2 -0
  32. package/dist/boot-host-script.js +1 -0
  33. package/dist/boot-mount.d.ts +21 -0
  34. package/dist/boot-mount.js +65 -0
  35. package/dist/browser/boot.d.ts +73 -0
  36. package/dist/browser/boot.js +502 -0
  37. package/dist/browser/browser.d.ts +104 -0
  38. package/dist/browser/browser.js +374 -0
  39. package/dist/browser/capability-source.d.ts +76 -0
  40. package/dist/browser/capability-source.js +338 -0
  41. package/dist/browser/cdp-client.d.ts +56 -0
  42. package/dist/browser/cdp-client.js +184 -0
  43. package/dist/browser/errors.d.ts +79 -0
  44. package/dist/browser/errors.js +32 -0
  45. package/dist/browser/index.d.ts +16 -0
  46. package/dist/browser/index.js +14 -0
  47. package/dist/browser/lease-registry.d.ts +61 -0
  48. package/dist/browser/lease-registry.js +101 -0
  49. package/dist/browser/operations.d.ts +225 -0
  50. package/dist/browser/operations.js +137 -0
  51. package/dist/browser/page.d.ts +25 -0
  52. package/dist/browser/page.js +172 -0
  53. package/dist/browser/quick-reads.d.ts +29 -0
  54. package/dist/browser/quick-reads.js +160 -0
  55. package/dist/browser/sessions.d.ts +52 -0
  56. package/dist/browser/sessions.js +118 -0
  57. package/dist/browser/url-policy.d.ts +18 -0
  58. package/dist/browser/url-policy.js +58 -0
  59. package/dist/build/app-imports.d.ts +12 -0
  60. package/dist/build/app-imports.js +105 -0
  61. package/dist/build/build-policy.d.ts +5 -3
  62. package/dist/build/build-policy.js +5 -3
  63. package/dist/build/candidate-builder.d.ts +1 -1
  64. package/dist/build/candidate-cli.mjs +8428 -337
  65. package/dist/build/local-build.d.ts +2 -0
  66. package/dist/build/local-build.js +28 -6
  67. package/dist/build/snapshot-package-store.d.ts +1 -1
  68. package/dist/build/snapshot-package-store.js +7 -2
  69. package/dist/build/workspace-build.d.ts +92 -5
  70. package/dist/build/workspace-build.js +93 -19
  71. package/dist/build-egress.js +4 -4
  72. package/dist/build.d.ts +1 -1
  73. package/dist/build.js +1 -1
  74. package/dist/catalog-frame.js +1 -1
  75. package/dist/cloudflare.d.ts +13 -3
  76. package/dist/cloudflare.js +106 -8
  77. package/dist/frame-binding-wire-script.js +1 -1
  78. package/dist/frame-binding-wire.d.ts +1 -1
  79. package/dist/frame-binding-wire.js +5 -3
  80. package/dist/git-auth.d.ts +0 -2
  81. package/dist/git-auth.js +0 -2
  82. package/dist/git-journal.js +4 -4
  83. package/dist/git.d.ts +0 -1
  84. package/dist/git.js +0 -2
  85. package/dist/host.d.ts +39 -13
  86. package/dist/host.js +268 -142
  87. package/dist/index.d.ts +26 -15
  88. package/dist/index.js +40 -16
  89. package/dist/install-release-cli.d.ts +2 -0
  90. package/dist/install-release-cli.js +46 -0
  91. package/dist/installation-client.d.ts +43 -0
  92. package/dist/installation-client.js +178 -0
  93. package/dist/installation-execution.d.ts +11 -0
  94. package/dist/installation-execution.js +184 -0
  95. package/dist/installation-operations.d.ts +1437 -0
  96. package/dist/installation-operations.js +664 -0
  97. package/dist/installation-store-generation.d.ts +3 -0
  98. package/dist/installation-store-generation.js +16 -0
  99. package/dist/instance-id.d.ts +10 -0
  100. package/dist/instance-id.js +10 -0
  101. package/dist/instance-refusal.d.ts +11 -0
  102. package/dist/instance-refusal.js +21 -0
  103. package/dist/internal/delivery.js +4 -2
  104. package/dist/internal/host-gate.js +7 -1
  105. package/dist/internal/registry-installations.js +4 -4
  106. package/dist/keys.d.ts +5 -1
  107. package/dist/keys.js +10 -2
  108. package/dist/machine-link/authority.d.ts +89 -0
  109. package/dist/machine-link/authority.js +37 -0
  110. package/dist/machine-link/client.d.ts +66 -0
  111. package/dist/machine-link/client.js +94 -0
  112. package/dist/machine-link/delivery.d.ts +18 -0
  113. package/dist/machine-link/delivery.js +55 -0
  114. package/dist/machine-link/enrollment.d.ts +111 -0
  115. package/dist/machine-link/enrollment.js +286 -0
  116. package/dist/machine-link/fences.d.ts +37 -0
  117. package/dist/machine-link/fences.js +166 -0
  118. package/dist/machine-link/home.d.ts +20 -0
  119. package/dist/machine-link/home.js +24 -0
  120. package/dist/machine-link/host.d.ts +43 -0
  121. package/dist/machine-link/host.js +189 -0
  122. package/dist/machine-link/index.d.ts +15 -0
  123. package/dist/machine-link/index.js +15 -0
  124. package/dist/machine-link/link.d.ts +48 -0
  125. package/dist/machine-link/link.js +223 -0
  126. package/dist/machine-link/presence.d.ts +10 -0
  127. package/dist/machine-link/presence.js +25 -0
  128. package/dist/machine-link/workers.d.ts +6 -0
  129. package/dist/machine-link/workers.js +6 -0
  130. package/dist/mcp-owner.d.ts +2 -2
  131. package/dist/mcp-owner.js +41 -33
  132. package/dist/mcp.d.ts +15 -3
  133. package/dist/mcp.js +146 -25
  134. package/dist/pack.d.ts +14 -3
  135. package/dist/pack.js +44 -19
  136. package/dist/page/app-access.d.ts +7 -0
  137. package/dist/page/app-access.js +75 -0
  138. package/dist/page/boot-host.js +57 -0
  139. package/dist/page/capabilities.d.ts +3 -3
  140. package/dist/page/capabilities.js +16 -12
  141. package/dist/page/computer-handles.d.ts +3 -3
  142. package/dist/page/computer-target.d.ts +3 -0
  143. package/dist/page/computer-target.js +9 -0
  144. package/dist/page/mount.d.ts +23 -7
  145. package/dist/page/mount.js +152 -16
  146. package/dist/page-assets.d.ts +22 -0
  147. package/dist/page-assets.js +28 -0
  148. package/dist/page-script.d.ts +3 -0
  149. package/dist/page-script.js +5 -4
  150. package/dist/release.d.ts +13 -6
  151. package/dist/release.js +10 -7
  152. package/dist/root-data.d.ts +16 -6
  153. package/dist/root-data.js +2 -2
  154. package/dist/source/index.d.ts +1 -1
  155. package/dist/source/team-source.d.ts +20 -8
  156. package/dist/source/team-source.js +39 -10
  157. package/dist/source/template-seed.d.ts +2 -2
  158. package/dist/standalone/agent-mail.d.ts +24 -0
  159. package/dist/standalone/agent-mail.js +62 -0
  160. package/dist/standalone/agent-store.d.ts +9 -0
  161. package/dist/standalone/agent-store.js +30 -0
  162. package/dist/standalone/agents.d.ts +78 -12
  163. package/dist/standalone/agents.js +470 -59
  164. package/dist/standalone/app-builds-binding.d.ts +20 -2
  165. package/dist/standalone/app-builds-binding.js +151 -27
  166. package/dist/standalone/app-set.d.ts +5 -2
  167. package/dist/standalone/app-set.js +174 -143
  168. package/dist/standalone/browser.d.ts +11 -0
  169. package/dist/standalone/browser.js +72 -0
  170. package/dist/standalone/builds.d.ts +13 -4
  171. package/dist/standalone/builds.js +28 -44
  172. package/dist/standalone/computer-egress.d.ts +2 -0
  173. package/dist/standalone/computer-egress.js +13 -4
  174. package/dist/standalone/computer-fleet.d.ts +43 -0
  175. package/dist/standalone/computer-fleet.js +200 -0
  176. package/dist/standalone/computer.d.ts +44 -9
  177. package/dist/standalone/computer.js +132 -25
  178. package/dist/standalone/install-apps-cli.js +43 -6
  179. package/dist/standalone/instance.d.ts +2 -2
  180. package/dist/standalone/instance.js +8 -11
  181. package/dist/standalone/machine-link.d.ts +354 -0
  182. package/dist/standalone/machine-link.js +368 -0
  183. package/dist/standalone/media.d.ts +2 -2
  184. package/dist/standalone/media.js +19 -16
  185. package/dist/standalone/preview.d.ts +2 -2
  186. package/dist/standalone/preview.js +39 -12
  187. package/dist/standalone/registry.d.ts +149 -9
  188. package/dist/standalone/registry.js +299 -8
  189. package/dist/standalone/worker.d.ts +19 -5
  190. package/dist/standalone/worker.js +90 -54
  191. package/dist/standalone/workflow-binding.d.ts +28 -32
  192. package/dist/standalone/workflow-binding.js +20 -20
  193. package/dist/standalone/workflow-control.d.ts +4 -0
  194. package/dist/standalone/workflow-control.js +40 -0
  195. package/dist/standalone/workflow-events.d.ts +2 -2
  196. package/dist/standalone/workflow-events.js +9 -10
  197. package/dist/standalone/workflow-git-capture.js +4 -3
  198. package/dist/standalone/workflow-host.d.ts +177 -152
  199. package/dist/standalone/workflow-host.js +137 -30
  200. package/dist/standalone/workflow-node-capture.js +11 -4
  201. package/dist/standalone/workflow-schedules.d.ts +5 -2
  202. package/dist/standalone/workflow-schedules.js +9 -10
  203. package/dist/standalone/workflow-trigger-host.d.ts +88 -6
  204. package/dist/standalone/workflow-trigger-host.js +2 -2
  205. package/dist/standalone/workflow-webhooks.d.ts +22 -0
  206. package/dist/standalone/workflow-webhooks.js +39 -0
  207. package/dist/standalone/workflows.d.ts +203 -50
  208. package/dist/standalone/workflows.js +198 -39
  209. package/dist/storage-frame.d.ts +6 -6
  210. package/dist/testing/capabilities.js +14 -3
  211. package/dist/testing/installation.js +26 -2
  212. package/dist/testing/worker.js +1 -1
  213. package/dist/testing/workers-runtime.d.ts +8 -2
  214. package/dist/testing/workers-runtime.js +20 -2
  215. package/dist/webhook-abuse.d.ts +25 -0
  216. package/dist/webhook-abuse.js +54 -0
  217. package/dist/webhook-custody.d.ts +144 -0
  218. package/dist/webhook-custody.js +222 -0
  219. package/dist/webhook-ingress.d.ts +36 -0
  220. package/dist/webhook-ingress.js +146 -0
  221. package/dist/webhooks.d.ts +111 -0
  222. package/dist/webhooks.js +189 -0
  223. package/dist/workers-git.d.ts +12 -17
  224. package/dist/workers-git.js +60 -55
  225. package/dist/workers.d.ts +224 -13
  226. package/dist/workers.js +351 -51
  227. package/dist/workflow-agent-authority.d.ts +8 -0
  228. package/dist/workflow-agent-authority.js +42 -0
  229. package/dist/workflows.d.ts +66 -0
  230. package/dist/workflows.js +97 -0
  231. package/docs/api/README.md +63 -37
  232. package/docs/api/agents.md +17 -0
  233. package/docs/api/app-storage.md +1 -1
  234. package/docs/api/apps.md +1394 -543
  235. package/docs/api/browser/boot.md +132 -0
  236. package/docs/api/browser.md +1440 -0
  237. package/docs/api/build-contract.md +38 -25
  238. package/docs/api/build.md +263 -84
  239. package/docs/api/cloudflare.md +93 -7
  240. package/docs/api/git.md +3 -13
  241. package/docs/api/host.md +944 -118
  242. package/docs/api/index.md +320 -18
  243. package/docs/api/keys.md +24 -1
  244. package/docs/api/mcp.md +59 -12
  245. package/docs/api/modules.md +33 -40
  246. package/docs/api/pack.md +57 -8
  247. package/docs/api/page/mount.md +92 -4
  248. package/docs/api/workers-git.md +103 -24
  249. package/docs/api/workers.md +1581 -244
  250. package/docs/site/agent-quickstart.mdx +28 -14
  251. package/examples/standalone/worker.ts +3 -1
  252. package/examples/standalone/wrangler.jsonc +2 -0
  253. package/package.json +73 -69
  254. package/dist/installation-consent.d.ts +0 -79
  255. package/dist/installation-consent.js +0 -82
  256. package/dist/shell/app-directory.d.ts +0 -542
  257. package/dist/shell/app-directory.js +0 -199
  258. package/dist/shell/browser-actions.d.ts +0 -7
  259. package/dist/shell/browser-actions.js +0 -56
  260. package/dist/shell/command-host.d.ts +0 -112
  261. package/dist/shell/command-host.js +0 -502
  262. package/dist/shell/commands.d.ts +0 -25
  263. package/dist/shell/commands.js +0 -78
  264. package/dist/shell/index.d.ts +0 -8
  265. package/dist/shell/index.js +0 -8
  266. package/dist/shell/keys.d.ts +0 -41
  267. package/dist/shell/keys.js +0 -134
  268. package/dist/shell/shell-module.d.ts +0 -55
  269. package/dist/shell/shell-module.js +0 -86
  270. package/dist/shell/window-manager.d.ts +0 -124
  271. package/dist/shell/window-manager.js +0 -711
  272. package/dist/shell/workbench-contract.d.ts +0 -68
  273. package/dist/shell/workbench-directory.d.ts +0 -332
  274. package/dist/shell/workbench-directory.js +0 -35
  275. package/dist/shell/workbench-host-types.d.ts +0 -96
  276. package/dist/shell/workbench-host-types.js +0 -1
  277. package/dist/shell/workbench-host.d.ts +0 -34
  278. package/dist/shell/workbench-host.js +0 -347
  279. package/dist/shell/workbench-model.d.ts +0 -192
  280. package/dist/shell/workbench-model.js +0 -53
  281. package/dist/shell/workbench.d.ts +0 -4
  282. package/dist/shell/workbench.js +0 -2
  283. package/docs/api/shell/app-directory.md +0 -599
  284. package/docs/api/shell/command-host.md +0 -224
  285. package/docs/api/shell/commands.md +0 -197
  286. package/docs/api/shell/keys.md +0 -183
  287. package/docs/api/shell/shell-module.md +0 -204
  288. package/docs/api/shell/window-manager.md +0 -267
  289. package/docs/api/shell/workbench-directory.md +0 -119
  290. package/docs/api/shell/workbench.md +0 -205
  291. package/docs/api/shell.md +0 -1915
  292. /package/dist/{shell/workbench-contract.js → page/boot-host.d.ts} +0 -0
@@ -0,0 +1,286 @@
1
+ import { defineIdFamily } from "@fungi.computer/cairn/id";
2
+ import { importSeamVerifiers } from "@fungi.computer/seam";
3
+ import { z } from "zod";
4
+ const identity = z.string().min(1).max(256);
5
+ const selectedAgent = z.strictObject({
6
+ name: z.string().trim().min(1).max(256),
7
+ });
8
+ /** Public keys and selected identities only; a local store path never crosses. */
9
+ export const machineEnrollmentSchema = z.strictObject({
10
+ requestId: identity,
11
+ publicKey: z.base64().length(44),
12
+ machineLabel: z.string().trim().min(1).max(256),
13
+ agents: z.array(selectedAgent).max(256),
14
+ });
15
+ /** Trusted Local OAuth facts, never supplied in the public enrollment body. */
16
+ export const machineMemberSchema = z.strictObject({
17
+ memberId: identity,
18
+ clientId: identity,
19
+ loginId: identity,
20
+ });
21
+ /** Server-selected issuer facts; omitted fields match all of this member's devices. */
22
+ export const machineLoginSelectionSchema = z.strictObject({
23
+ clientId: identity.optional(),
24
+ loginId: identity.optional(),
25
+ });
26
+ const ids = defineIdFamily({
27
+ family: "machine-enrollment",
28
+ version: 1,
29
+ kinds: { device: { prefix: "machine" } },
30
+ });
31
+ const rowSchema = z.strictObject({
32
+ device_id: identity,
33
+ member_id: identity,
34
+ client_id: identity,
35
+ login_id: identity,
36
+ input: z.string(),
37
+ });
38
+ const mappingSchema = z.strictObject({
39
+ selection: z.number().int().min(0).max(255),
40
+ agent_id: identity,
41
+ });
42
+ const limitsSchema = z.strictObject({
43
+ machines: z.number().int().positive(),
44
+ agentsPerMachine: z.number().int().positive().max(256),
45
+ });
46
+ /** Honest enrollment refusals; none denotes command acceptance. */
47
+ export class MachineEnrollmentRefused extends Error {
48
+ reason;
49
+ constructor(reason) {
50
+ super(reason);
51
+ this.reason = reason;
52
+ this.name = "MachineEnrollmentRefused";
53
+ }
54
+ }
55
+ /**
56
+ * Team-owned enrollment state over the ordinary Agent directory, not a second
57
+ * hosted runtime. Its caller must supply the live, member-authorized identity.
58
+ * Limits are captured host policy; this module does not invent a Team quota.
59
+ */
60
+ export function createMachineEnrollment(options) {
61
+ const { owner } = options;
62
+ const limits = limitsSchema.parse(options.limits);
63
+ const reserveAgent = options.agents(owner.sql);
64
+ owner.sql.exec(`CREATE TABLE IF NOT EXISTS hub_machine_devices (
65
+ device_id TEXT PRIMARY KEY,
66
+ member_id TEXT NOT NULL,
67
+ client_id TEXT NOT NULL,
68
+ login_id TEXT NOT NULL,
69
+ request_id TEXT NOT NULL UNIQUE,
70
+ input TEXT NOT NULL,
71
+ public_key TEXT NOT NULL UNIQUE,
72
+ machine_label TEXT NOT NULL,
73
+ revoked INTEGER NOT NULL DEFAULT 0 CHECK(revoked IN (0,1)),
74
+ fence_epoch INTEGER NOT NULL DEFAULT 0
75
+ CHECK(fence_epoch BETWEEN 0 AND 9007199254740991)
76
+ ) STRICT`);
77
+ owner.sql.exec(`CREATE TABLE IF NOT EXISTS hub_machine_agents (
78
+ device_id TEXT NOT NULL REFERENCES hub_machine_devices(device_id),
79
+ selection INTEGER NOT NULL,
80
+ agent_id TEXT NOT NULL UNIQUE,
81
+ PRIMARY KEY(device_id, selection)
82
+ ) STRICT`);
83
+ const mappings = (deviceId) => owner.sql
84
+ .exec("SELECT selection, agent_id FROM hub_machine_agents WHERE device_id = ? ORDER BY selection", deviceId)
85
+ .toArray()
86
+ .map((row) => {
87
+ const parsed = mappingSchema.parse(row);
88
+ return { selection: parsed.selection, agentId: parsed.agent_id };
89
+ });
90
+ const enroll = async (rawMember, raw) => {
91
+ const member = machineMemberSchema.parse(rawMember);
92
+ const { memberId } = member;
93
+ const input = machineEnrollmentSchema.parse(raw);
94
+ // Use the maintained Ed25519 decoder before storing an unusable pin.
95
+ await importSeamVerifiers({ device: input.publicKey });
96
+ await options.authorizeMember(memberId);
97
+ const canonical = JSON.stringify(input);
98
+ return owner.transactionSync(() => {
99
+ const prior = owner.sql
100
+ .exec("SELECT device_id, member_id, client_id, login_id, input FROM hub_machine_devices WHERE request_id = ?", input.requestId)
101
+ .toArray()[0];
102
+ if (prior !== undefined) {
103
+ const row = rowSchema.parse(prior);
104
+ if (row.member_id !== memberId ||
105
+ row.client_id !== member.clientId ||
106
+ row.login_id !== member.loginId ||
107
+ row.input !== canonical)
108
+ throw new MachineEnrollmentRefused("request_conflict");
109
+ const revoked = z
110
+ .object({ revoked: z.number() })
111
+ .parse(owner.sql
112
+ .exec("SELECT revoked FROM hub_machine_devices WHERE device_id = ?", row.device_id)
113
+ .toArray()[0]).revoked;
114
+ if (revoked !== 0)
115
+ throw new MachineEnrollmentRefused("device_revoked");
116
+ return { deviceId: row.device_id, agents: mappings(row.device_id) };
117
+ }
118
+ // Bound retained identity/replay history, including revoked devices;
119
+ // counting only active devices would leave that history unbounded.
120
+ const count = z
121
+ .object({ count: z.number().int().nonnegative() })
122
+ .parse(owner.sql
123
+ .exec("SELECT COUNT(*) AS count FROM hub_machine_devices")
124
+ .toArray()[0]).count;
125
+ if (count >= limits.machines ||
126
+ input.agents.length > limits.agentsPerMachine)
127
+ throw new MachineEnrollmentRefused("capacity");
128
+ if (owner.sql
129
+ .exec("SELECT 1 FROM hub_machine_devices WHERE public_key = ?", input.publicKey)
130
+ .toArray().length > 0)
131
+ throw new MachineEnrollmentRefused("request_conflict");
132
+ const deviceId = ids.device.generate();
133
+ owner.sql.exec("INSERT INTO hub_machine_devices(device_id, member_id, client_id, login_id, request_id, input, public_key, machine_label) VALUES(?,?,?,?,?,?,?,?)", deviceId, memberId, member.clientId, member.loginId, input.requestId, canonical, input.publicKey, input.machineLabel);
134
+ for (const [index, selected] of input.agents.entries()) {
135
+ // A bounded server-owned receipt, stable across retries.
136
+ const agent = reserveAgent({
137
+ memberId,
138
+ requestId: `${deviceId}:${String(index)}`,
139
+ name: selected.name,
140
+ });
141
+ owner.sql.exec("INSERT INTO hub_machine_agents(device_id, selection, agent_id) VALUES(?,?,?)", deviceId, index, agent.agentId);
142
+ options.onAgentCreated(agent.agentId);
143
+ }
144
+ return { deviceId, agents: mappings(deviceId) };
145
+ });
146
+ };
147
+ const device = (deviceId) => {
148
+ const row = owner.sql
149
+ .exec("SELECT device_id, member_id, client_id, login_id, public_key, machine_label, fence_epoch FROM hub_machine_devices WHERE device_id = ? AND revoked = 0", deviceId)
150
+ .toArray()[0];
151
+ if (row === undefined)
152
+ return null;
153
+ const value = z
154
+ .strictObject({
155
+ device_id: identity,
156
+ member_id: identity,
157
+ client_id: identity,
158
+ login_id: identity,
159
+ public_key: z.string(),
160
+ machine_label: z.string(),
161
+ fence_epoch: z.int().nonnegative(),
162
+ })
163
+ .parse(row);
164
+ return {
165
+ deviceId: value.device_id,
166
+ memberId: value.member_id,
167
+ clientId: value.client_id,
168
+ loginId: value.login_id,
169
+ publicKey: value.public_key,
170
+ machineLabel: value.machine_label,
171
+ fenceEpoch: value.fence_epoch,
172
+ };
173
+ };
174
+ const home = (agentId) => {
175
+ const row = owner.sql
176
+ .exec(`SELECT d.device_id FROM hub_machine_agents a
177
+ JOIN hub_machine_devices d ON d.device_id = a.device_id WHERE a.agent_id = ?`, agentId)
178
+ .toArray()[0];
179
+ if (row === undefined)
180
+ return null;
181
+ const value = z.strictObject({ device_id: identity }).parse(row);
182
+ return { kind: "machine", deviceId: value.device_id };
183
+ };
184
+ const revoke = (memberId, deviceId) => owner.transactionSync(() => {
185
+ const current = device(deviceId);
186
+ if (current?.memberId !== memberId)
187
+ return false;
188
+ owner.sql.exec("UPDATE hub_machine_devices SET revoked = 1, fence_epoch = fence_epoch + 1 WHERE device_id = ?", deviceId);
189
+ return true;
190
+ });
191
+ const authorizeDevice = async (deviceId) => {
192
+ const current = device(deviceId);
193
+ if (current === null)
194
+ throw new MachineEnrollmentRefused("device_revoked");
195
+ await options.authorizeMember(current.memberId);
196
+ if (!(await options.authorizeLogin({
197
+ memberId: current.memberId,
198
+ clientId: current.clientId,
199
+ loginId: current.loginId,
200
+ })))
201
+ throw new MachineEnrollmentRefused("login_not_current");
202
+ // The primary login read yields to other RPCs. A concurrent per-device
203
+ // revoke must win before this call lends the device to transport again.
204
+ const authorized = device(deviceId);
205
+ if (authorized === null)
206
+ throw new MachineEnrollmentRefused("device_revoked");
207
+ return authorized;
208
+ };
209
+ /** Retained rows include revoked devices: their replay owner still exists. */
210
+ const retainedDevices = () => owner.sql
211
+ .exec("SELECT device_id, member_id, client_id, login_id, revoked, fence_epoch FROM hub_machine_devices ORDER BY device_id")
212
+ .toArray()
213
+ .map((raw) => {
214
+ const row = z
215
+ .strictObject({
216
+ device_id: identity,
217
+ member_id: identity,
218
+ client_id: identity,
219
+ login_id: identity,
220
+ revoked: z.union([z.literal(0), z.literal(1)]),
221
+ fence_epoch: z.int().nonnegative(),
222
+ })
223
+ .parse(raw);
224
+ return {
225
+ deviceId: row.device_id,
226
+ memberId: row.member_id,
227
+ clientId: row.client_id,
228
+ loginId: row.login_id,
229
+ revoked: row.revoked === 1,
230
+ fenceEpoch: row.fence_epoch,
231
+ };
232
+ });
233
+ /** Commit the authority fence before attempting any remote socket close. */
234
+ const fenceDevice = (deviceId) => owner.transactionSync(() => {
235
+ const row = owner.sql
236
+ .exec("UPDATE hub_machine_devices SET fence_epoch = fence_epoch + 1 WHERE device_id = ? RETURNING fence_epoch", deviceId)
237
+ .toArray()[0];
238
+ return row === undefined
239
+ ? null
240
+ : z.strictObject({ fence_epoch: z.int().nonnegative() }).parse(row)
241
+ .fence_epoch;
242
+ });
243
+ const fenceSelected = (devices) => devices.map(({ deviceId }) => {
244
+ const fenceEpoch = fenceDevice(deviceId);
245
+ if (fenceEpoch === null)
246
+ throw new Error("Retained device disappeared");
247
+ return { deviceId, fenceEpoch };
248
+ });
249
+ /** Logout/removal closes old sockets, but does not retire keys or replay. */
250
+ const fenceLogins = (memberId, selection) => owner.transactionSync(() => fenceSelected(retainedDevices().filter((device) => device.memberId === memberId &&
251
+ (selection.clientId === undefined ||
252
+ device.clientId === selection.clientId) &&
253
+ (selection.loginId === undefined ||
254
+ device.loginId === selection.loginId))));
255
+ const fenceMember = (memberId) => fenceLogins(memberId, {});
256
+ /** Irreversible erasure acceptance, not suspension. Keep every roster row
257
+ * until its link's replay and storage have been erased and read back.
258
+ */
259
+ const retireAllForErasure = () => owner.transactionSync(() => {
260
+ for (const device of retainedDevices()) {
261
+ if (!device.revoked && !revoke(device.memberId, device.deviceId))
262
+ throw new Error("Device retirement did not commit");
263
+ }
264
+ return retainedDevices().map(({ deviceId, fenceEpoch }) => ({
265
+ deviceId,
266
+ fenceEpoch,
267
+ }));
268
+ });
269
+ /** Revocation is irreversible. Keep its UNIQUE public-key row, even after
270
+ * the device's link replay is erased; a resumable fence is not retirement.
271
+ */
272
+ const keyRetired = (deviceId) => retainedDevices().some((device) => device.deviceId === deviceId && device.revoked);
273
+ return {
274
+ enroll,
275
+ device,
276
+ home,
277
+ revoke,
278
+ authorizeDevice,
279
+ retainedDevices,
280
+ fenceDevice,
281
+ fenceMember,
282
+ fenceLogins,
283
+ retireAllForErasure,
284
+ keyRetired,
285
+ };
286
+ }
@@ -0,0 +1,37 @@
1
+ import type { WatchdogSqliteOwner } from "@fungi.computer/watchdog";
2
+ import type { createMachineEnrollment, MachineLoginSelection } from "./enrollment.js";
3
+ import { type MachineLinkFenceResult, type MachineLinkFenceFact } from "./authority.js";
4
+ /** One host outbox family: the host's drain asks when it is due and runs it. */
5
+ export type MachineLinkFenceFamily = Readonly<{
6
+ family: string;
7
+ nextDue(): number | null;
8
+ deliver(): Promise<void>;
9
+ }>;
10
+ /** Team-local durable push custody; device rows remain the authority. */
11
+ export declare function createMachineLinkFences(options: {
12
+ owner: WatchdogSqliteOwner;
13
+ enrollment: ReturnType<typeof createMachineEnrollment>;
14
+ readFact: (deviceId: string) => Promise<MachineLinkFenceFact | null>;
15
+ push: (fact: MachineLinkFenceFact) => Promise<MachineLinkFenceResult>;
16
+ /** Own an immediate post-commit push/alarm. When staging inside an outer
17
+ * transaction, the host must defer I/O until that transaction has committed.
18
+ */
19
+ wakeNow: () => void;
20
+ retryDelayMs: (failures: number) => number;
21
+ /** The host's failure log; a failed push still stays UNCONFIRMED and retries. */
22
+ report: (operation: string, cause: unknown) => void;
23
+ }): {
24
+ fenceAll: () => void;
25
+ fenceMember: (memberId: string, membershipVersion: number) => void;
26
+ fenceLogins: (memberId: string, selection: MachineLoginSelection) => Promise<{
27
+ kind: "unconfirmed" | "confirmed";
28
+ fenceEpoch: number;
29
+ }[]>;
30
+ retireAllForErasure: () => void;
31
+ revoke: (memberId: string, deviceId: string) => Promise<MachineLinkFenceResult | null>;
32
+ family: Readonly<{
33
+ family: string;
34
+ nextDue(): number | null;
35
+ deliver(): Promise<void>;
36
+ }>;
37
+ };
@@ -0,0 +1,166 @@
1
+ import { z } from "zod";
2
+ import { machineLinkFenceResultSchema, } from "./authority.js";
3
+ const intentSchema = z.strictObject({
4
+ device_id: z.string().min(1).max(256),
5
+ fence_epoch: z.int().nonnegative(),
6
+ failures: z.int().nonnegative(),
7
+ });
8
+ /** Team-local durable push custody; device rows remain the authority. */
9
+ export function createMachineLinkFences(options) {
10
+ const { owner, enrollment } = options;
11
+ owner.sql.exec(`CREATE TABLE IF NOT EXISTS hub_machine_link_fences (
12
+ device_id TEXT PRIMARY KEY,
13
+ fence_epoch INTEGER NOT NULL CHECK(fence_epoch >= 0),
14
+ failures INTEGER NOT NULL DEFAULT 0 CHECK(failures >= 0),
15
+ next_attempt_at INTEGER NOT NULL
16
+ ) STRICT`);
17
+ // Only members with retained devices get a receipt: bounded by enrollment.
18
+ // This records delivery versions, never admission or role authority.
19
+ owner.sql.exec(`CREATE TABLE IF NOT EXISTS hub_machine_link_member_fences (
20
+ member_id TEXT PRIMARY KEY,
21
+ membership_version INTEGER NOT NULL CHECK(membership_version > 0)
22
+ ) STRICT`);
23
+ const stage = (deviceId, fenceEpoch) => {
24
+ owner.sql.exec(`INSERT INTO hub_machine_link_fences
25
+ (device_id,fence_epoch,failures,next_attempt_at) VALUES (?,?,0,?)
26
+ ON CONFLICT(device_id) DO UPDATE SET fence_epoch=excluded.fence_epoch,
27
+ failures=0,next_attempt_at=excluded.next_attempt_at`, deviceId, fenceEpoch, Date.now());
28
+ };
29
+ const attempt = async (intent) => {
30
+ try {
31
+ const fact = await options.readFact(intent.device_id);
32
+ if (fact?.deviceId !== intent.device_id ||
33
+ fact.fenceEpoch < intent.fence_epoch)
34
+ throw new Error("Machine link fence fact is not current");
35
+ const result = machineLinkFenceResultSchema.parse(await options.push(fact));
36
+ if (result.kind !== "confirmed" || result.fenceEpoch < intent.fence_epoch)
37
+ throw new Error("Machine link did not confirm the committed fence");
38
+ // An older in-flight ACK cannot clear a newer persisted intent.
39
+ owner.sql.exec("DELETE FROM hub_machine_link_fences WHERE device_id=? AND fence_epoch=?", intent.device_id, intent.fence_epoch);
40
+ return result;
41
+ }
42
+ catch (cause) {
43
+ const failures = intent.failures + 1;
44
+ owner.sql.exec(`UPDATE hub_machine_link_fences SET failures=?,next_attempt_at=?
45
+ WHERE device_id=? AND fence_epoch=?`, failures, Date.now() + options.retryDelayMs(failures), intent.device_id, intent.fence_epoch);
46
+ options.report("machine-link.fence-push", cause);
47
+ return { kind: "unconfirmed", fenceEpoch: intent.fence_epoch };
48
+ }
49
+ };
50
+ const pending = (deviceId) => {
51
+ const row = owner.sql
52
+ .exec("SELECT device_id,fence_epoch,failures FROM hub_machine_link_fences WHERE device_id=?", deviceId)
53
+ .toArray()[0];
54
+ return row === undefined ? null : intentSchema.parse(row);
55
+ };
56
+ /** Called in the lifecycle transaction. Resume must not cancel these pushes:
57
+ * an old socket still needs closing even after admission becomes live again.
58
+ */
59
+ const fenceAll = () => {
60
+ owner.transactionSync(() => {
61
+ for (const { deviceId } of enrollment.retainedDevices()) {
62
+ const epoch = enrollment.fenceDevice(deviceId);
63
+ if (epoch === null)
64
+ throw new Error("Retained device disappeared");
65
+ stage(deviceId, epoch);
66
+ }
67
+ });
68
+ if (enrollment.retainedDevices().length > 0)
69
+ options.wakeNow();
70
+ };
71
+ /** Called inside the member-removal transaction; old deliveries cannot fence
72
+ * a member again after rejoin. The host owns the post-commit wake.
73
+ */
74
+ const fenceMember = (memberId, membershipVersion) => {
75
+ const staged = owner.transactionSync(() => {
76
+ const previous = owner.sql
77
+ .exec("SELECT membership_version FROM hub_machine_link_member_fences WHERE member_id=?", memberId)
78
+ .toArray()[0];
79
+ if (previous !== undefined &&
80
+ z
81
+ .strictObject({ membership_version: z.int().positive() })
82
+ .parse(previous).membership_version >= membershipVersion)
83
+ return false;
84
+ const devices = enrollment.fenceMember(memberId);
85
+ if (devices.length === 0)
86
+ return false;
87
+ for (const { deviceId, fenceEpoch } of devices)
88
+ stage(deviceId, fenceEpoch);
89
+ owner.sql.exec(`INSERT INTO hub_machine_link_member_fences(member_id,membership_version) VALUES (?,?)
90
+ ON CONFLICT(member_id) DO UPDATE SET membership_version=excluded.membership_version`, memberId, membershipVersion);
91
+ return true;
92
+ });
93
+ if (staged)
94
+ options.wakeNow();
95
+ };
96
+ const fenceLogins = async (memberId, selection) => {
97
+ const intents = owner.transactionSync(() => enrollment
98
+ .fenceLogins(memberId, selection)
99
+ .map(({ deviceId, fenceEpoch }) => {
100
+ stage(deviceId, fenceEpoch);
101
+ const intent = pending(deviceId);
102
+ if (intent === null)
103
+ throw new Error("Login fence was not staged");
104
+ return intent;
105
+ }));
106
+ // The first attempts start inline after commit, not at the retry deadline.
107
+ if (intents.length > 0)
108
+ options.wakeNow();
109
+ const receipts = [];
110
+ for (let offset = 0; offset < intents.length; offset += 16)
111
+ receipts.push(...(await Promise.all(intents.slice(offset, offset + 16).map(attempt))));
112
+ return receipts;
113
+ };
114
+ const retireAllForErasure = () => {
115
+ const devices = owner.transactionSync(() => {
116
+ const retired = enrollment.retireAllForErasure();
117
+ for (const { deviceId, fenceEpoch } of retired)
118
+ stage(deviceId, fenceEpoch);
119
+ return retired;
120
+ });
121
+ if (devices.length > 0)
122
+ options.wakeNow();
123
+ };
124
+ const revoke = async (memberId, deviceId) => {
125
+ const intent = owner.transactionSync(() => {
126
+ const device = enrollment
127
+ .retainedDevices()
128
+ .find((row) => row.deviceId === deviceId && row.memberId === memberId);
129
+ if (device === undefined)
130
+ return null;
131
+ if (!device.revoked && !enrollment.revoke(memberId, deviceId))
132
+ throw new Error("Device retirement did not commit");
133
+ const retired = enrollment
134
+ .retainedDevices()
135
+ .find((row) => row.deviceId === deviceId);
136
+ if (retired === undefined)
137
+ throw new Error("Retired device disappeared");
138
+ stage(deviceId, retired.fenceEpoch);
139
+ return pending(deviceId);
140
+ });
141
+ return intent === null ? null : await attempt(intent);
142
+ };
143
+ const family = {
144
+ family: "machine-link-fences",
145
+ nextDue: () => z
146
+ .strictObject({ due: z.number().nullable() })
147
+ .parse(owner.sql
148
+ .exec("SELECT MIN(next_attempt_at) AS due FROM hub_machine_link_fences")
149
+ .toArray()[0]).due,
150
+ deliver: async () => {
151
+ const due = z.array(intentSchema).parse(owner.sql
152
+ .exec(`SELECT device_id,fence_epoch,failures FROM hub_machine_link_fences
153
+ WHERE next_attempt_at<=? ORDER BY next_attempt_at,device_id LIMIT 16`, Date.now())
154
+ .toArray());
155
+ await Promise.all(due.map(attempt));
156
+ },
157
+ };
158
+ return {
159
+ fenceAll,
160
+ fenceMember,
161
+ fenceLogins,
162
+ retireAllForErasure,
163
+ revoke,
164
+ family,
165
+ };
166
+ }
@@ -0,0 +1,20 @@
1
+ import * as Schema from "effect/Schema";
2
+ /**
3
+ * A Team Agent's delivery home, not a second Agent or relationship identity.
4
+ * Routing needs only the device; the attaching member stays with the
5
+ * enrollment owner, which reauthorizes it for every act.
6
+ */
7
+ declare const machineAgentHomeSchema: Schema.Union<readonly [Schema.Struct<{
8
+ readonly kind: Schema.Literal<"hosted">;
9
+ }>, Schema.Struct<{
10
+ readonly kind: Schema.Literal<"machine">;
11
+ readonly deviceId: Schema.String;
12
+ }>]>;
13
+ /** The home published by a trusted Team directory projection. */
14
+ export type MachineAgentHome = typeof machineAgentHomeSchema.Type;
15
+ /**
16
+ * Decode one home without silently accepting another routing representation.
17
+ * This validates shape only; the host must attest the device.
18
+ */
19
+ export declare function decodeMachineAgentHome(input: unknown): MachineAgentHome;
20
+ export {};
@@ -0,0 +1,24 @@
1
+ import * as Schema from "effect/Schema";
2
+ // The directory's opaque identities use the same bound as Agent mail addresses.
3
+ const identifier = Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(256));
4
+ /**
5
+ * A Team Agent's delivery home, not a second Agent or relationship identity.
6
+ * Routing needs only the device; the attaching member stays with the
7
+ * enrollment owner, which reauthorizes it for every act.
8
+ */
9
+ const machineAgentHomeSchema = Schema.Union([
10
+ Schema.Struct({ kind: Schema.Literal("hosted") }),
11
+ Schema.Struct({
12
+ kind: Schema.Literal("machine"),
13
+ deviceId: identifier,
14
+ }),
15
+ ]);
16
+ /**
17
+ * Decode one home without silently accepting another routing representation.
18
+ * This validates shape only; the host must attest the device.
19
+ */
20
+ export function decodeMachineAgentHome(input) {
21
+ return Schema.decodeUnknownSync(machineAgentHomeSchema, {
22
+ onExcessProperty: "error",
23
+ })(input);
24
+ }
@@ -0,0 +1,43 @@
1
+ import type { PostEnvelope, PostDeliveryResult } from "@fungi.computer/carrier/post";
2
+ import type { WatchdogSqliteOwner } from "@fungi.computer/watchdog";
3
+ export type MachineLinkDevice = Readonly<{
4
+ deviceId: string;
5
+ memberId: string;
6
+ publicKey: string;
7
+ fenceEpoch: number;
8
+ }>;
9
+ /**
10
+ * Native device socket I/O. Pins/member authority and mail admission are supplied
11
+ * by their existing owners. Native attachments survive hibernation; pending
12
+ * Post I/O does not claim durable custody and is retried by Carrier.
13
+ */
14
+ export declare class MachineLinkHost {
15
+ #private;
16
+ private readonly context;
17
+ private readonly options;
18
+ private readonly channels;
19
+ private readonly replay;
20
+ private readonly routes;
21
+ constructor(context: Pick<DurableObjectState, "getWebSockets" | "acceptWebSocket" | "setWebSocketAutoResponse" | "waitUntil">, owner: WatchdogSqliteOwner, options: Readonly<{
22
+ teamId: string;
23
+ directory: () => Promise<readonly {
24
+ agentId: string;
25
+ name: string;
26
+ }[]>;
27
+ authorize: (deviceId: string) => Promise<MachineLinkDevice>;
28
+ receive: (device: MachineLinkDevice, envelope: PostEnvelope) => Promise<PostDeliveryResult>;
29
+ /** The host's failure log for refused dials and closed links. */
30
+ report: (operation: string, cause: unknown) => void;
31
+ }>);
32
+ fetch(request: Request): Promise<Response>;
33
+ private publishDirectory;
34
+ private channel;
35
+ deliver(deviceId: string, envelope: PostEnvelope): Promise<PostDeliveryResult>;
36
+ message(socket: WebSocket, message: string | ArrayBuffer): Promise<void>;
37
+ close(socket: WebSocket): void;
38
+ private authorizeSocket;
39
+ /** A delayed old fence cannot retire a fresh connection at the current epoch. */
40
+ fence(fenceEpoch: number, live: boolean): void;
41
+ /** Authorized physical erasure retires even hibernated sockets, at any epoch. */
42
+ retireForErasure(): void;
43
+ }