@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
package/dist/agents.js CHANGED
@@ -1,17 +1,9 @@
1
- /**
2
- * Agents for Apps on a standalone Oyster: one adapter shape and the routes
3
- * the App page calls. Apps reach agents through Agar's `agent.list`,
4
- * `agent.manage`, `agent.read` and `agent.send` contracts, so an App written
5
- * for the Hub (Grove) runs unchanged. Each agent's own transport is
6
- * Shiitake's HTTP routes (`/sessions…`, `/acp`) and Knapsack's Session browser
7
- * transport (`/browser/…`), served by the adapter behind the same App grants.
8
- */
9
- import { Hono } from "hono";
10
- import { keyAgentAccess } from "./keys.js";
1
+ import { agent, agents as agentCollection, owner, routes, } from "./authorization.js";
11
2
  import * as Schema from "effect/Schema";
12
3
  import { z } from "zod";
13
- import { oysterPrincipalIsOwner, } from "./host.js";
4
+ import {} from "./host.js";
14
5
  import { agentAvatarSchema } from "./agent-avatars.js";
6
+ import { computerTargetParam } from "./computer-target.js";
15
7
  import { agentAvatar, agentList, agentManage, agentRename, agentSessionRead, agentSessionWrite, agentToolPolicy, } from "@fungi.computer/agar/agents";
16
8
  /** At most this many MCP servers per Agent. */
17
9
  export const maxAgentMcpServers = 16;
@@ -45,6 +37,13 @@ const visitSchema = Schema.Struct({
45
37
  sessionId: Schema.String,
46
38
  });
47
39
  const agentIdPattern = /^[a-z0-9_-]{1,64}$/u;
40
+ /** Explicit owner-addressed settings remain reachable after list hides an archive. */
41
+ async function knownAgent(agents, agentId) {
42
+ if (!agentIdPattern.test(agentId))
43
+ return false;
44
+ return ((await agents.list()).some((agent) => agent.agentId === agentId) ||
45
+ (await agents.list({ archived: true })).some((agent) => agent.agentId === agentId));
46
+ }
48
47
  /** How often an open inbox stream re-reads the revision. */
49
48
  const inboxPollMs = 2_000;
50
49
  const maxAgentRequestBytes = 512 * 1024;
@@ -52,9 +51,8 @@ function refusal(status, error) {
52
51
  return Response.json({ error }, { status });
53
52
  }
54
53
  /** Agent adapters admitted against each App's declared capabilities. */
