@fungi.computer/oyster 0.5.0 → 0.7.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 (185) hide show
  1. package/README.md +155 -63
  2. package/dist/agent-archive-projection.d.ts +8 -0
  3. package/dist/agent-archive-projection.js +49 -0
  4. package/dist/agent-schedules.d.ts +50 -0
  5. package/dist/agent-schedules.js +17 -0
  6. package/dist/agents.d.ts +35 -14
  7. package/dist/agents.js +97 -144
  8. package/dist/app-builds.d.ts +7 -2
  9. package/dist/app-builds.js +19 -2
  10. package/dist/approval-capability.d.ts +1 -1
  11. package/dist/approval-capability.js +11 -16
  12. package/dist/approval-home.d.ts +2 -1
  13. package/dist/approval-home.js +5 -3
  14. package/dist/apps/catalog.js +1 -1
  15. package/dist/apps/gate.d.ts +4 -7
  16. package/dist/apps/gate.js +14 -29
  17. package/dist/apps/index.d.ts +1 -1
  18. package/dist/apps/installation.d.ts +4 -42
  19. package/dist/apps/installation.js +7 -75
  20. package/dist/apps/operations.js +0 -10
  21. package/dist/apps/permissions.d.ts +18 -8
  22. package/dist/apps/permissions.js +7 -1
  23. package/dist/auth.js +9 -1
  24. package/dist/authorization.d.ts +307 -0
  25. package/dist/authorization.js +542 -0
  26. package/dist/boot-host-script.js +1 -1
  27. package/dist/build/build-jobs.d.ts +2 -2
  28. package/dist/build/candidate-builder.d.ts +2 -0
  29. package/dist/build/candidate-builder.js +5 -1
  30. package/dist/build/candidate-cli.js +3 -0
  31. package/dist/build/candidate-cli.mjs +67 -22
  32. package/dist/build/job-log.d.ts +23 -0
  33. package/dist/build/job-log.js +88 -0
  34. package/dist/build/local-build.d.ts +2 -0
  35. package/dist/build/local-build.js +11 -9
  36. package/dist/channels/binding.d.ts +8 -1
  37. package/dist/channels/binding.js +12 -0
  38. package/dist/channels/conversations.d.ts +16 -5
  39. package/dist/channels/visitor-channel.d.ts +53 -0
  40. package/dist/channels/visitor-channel.js +124 -0
  41. package/dist/channels/visitor-edge.d.ts +115 -0
  42. package/dist/channels/visitor-edge.js +582 -0
  43. package/dist/channels/visitor-request.d.ts +7 -0
  44. package/dist/channels/visitor-request.js +17 -0
  45. package/dist/channels/visitor-stream-slots.d.ts +2 -0
  46. package/dist/channels/visitor-stream-slots.js +33 -0
  47. package/dist/channels/visitors.d.ts +47 -0
  48. package/dist/channels/visitors.js +181 -0
  49. package/dist/cloudflare.d.ts +11 -2
  50. package/dist/cloudflare.js +27 -1
  51. package/dist/{page/computer-target.d.ts → computer-target.d.ts} +2 -0
  52. package/dist/{page/computer-target.js → computer-target.js} +3 -1
  53. package/dist/files.js +9 -8
  54. package/dist/git-admission.d.ts +3 -3
  55. package/dist/git-admission.js +4 -6
  56. package/dist/git-door.d.ts +8 -7
  57. package/dist/git-door.js +18 -17
  58. package/dist/git.d.ts +1 -1
  59. package/dist/host.d.ts +38 -42
  60. package/dist/host.js +182 -239
  61. package/dist/installation-execution.js +10 -5
  62. package/dist/installation-operations.d.ts +4 -180
  63. package/dist/installation-operations.js +1 -5
  64. package/dist/installation-store-generation.d.ts +4 -1
  65. package/dist/installation-store-generation.js +59 -6
  66. package/dist/internal/delivery.js +0 -1
  67. package/dist/internal/host-gate.d.ts +9 -6
  68. package/dist/internal/host-gate.js +19 -25
  69. package/dist/internal/invocation-timing.d.ts +16 -0
  70. package/dist/internal/invocation-timing.js +23 -0
  71. package/dist/internal/registry-installations.js +1 -0
  72. package/dist/invocation.d.ts +19 -11
  73. package/dist/invocation.js +171 -96
  74. package/dist/keys.d.ts +2 -19
  75. package/dist/keys.js +1 -37
  76. package/dist/mcp.d.ts +15 -27
  77. package/dist/mcp.js +105 -155
  78. package/dist/page/agent-browser.d.ts +15 -0
  79. package/dist/page/agent-browser.js +162 -0
  80. package/dist/page/app-permissions-port.d.ts +3 -0
  81. package/dist/page/app-permissions-port.js +43 -0
  82. package/dist/page/app-permissions.d.ts +11 -0
  83. package/dist/page/app-permissions.js +296 -0
  84. package/dist/page/capabilities.js +1 -1
  85. package/dist/page/child-selection.d.ts +27 -0
  86. package/dist/page/child-selection.js +38 -0
  87. package/dist/page/isolated-page-theme.d.ts +2 -0
  88. package/dist/page/isolated-page-theme.js +11 -0
  89. package/dist/page/mount.d.ts +2 -13
  90. package/dist/page/mount.js +8 -94
  91. package/dist/page-script.d.ts +0 -3
  92. package/dist/page-script.js +3 -4
  93. package/dist/release.d.ts +13 -0
  94. package/dist/release.js +10 -0
  95. package/dist/request-origin.d.ts +4 -0
  96. package/dist/request-origin.js +10 -0
  97. package/dist/standalone/agent-archive-owner.d.ts +42 -0
  98. package/dist/standalone/agent-archive-owner.js +116 -0
  99. package/dist/standalone/agent-archive-schedules.d.ts +6 -0
  100. package/dist/standalone/agent-archive-schedules.js +76 -0
  101. package/dist/standalone/agent-archive.d.ts +31 -0
  102. package/dist/standalone/agent-archive.js +67 -0
  103. package/dist/standalone/agent-computers.d.ts +7 -6
  104. package/dist/standalone/agent-computers.js +4 -7
  105. package/dist/standalone/agent-mail.d.ts +2 -2
  106. package/dist/standalone/agents.d.ts +157 -20
  107. package/dist/standalone/agents.js +242 -38
  108. package/dist/standalone/app-builds-binding.js +1 -1
  109. package/dist/standalone/app-set.js +2 -54
  110. package/dist/standalone/app-source.d.ts +3 -4
  111. package/dist/standalone/app-source.js +12 -9
  112. package/dist/standalone/builds.d.ts +2 -14
  113. package/dist/standalone/builds.js +15 -13
  114. package/dist/standalone/computer-fleet.d.ts +4 -4
  115. package/dist/standalone/computer-fleet.js +20 -43
  116. package/dist/standalone/computer.d.ts +4 -3
  117. package/dist/standalone/computer.js +52 -53
  118. package/dist/standalone/dashboard-approval.d.ts +10 -0
  119. package/dist/standalone/dashboard-approval.js +66 -0
  120. package/dist/standalone/machine-link.d.ts +4 -221
  121. package/dist/standalone/machine-link.js +21 -15
  122. package/dist/standalone/media.d.ts +1 -0
  123. package/dist/standalone/media.js +20 -14
  124. package/dist/standalone/owner-routes.d.ts +16 -0
  125. package/dist/standalone/owner-routes.js +21 -0
  126. package/dist/standalone/permission-resources.d.ts +5 -0
  127. package/dist/standalone/permission-resources.js +31 -0
  128. package/dist/standalone/registry.d.ts +7 -0
  129. package/dist/standalone/registry.js +3 -0
  130. package/dist/standalone/tenancy.js +3 -1
  131. package/dist/standalone/worker-routes.d.ts +19 -0
  132. package/dist/standalone/worker-routes.js +9 -0
  133. package/dist/standalone/worker.d.ts +1 -1
  134. package/dist/standalone/worker.js +27 -35
  135. package/dist/standalone/workflow-binding.d.ts +8 -3
  136. package/dist/standalone/workflow-binding.js +14 -5
  137. package/dist/standalone/workflow-git-capture.js +2 -1
  138. package/dist/standalone/workflow-host.d.ts +8 -103
  139. package/dist/standalone/workflow-host.js +1 -1
  140. package/dist/standalone/workflow-node-capture.js +2 -2
  141. package/dist/standalone/workflow-root-authority.d.ts +1 -1
  142. package/dist/standalone/workflow-root-authority.js +1 -1
  143. package/dist/standalone/workflow-schedules.d.ts +14 -5
  144. package/dist/standalone/workflow-schedules.js +11 -5
  145. package/dist/standalone/workflow-webhooks.d.ts +4 -18
  146. package/dist/standalone/workflow-webhooks.js +5 -5
  147. package/dist/standalone/workflows.d.ts +36 -28
  148. package/dist/standalone/workflows.js +6 -3
  149. package/dist/storage-frame.d.ts +6 -6
  150. package/dist/testing/capabilities.d.ts +7 -6
  151. package/dist/testing/capabilities.js +3 -6
  152. package/dist/testing/installation.d.ts +0 -1
  153. package/dist/testing/installation.js +2 -5
  154. package/dist/turnstile.d.ts +27 -0
  155. package/dist/turnstile.js +51 -0
  156. package/dist/workers-git.d.ts +12 -4
  157. package/dist/workers-git.js +58 -36
  158. package/dist/workers.d.ts +9 -1
  159. package/dist/workers.js +59 -20
  160. package/dist/workflows.d.ts +2 -0
  161. package/dist/workflows.js +1 -0
  162. package/docs/api/README.md +7 -0
  163. package/docs/api/agent-schedules.md +175 -0
  164. package/docs/api/agents.md +34 -13
  165. package/docs/api/app-storage.md +1 -1
  166. package/docs/api/apps.md +323 -225
  167. package/docs/api/authorization.md +1300 -0
  168. package/docs/api/build-contract.md +2 -2
  169. package/docs/api/build.md +40 -37
  170. package/docs/api/cloudflare.md +27 -2
  171. package/docs/api/git.md +20 -11
  172. package/docs/api/host.md +80 -199
  173. package/docs/api/index.md +1 -1
  174. package/docs/api/invocation.md +15 -11
  175. package/docs/api/keys.md +1 -98
  176. package/docs/api/mcp.md +43 -40
  177. package/docs/api/modules.md +2 -0
  178. package/docs/api/page/mount.md +16 -19
  179. package/docs/api/workers-git.md +46 -12
  180. package/docs/api/workers.md +125 -235
  181. package/docs/site/agent-quickstart.mdx +6 -2
  182. package/examples/standalone/wrangler.celld.jsonc +66 -0
  183. package/package.json +64 -21
  184. package/dist/standalone/channel-session-door.d.ts +0 -4
  185. package/dist/standalone/channel-session-door.js +0 -42
