@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
@@ -7,13 +7,14 @@
7
7
  * makes its template the build source at the returned seed revision.
8
8
  *
9
9
  * - `POST /api/builds` starts a build of that source at one exact commit.
10
- * - `GET /api/builds/:jobId` reads it; a finished build carries its Release,
11
- * which the owner installs or selects through the installation routes.
10
+ * - `GET /api/builds/:jobId` reads it; `?log` reads its combined raw output.
11
+ * Both require the same owner admission. A finished build carries its
12
+ * Release, which the owner installs or selects through the installation routes.
12
13
  */
13
- import { Hono } from "hono";
14
14
  import * as Schema from "effect/Schema";
15
15
  import { oysterBuildStartMessages, } from "../app-builds.js";
16
- import { oysterPrincipalIsOwner } from "../host.js";
16
+ import {} from "../host.js";
17
+ import { owner as ownerRoute, routes as routeTable, } from "../authorization.js";
17
18
  import { appIdSchema, releaseSchema, sourceRevisionSchema, } from "@fungi.computer/puffball/effect";
18
19
  const buildInput = Schema.Struct({
19
20
  appId: appIdSchema,
@@ -38,14 +39,9 @@ export function buildStartFailureReply(reason) {
38
39
  /** The build routes over the tenant registry that owns builds, resolved per
39
40
  * request so mounting the routes reaches no registry. */
40
41
  export function standaloneBuildRoutes(registryFor) {
41
- const routes = new Hono();
42
- routes.use("/api/builds/*", async (context, next) => oysterPrincipalIsOwner(context.get("principal"))
43
- ? next()
44
- : Response.json({ error: "Owner only" }, { status: 403 }));
45
- routes.post("/api/builds", async (context) => {
46
- const principal = context.get("principal");
47
- if (!oysterPrincipalIsOwner(principal))
48
- return Response.json({ error: "Owner only" }, { status: 403 });
42
+ const routes = routeTable();
43
+ const owner = ownerRoute("direct-or-app");
44
+ routes.post("/api/builds", owner, async (context, { principal }) => {
49
45
  let input;
50
46
  try {
51
47
  input = Schema.decodeUnknownSync(buildInput, {
@@ -70,7 +66,13 @@ export function standaloneBuildRoutes(registryFor) {
70
66
  return Response.json(reply.body, { status: reply.status });
71
67
  }
72
68
  });
73
- routes.get("/api/builds/:jobId", async (context) => {
69
+ routes.get("/api/builds/:jobId", owner, async (context) => {
70
+ if (context.req.query("log") !== undefined) {
71
+ const log = await registryFor().readBuildLog(context.req.param("jobId"));
72
+ return log === null
73
+ ? Response.json({ error: "Not found" }, { status: 404 })
74
+ : Response.json(log);
75
+ }
74
76
  const build = await registryFor().readBuild(context.req.param("jobId"));
75
77
  return build === null
76
78
  ? Response.json({ error: "Not found" }, { status: 404 })
@@ -1,6 +1,6 @@
1
- import { Hono } from "hono";
2
1
  import { z } from "zod";
3
2
  import { type OysterAuth, type OysterEnv } from "../host.js";
3
+ import { type Routes } from "../authorization.js";
4
4
  import type { StandaloneRegistry } from "./registry.js";
5
5
  export declare const standaloneFleetRequestSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
6
6
  operation: z.ZodLiteral<"create">;
@@ -20,10 +20,10 @@ export declare const standaloneFleetRequestSchema: z.ZodDiscriminatedUnion<[z.Zo
20
20
  }, z.core.$strict>, z.ZodObject<{
21
21
  operation: z.ZodEnum<{
22
22
  read: "read";
23
- resolve: "resolve";
24
23
  select: "select";
25
- start: "start";
26
24
  state: "state";
25
+ resolve: "resolve";
26
+ start: "start";
27
27
  }>;
28
28
  computerId: z.ZodString;
29
29
  }, z.core.$strict>, z.ZodObject<{
@@ -41,4 +41,4 @@ export declare function standaloneComputerSelection(registry: Pick<StandaloneReg
41
41
  auth: OysterAuth;
42
42
  }>): Promise<string>;
43
43
  /** Tenant-owner management controls over the same durable fleet as Hub. */
44
- export declare function standaloneComputerFleetRoutes(registryFor: () => DurableObjectStub<StandaloneRegistry>): Hono<OysterEnv>;
44
+ export declare function standaloneComputerFleetRoutes(registryFor: () => DurableObjectStub<StandaloneRegistry>): Routes<OysterEnv>;
@@ -1,8 +1,10 @@
1
- import { Hono } from "hono";
1
+ import { endTime, startTime } from "hono/timing";
2
2
  import { HTTPException } from "hono/http-exception";
3
3
  import { z } from "zod";
4
4
  import { computerCreateRequestSchema, computerPageSchema, } from "@fungi.computer/computer/fleet";
5
- import { oysterPrincipalIsOwner, } from "../host.js";
5
+ import {} from "../host.js";
6
+ import { oysterPrincipalIsOwner, owner as ownerRoute, routes as routeTable, } from "../authorization.js";
7
+ import { computerTargetParam } from "../computer-target.js";
6
8
  /** Fleet controls never accept an actor from the request body. */
7
9
  const fleetComputerId = z.string().min(1).max(256);
8
10
  export const standaloneFleetRequestSchema = z.discriminatedUnion("operation", [
@@ -48,7 +50,7 @@ function fleetResponse(result) {
48
50
  export async function standaloneComputerSelection(registry, incoming) {
49
51
  if (incoming === undefined)
50
52
  return registry.selectedComputer();
51
- const requested = new URL(incoming.request.url).searchParams.getAll("computerId");
53
+ const requested = new URL(incoming.request.url).searchParams.getAll(computerTargetParam);
52
54
  if (requested.length === 0)
53
55
  return registry.selectedComputer();
54
56
  const admission = await incoming.auth.authenticate(incoming.request);
@@ -77,40 +79,36 @@ export async function standaloneComputerSelection(registry, incoming) {
77
79
  }
78
80
  /** Tenant-owner management controls over the same durable fleet as Hub. */
79
81
  export function standaloneComputerFleetRoutes(registryFor) {
80
- const routes = new Hono();
81
- routes.use("/api/computers/*", async (context, next) => oysterPrincipalIsOwner(context.get("principal"))
82
- ? next()
83
- : context.json({ error: "Owner only" }, 403));
82
+ const routes = routeTable();
83
+ const owner = ownerRoute("direct-or-app");
84
84
  routes.onError((error, context) => {
85
85
  if (error instanceof z.ZodError || error instanceof SyntaxError)
86
86
  return context.json({ error: "invalid_input" }, 400);
87
87
  throw error;
88
88
  });
89
- routes.get("/api/workbench/directory", async (context) => {
90
- const principal = context.get("principal");
91
- if (!oysterPrincipalIsOwner(principal))
92
- return context.json({ error: "Owner only" }, 403);
89
+ routes.get("/api/workbench/directory", owner, async (context, { principal }) => {
93
90
  const registry = registryFor();
91
+ startTime(context, "fleet");
94
92
  const admitted = await registry.computerFleet(principal.actor.memberId, {
95
93
  operation: "list",
96
94
  page: { limit: 1, offset: 0 },
97
95
  });
96
+ endTime(context, "fleet");
98
97
  if (admitted === null || "error" in admitted)
99
98
  return fleetResponse(admitted);
99
+ startTime(context, "registry");
100
100
  const [directory, computerId] = await Promise.all([
101
101
  registry.directory(),
102
102
  registry.selectedComputer(),
103
103
  ]);
104
+ endTime(context, "registry");
104
105
  return context.json({
105
106
  ...directory,
106
107
  computer: { computerId, name: computerId },
107
108
  computerContext: "owner-fleet",
108
109
  });
109
110
  });
110
- routes.get("/api/computers", async (context) => {
111
- const principal = context.get("principal");
112
- if (!oysterPrincipalIsOwner(principal))
113
- return context.json({ error: "Owner only" }, 403);
111
+ routes.get("/api/computers", owner, async (context, { principal }) => {
114
112
  const page = computerPageSchema.parse({
115
113
  limit: Number(context.req.query("limit") ?? 100),
116
114
  offset: Number(context.req.query("offset") ?? 0),
@@ -120,10 +118,7 @@ export function standaloneComputerFleetRoutes(registryFor) {
120
118
  page,
121
119
  }));
122
120
  });
123
- routes.get("/api/workbench/computers", async (context) => {
124
- const principal = context.get("principal");
125
- if (!oysterPrincipalIsOwner(principal))
126
- return context.json({ error: "Owner only" }, 403);
121
+ routes.get("/api/workbench/computers", owner, async (context, { principal }) => {
127
122
  return fleetResponse(await registryFor().computerFleet(principal.actor.memberId, {
128
123
  operation: "choices",
129
124
  page: computerPageSchema.parse({
@@ -132,39 +127,27 @@ export function standaloneComputerFleetRoutes(registryFor) {
132
127
  }),
133
128
  }));
134
129
  });
135
- routes.post("/api/computers", async (context) => {
136
- const principal = context.get("principal");
137
- if (!oysterPrincipalIsOwner(principal))
138
- return context.json({ error: "Owner only" }, 403);
130
+ routes.post("/api/computers", owner, async (context, { principal }) => {
139
131
  const input = computerCreateRequestSchema.parse(await context.req.json());
140
132
  return fleetResponse(await registryFor().computerFleet(principal.actor.memberId, {
141
133
  operation: "create",
142
134
  request: input,
143
135
  }));
144
136
  });
145
- routes.post("/api/computers/:computerId/start", async (context) => {
146
- const principal = context.get("principal");
147
- if (!oysterPrincipalIsOwner(principal))
148
- return context.json({ error: "Owner only" }, 403);
137
+ routes.post("/api/computers/:computerId/start", owner, async (context, { principal }) => {
149
138
  const result = await registryFor().computerFleet(principal.actor.memberId, {
150
139
  operation: "start",
151
140
  computerId: context.req.param("computerId"),
152
141
  });
153
142
  return fleetResponse(result);
154
143
  });
155
- routes.get("/api/computers/:computerId/internet-access", async (context) => {
156
- const principal = context.get("principal");
157
- if (!oysterPrincipalIsOwner(principal))
158
- return context.json({ error: "Owner only" }, 403);
144
+ routes.get("/api/computers/:computerId/internet-access", owner, async (context, { principal }) => {
159
145
  return fleetResponse(await registryFor().computerFleet(principal.actor.memberId, {
160
146
  operation: "read-internet-access",
161
147
  computerId: context.req.param("computerId"),
162
148
  }));
163
149
  });
164
- routes.put("/api/computers/:computerId/internet-access", async (context) => {
165
- const principal = context.get("principal");
166
- if (!oysterPrincipalIsOwner(principal))
167
- return context.json({ error: "Owner only" }, 403);
150
+ routes.put("/api/computers/:computerId/internet-access", owner, async (context, { principal }) => {
168
151
  const input = z
169
152
  .strictObject({ enabled: z.boolean() })
170
153
  .parse(await context.req.json());
@@ -174,20 +157,14 @@ export function standaloneComputerFleetRoutes(registryFor) {
174
157
  enabled: input.enabled,
175
158
  }));
176
159
  });
177
- routes.get("/api/computers/:computerId", async (context) => {
178
- const principal = context.get("principal");
179
- if (!oysterPrincipalIsOwner(principal))
180
- return context.json({ error: "Owner only" }, 403);
160
+ routes.get("/api/computers/:computerId", owner, async (context, { principal }) => {
181
161
  const result = await registryFor().computerFleet(principal.actor.memberId, {
182
162
  operation: "read",
183
163
  computerId: context.req.param("computerId"),
184
164
  });
185
165
  return fleetResponse(result);
186
166
  });
187
- routes.put("/api/computers/selected", async (context) => {
188
- const principal = context.get("principal");
189
- if (!oysterPrincipalIsOwner(principal))
190
- return context.json({ error: "Owner only" }, 403);
167
+ routes.put("/api/computers/selected", owner, async (context, { principal }) => {
191
168
  const input = z
192
169
  .strictObject({ computerId: fleetComputerId })
193
170
  .parse(await context.req.json());
@@ -109,6 +109,7 @@ export declare class ComputerDO extends NativeComputerDO implements ComputerFetc
109
109
  exitCode: number | null;
110
110
  stderr: string;
111
111
  } | null;
112
+ log: import("../build/job-log.js").BuildJobLog;
112
113
  }>>;
113
114
  /** Issue preview tickets for every exposed port, or refuse typed. */
114
115
  portPreviews(authority: ComputerGrant, selectorInput: PreviewSelector): Promise<ComputerPortPreviewsReply>;
@@ -160,10 +161,10 @@ export declare class ComputerDO extends NativeComputerDO implements ComputerFetc
160
161
  export declare function standaloneWorkspace(env: StandaloneComputerEnv, keysLive?: OysterKeysLivePort, selectedComputerId?: string): OysterWorkspace;
161
162
  /**
162
163
  * Owner/key capability routes over the selected Computer's native
163
- * namespace. `keysLive` is the same owner-checked port `standaloneWorkspace`
164
- * takes; a caller with none fails every team-key request closed.
164
+ * namespace. Each route declares the Computer requirement, so a key reaches
165
+ * it only with its own `computer.use` grant (`authorization.ts`).
165
166
  */
166
- export declare function standaloneComputerCapabilities(env: StandaloneComputerEnv, workspace: OysterWorkspace, keysLive?: OysterKeysLivePort): readonly OysterCapability[];
167
+ export declare function standaloneComputerCapabilities(env: StandaloneComputerEnv, workspace: OysterWorkspace): readonly OysterCapability[];
167
168
  /** The COMPUTER row's codecs, as the standalone Instance joins them. */
168
169
  export type ComputerFrameMethods = Readonly<{
169
170
  COMPUTER: OysterBindingRow<OysterWorkerEnv>["methods"];
@@ -1,10 +1,10 @@
1
- import { Hono } from "hono";
2
1
  import { attachedBackendId, hostedBackingSchema, } from "@fungi.computer/computer/backing";
3
2
  import { appMethodRefusalStatus, appMethodRefusedReplySchema, } from "@fungi.computer/agar/backend";
4
3
  import * as Schema from "effect/Schema";
5
4
  import { computerPortsSchema, } from "@fungi.computer/agar/computer";
6
5
  import { verifyRelease } from "../host.js";
7
6
  import { r2ArtifactStore } from "../workers.js";
7
+ import { createBuildJobLog } from "../build/job-log.js";
8
8
  import { createStandalonePreview, previewOrigins, previewSelectorSchema, PreviewRefused, } from "./preview.js";
9
9
  import { standaloneTenancy } from "./tenancy.js";
10
10
  import { Base64 } from "effect/encoding";
@@ -18,8 +18,8 @@ import { checkComputerGrant, computerActionSchema, computerGrantSchema, createFl
18
18
  import { AppInstanceError, selectionOf } from "../index.js";
19
19
  import {} from "../cloudflare.js";
20
20
  import { computerSlots } from "../computer.js";
21
- import { oysterPrincipalId, oysterPrincipalIsOwner, } from "../host.js";
22
- import { keyAllowsComputerUse } from "../keys.js";
21
+ import { oysterPrincipalId, } from "../host.js";
22
+ import { authorityOf, computer as computerRoute, owner, reachesComputer, routes as routeTable, } from "../authorization.js";
23
23
  import { oysterAppBuildTimeoutMs, } from "../app-builds.js";
24
24
  import { workflowGitStepGrant, } from "./workflow-binding.js";
25
25
  const missingFile = z.lazy(() => z.object({ code: z.literal("ENOENT") }));
@@ -66,24 +66,18 @@ function grant(computerId, actor) {
66
66
  };
67
67
  }
68
68
  /**
69
- * The Computer grant actor for an admitted owner or member; null for an
70
- * Agent or visitor. A key only when its own `computer.use` grant allows it
71
- * -- off by default, so an unknown, revoked, expired or suspended key, or
72
- * one issued with no Computer grant, is also null. `keysLive` is the one
73
- * owner-checked port every caller reaches a key's liveness through (the
74
- * same one `host.keys.live` is); a caller with none -- the Media binding's
75
- * own workspace selection never calls through to a principal at all --
76
- * fails every team-key closed rather than falling back to an unchecked read.
69
+ * The Computer grant actor for a principal `authorization.ts` lets reach the
70
+ * Computer; null otherwise. `keysLive` is the one owner-checked port every
71
+ * caller reaches a key's liveness through (the same one `host.keys.live`
72
+ * is); a caller with none -- the Media binding's own workspace selection
73
+ * never calls through to a principal at all -- fails every key closed.
77
74
  */
78
75
  async function computerActor(principal, keysLive) {
79
- if (principal.actor.kind === "visitor" || principal.actor.kind === "agent")
80
- return null;
81
- if (principal.actor.kind === "team-key") {
82
- const key = keysLive === undefined ? null : await keysLive(principal.actor.keyId);
83
- if (key === null || !keyAllowsComputerUse(key))
84
- return null;
85
- }
86
- return JSON.stringify(principal);
76
+ const authority = await authorityOf(principal, {
77
+ liveKey: keysLive ?? (() => Promise.resolve(null)),
78
+ appToolPolicy: () => null,
79
+ });
80
+ return reachesComputer(authority) ? JSON.stringify(principal) : null;
87
81
  }
88
82
  /** Each advertised Computer tool, the grant action that accepts it, and its result. */
89
83
  const computerTools = {
@@ -243,16 +237,25 @@ export class ComputerDO extends NativeComputerDO {
243
237
  const admitted = await this.previewBindings.REGISTRY.getByName("registry").admitBuildExecution(input.jobId, this.state.id.toString());
244
238
  if (!admitted)
245
239
  throw new AppInstanceError("revoked");
240
+ const log = createBuildJobLog();
246
241
  const result = await this.executeOperationAfterAcceptance({
247
242
  command: input.command,
248
243
  cwd: "/workspace",
249
244
  timeoutMs: input.timeoutMs,
250
245
  requestId: mintComputerRequestId(),
251
- }, () => undefined, "hosted", { maximumTimeMs: oysterAppBuildTimeoutMs });
246
+ }, () => undefined, "hosted", { maximumTimeMs: oysterAppBuildTimeoutMs }, undefined, undefined, {
247
+ onEvent(event) {
248
+ if (event.name === "stdout" || event.name === "stderr")
249
+ log.append(event.value);
250
+ },
251
+ });
252
252
  const output = z
253
253
  .object({ exitCode: z.number().nullable(), stderr: z.string() })
254
254
  .safeParse(result.output);
255
- return { output: output.success ? output.data : null };
255
+ return {
256
+ output: output.success ? output.data : null,
257
+ log: log.finish(),
258
+ };
256
259
  }
257
260
  /** Issue preview tickets for every exposed port, or refuse typed. */
258
261
  async portPreviews(authority, selectorInput) {
@@ -538,30 +541,26 @@ export function standaloneWorkspace(env, keysLive, selectedComputerId = env.OYST
538
541
  }
539
542
  /**
540
543
  * Owner/key capability routes over the selected Computer's native
541
- * namespace. `keysLive` is the same owner-checked port `standaloneWorkspace`
542
- * takes; a caller with none fails every team-key request closed.
544
+ * namespace. Each route declares the Computer requirement, so a key reaches
545
+ * it only with its own `computer.use` grant (`authorization.ts`).
543
546
  */
544
- export function standaloneComputerCapabilities(env, workspace, keysLive) {
545
- const routes = new Hono();
546
- routes.use("*", async (context, next) => {
547
- const actor = await computerActor(context.env.principal, keysLive);
548
- if (actor === null)
549
- return context.json({ error: "Computer access requires an owner or key" }, 403);
550
- context.set("authority", grant(workspace.computerId, actor));
551
- return next();
552
- });
547
+ export function standaloneComputerCapabilities(env, workspace) {
548
+ const routes = routeTable();
549
+ const reach = computerRoute();
550
+ const authority = (principal) => grant(workspace.computerId, JSON.stringify(principal));
553
551
  const computer = () => env.COMPUTER.getByName(workspace.computerId);
554
- routes.get("/ports", async (context) => {
552
+ routes.get("/ports", owner("direct-or-app"), async (context, { principal }) => {
555
553
  if (previewOrigins(env) === null)
556
554
  return context.json({ error: "port_previews_not_configured" });
557
- const { principal, installation } = context.env;
558
- if (!oysterPrincipalIsOwner(principal))
559
- return context.json({ error: "Owner preview access required" }, 403);
555
+ const { installation } = context.env;
560
556
  const ownerId = oysterPrincipalId(principal);
561
- const authority = context.get("authority");
557
+ const ownerAuthority = authority(principal);
562
558
  const reply = await computer().portPreviews({
563
- ...authority,
564
- provenance: { ...authority.provenance, actor: `owner:${ownerId}` },
559
+ ...ownerAuthority,
560
+ provenance: {
561
+ ...ownerAuthority.provenance,
562
+ actor: `owner:${ownerId}`,
563
+ },
565
564
  }, {
566
565
  ownerId,
567
566
  installationId: installation.installationId,
@@ -573,47 +572,47 @@ export function standaloneComputerCapabilities(env, workspace, keysLive) {
573
572
  : context.json({ error: reply.code, message: reply.message }, appMethodRefusalStatus[reply.code]);
574
573
  });
575
574
  // The host wire body is `{ port }`, as on Hub; it is parsed here.
576
- routes.post("/ports/expose", async (context) => {
575
+ routes.post("/ports/expose", reach, async (context, { principal }) => {
577
576
  const input = computerPortInputSchema.safeParse(await context.req.json());
578
577
  if (!input.success)
579
578
  return context.json({ error: "invalid_input" }, 400);
580
- return Response.json(await computer().value(context.get("authority"), "exposePort", input.data));
579
+ return Response.json(await computer().value(authority(principal), "exposePort", input.data));
581
580
  });
582
581
  // The host wire body is `{ port }`, as on Hub; it is parsed here.
583
- routes.post("/ports/unexpose", async (context) => {
582
+ routes.post("/ports/unexpose", reach, async (context, { principal }) => {
584
583
  const input = computerPortInputSchema.safeParse(await context.req.json());
585
584
  if (!input.success)
586
585
  return context.json({ error: "invalid_input" }, 400);
587
- return Response.json(await computer().value(context.get("authority"), "unexposePort", input.data));
586
+ return Response.json(await computer().value(authority(principal), "unexposePort", input.data));
588
587
  });
589
- routes.get("/processes", async (context) => Response.json(await computer().value(context.get("authority"), "processList", {})));
590
- routes.post("/processes/start", async (context) => {
588
+ routes.get("/processes", reach, async (_context, { principal }) => Response.json(await computer().value(authority(principal), "processList", {})));
589
+ routes.post("/processes/start", reach, async (context, { principal }) => {
591
590
  const input = processStartHttpBody.safeParse(await context.req.json());
592
591
  if (!input.success)
593
592
  return context.json({ error: "invalid_input" }, 400);
594
- const result = processStartReply.parse(await computer().value(context.get("authority"), "processStart", computerValueSchema.parse({
593
+ const result = processStartReply.parse(await computer().value(authority(principal), "processStart", computerValueSchema.parse({
595
594
  ...input.data,
596
595
  requestScope: `owner:${env.OYSTER_OWNER_NAME ?? "owner"}`,
597
596
  })));
598
597
  return Response.json(result, { status: "error" in result ? 409 : 200 });
599
598
  });
600
599
  // The host wire body is `{ id }`, as on Hub; it is parsed here.
601
- routes.post("/processes/kill", async (context) => {
600
+ routes.post("/processes/kill", reach, async (context, { principal }) => {
602
601
  const input = processIdInputSchema.safeParse(await context.req.json());
603
602
  if (!input.success)
604
603
  return context.json({ error: "invalid_input" }, 400);
605
- return Response.json(await computer().value(context.get("authority"), "processKill", input.data));
604
+ return Response.json(await computer().value(authority(principal), "processKill", input.data));
606
605
  });
607
- routes.get("/terminal", async (context) => {
608
- const authority = context.get("authority");
606
+ routes.get("/terminal", reach, async (context, { principal }) => {
607
+ const terminal = authority(principal);
609
608
  const url = new URL(context.req.url);
610
609
  url.pathname = "/_oyster/terminal";
611
610
  return computer().fetch(new Request(url, {
612
611
  headers: {
613
612
  Upgrade: "websocket",
614
613
  [terminalGrantHeader]: JSON.stringify({
615
- ...authority,
616
- actions: [...authority.actions],
614
+ ...terminal,
615
+ actions: [...terminal.actions],
617
616
  }),
618
617
  },
619
618
  }));
@@ -621,7 +620,7 @@ export function standaloneComputerCapabilities(env, workspace, keysLive) {
621
620
  return Object.values(computerSlots).map((slot) => ({
622
621
  ...slot,
623
622
  permission: "computer.execute",
624
- routes: new Hono().route("/", routes),
623
+ routes,
625
624
  }));
626
625
  }
627
626
  /** Closed COMPUTER binding codecs, joined into the standalone Instance. */
@@ -0,0 +1,10 @@
1
+ import * as Schema from "effect/Schema";
2
+ import { type Installation } from "../apps/installation.js";
3
+ import type { InstallationOperation } from "../installation-operations.js";
4
+ /**
5
+ * An owner choosing a freshly installed dashboard approves its declared shell
6
+ * operations only. Replacements and tombstone remints never replay consent.
7
+ */
8
+ export declare function approveChosenDashboard(operation: Extract<InstallationOperation, {
9
+ state: "succeeded";
10
+ }>, owner: (method: "GET" | "PUT", path: string, body?: Schema.Json) => Promise<Response>): Promise<Installation | undefined>;
@@ -0,0 +1,66 @@
1
+ import * as Schema from "effect/Schema";
2
+ import { grantCommandSchema, installationSchema, } from "../apps/installation.js";
3
+ import { appPermissionPageSchema } from "../apps/permissions.js";
4
+ /**
5
+ * An owner choosing a freshly installed dashboard approves its declared shell
6
+ * operations only. Replacements and tombstone remints never replay consent.
7
+ */
8
+ export async function approveChosenDashboard(operation, owner) {
9
+ if (operation.creation !== "fresh")
10
+ return;
11
+ const installed = operation.installation;
12
+ const name = operation.installationId;
13
+ if (installed === null)
14
+ throw new Error(`${name} completed installation version is unavailable`);
15
+ if (installed.status !== "enabled")
16
+ throw new Error(`${name} fresh installation is not enabled`);
17
+ const path = `/api/installations/${encodeURIComponent(name)}/grants`;
18
+ const projectionResponse = await owner("GET", path);
19
+ if (!projectionResponse.ok)
20
+ throw new Error(`${name} approval read failed (${String(projectionResponse.status)})`);
21
+ const projection = appPermissionPageSchema.parse(await projectionResponse.json());
22
+ const { permissions } = projection;
23
+ const digest = installed.release.artifact.artifactDigest;
24
+ if (projection.storeGeneration !== operation.reference.generation ||
25
+ projection.installationId !== name ||
26
+ projection.artifactDigest !== digest ||
27
+ permissions.declaredOperations.artifactDigest !== digest ||
28
+ permissions.authority.authorityVersion !==
29
+ installed.authority.authorityVersion ||
30
+ permissions.authority.declaredOperationsDigest !==
31
+ installed.authority.declaredOperationsDigest ||
32
+ permissions.declaredOperations.digest !==
33
+ installed.authority.declaredOperationsDigest)
34
+ throw new Error(`${name} approval snapshot changed`);
35
+ const shell = permissions.declaredOperations.capabilities.find((row) => row.capability === "team.workbenchHost");
36
+ if (shell === undefined)
37
+ return;
38
+ const command = grantCommandSchema.parse({
39
+ kind: "change-grants",
40
+ changes: [
41
+ {
42
+ capability: shell.capability,
43
+ grant: { operations: shell.operations, resources: "tenant-scope" },
44
+ },
45
+ ],
46
+ });
47
+ const saved = await owner("PUT", path, Schema.decodeUnknownSync(Schema.Json)({
48
+ storeGeneration: projection.storeGeneration,
49
+ authorityVersion: installed.authority.authorityVersion,
50
+ artifactDigest: digest,
51
+ command,
52
+ }));
53
+ if (!saved.ok)
54
+ throw new Error(`${name} approval failed (${String(saved.status)})`);
55
+ const receipt = installationSchema.parse(await saved.json());
56
+ if (receipt.installationId !== name ||
57
+ receipt.instanceId !== installed.instanceId ||
58
+ receipt.release.artifact.artifactDigest !== digest ||
59
+ receipt.authority.authorityVersion !==
60
+ installed.authority.authorityVersion + 1 ||
61
+ !receipt.authority.approvedGrants.some((grant) => grant.capability === "team.workbenchHost" &&
62
+ grant.resources === "tenant-scope" &&
63
+ shell.operations.every((operation) => grant.operations.includes(operation))))
64
+ throw new Error(`${name} approval incomplete`);
65
+ return receipt;
66
+ }