@fungi.computer/oyster 0.6.0 → 0.8.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 (163) hide show
  1. package/README.md +133 -78
  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 +61 -0
  5. package/dist/agent-schedules.js +19 -0
  6. package/dist/agents.d.ts +35 -14
  7. package/dist/agents.js +93 -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.d.ts +1 -24
  24. package/dist/auth.js +9 -39
  25. package/dist/authorization.d.ts +307 -0
  26. package/dist/authorization.js +542 -0
  27. package/dist/boot-host-script.js +1 -1
  28. package/dist/build/build-jobs.d.ts +2 -2
  29. package/dist/build/candidate-builder.d.ts +2 -0
  30. package/dist/build/candidate-builder.js +5 -1
  31. package/dist/build/candidate-cli.js +3 -0
  32. package/dist/build/candidate-cli.mjs +70 -22
  33. package/dist/build/job-log.d.ts +23 -0
  34. package/dist/build/job-log.js +88 -0
  35. package/dist/build/local-build.d.ts +9 -1
  36. package/dist/build/local-build.js +11 -9
  37. package/dist/cloudflare.d.ts +4 -1
  38. package/dist/cloudflare.js +1 -1
  39. package/dist/files.js +9 -8
  40. package/dist/git-admission.d.ts +3 -3
  41. package/dist/git-admission.js +4 -6
  42. package/dist/git-door.d.ts +8 -7
  43. package/dist/git-door.js +18 -17
  44. package/dist/git.d.ts +1 -1
  45. package/dist/host.d.ts +33 -40
  46. package/dist/host.js +156 -275
  47. package/dist/installation-execution.js +14 -5
  48. package/dist/installation-operations.d.ts +4 -180
  49. package/dist/installation-operations.js +1 -5
  50. package/dist/installation-store-generation.d.ts +4 -1
  51. package/dist/installation-store-generation.js +59 -6
  52. package/dist/internal/delivery.js +0 -1
  53. package/dist/internal/host-gate.d.ts +3 -2
  54. package/dist/internal/host-gate.js +5 -18
  55. package/dist/internal/invocation-timing.d.ts +16 -0
  56. package/dist/internal/invocation-timing.js +23 -0
  57. package/dist/internal/registry-installations.js +1 -0
  58. package/dist/invocation.d.ts +15 -14
  59. package/dist/invocation.js +58 -26
  60. package/dist/keys.d.ts +2 -19
  61. package/dist/keys.js +1 -37
  62. package/dist/mcp.d.ts +15 -27
  63. package/dist/mcp.js +105 -155
  64. package/dist/page/agent-browser.d.ts +15 -0
  65. package/dist/page/agent-browser.js +162 -0
  66. package/dist/page/app-permissions-port.d.ts +3 -0
  67. package/dist/page/app-permissions-port.js +43 -0
  68. package/dist/page/app-permissions.d.ts +11 -0
  69. package/dist/page/app-permissions.js +296 -0
  70. package/dist/page/isolated-page-theme.d.ts +2 -0
  71. package/dist/page/isolated-page-theme.js +11 -0
  72. package/dist/page/mount.d.ts +2 -13
  73. package/dist/page/mount.js +7 -93
  74. package/dist/page-script.d.ts +0 -3
  75. package/dist/page-script.js +3 -4
  76. package/dist/standalone/agent-archive-owner.d.ts +42 -0
  77. package/dist/standalone/agent-archive-owner.js +116 -0
  78. package/dist/standalone/agent-archive-schedules.d.ts +6 -0
  79. package/dist/standalone/agent-archive-schedules.js +76 -0
  80. package/dist/standalone/agent-archive.d.ts +31 -0
  81. package/dist/standalone/agent-archive.js +67 -0
  82. package/dist/standalone/agent-computers.d.ts +7 -6
  83. package/dist/standalone/agent-computers.js +4 -7
  84. package/dist/standalone/agent-mail.d.ts +2 -2
  85. package/dist/standalone/agents.d.ts +186 -20
  86. package/dist/standalone/agents.js +251 -30
  87. package/dist/standalone/app-builds-binding.js +1 -1
  88. package/dist/standalone/app-set.js +2 -54
  89. package/dist/standalone/app-source.d.ts +3 -4
  90. package/dist/standalone/app-source.js +12 -9
  91. package/dist/standalone/builds.d.ts +2 -14
  92. package/dist/standalone/builds.js +15 -13
  93. package/dist/standalone/computer-fleet.d.ts +4 -4
  94. package/dist/standalone/computer-fleet.js +13 -42
  95. package/dist/standalone/computer.d.ts +4 -3
  96. package/dist/standalone/computer.js +52 -53
  97. package/dist/standalone/dashboard-approval.d.ts +10 -0
  98. package/dist/standalone/dashboard-approval.js +66 -0
  99. package/dist/standalone/machine-link.d.ts +4 -221
  100. package/dist/standalone/machine-link.js +21 -15
  101. package/dist/standalone/media.d.ts +1 -0
  102. package/dist/standalone/media.js +20 -14
  103. package/dist/standalone/owner-routes.d.ts +16 -0
  104. package/dist/standalone/owner-routes.js +21 -0
  105. package/dist/standalone/permission-resources.d.ts +5 -0
  106. package/dist/standalone/permission-resources.js +31 -0
  107. package/dist/standalone/registry.d.ts +7 -0
  108. package/dist/standalone/registry.js +3 -0
  109. package/dist/standalone/tenancy.d.ts +0 -3
  110. package/dist/standalone/tenancy.js +2 -5
  111. package/dist/standalone/worker-routes.d.ts +19 -0
  112. package/dist/standalone/worker-routes.js +9 -0
  113. package/dist/standalone/worker.d.ts +1 -1
  114. package/dist/standalone/worker.js +27 -35
  115. package/dist/standalone/workflow-binding.d.ts +10 -3
  116. package/dist/standalone/workflow-binding.js +22 -5
  117. package/dist/standalone/workflow-git-capture.js +2 -1
  118. package/dist/standalone/workflow-host.d.ts +8 -103
  119. package/dist/standalone/workflow-host.js +1 -1
  120. package/dist/standalone/workflow-node-capture.js +2 -2
  121. package/dist/standalone/workflow-root-authority.d.ts +1 -1
  122. package/dist/standalone/workflow-root-authority.js +1 -1
  123. package/dist/standalone/workflow-schedules.d.ts +31 -4
  124. package/dist/standalone/workflow-schedules.js +18 -5
  125. package/dist/standalone/workflow-webhooks.d.ts +4 -18
  126. package/dist/standalone/workflow-webhooks.js +5 -5
  127. package/dist/standalone/workflows.d.ts +53 -27
  128. package/dist/standalone/workflows.js +9 -3
  129. package/dist/storage-frame.d.ts +6 -6
  130. package/dist/testing/capabilities.d.ts +7 -6
  131. package/dist/testing/capabilities.js +3 -6
  132. package/dist/testing/installation.d.ts +0 -1
  133. package/dist/testing/installation.js +2 -5
  134. package/dist/workers-git.d.ts +12 -4
  135. package/dist/workers-git.js +38 -35
  136. package/dist/workers.d.ts +9 -1
  137. package/dist/workers.js +44 -8
  138. package/dist/workflows.d.ts +4 -0
  139. package/dist/workflows.js +2 -0
  140. package/docs/api/README.md +7 -0
  141. package/docs/api/agent-schedules.md +176 -0
  142. package/docs/api/agents.md +34 -13
  143. package/docs/api/app-storage.md +1 -1
  144. package/docs/api/apps.md +323 -225
  145. package/docs/api/auth.md +0 -55
  146. package/docs/api/authorization.md +1300 -0
  147. package/docs/api/build.md +42 -38
  148. package/docs/api/cloudflare.md +3 -3
  149. package/docs/api/git.md +20 -11
  150. package/docs/api/host.md +64 -192
  151. package/docs/api/index.md +1 -1
  152. package/docs/api/invocation.md +14 -14
  153. package/docs/api/keys.md +1 -98
  154. package/docs/api/mcp.md +43 -40
  155. package/docs/api/modules.md +2 -0
  156. package/docs/api/page/mount.md +16 -19
  157. package/docs/api/workers-git.md +46 -12
  158. package/docs/api/workers.md +111 -222
  159. package/package.json +41 -22
  160. package/dist/page/computer-target.d.ts +0 -3
  161. package/dist/page/computer-target.js +0 -9
  162. package/dist/standalone/channel-session-door.d.ts +0 -4
  163. package/dist/standalone/channel-session-door.js +0 -72