@@ -2,6 +2,8 @@ import { createMachineEnrollment, type MachineEnrollment, type MachineLinkDevice
2
2
  import { type LiveOysterKey, type OysterKeyStore } from "../keys.js";
3
3
  import type { WatchdogSqliteOwner } from "@fungi.computer/watchdog";
4
4
  import { DurableObject } from "cloudflare:workers";
5
+ import type { BlankEnv } from "hono/types";
6
+ import { type Routes } from "../authorization.js";
5
7
  import { type MachineLinkAuthority, type MachineLinkFenceResult } from "../machine-link/index.js";
6
8
  import type { PostEnvelope, PostDeliveryResult } from "@fungi.computer/carrier/post";
7
9
  import type { MachineAgentOperation } from "@fungi.computer/seam/machine-link-protocol";
@@ -40,228 +42,9 @@ export declare function standaloneMachineJoinRoutes(options: {
40
42
  live(keyId: string): Promise<LiveOysterKey | null>;
41
43
  };
42
44
  enroll(keyId: string, input: MachineEnrollment): Promise<StandaloneMachineJoinResult>;
43
- }): import("hono/hono-base").HonoBase<import("hono/types").BlankEnv, {
44
- "/local/mesh/join": {
45
- $post: {
46
- input: {
47
- json: {
48
- requestId: string;
49
- publicKey: string;
50
- machineLabel: string;
51
- agents: {
52
- name: string;
53
- }[];
54
- };
55
- };
56
- output: null;
57
- outputFormat: "body";
58
- status: 403;
59
- } | {
60
- input: {
61
- json: {
62
- requestId: string;
63
- publicKey: string;
64
- machineLabel: string;
65
- agents: {
66
- name: string;
67
- }[];
68
- };
69
- };
70
- output: {
71
- error: string;
72
- };
73
- outputFormat: "json";
74
- status: 400;
75
- } | {
76
- input: {
77
- json: {
78
- requestId: string;
79
- publicKey: string;
80
- machineLabel: string;
81
- agents: {
82
- name: string;
83
- }[];
84
- };
85
- };
86
- output: null;
87
- outputFormat: "body";
88
- status: 401;
89
- } | {
90
- input: {
91
- json: {
92
- requestId: string;
93
- publicKey: string;
94
- machineLabel: string;
95
- agents: {
96
- name: string;
97
- }[];
98
- };
99
- };
100
- output: {
101
- error: "request_conflict" | "capacity";
102
- };
103
- outputFormat: "json";
104
- status: 409;
105
- } | {
106
- input: {
107
- json: {
108
- requestId: string;
109
- publicKey: string;
110
- machineLabel: string;
111
- agents: {
112
- name: string;
113
- }[];
114
- };
115
- };
116
- output: {
117
- error: string;
118
- };
119
- outputFormat: "json";
120
- status: 400;
121
- } | {
122
- input: {
123
- json: {
124
- requestId: string;
125
- publicKey: string;
126
- machineLabel: string;
127
- agents: {
128
- name: string;
129
- }[];
130
- };
131
- };
132
- output: {
133
- deviceId: string;
134
- agents: {
135
- selection: number;
136
- agentId: string;
137
- }[];
138
- kind: string;
139
- scopeId: string;
140
- };
141
- outputFormat: "json";
142
- status: 200;
143
- };
144
- };
145
- }, "/", "/local/mesh/join">;
45
+ }): Routes<BlankEnv>;
146
46
  /** Only join accepts an oyk credential; every subsequent dial uses Seam. */