55
- export function agentCapabilities(agents, liveKey) {
56
- const routes = createAgentRoutes(agents, liveKey);
57
- const shared = { base: "/", routes };
54
+ export function agentCapabilities(agents) {
55
+ const shared = { base: "/", routes: createAgentRoutes(agents) };
58
56
  return [
59
57
  {
60
58
  ...shared,
@@ -135,45 +133,22 @@ export function agentCapabilities(agents, liveKey) {
135
133
  },
136
134
  ];
137
135
  }
138
- function createAgentRoutes(agents, liveKey) {
139
- const routes = new Hono();
140
- // Every Agent and Session lookup shares this live principal-scoped door.
141
- const access = async (principal, agentId) => {
142
- if (oysterPrincipalIsOwner(principal))
143
- return true;
144
- if (principal.actor.kind !== "team-key")
145
- return false;
146
- const key = await liveKey(principal.actor.keyId);
147
- return key !== null && keyAgentAccess(key, agentId);
148
- };
149
- routes.use("/agents/:agentId/*", async (context, next) => {
150
- if (!(await access(context.env.principal, context.req.param("agentId"))))
151
- return refusal(403, "Agent access denied");
152
- await next();
153
- return undefined;
154
- });
155
- routes.use("/agent-policies/:agentId", async (context, next) => {
156
- if (!(await access(context.env.principal, context.req.param("agentId"))))
157
- return refusal(403, "Agent access denied");
158
- await next();
159
- return undefined;
160
- });
161
- routes.get("/agents", async (context) => {
162
- const listed = await agents.list();
163
- const actor = context.env.principal.actor;
164
- if (oysterPrincipalIsOwner(context.env.principal))
165
- return Response.json(listed);
166
- if (actor.kind !== "team-key")
167
- return Response.json([]);
168
- const key = await liveKey(actor.keyId);
169
- return Response.json(key === null
170
- ? []
171
- : listed.filter((agent) => keyAgentAccess(key, agent.agentId)));
172
- });
173
- routes.post("/agents", async (context) => {
174
- const { principal } = context.env;
175
- if (!oysterPrincipalIsOwner(principal))
176
- return refusal(403, "Only the owner creates agents");
136
+ function createAgentRoutes(agents) {
137
+ return (routes()
138
+ .get("/agents", agentCollection(), async (context, { reaches }) => {
139
+ const selected = context.req.query("archived");
140
+ if (selected !== undefined &&
141
+ selected !== "true" &&
142
+ selected !== "false")
143
+ return refusal(400, "Choose a valid archive view");
144
+ const listed = await agents.list({ archived: selected === "true" });
145
+ const reached = [];
146
+ for (const item of listed)
147
+ if (await reaches(item.agentId))
148
+ reached.push(item);
149
+ return Response.json(reached);
150
+ })
151
+ .post("/agents", owner("direct-or-app"), async (context, { principal }) => {
177
152
  const body = Schema.decodeUnknownOption(createSchema)(await context.req.json());
178
153
  if (body._tag === "None")
179
154
  return refusal(400, "Name the agent");
@@ -185,11 +160,8 @@ function createAgentRoutes(agents, liveKey) {
185
160
  // The owner's own authenticated request creates this Agent; its App tools
186
161
  // are approved the same way direct owner approval is, with no second path.
187
162
  return Response.json(await agents.create(body.value.name, body.value.requestId, principal, "owner-direct", avatar?.data));
188
- });
189
- routes.put("/agent-names/:agentId", async (context) => {
190
- const { principal } = context.env;
191
- if (!oysterPrincipalIsOwner(principal))
192
- return refusal(403, "Only the owner renames agents");
163
+ })
164
+ .put("/agent-names/:agentId", owner("direct-or-app"), async (context, { principal }) => {
193
165
  const agentId = context.req.param("agentId");
194
166
  if (!agentIdPattern.test(agentId))
195
167
  return refusal(404, "No such agent");
@@ -204,11 +176,8 @@ function createAgentRoutes(agents, liveKey) {
204
176
  ? 409
205
177
  : 403, result.error);
206
178
  return Response.json(result);
207
- });
208
- routes.put("/agent-avatars/:agentId", async (context) => {
209
- const { principal } = context.env;
210
- if (!oysterPrincipalIsOwner(principal))
211
- return refusal(403, "Only the owner changes avatars");
179
+ })
180
+ .put("/agent-avatars/:agentId", owner("direct-or-app"), async (context, { principal }) => {
212
181
  const agentId = context.req.param("agentId");
213
182
  if (!agentIdPattern.test(agentId))
214
183
  return refusal(404, "No such agent");
@@ -219,19 +188,19 @@ function createAgentRoutes(agents, liveKey) {
219
188
  if ("error" in result)
220
189
  return refusal(result.error === "not_found" ? 404 : 403, result.error);
221
190
  return Response.json(result);
222
- });
223
- routes.get("/agent-policies/:agentId", async (context) => {
224
- const agentId = context.req.param("agentId");
191
+ })
192
+ .get("/agent-policies/:agentId", agent(), async (_context, { agentId }) => {
225
193
  if (!agentIdPattern.test(agentId))
226
194
  return refusal(404, "No such agent");
227
195
  const status = await agents.policyStatus(agentId);
228
196
  return status === null
229
197
  ? refusal(404, "No such agent")
230
198
  : Response.json(status);
231
- });
232
- routes.put("/agent-policies/:agentId", async (context) => approvePolicy(agents, context.env.principal, "owner-direct", context.req.param("agentId"), await context.req.text()));
233
- routes.all("/agents/:agentId/*", async (context) => {
234
- const agentId = context.req.param("agentId");
199
+ })
200
+ .put("/agent-policies/:agentId", owner("direct-or-app"), async (context, { principal }) => approvePolicy(agents, principal, "owner-direct", context.req.param("agentId"), await context.req.text()))
201
+ .post("/agents/:agentId/archive", owner("direct-or-app"), (context, { principal }) => changeArchive(agents, context.req.param("agentId"), true, principal))
202
+ .post("/agents/:agentId/unarchive", owner("direct-or-app"), (context, { principal }) => changeArchive(agents, context.req.param("agentId"), false, principal))
203
+ .all("/agents/:agentId/*", agent(), async (context, { agentId }) => {
235
204
  if (!agentIdPattern.test(agentId))
236
205
  return refusal(404, "No such agent");
237
206
  const incoming = context.req.raw;
@@ -246,51 +215,45 @@ function createAgentRoutes(agents, liveKey) {
246
215
  if (length > maxAgentRequestBytes)
247
216
  return refusal(413, "Request too large");
248
217
  const target = new URL(`https://agent.internal${path}${url.search}`);
218
+ // A desk window's Computer routing hint is the host's to admit; the
219
+ // Agent's Session routes decode a closed query that refuses it.
220
+ target.searchParams.delete(computerTargetParam);
249
221
  const response = await agents.fetch(agentId, new Request(target, incoming));
250
222
  return response ?? refusal(404, "No such agent");
251
- });
252
- routes.get("/member/inbox", async (context) => {
223
+ })
224
+ .get("/member/inbox", agentCollection(), async (_context, { authority, reaches }) => {
253
225
  const inbox = await agents.inbox();
254
- const principal = context.env.principal;
255
- if (oysterPrincipalIsOwner(principal))
226
+ if (authority.kind === "owner")
256
227
  return Response.json({
257
228
  items: inbox.items,
258
229
  next: null,
259
230
  cursor: inbox.revision,
260
231
  });
261
- if (principal.actor.kind !== "team-key")
262
- return refusal(403, "Agent access denied");
263
- const key = await liveKey(principal.actor.keyId);
232
+ const items = [];
233
+ for (const item of inbox.items)
234
+ if ("agentId" in item && (await reaches(item.agentId)))
235
+ items.push(item);
264
236
  return Response.json({
265
- items: key === null
266
- ? []
267
- : inbox.items.filter((item) => "agentId" in item && keyAgentAccess(key, item.agentId)),
237
+ items,
268
238
  next: null,
269
239
  // The global revision changes for ungranted Agents too. Key feeds have
270
240
  // no scoped event stream, so expose neither that revision nor its events.
271
241
  cursor: 0,
272
242
  });
273
- });
274
- routes.get("/member/inbox/notices", (context) => oysterPrincipalIsOwner(context.env.principal)
275
- ? Response.json({ items: [], next: null })
276
- : refusal(403, "Agent access denied"));
277
- routes.post("/member/inbox/visits", async (context) => {
243
+ })
244
+ .get("/member/inbox/notices", owner("direct-or-app"), () => Response.json({ items: [], next: null }))
245
+ .post("/member/inbox/visits", agent(async (facts) => (await facts.body(visitSchema))?.agentId ?? null), async (context, { agentId }) => {
278
246
  const body = Schema.decodeUnknownOption(visitSchema)(await context.req.json());
279
247
  if (body._tag === "None")
280
248
  return refusal(400, "Invalid visit");
281
- if (!(await access(context.env.principal, body.value.agentId)))
282
- return refusal(403, "Agent access denied");
283
- return (await agents.visit(body.value.agentId, body.value.sessionId))
249
+ return (await agents.visit(agentId, body.value.sessionId))
284
250
  ? new Response(null, { status: 204 })
285
251
  : refusal(404, "No such Session");
286
- });
287
- // The feed is small (one person's agents), so a change is announced as a
288
- // `reset` and the client re-reads the whole inbox.
289
- routes.get("/member/inbox/events", (context) => {
290
- // The unscoped revision also changes for other Agents. Do not expose it
291
- // until a principal-scoped revision exists.
292
- if (!oysterPrincipalIsOwner(context.env.principal))
293
- return refusal(403, "Agent access denied");
252
+ })
253
+ // The feed is small (one person's agents), so a change is announced as a
254
+ // `reset` and the client re-reads the whole inbox. The unscoped revision
255
+ // also changes for other Agents, so only the owner reads it.
256
+ .get("/member/inbox/events", owner("direct-or-app"), (context) => {
294
257
  const after = Number(context.req.query("after") ?? "0");
295
258
  const signal = context.req.raw.signal;
296
259
  const encoder = new TextEncoder();
@@ -317,20 +280,33 @@ function createAgentRoutes(agents, liveKey) {
317
280
  "Cache-Control": "no-store",
318
281
  },
319
282
  });
320
- });
321
- return routes;
283
+ }));
284
+ }
285
+ /** Owner-only archive transition; creator Agents use their own control tool. */
286
+ async function changeArchive(agents, agentId, archived, principal) {
287
+ if (!agentIdPattern.test(agentId))
288
+ return refusal(404, "No such agent");
289
+ const result = await agents.setArchived(agentId, archived, principal);
290
+ if ("error" in result)
291
+ return Response.json(result, {
292
+ status: result.error === "not_found"
293
+ ? 404
294
+ : result.error === "not_granted"
295
+ ? 403
296
+ : result.archived !== undefined
297
+ ? 503
298
+ : 409,
299
+ });
300
+ return Response.json(result);
322
301
  }
323
302
  async function approvePolicy(agents, principal, actor, agentId, body) {
324
- if (!oysterPrincipalIsOwner(principal))
325
- return refusal(403, "Only the owner approves Agent tools");
326
303
  const input = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Struct({
327
304
  level: Schema.Literals(["read", "write"]),
328
305
  expectedRevision: Schema.NullOr(Schema.NonEmptyString),
329
306
  })), { onExcessProperty: "error" })(body);
330
307
  if (input._tag === "None")
331
308
  return refusal(400, "Choose read or write tools");
332
- if (!agentIdPattern.test(agentId) ||
333
- !(await agents.list()).some((agent) => agent.agentId === agentId))
309
+ if (!(await knownAgent(agents, agentId)))
334
310
  return refusal(404, "No such agent");
335
311
  const result = await agents.approveAppToolPolicy(agentId, principal, input.value.level, input.value.expectedRevision, actor);
336
312
  const failureStatuses = {
@@ -344,11 +320,8 @@ async function approvePolicy(agents, principal, actor, agentId, body) {
344
320
  }
345
321
  /** Host-owned direct approval under the admitted owner; never an App capability. */
346
322
  export function agentApprovalRoutes(agents) {
347
- return new Hono()
348
- .get("/api/agent-models/:agentId", async (context) => {
349
- if (context.req.raw.headers.has("X-Oyster-Installation") ||
350
- !oysterPrincipalIsOwner(context.get("principal")))
351
- return refusal(403, "Owner only");
323
+ return routes()
324
+ .get("/api/agent-models/:agentId", owner("direct"), async (context) => {
352
325
  const agentId = context.req.param("agentId");
353
326
  if (!agentIdPattern.test(agentId))
354
327
  return refusal(404, "No such agent");
@@ -357,17 +330,14 @@ export function agentApprovalRoutes(agents) {
357
330
  ? refusal(404, "No such agent")
358
331
  : Response.json(choice);
359
332
  })
360
- .put("/api/agent-models/:agentId", async (context) => {
361
- if (context.req.raw.headers.has("X-Oyster-Installation") ||
362
- !oysterPrincipalIsOwner(context.get("principal")))
363
- return refusal(403, "Owner only");
333
+ .put("/api/agent-models/:agentId", owner("direct"), async (context, { principal }) => {
364
334
  const agentId = context.req.param("agentId");
365
335
  if (!agentIdPattern.test(agentId))
366
336
  return refusal(404, "No such agent");
367
337
  const input = agents.decodeModelChoice(await context.req.text());
368
338
  if (input === null)
369
339
  return refusal(400, "Invalid model choice");
370
- const choice = await agents.setModelChoice(agentId, input, context.get("principal"));
340
+ const choice = await agents.setModelChoice(agentId, input, principal);
371
341
  if ("error" in choice && choice.error === "invalid_model_choice")
372
342
  return refusal(400, "Invalid model choice");
373
343
  return "error" in choice
@@ -378,21 +348,15 @@ export function agentApprovalRoutes(agents) {
378
348
  : 409, choice.error)
379
349
  : Response.json(choice);
380
350
  })
381
- .get("/api/agent-policies/:agentId", async (context) => {
382
- if (context.req.raw.headers.has("X-Oyster-Installation"))
383
- return refusal(403, "Owner direct only");
384
- if (!oysterPrincipalIsOwner(context.get("principal")))
385
- return refusal(403, "Owner only");
351
+ .get("/api/agent-policies/:agentId", owner("direct"), async (context) => {
386
352
  const agentId = context.req.param("agentId");
387
- if (!(await agents.list()).some((agent) => agent.agentId === agentId))
353
+ if (!(await knownAgent(agents, agentId)))
388
354
  return refusal(404, "No such agent");
389
355
  return Response.json({
390
356
  expectedRevision: await agents.policyRevision(agentId),
391
357
  });
392
358
  })
393
- .put("/api/agent-policies/:agentId", async (context) => context.req.raw.headers.has("X-Oyster-Installation")
394
- ? refusal(403, "Missing admitted Agent installation")
395
- : approvePolicy(agents, context.get("principal"), "owner-direct", context.req.param("agentId"), await context.req.text()));
359
+ .put("/api/agent-policies/:agentId", owner("direct"), async (context, { principal }) => approvePolicy(agents, principal, "owner-direct", context.req.param("agentId"), await context.req.text()));
396
360
  }
397
361
  const serverIdPattern = /^[a-z0-9_-]{1,64}$/u;
398
362
  const mcpRefusalStatus = { not_found: 404, duplicate_name: 409, limit: 409 };
@@ -401,28 +365,18 @@ const mcpRefusalStatus = { not_found: 404, duplicate_name: 409, limit: 409 };
401
365
  * capability. Bearer tokens go in and never come back out.
402
366
  */
403
367
  export function agentMcpServerRoutes(agents) {
404
- const routes = new Hono();
405
- const ownerOnly = async (context, next) => {
406
- if (context.req.raw.headers.has("X-Oyster-Installation"))
407
- return refusal(403, "Owner direct only");
408
- if (!oysterPrincipalIsOwner(context.get("principal")))
409
- return refusal(403, "Only the owner connects MCP servers");
410
- await next();
411
- return undefined;
412
- };
413
- routes.use("/api/agent-mcp-servers", ownerOnly);
414
- routes.use("/api/agent-mcp-servers/*", ownerOnly);
415
- // One owner read for the settings panel: every Agent with its servers.
416
- routes.get("/api/agent-mcp-servers", async () => {
368
+ return (routes()
369
+ // One owner read for the settings panel: every Agent with its servers.
370
+ .get("/api/agent-mcp-servers", owner("direct"), async () => {
417
371
  const listed = await agents.list();
418
- const rows = await Promise.all(listed.map(async (agent) => ({
419
- agentId: agent.agentId,
420
- name: agent.name,
421
- servers: (await agents.mcpServers(agent.agentId)) ?? [],
372
+ const rows = await Promise.all(listed.map(async (item) => ({
373
+ agentId: item.agentId,
374
+ name: item.name,
375
+ servers: (await agents.mcpServers(item.agentId)) ?? [],
422
376
  })));
423
377
  return Response.json({ agents: rows });
424
- });
425
- routes.post("/api/agent-mcp-servers/:agentId", async (context) => {
378
+ })
379
+ .post("/api/agent-mcp-servers/:agentId", owner("direct"), async (context) => {
426
380
  if (!agentIdPattern.test(context.req.param("agentId")))
427
381
  return refusal(404, "No such agent");
428
382
  const length = Number(context.req.header("content-length") ?? "0");
@@ -435,8 +389,8 @@ export function agentMcpServerRoutes(agents) {
435
389
  return "error" in added
436
390
  ? refusal(mcpRefusalStatus[added.error], added.error)
437
391
  : Response.json(added, { status: 201 });
438
- });
439
- routes.delete("/api/agent-mcp-servers/:agentId/:serverId", async (context) => {
392
+ })
393
+ .delete("/api/agent-mcp-servers/:agentId/:serverId", owner("direct"), async (context) => {
440
394
  const serverId = context.req.param("serverId");
441
395
  if (!agentIdPattern.test(context.req.param("agentId")) ||
442
396
  !serverIdPattern.test(serverId))
@@ -444,6 +398,5 @@ export function agentMcpServerRoutes(agents) {
444
398
  return (await agents.removeMcpServer(context.req.param("agentId"), serverId))
445
399
  ? new Response(null, { status: 204 })
446
400
  : refusal(404, "No such server");
447
- });
448
- return routes;
401
+ }));
449
402
  }
@@ -18,6 +18,7 @@
18
18
  */
19
19
  import * as Schema from "effect/Schema";
20
20
  import type { WatchdogSqliteOwner } from "@fungi.computer/watchdog";
21
+ import { type BuildJobLog } from "./build/job-log.js";
21
22
  import { type Release } from "@fungi.computer/puffball";
22
23
  import { type AppPublisherCoreConfigInput } from "./build/effect.js";
23
24
  import { type TeamAppCandidateManifest } from "./build/submission-contract.js";
@@ -144,10 +145,12 @@ export type OysterBuildComputer = Readonly<{
144
145
  command: string;
145
146
  timeoutMs: number;
146
147
  }): Promise<Readonly<{
147
- output?: Readonly<{
148
+ output: Readonly<{
148
149
  exitCode: number | null;
149
150
  stderr?: string;
150
151
  }> | null;
152
+ /** Raw combined events captured before the Computer reply's stream bounds. */
153
+ log: BuildJobLog;
151
154
  }>>;
152
155
  }>;
153
156
  export type OysterAppBuildOptions = Readonly<{
@@ -208,6 +211,8 @@ export declare function createOysterAppBuilds(options: OysterAppBuildOptions): R
208
211
  run: () => Promise<OysterBuildView>;
209
212
  }>>;
210
213
  read(jobId: string): OysterBuildView | null;
214
+ /** Full raw output; the host applies the same principal guard as `read`. */
215
+ readLog(jobId: string): Promise<BuildJobLog | null>;
211
216
  /** Discover the current source's latest job from the caller's Git owner,
212
217
  * never a job id remembered in a browser or another installation's repo. */
213
218
  latestRepositoryBuild(installationId: string, repository: string): OysterBuildView | null;
@@ -280,4 +285,4 @@ export declare function createOysterAppBuilds(options: OysterAppBuildOptions): R
280
285
  submitCandidate(jobId: string, manifest: TeamAppCandidateManifest, grant: string, credentialId: string): Promise<Response>;
281
286
  }>;
282
287
  export type OysterAppBuilds = ReturnType<typeof createOysterAppBuilds>;
283
- export {};
288
+ export { buildJobLogMaxBytes, type BuildJobLog } from "./build/job-log.js";
@@ -20,6 +20,7 @@ import * as Schema from "effect/Schema";
20
20
  import * as Data from "effect/Data";
21
21
  import * as Option from "effect/Option";
22
22
  import { candidateFailureDiagnostic } from "./build/candidate-diagnostic.js";
23
+ import { buildJobDiagnostic, buildJobLogSchema, } from "./build/job-log.js";
23
24
  import { parseArtifactRef, parseReleaseId, } from "@fungi.computer/puffball";
24
25
  import { appIdSchema, releaseSchema, sha256DigestSchema, sourceRevisionSchema, } from "@fungi.computer/puffball/effect";
25
26
  import { capturedPinnedRecipeSchema, normalizeAppPublisherRecipe, digestPinnedRecipe, fitsPinnedRecipe, pinnedRecipeObjectBound, } from "./build/effect.js";
@@ -235,8 +236,9 @@ export function createOysterAppBuilds(options) {
235
236
  detail: job.detail,
236
237
  });
237
238
  const settle = (jobId, status, detail) => {
239
+ const diagnostic = detail === null ? null : buildJobDiagnostic(jobId, detail);
238
240
  owner.sql.exec(`UPDATE oyster_build_jobs SET installation=CASE WHEN ?='succeeded' THEN installation ELSE ${failedInstallation} END,
239
- status=?, detail=?, expires_at=MIN(expires_at, ?) WHERE job_id=? AND status='running'`, status, detail, status, detail, now(), jobId);
241
+ status=?, detail=?, expires_at=MIN(expires_at, ?) WHERE job_id=? AND status='running'`, status, diagnostic, status, diagnostic, now(), jobId);
240
242
  };
241
243
  const required = (jobId) => {
242
244
  const job = readJob(jobId);
@@ -244,6 +246,10 @@ export function createOysterAppBuilds(options) {
244
246
  throw new Error("Unknown build job");
245
247
  return job;
246
248
  };
249
+ // A job-scoped immutable object in the same artifact store as its build.
250
+ // It is read only through the owner build door, never exposed as a Release.
251
+ const logRef = (jobId) => parseArtifactRef(`artifact:build-log-${jobId}`);
252
+ const logCodec = Schema.fromJsonString(buildJobLogSchema);
247
253
  const publisher = createTeamBuildPublisher({
248
254
  teamId: options.tenantId,
249
255
  now,
@@ -284,7 +290,8 @@ export function createOysterAppBuilds(options) {
284
290
  command: input.command,
285
291
  timeoutMs: input.timeoutMs,
286
292
  });
287
- if (result.output === undefined || result.output === null)
293
+ await options.artifacts.put(logRef(input.jobId), new TextEncoder().encode(Schema.encodeUnknownSync(logCodec)(result.log)));
294
+ if (result.output === null)
288
295
  throw new BuildOutcomeUnknown();
289
296
  return result;
290
297
  },
@@ -458,6 +465,15 @@ export function createOysterAppBuilds(options) {
458
465
  const job = readJob(jobId);
459
466
  return job === null ? null : view(job);
460
467
  },
468
+ /** Full raw output; the host applies the same principal guard as `read`. */
469
+ async readLog(jobId) {
470
+ if (readJob(jobId) === null)
471
+ return null;
472
+ const bytes = await options.artifacts.get(logRef(jobId));
473
+ return bytes === null
474
+ ? null
475
+ : Schema.decodeUnknownSync(logCodec)(new TextDecoder().decode(bytes));
476
+ },
461
477
  /** Discover the current source's latest job from the caller's Git owner,
462
478
  * never a job id remembered in a browser or another installation's repo. */
463
479
  latestRepositoryBuild(installationId, repository) {
@@ -660,3 +676,4 @@ export function createOysterAppBuilds(options) {
660
676
  },
661
677
  });
662
678
  }
679
+ export { buildJobLogMaxBytes } from "./build/job-log.js";
@@ -1,5 +1,5 @@
1
1
  import { type Approvals } from "@fungi.computer/approvals";
2
2
  import type { HostActor } from "@fungi.computer/puffball/app";
3
3
  import type { OysterCapability } from "./host.js";
4
- /** Ordinary capability routes; the outer App gate supplies identity and consent. */
4
+ /** Ordinary capability routes; the outer App gate supplies consent, and only members answer. */
5
5
  export declare function approvalCapability(control: (actor: HostActor) => Approvals): OysterCapability;
@@ -1,19 +1,19 @@
1
- import { Hono } from "hono";
1
+ import { member, routes as routesFor } from "./authorization.js";
2
2
  import { bodyLimit } from "hono/body-limit";
3
3
  import { causeClass } from "./cause-class.js";
4
4
  import * as Schema from "effect/Schema";
5
5
  import { ApprovalListInputSchema, ApprovalGetInputSchema, ApprovalAnswerInputSchema, AutoApproveInputSchema, } from "@fungi.computer/approvals";
6
- /** Ordinary capability routes; the outer App gate supplies identity and consent. */
6
+ /** Ordinary capability routes; the outer App gate supplies consent, and only members answer. */
7
7
  export function approvalCapability(control) {
8
- const routes = new Hono();
9
- routes.use("*", bodyLimit({ maxSize: 4096 }));
10
- routes.use("*", async (context, next) => {
11
- if (context.env.principal.actor.kind !== "member")
12
- return context.json({ kind: "refused", reason: "not-eligible" }, 403);
13
- await next();
14
- return undefined;
15
- });
16
- routes.onError((cause, context) => {
8
+ const strict = { onExcessProperty: "error" };
9
+ const eligible = member(() => Response.json({ kind: "refused", reason: "not-eligible" }, { status: 403 }));
10
+ const options = { middleware: [bodyLimit({ maxSize: 4096 })] };
11
+ const routes = routesFor()
12
+ .post("/list", eligible, async (c, { principal }) => c.json(await control(principal.actor).list(Schema.decodeUnknownSync(ApprovalListInputSchema, strict)(await c.req.json()))), options)
13
+ .post("/get", eligible, async (c, { principal }) => c.json(await control(principal.actor).get(Schema.decodeUnknownSync(ApprovalGetInputSchema, strict)(await c.req.json()))), options)
14
+ .post("/answer", eligible, async (c, { principal }) => c.json(await control(principal.actor).answer(Schema.decodeUnknownSync(ApprovalAnswerInputSchema, strict)(await c.req.json()))), options)
15
+ .post("/set-auto-approve", eligible, async (c, { principal }) => c.json(await control(principal.actor).setAutoApprove(Schema.decodeUnknownSync(AutoApproveInputSchema, strict)(await c.req.json()))), options)
16
+ .onError((cause, context) => {
17
17
  if (cause instanceof SyntaxError || cause instanceof Schema.SchemaError)
18
18
  return context.json({ error: "Invalid approval request" }, 400);
19
19
  console.warn("Native approval control failed", {
@@ -21,11 +21,6 @@ export function approvalCapability(control) {
21
21
  });
22
22
  return context.json({ error: "Approval home unavailable" }, 503);
23
23
  });
24
- const strict = { onExcessProperty: "error" };
25
- routes.post("/list", async (c) => c.json(await control(c.env.principal.actor).list(Schema.decodeUnknownSync(ApprovalListInputSchema, strict)(await c.req.json()))));
26
- routes.post("/get", async (c) => c.json(await control(c.env.principal.actor).get(Schema.decodeUnknownSync(ApprovalGetInputSchema, strict)(await c.req.json()))));
27
- routes.post("/answer", async (c) => c.json(await control(c.env.principal.actor).answer(Schema.decodeUnknownSync(ApprovalAnswerInputSchema, strict)(await c.req.json()))));
28
- routes.post("/set-auto-approve", async (c) => c.json(await control(c.env.principal.actor).setAutoApprove(Schema.decodeUnknownSync(AutoApproveInputSchema, strict)(await c.req.json()))));
29
24
  return {
30
25
  id: "approvals",
31
26
  version: 1,
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { ApprovalLedger, type Approvals, type PendingApproval, type ApprovalApplyResult } from "@fungi.computer/approvals";
3
3
  import type { WatchdogSqliteOwner } from "@fungi.computer/watchdog";
4
- import { type OysterHost } from "./host.js";
4
+ import type { OysterHost } from "./host.js";
5
5
  import type { HostActor } from "@fungi.computer/puffball/app";
6
6
  import type { TenantScope } from "./tenant.js";
7
7
  declare const callSchema: z.ZodObject<{
@@ -32,6 +32,7 @@ export type OysterApprovalCall = z.output<typeof callSchema>;
32
32
  export declare class OysterApprovalHome {
33
33
  private readonly host;
34
34
  readonly ledger: ApprovalLedger;
35
+ private surfaces;
35
36
  constructor(host: Readonly<{
36
37
  owner: WatchdogSqliteOwner;
37
38
  scope: TenantScope;
@@ -3,9 +3,8 @@ import * as Schema from "effect/Schema";
3
3
  import { hostActorSchema } from "@fungi.computer/puffball/app-method-runtime";
4
4
  import { ApprovalLedger, } from "@fungi.computer/approvals";
5
5
  import { invokeModuleOperation } from "@fungi.computer/mycelium/remote";
6
- import { oysterPrincipalIsOwner } from "./host.js";
6
+ import { keyAppAccess, oysterPrincipalIsOwner } from "./authorization.js";
7
7
  import { sha256 } from "./release.js";
8
- import { keyAppAccess } from "./keys.js";
9
8
  const identity = z.string().min(1).max(256);
10
9
  const targetSchema = z.strictObject({
11
10
  actor: hostActorSchema,
@@ -25,6 +24,9 @@ const intentRow = Schema.decodeUnknownSync(Schema.Struct({ target_json: Schema.S
25
24
  export class OysterApprovalHome {
26
25
  host;
27
26
  ledger;
27
+ // The home's artifact store and capability configuration are fixed, unlike
28
+ // the authority read fresh from current() for every recheck and dispatch.
29
+ surfaces;
28
30
  constructor(host) {
29
31
  this.host = host;
30
32
  host.owner.sql.exec("CREATE TABLE IF NOT EXISTS oyster_approval_targets (record_id TEXT PRIMARY KEY, target_json TEXT NOT NULL) STRICT");
@@ -180,7 +182,7 @@ export class OysterApprovalHome {
180
182
  byInstallation: new Map([[target.installationId, policy]]),
181
183
  };
182
184
  }
183
- const snapshot = await oysterAppSnapshot(host, appSurfaceCache(host), principal, [installation], access, target.sessionId ?? undefined);
185
+ const snapshot = await oysterAppSnapshot(host, (this.surfaces ??= appSurfaceCache(host)), principal, [installation], access, target.sessionId, "direct");
184
186
  const module = snapshot.modules[record.module];
185
187
  const fence = snapshot.fences[record.module];
186
188
  const effect = snapshot.effects[record.module]?.[record.operation];
@@ -34,7 +34,7 @@ export const permissionCatalog = {
34
34
  "apps.call": entry("apps", "Call declared App methods", "call the listed methods in other Apps as you. Renamed target methods need approval again.", "run", "install"),
35
35
  "app.storage": entry("data", "Store App data", "save its App data.", "write", "install"),
36
36
  "app.events.emit": entry("workflows", "Send events", "tell your Workflows when something happens.", "sendPrompts", "ask"),
37
- "team.workbenchHost": entry("shell", "Arrange the workbench", "arrange Apps on your workbench.", "list", "ask", null, "The shell runs with full access to your Team and every App inside it. Only pick a shell you trust."),
37
+ "team.workbenchHost": entry("shell", "Shell", "Full access to your Team and every App", "list", "ask", null, "Only pick a shell you trust."),
38
38
  "marketplace.browse": entry("bazaar", "Browse the marketplace", "browse Apps in the marketplace.", "browseBazaar", "install"),
39
39
  "marketplace.purchase": entry("bazaar", "Purchase from the marketplace", "purchase Apps from the marketplace.", "buyApps", "install"),
40
40
  "workflow.run": entry("workflows", "Run Workflows", "start Workflows.", "run", "install"),