@@ -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,9 +1,9 @@
1
- import { Hono } from "hono";
2
1
  import { endTime, startTime } from "hono/timing";
3
2
  import { HTTPException } from "hono/http-exception";
4
3
  import { z } from "zod";
5
4
  import { computerCreateRequestSchema, computerPageSchema, } from "@fungi.computer/computer/fleet";
6
- import { oysterPrincipalIsOwner, } from "../host.js";
5
+ import {} from "../host.js";
6
+ import { oysterPrincipalIsOwner, owner as ownerRoute, routes as routeTable, } from "../authorization.js";
7
7
  import { computerTargetParam } from "../computer-target.js";
8
8
  /** Fleet controls never accept an actor from the request body. */
9
9
  const fleetComputerId = z.string().min(1).max(256);
@@ -79,19 +79,14 @@ export async function standaloneComputerSelection(registry, incoming) {
79
79
  }
80
80
  /** Tenant-owner management controls over the same durable fleet as Hub. */
81
81
  export function standaloneComputerFleetRoutes(registryFor) {
82
- const routes = new Hono();
83
- routes.use("/api/computers/*", async (context, next) => oysterPrincipalIsOwner(context.get("principal"))
84
- ? next()
85
- : context.json({ error: "Owner only" }, 403));
82
+ const routes = routeTable();
83
+ const owner = ownerRoute("direct-or-app");
86
84
  routes.onError((error, context) => {
87
85
  if (error instanceof z.ZodError || error instanceof SyntaxError)
88
86
  return context.json({ error: "invalid_input" }, 400);
89
87
  throw error;
90
88
  });
91
- routes.get("/api/workbench/directory", async (context) => {
92
- const principal = context.get("principal");
93
- if (!oysterPrincipalIsOwner(principal))
94
- return context.json({ error: "Owner only" }, 403);
89
+ routes.get("/api/workbench/directory", owner, async (context, { principal }) => {
95
90
  const registry = registryFor();
96
91
  startTime(context, "fleet");
97
92
  const admitted = await registry.computerFleet(principal.actor.memberId, {
@@ -113,10 +108,7 @@ export function standaloneComputerFleetRoutes(registryFor) {
113
108
  computerContext: "owner-fleet",
114
109
  });
115
110
  });
116
- routes.get("/api/computers", async (context) => {
117
- const principal = context.get("principal");
118
- if (!oysterPrincipalIsOwner(principal))
119
- return context.json({ error: "Owner only" }, 403);
111
+ routes.get("/api/computers", owner, async (context, { principal }) => {
120
112
  const page = computerPageSchema.parse({
121
113
  limit: Number(context.req.query("limit") ?? 100),
122
114
  offset: Number(context.req.query("offset") ?? 0),
@@ -126,10 +118,7 @@ export function standaloneComputerFleetRoutes(registryFor) {
126
118
  page,
127
119
  }));
128
120
  });
129
- routes.get("/api/workbench/computers", async (context) => {
130
- const principal = context.get("principal");
131
- if (!oysterPrincipalIsOwner(principal))
132
- return context.json({ error: "Owner only" }, 403);
121
+ routes.get("/api/workbench/computers", owner, async (context, { principal }) => {
133
122
  return fleetResponse(await registryFor().computerFleet(principal.actor.memberId, {
134
123
  operation: "choices",
135
124
  page: computerPageSchema.parse({
@@ -138,39 +127,27 @@ export function standaloneComputerFleetRoutes(registryFor) {
138
127
  }),
139
128
  }));
140
129
  });
141
- routes.post("/api/computers", async (context) => {
142
- const principal = context.get("principal");
143
- if (!oysterPrincipalIsOwner(principal))
144
- return context.json({ error: "Owner only" }, 403);
130
+ routes.post("/api/computers", owner, async (context, { principal }) => {
145
131
  const input = computerCreateRequestSchema.parse(await context.req.json());
146
132
  return fleetResponse(await registryFor().computerFleet(principal.actor.memberId, {
147
133
  operation: "create",
148
134
  request: input,
149
135
  }));
150
136
  });
151
- routes.post("/api/computers/:computerId/start", async (context) => {
152
- const principal = context.get("principal");
153
- if (!oysterPrincipalIsOwner(principal))
154
- return context.json({ error: "Owner only" }, 403);
137
+ routes.post("/api/computers/:computerId/start", owner, async (context, { principal }) => {
155
138
  const result = await registryFor().computerFleet(principal.actor.memberId, {
156
139
  operation: "start",
157
140
  computerId: context.req.param("computerId"),
158
141
  });
159
142
  return fleetResponse(result);
160
143
  });
161
- routes.get("/api/computers/:computerId/internet-access", async (context) => {
162
- const principal = context.get("principal");
163
- if (!oysterPrincipalIsOwner(principal))
164
- return context.json({ error: "Owner only" }, 403);
144
+ routes.get("/api/computers/:computerId/internet-access", owner, async (context, { principal }) => {
165
145
  return fleetResponse(await registryFor().computerFleet(principal.actor.memberId, {
166
146
  operation: "read-internet-access",
167
147
  computerId: context.req.param("computerId"),
168
148
  }));
169
149
  });
170
- routes.put("/api/computers/:computerId/internet-access", async (context) => {
171
- const principal = context.get("principal");
172
- if (!oysterPrincipalIsOwner(principal))
173
- return context.json({ error: "Owner only" }, 403);
150
+ routes.put("/api/computers/:computerId/internet-access", owner, async (context, { principal }) => {
174
151
  const input = z
175
152
  .strictObject({ enabled: z.boolean() })
176
153
  .parse(await context.req.json());
@@ -180,20 +157,14 @@ export function standaloneComputerFleetRoutes(registryFor) {
180
157
  enabled: input.enabled,
181
158
  }));
182
159
  });
183
- routes.get("/api/computers/:computerId", async (context) => {
184
- const principal = context.get("principal");
185
- if (!oysterPrincipalIsOwner(principal))
186
- return context.json({ error: "Owner only" }, 403);
160
+ routes.get("/api/computers/:computerId", owner, async (context, { principal }) => {
187
161
  const result = await registryFor().computerFleet(principal.actor.memberId, {
188
162
  operation: "read",
189
163
  computerId: context.req.param("computerId"),
190
164
  });
191
165
  return fleetResponse(result);
192
166
  });
193
- routes.put("/api/computers/selected", async (context) => {
194
- const principal = context.get("principal");
195
- if (!oysterPrincipalIsOwner(principal))
196
- return context.json({ error: "Owner only" }, 403);
167
+ routes.put("/api/computers/selected", owner, async (context, { principal }) => {
197
168
  const input = z
198
169
  .strictObject({ computerId: fleetComputerId })
199
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
+ }