147
- export declare function standaloneMachineLinkRoutes(env: StandaloneMachineLinkEnv): import("hono/hono-base").HonoBase<import("hono/types").BlankEnv, {
148
- "/local/mesh/join": {
149
- $post: {
150
- input: {
151
- json: {
152
- requestId: string;
153
- publicKey: string;
154
- machineLabel: string;
155
- agents: {
156
- name: string;
157
- }[];
158
- };
159
- };
160
- output: null;
161
- outputFormat: "body";
162
- status: 403;
163
- } | {
164
- input: {
165
- json: {
166
- requestId: string;
167
- publicKey: string;
168
- machineLabel: string;
169
- agents: {
170
- name: string;
171
- }[];
172
- };
173
- };
174
- output: {
175
- error: string;
176
- };
177
- outputFormat: "json";
178
- status: 400;
179
- } | {
180
- input: {
181
- json: {
182
- requestId: string;
183
- publicKey: string;
184
- machineLabel: string;
185
- agents: {
186
- name: string;
187
- }[];
188
- };
189
- };
190
- output: null;
191
- outputFormat: "body";
192
- status: 401;
193
- } | {
194
- input: {
195
- json: {
196
- requestId: string;
197
- publicKey: string;
198
- machineLabel: string;
199
- agents: {
200
- name: string;
201
- }[];
202
- };
203
- };
204
- output: {
205
- error: "request_conflict" | "capacity";
206
- };
207
- outputFormat: "json";
208
- status: 409;
209
- } | {
210
- input: {
211
- json: {
212
- requestId: string;
213
- publicKey: string;
214
- machineLabel: string;
215
- agents: {
216
- name: string;
217
- }[];
218
- };
219
- };
220
- output: {
221
- error: string;
222
- };
223
- outputFormat: "json";
224
- status: 400;
225
- } | {
226
- input: {
227
- json: {
228
- requestId: string;
229
- publicKey: string;
230
- machineLabel: string;
231
- agents: {
232
- name: string;
233
- }[];
234
- };
235
- };
236
- output: {
237
- deviceId: string;
238
- agents: {
239
- selection: number;
240
- agentId: string;
241
- }[];
242
- kind: string;
243
- scopeId: string;
244
- };
245
- outputFormat: "json";
246
- status: 200;
247
- };
248
- };
249
- } & {
250
- "/api/teams/:teamId/local/mesh/:deviceId/connect": {
251
- $get: {
252
- input: {
253
- param: {
254
- teamId: string;
255
- } & {
256
- deviceId: string;
257
- };
258
- };
259
- output: {};
260
- outputFormat: string;
261
- status: import("hono/utils/http-status").StatusCode;
262
- };
263
- };
264
- }, "/", "/api/teams/:teamId/local/mesh/:deviceId/connect">;
47
+ export declare function standaloneMachineLinkRoutes(env: StandaloneMachineLinkEnv): Routes<BlankEnv>;
265
48
  /** One device's link; directory, credentials and owner state remain elsewhere. */
