@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,368 @@
1
+ import { machineAgentReservations } from "@fungi.computer/shiitake/delegation";
2
+ import { createMachineEnrollment, MachineEnrollmentRefused, MachineLinkRefused, } from "../machine-link/index.js";
3
+ import { keyAllowsMachineEnroll, } from "../keys.js";
4
+ import { agentIds, createAgentDirectory, } from "@fungi.computer/shiitake/fleet";
5
+ import { DurableObject } from "cloudflare:workers";
6
+ import { Hono } from "hono";
7
+ import { bodyLimit } from "hono/body-limit";
8
+ import { validator } from "hono/validator";
9
+ import { machineEnrollmentSchema, machineLinkName, } from "../machine-link/index.js";
10
+ import { createMachineLink } from "../machine-link/workers.js";
11
+ import { defaultTenant, tenantScope } from "../tenant.js";
12
+ import { presentedGitKey } from "../git-auth.js";
13
+ import { standaloneTenancy } from "./tenancy.js";
14
+ /** Standalone's fresh directory projection for the shared hosted client. */
15
+ export function standaloneMachineAgentHome(options) {
16
+ return async (agentId) => {
17
+ const actor = options.identity();
18
+ if (actor === null)
19
+ throw new MachineLinkRefused("actor_not_allowed");
20
+ const directory = options.directory.getByName(actor.teamId);
21
+ const home = await directory.agentHome(agentId);
22
+ if (home === null)
23
+ throw new MachineLinkRefused("actor_not_allowed");
24
+ if (home.kind === "hosted")
25
+ return home;
26
+ // Admit the source and live device before the client addresses its link.
27
+ return (await directory.machineTarget({ kind: "hosted", agentId: actor.agentId }, agentId, null)).targetHome;
28
+ };
29
+ }
30
+ /** Machine-link teamId is the existing owner's Agent directory scope. */
31
+ const scopeFor = (env) => tenantScope(defaultTenant, env.OYSTER_ORIGIN);
32
+ async function liveOwner(env) {
33
+ const tenant = await env.TENANTS.getByName("registry").get(defaultTenant);
34
+ // A mesh request cannot provision a missing owner scope or cold-start
35
+ // tenancy's seed path while that scope is held inactive.
36
+ if (tenant?.status !== "active")
37
+ return null;
38
+ return (await standaloneTenancy(env)).owner();
39
+ }
40
+ /** Fresh host authority; no answer is retained by the adapter or the link. */
41
+ export function standaloneMachineLinkAuthority(env) {
42
+ const scope = scopeFor(env);
43
+ const teamId = scope.durableName("agents");
44
+ const directory = () => env.AGENT_DIRECTORY.getByName(teamId);
45
+ const authority = {
46
+ admitDevice: async (id) => {
47
+ if (id.teamId !== teamId || (await liveOwner(env)) === null)
48
+ return { live: false };
49
+ return directory().machineDevice(id);
50
+ },
51
+ currentFence: async (id) => id.teamId !== teamId ? null : directory().machineFence(id),
52
+ directory: async (id) => {
53
+ if (!(await authority.admitDevice(id)).live)
54
+ throw new MachineLinkRefused("actor_not_allowed");
55
+ return directory().machineDirectory(id);
56
+ },
57
+ resolveTarget: async (id, caller, agentId, requester) => {
58
+ if (!(await authority.admitDevice(id)).live)
59
+ throw new MachineLinkRefused("actor_not_allowed");
60
+ return directory().machineTarget(caller, agentId, requester);
61
+ },
62
+ deliverHome: async (id, caller, target, envelope) => {
63
+ // The projection used to prepare this delivery lends no later authority.
64
+ const facts = await authority.resolveTarget(id, caller, target.targetAgentId, null);
65
+ if (facts.targetHome.kind === "hosted")
66
+ return env.AGENTS.getByName(facts.targetAgentId).deliver(envelope);
67
+ const next = { teamId, deviceId: facts.targetHome.deviceId };
68
+ if (!(await authority.admitDevice(next)).live)
69
+ throw new MachineLinkRefused("actor_not_allowed");
70
+ return env.MACHINE_LINK.getByName(machineLinkName(next)).deliver(next, caller, facts.targetAgentId, envelope);
71
+ },
72
+ canErase: async (id) => {
73
+ if (id.teamId !== teamId)
74
+ return false;
75
+ const tenant = await env.TENANTS.getByName("registry").get(defaultTenant);
76
+ // Losing a login or a failed owner read is not erasure permission.
77
+ // Like the Hub: the owner scope is held inactive AND the device key is
78
+ // irreversibly retired in its retained enrollment owner.
79
+ return (tenant !== null &&
80
+ tenant.origin === scope.origin &&
81
+ (tenant.status === "suspended" || tenant.status === "deleting") &&
82
+ (await directory().machineKeyRetired(id.deviceId)));
83
+ },
84
+ deliverControlFact: async (fact) => {
85
+ const tenant = await env.TENANTS.getByName("registry").get(defaultTenant);
86
+ if (fact.teamId !== teamId ||
87
+ tenant?.origin !== scope.origin ||
88
+ (await authority.currentFence(fact)) === null)
89
+ throw new MachineLinkRefused("actor_not_allowed");
90
+ return env.MACHINE_LINK.getByName(machineLinkName(fact)).applyFence(fact);
91
+ },
92
+ };
93
+ return authority;
94
+ }
95
+ /** HTTP enrollment over the existing live KEYS and directory owners. */
96
+ export function standaloneMachineJoinRoutes(options) {
97
+ const { scope } = options;
98
+ const teamId = scope.durableName("agents");
99
+ return new Hono().post("/local/mesh/join", bodyLimit({ maxSize: 512 * 1024 }), validator("json", (value, context) => {
100
+ const parsed = machineEnrollmentSchema.safeParse(value);
101
+ return parsed.success
102
+ ? parsed.data
103
+ : context.json({ error: "invalid_enrollment" }, 400);
104
+ }), async (context) => {
105
+ if (new URL(context.req.url).origin !== scope.origin)
106
+ return context.body(null, 403);
107
+ const ownerId = await options.ownerId();
108
+ if (ownerId === null)
109
+ return context.body(null, 403);
110
+ const presented = presentedGitKey(context.req.raw);
111
+ if (presented.kind !== "key")
112
+ return context.body(null, 401);
113
+ const keys = options.keys();
114
+ const verified = await keys.verify(presented.value);
115
+ if (verified.kind !== "ok")
116
+ return context.body(null, 401);
117
+ const live = await keys.live(verified.value.keyId);
118
+ if (live?.owner !== ownerId || !keyAllowsMachineEnroll(live))
119
+ return context.body(null, 403);
120
+ const joined = await options.enroll(live.keyId, context.req.valid("json"));
121
+ // Domain refusals cross RPC as data, not an instanceof claim about a
122
+ // platform-reconstructed Error.
123
+ if (joined.kind === "refused") {
124
+ if (joined.error === "request_conflict" || joined.error === "capacity")
125
+ return context.json({ error: joined.error }, 409);
126
+ if (joined.error === "invalid_enrollment")
127
+ return context.json({ error: "invalid_enrollment" }, 400);
128
+ return context.body(null, 403);
129
+ }
130
+ return context.json({ kind: "joined", scopeId: teamId, ...joined.value }, 200);
131
+ });
132
+ }
133
+ /** Only join accepts an oyk credential; every subsequent dial uses Seam. */
134
+ export function standaloneMachineLinkRoutes(env) {
135
+ const scope = scopeFor(env);
136
+ const teamId = scope.durableName("agents");
137
+ return standaloneMachineJoinRoutes({
138
+ scope,
139
+ ownerId: async () => (await liveOwner(env))?.actor.memberId ?? null,
140
+ keys: () => env.KEYS.getByName(scope.durableName("keys")),
141
+ enroll: (keyId, input) => env.AGENT_DIRECTORY.getByName(teamId).enrollMachine(keyId, input),
142
+ }).get("/api/teams/:teamId/local/mesh/:deviceId/connect", async (context) => {
143
+ const id = {
144
+ teamId: context.req.param("teamId"),
145
+ deviceId: context.req.param("deviceId"),
146
+ };
147
+ if (new URL(context.req.url).origin !== scope.origin ||
148
+ id.teamId !== teamId ||
149
+ !(await standaloneMachineLinkAuthority(env).admitDevice(id)).live)
150
+ return context.body(null, 403);
151
+ // F1: never address an unknown scope/device, even for a refused dial.
152
+ // The shared link independently rereads admission and verifies Seam.
153
+ return env.MACHINE_LINK.getByName(machineLinkName(id)).fetch(context.req.raw);
154
+ });
155
+ }
156
+ const erasedKey = "machine-link:erased";
157
+ /** One device's link; directory, credentials and owner state remain elsewhere. */
158
+ export class MachineLinkDO extends DurableObject {
159
+ link = createMachineLink(this.ctx, {
160
+ authority: standaloneMachineLinkAuthority(this.env),
161
+ addressed: (identity) => this.ctx.id.equals(this.env.MACHINE_LINK.idFromName(machineLinkName(identity))),
162
+ erasure: {
163
+ erased: async () => (await this.ctx.storage.get(erasedKey)) === true,
164
+ read: async () => ({
165
+ erased: (await this.ctx.storage.get(erasedKey)) === true,
166
+ }),
167
+ erase: async () => {
168
+ // Only this retired device's socket/replay owner, never an Agent,
169
+ // directory or KEYS store. The shared factory checks canErase first.
170
+ await this.ctx.storage.deleteAll();
171
+ await this.ctx.storage.put(erasedKey, true);
172
+ return { erased: true };
173
+ },
174
+ },
175
+ report: (operation, cause) => {
176
+ console.error(operation, cause);
177
+ },
178
+ });
179
+ fetch(request) {
180
+ return this.link.fetch(request);
181
+ }
182
+ deliver(id, caller, agentId, envelope) {
183
+ return this.link.deliver(id, caller, agentId, envelope);
184
+ }
185
+ operation(id, caller, agentId, operation) {
186
+ return this.link.operation(id, caller, agentId, operation);
187
+ }
188
+ applyFence(fact) {
189
+ return Promise.resolve(this.link.applyFence(fact));
190
+ }
191
+ preflightErasure(teamId, deviceId) {
192
+ return this.link.preflightErasure(teamId, deviceId);
193
+ }
194
+ eraseForTeam(teamId, deviceId) {
195
+ return this.link.eraseForTeam(teamId, deviceId);
196
+ }
197
+ readErasure() {
198
+ return this.link.readErasure();
199
+ }
200
+ webSocketMessage(socket, message) {
201
+ return this.link.webSocketMessage(socket, message);
202
+ }
203
+ webSocketClose(socket, code, reason) {
204
+ this.link.webSocketClose(socket, code, reason);
205
+ }
206
+ webSocketError(socket) {
207
+ this.link.webSocketError(socket);
208
+ }
209
+ }
210
+ /** Enrollment and the ordinary directory share this one SQLite owner. */
211
+ export function createStandaloneMachineEnrollment(options) {
212
+ options.owner.sql.exec("CREATE TABLE IF NOT EXISTS standalone_machine_enroll_keys (key_id TEXT PRIMARY KEY, request_id TEXT NOT NULL) STRICT");
213
+ const configured = {
214
+ // Retained host's private storage/representation ceilings, not plan quotas.
215
+ limits: { machines: 256, agentsPerMachine: 256 },
216
+ authorizeMember: async (memberId) => {
217
+ if ((await options.ownerId()) !== memberId)
218
+ throw new MachineLinkRefused("actor_not_allowed");
219
+ },
220
+ // Enrollment keys buy identity once; they are not ongoing device credentials.
221
+ authorizeLogin: async ({ memberId }) => (await options.ownerId()) === memberId,
222
+ onAgentCreated: options.onAgentCreated,
223
+ agents: machineAgentReservations,
224
+ };
225
+ const enrollment = createMachineEnrollment({
226
+ ...configured,
227
+ owner: options.owner,
228
+ });
229
+ // A view of the ordinary directory in this same store, not another catalog.
230
+ const directory = createAgentDirectory(options.owner.sql);
231
+ const grant = (memberId) => ({
232
+ agents: "all",
233
+ actions: ["read"],
234
+ expiresAt: Date.now() + 60_000,
235
+ provenance: { actor: memberId },
236
+ });
237
+ const owner = async () => {
238
+ const memberId = await options.ownerId();
239
+ if (memberId === null)
240
+ throw new MachineLinkRefused("actor_not_allowed");
241
+ return memberId;
242
+ };
243
+ const admitDevice = async (id) => {
244
+ if (id.teamId !== options.scopeId || (await options.ownerId()) === null)
245
+ return { live: false };
246
+ try {
247
+ const device = await enrollment.authorizeDevice(id.deviceId);
248
+ return {
249
+ live: true,
250
+ teamId: options.scopeId,
251
+ deviceId: device.deviceId,
252
+ memberId: device.memberId,
253
+ publicKey: device.publicKey,
254
+ fenceEpoch: device.fenceEpoch,
255
+ };
256
+ }
257
+ catch (cause) {
258
+ if (cause instanceof MachineEnrollmentRefused ||
259
+ cause instanceof MachineLinkRefused)
260
+ return { live: false };
261
+ throw cause;
262
+ }
263
+ };
264
+ const currentFence = async (id) => {
265
+ if (id.teamId !== options.scopeId)
266
+ return null;
267
+ const memberId = await options.ownerId();
268
+ const device = enrollment
269
+ .retainedDevices()
270
+ .find((row) => row.deviceId === id.deviceId);
271
+ return device === undefined
272
+ ? null
273
+ : {
274
+ ...id,
275
+ fenceEpoch: device.fenceEpoch,
276
+ live: memberId === device.memberId && !device.revoked,
277
+ };
278
+ };
279
+ const target = async (caller, agentId, requester) => {
280
+ const memberId = await owner();
281
+ const exists = (id) => {
282
+ const parsed = agentIds.agent.parse(id);
283
+ return (parsed.ok && directory.read(grant(memberId), parsed.value) !== null);
284
+ };
285
+ if (!exists(caller.agentId) ||
286
+ !exists(agentId) ||
287
+ (requester !== null &&
288
+ (!exists(requester) ||
289
+ (caller.agentId !== agentId && caller.agentId !== requester))))
290
+ throw new MachineLinkRefused("actor_not_allowed");
291
+ const source = enrollment.home(caller.agentId);
292
+ if (caller.kind === "device"
293
+ ? source?.kind !== "machine" || source.deviceId !== caller.deviceId
294
+ : source !== null)
295
+ throw new MachineLinkRefused("actor_not_allowed");
296
+ if (source?.kind === "machine")
297
+ await enrollment.authorizeDevice(source.deviceId);
298
+ const targetHome = enrollment.home(agentId) ?? {
299
+ kind: "hosted",
300
+ };
301
+ if (targetHome.kind === "machine")
302
+ await enrollment.authorizeDevice(targetHome.deviceId);
303
+ return {
304
+ teamId: options.scopeId,
305
+ callerAgentId: caller.agentId,
306
+ targetAgentId: agentId,
307
+ actorAgentId: requester ?? caller.agentId,
308
+ callerHome: source ?? { kind: "hosted" },
309
+ targetHome,
310
+ };
311
+ };
312
+ return {
313
+ ...enrollment,
314
+ admitDevice,
315
+ currentFence,
316
+ target,
317
+ async publicDirectory(id) {
318
+ if (!(await admitDevice(id)).live)
319
+ throw new MachineLinkRefused("actor_not_allowed");
320
+ const memberId = await owner();
321
+ return directory
322
+ .list(grant(memberId))
323
+ .map(({ agentId, name }) => ({ agentId, name }));
324
+ },
325
+ async enrollWithKey(keyId, input) {
326
+ const memberId = await options.ownerId();
327
+ const key = await options.liveKey(keyId);
328
+ if (memberId === null ||
329
+ key?.owner !== memberId ||
330
+ !keyAllowsMachineEnroll(key))
331
+ throw new MachineLinkRefused("actor_not_allowed");
332
+ // This wrapper is per call: concurrent joins never share a mutable key
333
+ // slot. createMachineEnrollment invokes it only around its synchronous
334
+ // identity/directory commit, after awaited validation and authority reads.
335
+ const consumingOwner = {
336
+ sql: options.owner.sql,
337
+ transactionSync: (body) => options.owner.transactionSync(() => {
338
+ const consumed = options.owner.sql
339
+ .exec("SELECT key_id FROM standalone_machine_enroll_keys WHERE key_id=?", keyId)
340
+ .toArray().length !== 0;
341
+ if (consumed) {
342
+ // Replay is an ordinary enrollment receipt, not another spend.
343
+ // The shared owner checks canonical body, member, login and
344
+ // retirement before returning its original directory identities.
345
+ if (options.owner.sql
346
+ .exec("SELECT 1 FROM standalone_machine_enroll_keys WHERE key_id=? AND request_id=?", keyId, input.requestId)
347
+ .toArray().length === 0)
348
+ throw new MachineLinkRefused("actor_not_allowed");
349
+ }
350
+ else {
351
+ options.owner.sql.exec("INSERT INTO standalone_machine_enroll_keys (key_id, request_id) VALUES (?, ?)", keyId, input.requestId);
352
+ }
353
+ return body();
354
+ }),
355
+ };
356
+ return createMachineEnrollment({
357
+ ...configured,
358
+ owner: consumingOwner,
359
+ authorizeMember: async (member) => {
360
+ await configured.authorizeMember(member);
361
+ const current = await options.liveKey(keyId);
362
+ if (current?.owner !== member || !keyAllowsMachineEnroll(current))
363
+ throw new MachineLinkRefused("actor_not_allowed");
364
+ },
365
+ }).enroll({ memberId, clientId: "oyster-key", loginId: key.keyId }, input);
366
+ },
367
+ };
368
+ }
@@ -41,7 +41,7 @@ export declare const mediaFrameMethods: {
41
41
  aspectRatio?: "16:9" | "4:3" | "1:1" | "3:4" | "9:16" | "21:9" | "9:21" | undefined;
42
42
  references?: {
43
43
  path: string;
44
- role?: "first-frame" | "last-frame" | "reference" | undefined;
44
+ role?: "reference" | "first-frame" | "last-frame" | undefined;
45
45
  }[] | undefined;
46
46
  generateAudio?: boolean | undefined;
47
47
  steps?: number | undefined;
@@ -60,7 +60,7 @@ export declare const mediaFrameMethods: {
60
60
  generateAudio?: boolean | undefined;
61
61
  references?: {
62
62
  path: string;
63
- role: "first-frame" | "last-frame" | "reference";
63
+ role: "reference" | "first-frame" | "last-frame";
64
64
  }[] | undefined;
65
65
  };
66
66
  }];