266
49
  export declare class MachineLinkDO extends DurableObject<StandaloneMachineLinkEnv> {
267
50
  private readonly link;
@@ -1,11 +1,10 @@
1
1
  import { machineAgentCodecs, machineAgentReservations, } from "@fungi.computer/shiitake/delegation";
2
2
  import { createMachineEnrollment, MachineEnrollmentRefused, MachineLinkRefused, } from "../machine-link/index.js";
3
- import { keyAllowsMachineEnroll, } from "../keys.js";
3
+ import {} from "../keys.js";
4
4
  import { agentIds, createAgentDirectory, } from "@fungi.computer/shiitake/fleet";
5
5
  import { DurableObject } from "cloudflare:workers";
6
- import { Hono } from "hono";
6
+ import { allowsMachineEnrollment, door, routes, } from "../authorization.js";
7
7
  import { bodyLimit } from "hono/body-limit";
8
- import { validator } from "hono/validator";
9
8
  import { machineEnrollmentSchema, machineLinkName, } from "../machine-link/index.js";
10
9
  import { createMachineLink } from "../machine-link/workers.js";
11
10
  import { deliverUndeliverableMail } from "../machine-link/delivery.js";
@@ -123,12 +122,19 @@ export function standaloneMachineLinkAuthority(env) {
123
122
  export function standaloneMachineJoinRoutes(options) {
124
123
  const { scope } = options;
125
124
  const teamId = scope.durableName("agents");
126
- return new Hono().post("/local/mesh/join", bodyLimit({ maxSize: 512 * 1024 }), validator("json", (value, context) => {
127
- const parsed = machineEnrollmentSchema.safeParse(value);
128
- return parsed.success
129
- ? parsed.data
130
- : context.json({ error: "invalid_enrollment" }, 400);
131
- }), async (context) => {
125
+ return routes().post("/local/mesh/join", door("an oyk key whose only grant is machine.enroll, verified here"), async (context) => {
126
+ let body;
127
+ try {
128
+ body = await context.req.json();
129
+ }
130
+ catch (error) {
131
+ if (!(error instanceof SyntaxError))
132
+ throw error;
133
+ return context.json({ error: "invalid_enrollment" }, 400);
134
+ }
135
+ const enrollment = machineEnrollmentSchema.safeParse(body);
136
+ if (!enrollment.success)
137
+ return context.json({ error: "invalid_enrollment" }, 400);
132
138
  if (new URL(context.req.url).origin !== scope.origin)
133
139
  return context.body(null, 403);
134
140
  const ownerId = await options.ownerId();
@@ -142,9 +148,9 @@ export function standaloneMachineJoinRoutes(options) {
142
148
  if (verified.kind !== "ok")
143
149
  return context.body(null, 401);
144
150
  const live = await keys.live(verified.value.keyId);
145
- if (live?.owner !== ownerId || !keyAllowsMachineEnroll(live))
151
+ if (live?.owner !== ownerId || !allowsMachineEnrollment(live))
146
152
  return context.body(null, 403);
147
- const joined = await options.enroll(live.keyId, context.req.valid("json"));
153
+ const joined = await options.enroll(live.keyId, enrollment.data);
148
154
  // Domain refusals cross RPC as data, not an instanceof claim about a
149
155
  // platform-reconstructed Error.
150
156
  if (joined.kind === "refused") {
@@ -155,7 +161,7 @@ export function standaloneMachineJoinRoutes(options) {
155
161
  return context.body(null, 403);
156
162
  }
157
163
  return context.json({ kind: "joined", scopeId: teamId, ...joined.value }, 200);
158
- });
164
+ }, { middleware: [bodyLimit({ maxSize: 512 * 1024 })] });
159
165
  }
160
166
  /** Only join accepts an oyk credential; every subsequent dial uses Seam. */
161
167
  export function standaloneMachineLinkRoutes(env) {
@@ -166,7 +172,7 @@ export function standaloneMachineLinkRoutes(env) {
166
172
  ownerId: async () => (await liveOwner(env))?.actor.memberId ?? null,
167
173
  keys: () => env.KEYS.getByName(scope.durableName("keys")),
168
174
  enroll: (keyId, input) => env.AGENT_DIRECTORY.getByName(teamId).enrollMachine(keyId, input),
169
- }).get("/api/teams/:teamId/local/mesh/:deviceId/connect", async (context) => {
175
+ }).get("/api/teams/:teamId/local/mesh/:deviceId/connect", door("a live enrolled device; its link verifies Seam on every dial"), async (context) => {
170
176
  const id = {
171
177
  teamId: context.req.param("teamId"),
172
178
  deviceId: context.req.param("deviceId"),
@@ -366,7 +372,7 @@ export function createStandaloneMachineEnrollment(options) {
366
372
  const key = await options.liveKey(keyId);
367
373
  if (memberId === null ||
368
374
  key?.owner !== memberId ||
369
- !keyAllowsMachineEnroll(key))
375
+ !allowsMachineEnrollment(key))
370
376
  throw new MachineLinkRefused("actor_not_allowed");
371
377
  // This wrapper is per call: concurrent joins never share a mutable key
372
378
  // slot. createMachineEnrollment invokes it only around its synchronous
@@ -398,7 +404,7 @@ export function createStandaloneMachineEnrollment(options) {
398
404
  authorizeMember: async (member) => {
399
405
  await configured.authorizeMember(member);
400
406
  const current = await options.liveKey(keyId);
401
- if (current?.owner !== member || !keyAllowsMachineEnroll(current))
407
+ if (current?.owner !== member || !allowsMachineEnrollment(current))
402
408
  throw new MachineLinkRefused("actor_not_allowed");
403
409
  },
404
410
  }).enroll({ memberId, clientId: "oyster-key", loginId: key.keyId }, input);
@@ -41,6 +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
+ computerId?: string | undefined;
44
45
  role?: "reference" | "first-frame" | "last-frame" | undefined;
45
46
  }[] | undefined;
46
47
  generateAudio?: boolean | undefined;
@@ -2,30 +2,31 @@ import { z } from "zod";
2
2
  import { MAX_REFERENCE_BYTES, MediaSettingsError, } from "@fungi.computer/darkroom";
3
3
  import { AppInstanceError } from "../index.js";
4
4
  import { readMediaFailure } from "@fungi.computer/darkroom/browser";
5
- import { appMediaGenerationResultSchema } from "@fungi.computer/darkroom/app-media-contracts";
5
+ import { appMediaGenerationResultSchema, mediaModelsSchema, } from "@fungi.computer/darkroom/app-media-contracts";
6
6
  import { mediaSlot } from "../media.js";
7
- import { Hono } from "hono";
7
+ import { computer, routes as routeTable } from "../authorization.js";
8
8
  import * as Schema from "effect/Schema";
9
9
  import { pendingSchema } from "@fungi.computer/agar/backend-definition";
10
10
  import { mediaGenerationDescriptionSchema, mediaGenerationJsonRequestSchema, mediaGenerationReferenceSchema, } from "@fungi.computer/agar/media-json";
11
- import { createMediaService, mediaPendingSchema, mediaResultSchema, mediaBindingCallSchema, mediaBindingRequest, parseMediaBindingCall, readMediaBindingReply, } from "@fungi.computer/darkroom/server";
11
+ import { createMediaService, estimateMediaModel, mediaPendingSchema, mediaResultSchema, mediaBindingCallSchema, mediaBindingRequest, parseMediaBindingCall, readMediaBindingReply, } from "@fungi.computer/darkroom/server";
12
12
  import { createWorkersAiMediaProvider, createImagesReferenceFitter, } from "@fungi.computer/darkroom/cloudflare";
13
13
  import { oysterPrincipalId, } from "../host.js";
14
14
  import { normalizeFilePath } from "../files.js";
15
15
  /** Media capability using Darkroom execution and the existing workspace FileSource. */
16
16
  export function workersMedia(env, workspace) {
17
17
  const { computerId } = workspace;
18
- const routes = new Hono();
18
+ // Media reads and writes the Computer's workspace: a key needs `computer.use`.
19
+ const routes = routeTable();
19
20
  const service = (principal) => standaloneMediaService(env, oysterPrincipalId(principal), workspace);
20
- routes.get("/describe", async (context) => {
21
- const response = await service(context.env.principal).request("http://media/models");
21
+ routes.get("/describe", computer(), async (context, { principal }) => {
22
+ const response = await service(principal).request("http://media/models");
22
23
  return context.json(Schema.decodeUnknownSync(mediaGenerationDescriptionSchema)({
23
24
  computerId,
24
25
  canGenerate: true,
25
26
  models: await response.json(),
26
27
  }));
27
28
  });
28
- routes.post("/generate", async (context) => {
29
+ routes.post("/generate", computer(), async (context, { principal }) => {
29
30
  const input = Schema.decodeUnknownOption(Schema.fromJsonString(mediaGenerationJsonRequestSchema), { onExcessProperty: "error" })(await context.req.text());
30
31
  if (input._tag === "None")
31
32
  return context.json(invalidRequest());
@@ -33,13 +34,13 @@ export function workersMedia(env, workspace) {
33
34
  if (path === null || path === "/")
34
35
  return context.json(invalidRequest());
35
36
  const references = input.value.settings.references?.map((reference) => ({
36
- ...reference,
37
37
  computerId,
38
+ ...reference,
38
39
  path: normalizeFilePath(reference.path),
39
40
  }));
40
41
  if (references?.some((reference) => reference.path === null || reference.path === "/"))
41
42
  return context.json(invalidRequest());
42
- const response = await service(context.env.principal).request("http://media/generate", {
43
+ const response = await service(principal).request("http://media/generate", {
43
44
  method: "POST",
44
45
  signal: context.req.raw.signal,
45
46
  headers: {
@@ -88,7 +89,7 @@ export function workersMedia(env, workspace) {
88
89
  },
89
90
  });
90
91
  });
91
- routes.post("/cancel-attempt", async (context) => {
92
+ routes.post("/cancel-attempt", computer(), async (context) => {
92
93
  const reference = Schema.decodeUnknownOption(Schema.fromJsonString(mediaGenerationReferenceSchema), { onExcessProperty: "error" })(await context.req.text());
93
94
  if (reference._tag === "None" || reference.value.computerId !== computerId)
94
95
  return context.json({ error: "Invalid workspace media reference" }, 400);
@@ -138,7 +139,7 @@ function standaloneMediaService(env, actorId, workspace, owner = "owner", librar
138
139
  ? destination
139
140
  : {
140
141
  ...destination,
141
- readReference: async (reference) => {
142
+ readReference: async (reference, model) => {
142
143
  if (reference.computerId !== computerId)
143
144
  throw new MediaSettingsError("reference Computer is not selected");
144
145
  const bytes = await files.read(workspacePath(reference.path));
@@ -147,7 +148,7 @@ function standaloneMediaService(env, actorId, workspace, owner = "owner", librar
147
148
  bytes.byteLength > MAX_REFERENCE_BYTES)
148
149
  throw new MediaSettingsError("reference image is unavailable or exceeds the byte limit");
149
150
  return {
150
- ...(await fit(bytes)),
151
+ ...(await fit(bytes, model)),
151
152
  filename: reference.path.split("/").at(-1) ?? "reference",
152
153
  };
153
154
  },
@@ -201,8 +202,13 @@ export async function dispatchMediaBinding(env, frame, props, method, args, work
201
202
  const input = parsed.call;
202
203
  const service = standaloneMediaService(env, actorId, workspace, frame.facts.selection.owner.id, input.operation === "generate" ? input.input.libraryPath : undefined);
203
204
  let response;
204
- if (input.operation === "models")
205
- response = await service.request("/models");
205
+ if (input.operation === "models") {
206
+ const catalog = await service.request("/models");
207
+ response = Response.json(mediaModelsSchema.parse(await catalog.json()).map((model) => ({
208
+ ...model,
209
+ priceEstimate: estimateMediaModel(model),
210
+ })));
211
+ }
206
212
  else {
207
213
  const body = mediaBindingRequest(input.input, workspace.computerId);
208
214
  const generated = await service.request("/generate", {
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The standalone root's owner routes: App source seeding, builds, the Computer
3
+ * fleet, Agent Computer attachment and Workflow webhook controls. The Worker
4
+ * and the authorization law both compose them from this one function.
5
+ */
6
+ import type { OysterEnv, OysterHost } from "../host.js";
7
+ import type { Routes } from "../authorization.js";
8
+ import type { TenantScope } from "../tenant.js";
9
+ import type { StandaloneAgentsEnv } from "./agents.js";
10
+ import { type AppSourceBindingEnv } from "./app-source.js";
11
+ import type { StandaloneComputerEnv } from "./computer.js";
12
+ import type { StandaloneWorkflowEnv } from "./workflows.js";
13
+ /** The bindings the standalone owner routes reach, resolved per request. */
14
+ export type StandaloneOwnerRoutesEnv = AppSourceBindingEnv & Pick<StandaloneComputerEnv, "REGISTRY"> & Pick<StandaloneAgentsEnv, "AGENT_DIRECTORY"> & Pick<StandaloneWorkflowEnv, "WORKFLOW_RUNS">;
15
+ /** Every owner route the standalone root adds to its tenant host. */
16
+ export declare function standaloneOwnerRoutes(env: StandaloneOwnerRoutesEnv, scope: TenantScope, hostFor: () => Promise<Pick<OysterHost, "installations">>): Routes<OysterEnv>;
@@ -0,0 +1,21 @@
1
+ import { agentDirectoryForScope } from "./agent-directory.js";
2
+ import { standaloneAgentComputerRoutes } from "./agent-computers.js";
3
+ import { appSourceOwnerRoutes, } from "./app-source.js";
4
+ import { standaloneBuildRoutes } from "./builds.js";
5
+ import { standaloneComputerFleetRoutes } from "./computer-fleet.js";
6
+ import { standaloneWorkflowWebhookControls } from "./workflow-webhooks.js";
7
+ /** Every owner route the standalone root adds to its tenant host. */
8
+ export function standaloneOwnerRoutes(env, scope, hostFor) {
9
+ // A tenant's registry owns its installations, builds and Computer fleet.
10
+ const registry = () => env.REGISTRY.getByName(scope.durableName("registry"));
11
+ const directory = () => agentDirectoryForScope(env, scope);
12
+ return appSourceOwnerRoutes(env, scope)
13
+ .include(standaloneBuildRoutes(registry))
14
+ .include(standaloneComputerFleetRoutes(registry))
15
+ .include(standaloneAgentComputerRoutes({
16
+ attach: (principal, command) => directory().attachComputer(command, principal, scope.tenantId),
17
+ detach: (principal, command) => directory().detachComputer(command, principal, scope.tenantId),
18
+ read: (principal, agentId) => directory().readAgentComputers(agentId, principal, scope.tenantId),
19
+ }))
20
+ .include(standaloneWorkflowWebhookControls(env, hostFor));
21
+ }
@@ -0,0 +1,5 @@
1
+ import type { OysterHost } from "../host.js";
2
+ import type { OysterAgents } from "../agents.js";
3
+ import type { StandaloneRegistryRpc } from "./registry.js";
4
+ /** The existing owner picker includes its configured Computer as well as the fleet. */
5
+ export declare function standalonePermissionResources(registry: Pick<StandaloneRegistryRpc, "computerFleet">, agents: Pick<OysterAgents, "list">): NonNullable<OysterHost["permissionResources"]>;
@@ -0,0 +1,31 @@
1
+ import { HTTPException } from "hono/http-exception";
2
+ import { workbenchComputerDirectorySchema } from "@fungi.computer/nacre/workbench-directory";
3
+ import { oysterPrincipalIsOwner } from "../authorization.js";
4
+ /** The existing owner picker includes its configured Computer as well as the fleet. */
5
+ export function standalonePermissionResources(registry, agents) {
6
+ return async (principal) => {
7
+ if (!oysterPrincipalIsOwner(principal))
8
+ throw new HTTPException(403, { message: "Owner only" });
9
+ const resources = (await agents.list()).map((agent) => ({
10
+ kind: "agent",
11
+ id: agent.agentId,
12
+ label: agent.name,
13
+ }));
14
+ // Follow the native picker cursor, as inventory.ids() does for inventory.
15
+ // A transport failure refuses the read; it is never an empty directory.
16
+ let offset = 0;
17
+ while (offset !== null) {
18
+ const page = workbenchComputerDirectorySchema.parse(await registry.computerFleet(principal.actor.memberId, {
19
+ operation: "choices",
20
+ page: { offset, limit: 100 },
21
+ }));
22
+ resources.push(...page.computers.map((computer) => ({
23
+ kind: "computer",
24
+ id: computer.computerId,
25
+ label: computer.name,
26
+ })));
27
+ offset = page.nextOffset;
28
+ }
29
+ return resources;
30
+ };
31
+ }
@@ -179,6 +179,13 @@ export declare abstract class StandaloneRegistry extends OysterRegistry implemen
179
179
  appId: import("@fungi.computer/puffball").AppId;
180
180
  } | null>;
181
181
  readBuild(jobId: string): Promise<OysterBuildView | null>;
182
+ readBuildLog(jobId: string): Promise<{
183
+ readonly encoding: "base64url";
184
+ readonly output: string;
185
+ readonly truncated: {
186
+ readonly droppedBytes: number;
187
+ } | null;
188
+ } | null>;
182
189
  /** A job ID alone never grants another installation access to its status. */
183
190
  readInstallationBuild(installationId: string, jobId: string): Promise<OysterBuildView | null>;
184
191
  /** Repository status belongs to the same captured Git owner as registration. */
@@ -372,6 +372,9 @@ export class StandaloneRegistry extends OysterRegistry {
372
372
  async readBuild(jobId) {
373
373
  return (await this.#appBuilds()).read(jobId);
374
374
  }
375
+ async readBuildLog(jobId) {
376
+ return (await this.#appBuilds()).readLog(jobId);
377
+ }
375
378
  /** A job ID alone never grants another installation access to its status. */
376
379
  async readInstallationBuild(installationId, jobId) {
377
380
  const builds = await this.#appBuilds();
@@ -173,7 +173,9 @@ async function composeTenancy(env) {
173
173
  async authenticate(request) {
174
174
  if (owner === null)
175
175
  return { kind: "refused", status: 403 };
176
- const admission = await ownerOrKeyAuth(env.OYSTER_OWNER_KEY, owner.actor.memberId, () => env.KEYS.getByName(scope.durableName("keys"))).authenticate(request);
176
+ const keys = () => env.KEYS.getByName(scope.durableName("keys"));
177
+ const auth = ownerOrKeyAuth(env.OYSTER_OWNER_KEY, owner.actor.memberId, keys);
178
+ const admission = await auth.authenticate(request);
177
179
  // This entry admits only named keys; the owner key has its own entry.
178
180
  return admission.kind === "admitted" &&
179
181
  admission.principal.actor.kind === "team-key"
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The standalone Worker's top-level table, before any tenant is selected:
3
+ * device enrollment and links, signed Workflow webhook delivery, then the
4
+ * selected tenant's own declared host table for every other path. The Worker
5
+ * and the authorization law both compose it here.
6
+ */
7
+ import type { BlankEnv } from "hono/types";
8
+ import { type Routes } from "../authorization.js";
9
+ /** What the Worker-top doors forward to. */
10
+ export type StandaloneWorkerRoutePorts = Readonly<{
11
+ /** The machine-link doors' own table, served for this request. */
12
+ machineLink: (request: Request) => Promise<Response>;
13
+ /** Signed Workflow webhook delivery, verified by the library's run owner. */
14
+ workflowWebhooks: Routes<BlankEnv>;
15
+ /** The selected tenant's declared host table. */
16
+ tenant: (request: Request) => Promise<Response>;
17
+ }>;
18
+ /** The Worker-top table; `serveDoors` serves it, so no route admits a principal. */
19
+ export declare function standaloneWorkerRoutes(ports: StandaloneWorkerRoutePorts): Routes<BlankEnv>;
@@ -0,0 +1,9 @@
1
+ import { door, routes } from "../authorization.js";
2
+ /** The Worker-top table; `serveDoors` serves it, so no route admits a principal. */
3
+ export function standaloneWorkerRoutes(ports) {
4
+ return routes()
5
+ .all("/local/mesh/join", door("machine enrollment; the join route verifies its key"), (context) => ports.machineLink(context.req.raw))
6
+ .all("/api/teams/:teamId/local/mesh/:deviceId/connect", door("a machine link; the device's link verifies Seam"), (context) => ports.machineLink(context.req.raw))
7
+ .include(ports.workflowWebhooks)
8
+ .all("*", door("the selected tenant's own declared host table"), (context) => ports.tenant(context.req.raw));
9
+ }
@@ -17,7 +17,7 @@ export { StandaloneTenants } from "./tenancy.js";
17
17
  import { type StandaloneMachineLinkBindings } from "./machine-link.js";
18
18
  export { MachineLinkDO } from "./machine-link.js";
19
19
  import { type OysterComputerEgressProps } from "./computer-egress.js";
20
- import { type AppSourceBindingEnv } from "./app-source.js";
20
+ import type { AppSourceBindingEnv } from "./app-source.js";
21
21
  export { StandaloneGraphWorkflow } from "./workflows.js";
22
22
  export { MediaOperationDO } from "@fungi.computer/darkroom/cloudflare";
23
23
  /** Every binding the standalone Bonsai root receives. */