@@ -53,21 +53,24 @@ export function workersMedia(env, workspace) {
53
53
  });
54
54
  if (!response.ok) {
55
55
  const failure = await readMediaFailure(response);
56
- return context.json({
57
- kind: "failed",
58
- failure: failure === null
59
- ? {
60
- message: "Invalid media service response",
61
- code: "PROTOCOL",
62
- retryable: false,
63
- }
64
- : {
65
- message: failure.error,
66
- code: failure.code,
67
- stage: failure.stage,
68
- retryable: failure.retryable,
69
- },
70
- });
56
+ const projected = failure === null
57
+ ? {
58
+ message: "Invalid media service response",
59
+ code: "PROTOCOL",
60
+ retryable: false,
61
+ }
62
+ : {
63
+ message: failure.error,
64
+ code: failure.code,
65
+ stage: failure.stage,
66
+ retryable: failure.retryable,
67
+ };
68
+ if (failure?.provider !== undefined)
69
+ return context.json({
70
+ kind: "failed",
71
+ failure: { ...projected, provider: failure.provider },
72
+ });
73
+ return context.json({ kind: "failed", failure: projected });
71
74
  }
72
75
  if (response.status === 202) {
73
76
  const pending = mediaPendingSchema.parse(await response.json());
@@ -186,7 +189,7 @@ export async function dispatchMediaBinding(env, frame, props, method, args, work
186
189
  if (caller.computerId !== workspace.computerId ||
187
190
  caller.actor.kind === "team-key")
188
191
  throw new AppInstanceError("revoked");
189
- const parsed = parseMediaBindingCall(Schema.decodeUnknownSync(Schema.Json)({
192
+ const parsed = parseMediaBindingCall(z.json().parse({
190
193
  operation: method,
191
194
  input: args[0],
192
195
  }));
@@ -4,7 +4,7 @@ export declare const previewSelectorSchema: Schema.Struct<{
4
4
  readonly ownerId: Schema.NonEmptyString;
5
5
  readonly installationId: Schema.NonEmptyString;
6
6
  readonly selection: Schema.Struct<{
7
- readonly instanceId: Schema.Codec<import("../index.js").InstanceId, string, never, never>;
7
+ readonly instanceId: Schema.Codec<import("../instance-id.js").InstanceId, string, never, never>;
8
8
  readonly owner: Schema.Struct<{
9
9
  readonly kind: Schema.Literals<readonly ["installation", "deployment"]>;
10
10
  readonly id: Schema.NonEmptyString;
@@ -48,4 +48,4 @@ export declare function createStandalonePreview(storage: ConstructorParameters<t
48
48
  fetch(signature: string, port: number, request: Request): Promise<Response>;
49
49
  };
50
50
  export declare function passivePreviewResponse(response: Response): Promise<Response>;
51
- export declare function standalonePreviewRequest(request: Request, env: StandalonePreviewEnv, computerId: string, forward: (signature: string, port: number, request: Request) => Promise<Response>): Promise<Response | null>;
51
+ export declare function standalonePreviewRequest(request: Request, env: StandalonePreviewEnv, ownsComputer: (computerId: string) => Promise<boolean>, forward: (computerId: string, signature: string, port: number, request: Request) => Promise<Response>): Promise<Response | null>;
@@ -3,7 +3,7 @@ import * as z from "zod";
3
3
  import { ComputerPortPreview } from "@fungi.computer/computer/preview";
4
4
  import { selectionSchema } from "../index.js";
5
5
  import { sha256DigestSchema } from "@fungi.computer/puffball/effect";
6
- import { computerPortInputSchema } from "@fungi.computer/computer/operations";
6
+ import { computerPortInputSchema } from "@fungi.computer/computer-contract";
7
7
  export const previewSelectorSchema = Schema.Struct({
8
8
  ownerId: Schema.NonEmptyString,
9
9
  installationId: Schema.NonEmptyString,
@@ -129,7 +129,7 @@ export async function passivePreviewResponse(response) {
129
129
  });
130
130
  return new Response(response.body, { status: response.status, headers });
131
131
  }
132
- export async function standalonePreviewRequest(request, env, computerId, forward) {
132
+ export async function standalonePreviewRequest(request, env, ownsComputer, forward) {
133
133
  const incoming = new URL(request.url);
134
134
  const previewHost = configuredPreviewOrigin(env);
135
135
  // A host nothing configured as the preview tunnel is left to the caller's
@@ -143,23 +143,50 @@ export async function standalonePreviewRequest(request, env, computerId, forward
143
143
  if (!["GET", "HEAD"].includes(request.method) ||
144
144
  request.headers.has("Upgrade"))
145
145
  return new Response(null, { status: 404 });
146
- const path = /^\/preview\/([^/]+)\/([0-9]{1,5})\/([a-z2-7]{20})(\/.*)$/u.exec(incoming.pathname);
147
- if (path?.[1] !== encodeURIComponent(computerId) ||
148
- path[3] === undefined ||
149
- path[4] === undefined)
146
+ const path = new URLPattern({
147
+ pathname: "/preview/:computerId/:port/:signature/*",
148
+ }).exec(incoming)?.pathname.groups;
149
+ if (path === undefined)
150
150
  return new Response(null, { status: 404 });
151
151
  if (previewOrigins(env) === null)
152
152
  return new Response(null, { status: 404 });
153
- const input = computerPortInputSchema.safeParse({ port: Number(path[2]) });
154
- if (!input.success || String(input.data.port) !== path[2])
153
+ const ticket = z
154
+ .object({
155
+ computerId: z.string().min(1),
156
+ port: z.string().regex(/^[0-9]{1,5}$/u),
157
+ signature: z.string().regex(/^[a-z2-7]{20}$/u),
158
+ "0": z.string(),
159
+ })
160
+ .safeParse(path);
161
+ if (!ticket.success)
162
+ return new Response(null, { status: 404 });
163
+ let computerId;
164
+ try {
165
+ computerId = decodeURIComponent(ticket.data.computerId);
166
+ }
167
+ catch (cause) {
168
+ if (cause instanceof URIError)
169
+ return new Response(null, { status: 404 });
170
+ throw cause;
171
+ }
172
+ if (computerId.length === 0 ||
173
+ computerId.length > 256 ||
174
+ encodeURIComponent(computerId) !== ticket.data.computerId)
175
+ return new Response(null, { status: 404 });
176
+ const input = computerPortInputSchema.safeParse({
177
+ port: Number(ticket.data.port),
178
+ });
179
+ if (!input.success || String(input.data.port) !== ticket.data.port)
180
+ return new Response(null, { status: 404 });
181
+ if (!(await ownsComputer(computerId)))
155
182
  return new Response(null, { status: 404 });
156
183
  const service = new URL("http://localhost");
157
- service.pathname = path[4];
184
+ service.pathname = `/${ticket.data["0"]}`;
158
185
  service.search = incoming.search;
159
186
  const headers = new Headers(request.headers);
160
187
  for (const name of ["cookie", "authorization", "proxy-authorization", "host"])
161
188
  headers.delete(name);
162
- return forward(path[3], input.data.port, new Request(service, {
189
+ return forward(computerId, ticket.data.signature, input.data.port, new Request(service, {
163
190
  method: request.method,
164
191
  headers,
165
192
  signal: request.signal,