@fungi.computer/oyster 0.4.1 → 0.5.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 (45) hide show
  1. package/dist/agents.d.ts +2 -1
  2. package/dist/agents.js +63 -8
  3. package/dist/apps/gate.d.ts +4 -0
  4. package/dist/apps/gate.js +3 -0
  5. package/dist/boot-host-script.js +1 -1
  6. package/dist/build/candidate-cli.mjs +19 -4
  7. package/dist/channels/binding.d.ts +63 -0
  8. package/dist/channels/binding.js +85 -0
  9. package/dist/channels/conversations.d.ts +235 -0
  10. package/dist/channels/conversations.js +705 -0
  11. package/dist/channels/ids.d.ts +8 -0
  12. package/dist/channels/ids.js +15 -0
  13. package/dist/host.d.ts +1 -1
  14. package/dist/host.js +21 -4
  15. package/dist/keys.d.ts +4 -1
  16. package/dist/keys.js +15 -4
  17. package/dist/machine-link/authority.d.ts +7 -0
  18. package/dist/machine-link/delivery.d.ts +12 -0
  19. package/dist/machine-link/delivery.js +22 -1
  20. package/dist/machine-link/disposition.d.ts +16 -0
  21. package/dist/machine-link/disposition.js +65 -0
  22. package/dist/machine-link/host.d.ts +11 -2
  23. package/dist/machine-link/host.js +125 -5
  24. package/dist/machine-link/link.d.ts +4 -0
  25. package/dist/machine-link/link.js +47 -1
  26. package/dist/page-script.js +4 -4
  27. package/dist/standalone/agent-mail.js +4 -1
  28. package/dist/standalone/agents.d.ts +8 -0
  29. package/dist/standalone/agents.js +21 -2
  30. package/dist/standalone/channel-session-door.d.ts +4 -0
  31. package/dist/standalone/channel-session-door.js +42 -0
  32. package/dist/standalone/machine-link.d.ts +2 -0
  33. package/dist/standalone/machine-link.js +34 -1
  34. package/dist/testing/capabilities.d.ts +2 -0
  35. package/dist/testing/capabilities.js +3 -1
  36. package/dist/workers-git.js +1 -1
  37. package/dist/workers.d.ts +3 -0
  38. package/docs/api/agents.md +8 -4
  39. package/docs/api/apps.md +8 -4
  40. package/docs/api/browser.md +40 -16
  41. package/docs/api/keys.md +25 -3
  42. package/docs/api/workers.md +21 -14
  43. package/docs/site/agent-quickstart.mdx +23 -0
  44. package/examples/standalone/wrangler.jsonc +3 -4
  45. package/package.json +28 -16
@@ -23,6 +23,7 @@ export function agentMail(options) {
23
23
  if (tick === undefined)
24
24
  throw new Error("Agent Post lane is not open");
25
25
  await tick();
26
+ await mail.stageUndeliverableReplies();
26
27
  };
27
28
  const mail = new AgentMail({ ...options, post: runtime, drain });
28
29
  const host = {
@@ -57,6 +58,8 @@ export function agentMail(options) {
57
58
  };
58
59
  return {
59
60
  host,
60
- deliver: (value) => mail.send(value),
61
+ deliver: (value) => {
62
+ return mail.send(value);
63
+ },
61
64
  };
62
65
  }
@@ -1,5 +1,7 @@
1
1
  import { type StandaloneTenancyEnv } from "./tenancy.js";
2
2
  import type { PostEnvelope } from "@fungi.computer/carrier/post";
3
+ import { type ChannelConversations } from "../channels/conversations.js";
4
+ import type { ConversationBinding } from "../channels/binding.js";
3
5
  import { type StandaloneMachineLinkBindings, type StandaloneMachineJoinResult } from "./machine-link.js";
4
6
  import { type MachineAgentSettlementPrincipal, type MachineAgentHome, type MachineEnrollment, type MachineLinkCaller, type MachineLinkIdentity } from "../machine-link/index.js";
5
7
  /** Workers AI binding surface used by the standalone provider adapter. */
@@ -78,6 +80,12 @@ export declare abstract class StandaloneAgentBase<Env extends StandaloneAgentEnv
78
80
  #private;
79
81
  /** The deployable root's host for this Agent's pinned tenant. */
80
82
  protected abstract hostFor(scope: TenantScope): Promise<OysterHost>;
83
+ /**
84
+ * Compose Oyster's channel owner on this Agent's real store and Shiitake.
85
+ * A host must supply its own binding lookup after authenticating its edge;
86
+ * this does not install an inbound route or mint provider credentials.
87
+ */
88
+ protected channelConversations(binding: (bindingId: string) => ConversationBinding | null): ChannelConversations;
81
89
  private uiOwner;
82
90
  private machineClient;
83
91
  private computerSnapshot;
@@ -7,6 +7,8 @@ import { AgentCreateRefused, } from "@fungi.computer/shiitake/agent-control";
7
7
  import { AgentDelegation, AgentDisclosureRefused, machineAgentCodecs, } from "@fungi.computer/shiitake/delegation";
8
8
  import { FungiMessageMetaSchema } from "@fungi.computer/agar/agents";
9
9
  import { agentMail } from "./agent-mail.js";
10
+ import { createChannelConversations, } from "../channels/conversations.js";
11
+ import { channelSessionDoor } from "./channel-session-door.js";
10
12
  import { createStandaloneMachineEnrollment, standaloneMachineAgentHome, standaloneMachineLinkAuthority, } from "./machine-link.js";
11
13
  import { createMachineLinkFences, MachineEnrollmentRefused, MachineLinkRefused, createMachineAgentClient, } from "../machine-link/index.js";
12
14
  import { memberInboxItemSchema } from "@fungi.computer/puffball/inbox";
@@ -228,6 +230,22 @@ const instructions = [
228
230
  ].join("\n");
229
231
  /** One agent: Shiitake's Cloudflare host over this Durable Object; its root supplies `hostFor`. */
230
232
  export class StandaloneAgentBase extends ShiitakeDO {
233
+ /**
234
+ * Compose Oyster's channel owner on this Agent's real store and Shiitake.
235
+ * A host must supply its own binding lookup after authenticating its edge;
236
+ * this does not install an inbound route or mint provider credentials.
237
+ */
238
+ channelConversations(binding) {
239
+ return createChannelConversations({
240
+ sql: this.ctx.storage.sql,
241
+ transactionSync: (body) => this.ctx.storage.transactionSync(body),
242
+ binding,
243
+ door: channelSessionDoor(() => this.shiitake(), () => {
244
+ // Standalone Agents have no Hub Team grants to deliver.
245
+ }),
246
+ now: () => Date.now(),
247
+ });
248
+ }
231
249
  /** Each installed Release's verified catalog surface, built once per Release for this Agent's lifetime. */
232
250
  #appSurfaces;
233
251
  uiOwner;
@@ -479,10 +497,11 @@ export class StandaloneAgentBase extends ShiitakeDO {
479
497
  throw new Error("Delegated reply identity is unavailable");
480
498
  if (mail.reply.outcome === "undeliverable")
481
499
  return undefined;
482
- if (mail.from.sessionId === undefined)
483
- throw new Error("Delegated reply Session is unavailable");
500
+ if (mail.from.kind !== "agent")
501
+ throw new Error("Delegated reply Agent identity is unavailable");
484
502
  return (await this.machineClient()).settlement(mail.from.agentId, mail.from.sessionId, mail.reply.commandId, mail.to.agentId);
485
503
  },
504
+ onUndeliverable: (original) => this.delegation().localUndeliverable(original),
486
505
  wake: () => this.wakeRuntime(),
487
506
  }));
488
507
  }
@@ -0,0 +1,4 @@
1
+ import type { ShiitakeOwnerAgent } from "@fungi.computer/shiitake/cloudflare";
2
+ import type { SessionDoor } from "../channels/conversations.js";
3
+ /** Translate a standalone Agent's Shiitake owner into Oyster's channel Session port. */
4
+ export declare function channelSessionDoor(open: () => Promise<ShiitakeOwnerAgent>, deliverGrants: (sessionId: string) => void): SessionDoor;
@@ -0,0 +1,42 @@
1
+ import { authorMeta } from "@fungi.computer/agar/agents";
2
+ import { ShiitakeError } from "@fungi.computer/shiitake";
3
+ /** Translate a standalone Agent's Shiitake owner into Oyster's channel Session port. */
4
+ export function channelSessionDoor(open, deliverGrants) {
5
+ return {
6
+ createSession: async (requestId) => (await open()).createSession(requestId),
7
+ deliverGrants,
8
+ withdraw: async (sessionId, requestId) => (await open()).session(sessionId).withdraw(requestId),
9
+ latestPending: async (sessionId, visitor) => (await open())
10
+ .withMeta(authorMeta(visitor))
11
+ .session(sessionId)
12
+ .latestPending(),
13
+ prompt: async (sessionId, { requestId, text, meta }) => {
14
+ // The conversation row exists now, so its channel admission is known;
15
+ // deliver Agent tools before the first run can call one.
16
+ deliverGrants(sessionId);
17
+ const agent = await open();
18
+ return await agent.withMeta(meta).session(sessionId).command({
19
+ type: "prompt",
20
+ requestId,
21
+ text,
22
+ delivery: "followUp",
23
+ });
24
+ },
25
+ line: async (sessionId, input) => {
26
+ const agent = await open();
27
+ const meta = input.author === "operator" ? authorMeta(input.identity) : input.meta;
28
+ await agent.withMeta(meta).session(sessionId).command({
29
+ type: "operator",
30
+ requestId: input.requestId,
31
+ author: input.author,
32
+ text: input.text,
33
+ });
34
+ },
35
+ errorKind: (error, requestId) => error instanceof ShiitakeError &&
36
+ error.boundary === "contract" &&
37
+ error.cause === "closed" &&
38
+ error.requestId === requestId
39
+ ? "closed_request"
40
+ : null,
41
+ };
42
+ }
@@ -268,6 +268,8 @@ export declare class MachineLinkDO extends DurableObject<StandaloneMachineLinkEn
268
268
  alarm(): Promise<void>;
269
269
  fetch(request: Request): Promise<Response>;
270
270
  deliver(id: MachineLinkIdentity, caller: MachineLinkCaller, agentId: string, envelope: PostEnvelope): Promise<PostDeliveryResult>;
271
+ deliverRelayDisposition(id: MachineLinkIdentity, relay: MachineLinkIdentity, original: PostEnvelope, reply: PostEnvelope): Promise<PostDeliveryResult>;
272
+ hasStagedDisposition(id: MachineLinkIdentity, original: PostEnvelope, reply: PostEnvelope): Promise<boolean>;
271
273
  operation(id: MachineLinkIdentity, caller: MachineLinkCaller, agentId: string, operation: MachineAgentOperation): Promise<import("effect/Schema").Json>;
272
274
  applyFence(fact: MachineLinkFenceFact): Promise<MachineLinkFenceResult>;
273
275
  preflightErasure(teamId: string, deviceId: string): Promise<void>;
@@ -1,4 +1,4 @@
1
- import { machineAgentReservations } from "@fungi.computer/shiitake/delegation";
1
+ import { machineAgentCodecs, machineAgentReservations, } from "@fungi.computer/shiitake/delegation";
2
2
  import { createMachineEnrollment, MachineEnrollmentRefused, MachineLinkRefused, } from "../machine-link/index.js";
3
3
  import { keyAllowsMachineEnroll, } from "../keys.js";
4
4
  import { agentIds, createAgentDirectory, } from "@fungi.computer/shiitake/fleet";
@@ -8,6 +8,7 @@ import { bodyLimit } from "hono/body-limit";
8
8
  import { validator } from "hono/validator";
9
9
  import { machineEnrollmentSchema, machineLinkName, } from "../machine-link/index.js";
10
10
  import { createMachineLink } from "../machine-link/workers.js";
11
+ import { deliverUndeliverableMail } from "../machine-link/delivery.js";
11
12
  import { defaultTenant, tenantScope } from "../tenant.js";
12
13
  import { presentedGitKey } from "../git-auth.js";
13
14
  import { standaloneTenancy } from "./tenancy.js";
@@ -49,6 +50,7 @@ export function standaloneMachineLinkAuthority(env) {
49
50
  return directory().machineDevice(id);
50
51
  },
51
52
  currentFence: async (id) => id.teamId !== teamId ? null : directory().machineFence(id),
53
+ agentHome: async (id, agentId) => id.teamId !== teamId ? null : directory().agentHome(agentId),
52
54
  directory: async (id) => {
53
55
  if (!(await authority.admitDevice(id)).live)
54
56
  throw new MachineLinkRefused("actor_not_allowed");
@@ -69,6 +71,31 @@ export function standaloneMachineLinkAuthority(env) {
69
71
  throw new MachineLinkRefused("actor_not_allowed");
70
72
  return env.MACHINE_LINK.getByName(machineLinkName(next)).deliver(next, caller, facts.targetAgentId, envelope);
71
73
  },
74
+ deliverUndeliverable: async (id, original, reply) => {
75
+ if ((await authority.currentFence(id)) === null)
76
+ return "refused";
77
+ const mail = original.event;
78
+ if (mail.from.kind !== "agent")
79
+ return "refused";
80
+ const home = await authority.agentHome(id, mail.from.agentId);
81
+ if (home === null)
82
+ throw new MachineLinkRefused("unavailable");
83
+ return await deliverUndeliverableMail({
84
+ identity: id,
85
+ original,
86
+ reply,
87
+ senderHome: home,
88
+ hosted: env.AGENTS,
89
+ accepted: machineAgentCodecs.accepted,
90
+ machine: async (deviceId, envelope) => {
91
+ const destination = { teamId, deviceId };
92
+ return await env.MACHINE_LINK.getByName(machineLinkName(destination)).deliverRelayDisposition(destination, id, original, envelope);
93
+ },
94
+ });
95
+ },
96
+ verifyRelayDisposition: async (relay, original, reply) => {
97
+ return await env.MACHINE_LINK.getByName(machineLinkName(relay)).hasStagedDisposition(relay, original, reply);
98
+ },
72
99
  canErase: async (id) => {
73
100
  if (id.teamId !== teamId)
74
101
  return false;
@@ -185,6 +212,12 @@ export class MachineLinkDO extends DurableObject {
185
212
  deliver(id, caller, agentId, envelope) {
186
213
  return this.link.deliver(id, caller, agentId, envelope);
187
214
  }
215
+ deliverRelayDisposition(id, relay, original, reply) {
216
+ return this.link.deliverRelayDisposition(id, relay, original, reply);
217
+ }
218
+ hasStagedDisposition(id, original, reply) {
219
+ return this.link.hasStagedDisposition(id, original, reply);
220
+ }
188
221
  operation(id, caller, agentId, operation) {
189
222
  return this.link.operation(id, caller, agentId, operation);
190
223
  }
@@ -1,6 +1,7 @@
1
1
  import { Hono } from "hono";
2
2
  import { type AppCapabilityId } from "@fungi.computer/puffball/app";
3
3
  import { type OysterAgents } from "../agents.js";
4
+ import type { LiveOysterKey } from "../keys.js";
4
5
  import { type FileSource } from "../files.js";
5
6
  import { type OysterAuthResult, type OysterCapability } from "../host.js";
6
7
  /** How one capability fixture host admits its single installation. */
@@ -19,6 +20,7 @@ export type CapabilityFixtureOptions = Readonly<{
19
20
  visitor?: boolean;
20
21
  authenticate?: (request: Request) => Promise<OysterAuthResult>;
21
22
  agents?: OysterAgents;
23
+ liveKey?: (keyId: string) => Promise<LiveOysterKey | null>;
22
24
  }>;
23
25
  /** One Oyster host with a single installation whose capability grants the options shape. */
24
26
  export declare function capabilityFixture(options?: CapabilityFixtureOptions): Promise<{
@@ -84,7 +84,7 @@ export async function capabilityFixture(options = {}) {
84
84
  if (options.visitor)
85
85
  installation = { ...installation, visibility: "public" };
86
86
  const readOnly = () => Promise.reject(new Error("Fixture installations are read-only"));
87
- const base = {
87
+ let base = {
88
88
  bootUi: () => Promise.resolve({ kind: "passed" }),
89
89
  auth: {
90
90
  resolve: () => Promise.reject(new Error("Capability fixture resolves no actor")),
@@ -116,6 +116,8 @@ export async function capabilityFixture(options = {}) {
116
116
  },
117
117
  capabilities,
118
118
  };
119
+ if (options.liveKey !== undefined)
120
+ base = { ...base, keys: { live: options.liveKey } };
119
121
  const host = options.agents === undefined
120
122
  ? base
121
123
  : { ...base, ownerRoutes: agentApprovalRoutes(options.agents) };
@@ -1117,7 +1117,7 @@ export async function oysterGitWorkerHost(env, options, selected, request) {
1117
1117
  const base = oysterWorkerHost(env, auth, policyOptions);
1118
1118
  const capabilities = [
1119
1119
  ...(base.capabilities ?? []),
1120
- ...(agents === undefined ? [] : agentCapabilities(agents)),
1120
+ ...(agents === undefined ? [] : agentCapabilities(agents, keysLive)),
1121
1121
  ...(options.capabilities?.(env, workspace, keysLive) ?? []),
1122
1122
  ];
1123
1123
  let gitOptions = { scope };
package/dist/workers.d.ts CHANGED
@@ -138,6 +138,7 @@ export declare class OysterRegistry<Env extends OysterWorkerEnv = OysterWorkerEn
138
138
  };
139
139
  readonly reversibility: "reversible" | "irreversible-external";
140
140
  readonly requestedAt: number;
141
+ readonly denialNote?: string | undefined;
141
142
  readonly evidence?: readonly {
142
143
  readonly kind: "artifact";
143
144
  readonly ref: string;
@@ -155,6 +156,7 @@ export declare class OysterRegistry<Env extends OysterWorkerEnv = OysterWorkerEn
155
156
  }>, actor: HostActor, input: ApprovalAnswerInput): Promise<Readonly<{
156
157
  kind: "refused";
157
158
  reason: "pending" | "expired" | "not-found" | "not-eligible" | "already-answered" | "stale-revision" | "declined" | "fenced" | "not-granted" | "read-only" | "in-flight" | "outcome-unknown" | "identity-conflict";
159
+ denialNote?: string;
158
160
  }> | Readonly<{
159
161
  kind: "answered";
160
162
  view: import("@fungi.computer/approvals").ApprovalView;
@@ -166,6 +168,7 @@ export declare class OysterRegistry<Env extends OysterWorkerEnv = OysterWorkerEn
166
168
  }>, actor: HostActor, input: AutoApproveInput): Promise<Readonly<{
167
169
  kind: "refused";
168
170
  reason: "pending" | "expired" | "not-found" | "not-eligible" | "already-answered" | "stale-revision" | "declined" | "fenced" | "not-granted" | "read-only" | "in-flight" | "outcome-unknown" | "identity-conflict";
171
+ denialNote?: string;
169
172
  }> | Readonly<{
170
173
  kind: "updated";
171
174
  rule: AutoApproveInput;
@@ -358,16 +358,20 @@ optional bearer.
358
358
  ### agentCapabilities()
359
359
 
360
360
  ```ts
361
- function agentCapabilities(agents): readonly Readonly<object & object>[];
361
+ function agentCapabilities(
362
+ agents,
363
+ liveKey,
364
+ ): readonly Readonly<object & object>[];
362
365
  ```
363
366
 
364
367
  Agent adapters admitted against each App's declared capabilities.
365
368
 
366
369
  #### Parameters
367
370
 
368
- | Parameter | Type |
369
- | --------- | ------------------------------- |
370
- | `agents` | [`OysterAgents`](#oysteragents) |
371
+ | Parameter | Type |
372
+ | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
373
+ | `agents` | [`OysterAgents`](#oysteragents) |
374
+ | `liveKey` | (`keyId`) => `Promise`\< \| `Readonly`\<\{ `keyId`: `string`; `owner`: `string`; `grantVersion`: `number`; `grants`: readonly `Readonly`\<\{ `capability`: [`OysterKeyCapability`](keys.md#oysterkeycapability); `targets`: `GrantTargets`; \}\>[]; \}\> \| `null`\> |
371
375
 
372
376
  #### Returns
373
377
 
package/docs/api/apps.md CHANGED
@@ -852,6 +852,10 @@ const gateRequestSchema: ZodLazy<ZodDiscriminatedUnion<[ZodObject<{
852
852
  | "channels.read"
853
853
  | "channels.personal.manage", unknown>>;
854
854
  operation: ZodString;
855
+ effect: ZodEnum<{
856
+ read: "read";
857
+ write: "write";
858
+ }>;
855
859
  }, $strict>], "kind">>;
856
860
  ```
857
861
 
@@ -1676,10 +1680,10 @@ stay outside this pure gate.
1676
1680
 
1677
1681
  #### Parameters
1678
1682
 
1679
- | Parameter | Type |
1680
- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1681
- | `facts` | [`GateFacts`](#gatefacts) |
1682
- | `rawRequest` | \| \{ `kind`: `"method"`; `name`: `string`; `input`: `unknown`; `via`: `"ui"` \| `"agent"` \| `"mcp"` \| `"workflow"`; `resource?`: `Readonly`\<\{ `kind`: `string`; `id`: `string`; \}\>; \} \| \{ `kind`: `"capability"`; `capability`: \| `"computer.list"` \| `"computer.readFile"` \| `"computer.writeFile"` \| `"computer.execute"` \| `"approvals"` \| `"agent.list"` \| `"agent.manage"` \| `"agent.rename"` \| `"agent.avatar"` \| `"agent.toolPolicy"` \| `"agent.read"` \| `"agent.send"` \| `"llm.generation"` \| `"media.generation"` \| `"games:servers"` \| `"git.repository"` \| `"apps.open"` \| `"apps.call"` \| `"app.storage"` \| `"app.events.emit"` \| `"team.workbenchHost"` \| `"marketplace.browse"` \| `"marketplace.purchase"` \| `"workflow.run"` \| `"workflow.publish"` \| `"workflow.arm"` \| `"world"` \| `"channels.read"` \| `"channels.personal.manage"`; `operation?`: `string`; `resource`: \| `"any"` \| `Readonly`\<\{ `kind`: `string`; `id`: `string`; \}\>; \} \| \{ `kind`: `"capability-route"`; `capability`: \| `"computer.list"` \| `"computer.readFile"` \| `"computer.writeFile"` \| `"computer.execute"` \| `"approvals"` \| `"agent.list"` \| `"agent.manage"` \| `"agent.rename"` \| `"agent.avatar"` \| `"agent.toolPolicy"` \| `"agent.read"` \| `"agent.send"` \| `"llm.generation"` \| `"media.generation"` \| `"games:servers"` \| `"git.repository"` \| `"apps.open"` \| `"apps.call"` \| `"app.storage"` \| `"app.events.emit"` \| `"team.workbenchHost"` \| `"marketplace.browse"` \| `"marketplace.purchase"` \| `"workflow.run"` \| `"workflow.publish"` \| `"workflow.arm"` \| `"world"` \| `"channels.read"` \| `"channels.personal.manage"`; `operation`: `string`; \} \| \{ `kind`: `"binding"`; `binding`: \| `"BACKGROUND"` \| `"GIT"` \| `"EVENTS"` \| `"APPS"` \| `"WORKFLOWS"` \| `"APP_STORAGE"` \| `"MEDIA"` \| `"COMPUTER"` \| `"INBOX"` \| `"AGENTS"`; `operation`: `string`; \} \| \{ `kind`: `"list-tools"`; \} \| \{ `kind`: `"backend"`; \} |
1683
+ | Parameter | Type |
1684
+ | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1685
+ | `facts` | [`GateFacts`](#gatefacts) |
1686
+ | `rawRequest` | \| \{ `kind`: `"method"`; `name`: `string`; `input`: `unknown`; `via`: `"ui"` \| `"agent"` \| `"mcp"` \| `"workflow"`; `resource?`: `Readonly`\<\{ `kind`: `string`; `id`: `string`; \}\>; \} \| \{ `kind`: `"capability"`; `capability`: \| `"computer.list"` \| `"computer.readFile"` \| `"computer.writeFile"` \| `"computer.execute"` \| `"approvals"` \| `"agent.list"` \| `"agent.manage"` \| `"agent.rename"` \| `"agent.avatar"` \| `"agent.toolPolicy"` \| `"agent.read"` \| `"agent.send"` \| `"llm.generation"` \| `"media.generation"` \| `"games:servers"` \| `"git.repository"` \| `"apps.open"` \| `"apps.call"` \| `"app.storage"` \| `"app.events.emit"` \| `"team.workbenchHost"` \| `"marketplace.browse"` \| `"marketplace.purchase"` \| `"workflow.run"` \| `"workflow.publish"` \| `"workflow.arm"` \| `"world"` \| `"channels.read"` \| `"channels.personal.manage"`; `operation?`: `string`; `resource`: \| `"any"` \| `Readonly`\<\{ `kind`: `string`; `id`: `string`; \}\>; \} \| \{ `kind`: `"capability-route"`; `capability`: \| `"computer.list"` \| `"computer.readFile"` \| `"computer.writeFile"` \| `"computer.execute"` \| `"approvals"` \| `"agent.list"` \| `"agent.manage"` \| `"agent.rename"` \| `"agent.avatar"` \| `"agent.toolPolicy"` \| `"agent.read"` \| `"agent.send"` \| `"llm.generation"` \| `"media.generation"` \| `"games:servers"` \| `"git.repository"` \| `"apps.open"` \| `"apps.call"` \| `"app.storage"` \| `"app.events.emit"` \| `"team.workbenchHost"` \| `"marketplace.browse"` \| `"marketplace.purchase"` \| `"workflow.run"` \| `"workflow.publish"` \| `"workflow.arm"` \| `"world"` \| `"channels.read"` \| `"channels.personal.manage"`; `operation`: `string`; `effect`: `"read"` \| `"write"`; \} \| \{ `kind`: `"binding"`; `binding`: \| `"BACKGROUND"` \| `"GIT"` \| `"EVENTS"` \| `"APPS"` \| `"WORKFLOWS"` \| `"APP_STORAGE"` \| `"MEDIA"` \| `"COMPUTER"` \| `"INBOX"` \| `"AGENTS"`; `operation`: `string`; \} \| \{ `kind`: `"list-tools"`; \} \| \{ `kind`: `"backend"`; \} |
1683
1687
 
1684
1688
  #### Returns
1685
1689
 
@@ -91,7 +91,7 @@ Data.TaggedError("CloudBrowserUrlDenied").message;
91
91
 
92
92
  ### CloudBrowserUpstreamError
93
93
 
94
- The provider (Cloudflare Browser Run) answered with a failure.
94
+ The configured provider answered with a failure or is unreachable.
95
95
 
96
96
  #### Extends
97
97
 
@@ -593,10 +593,11 @@ type BrowserLimits = Readonly<{
593
593
 
594
594
  `@fungi.computer/oyster/browser`: the one agent browser for the Hub and a
595
595
  standalone Oyster. A host builds the port from a provider (`browserRunSessions`
596
- or `bindingSessions`, plus Browser Run Quick Reads when it has them), its
597
- required metering owner and an event sink; `createBrowser` owns lease ownership,
598
- egress policy and billing order; `browserCapabilitySource` is the agent's
599
- `cloudBrowser` tool. Raw CDP stays internal to this directory.
596
+ or `bindingSessions` or `onboxBrowserSessions` on staging, plus Browser Run
597
+ Quick Reads when it has them), its required metering owner and an event sink;
598
+ `createBrowser` owns lease ownership, egress policy and billing order;
599
+ `browserCapabilitySource` is the agent's `cloudBrowser` tool. Raw CDP stays
600
+ internal to this directory.
600
601
 
601
602
  ---
602
603
 
@@ -643,10 +644,11 @@ type Browser = Readonly<{
643
644
 
644
645
  `@fungi.computer/oyster/browser`: the one agent browser for the Hub and a
645
646
  standalone Oyster. A host builds the port from a provider (`browserRunSessions`
646
- or `bindingSessions`, plus Browser Run Quick Reads when it has them), its
647
- required metering owner and an event sink; `createBrowser` owns lease ownership,
648
- egress policy and billing order; `browserCapabilitySource` is the agent's
649
- `cloudBrowser` tool. Raw CDP stays internal to this directory.
647
+ or `bindingSessions` or `onboxBrowserSessions` on staging, plus Browser Run
648
+ Quick Reads when it has them), its required metering owner and an event sink;
649
+ `createBrowser` owns lease ownership, egress policy and billing order;
650
+ `browserCapabilitySource` is the agent's `cloudBrowser` tool. Raw CDP stays
651
+ internal to this directory.
650
652
 
651
653
  ---
652
654
 
@@ -1058,9 +1060,9 @@ caller (`browser.ts`) owns the deadline through `signal`.
1058
1060
  type BrowserSessions = Readonly<{}>;
1059
1061
  ```
1060
1062
 
1061
- The provider half of the host port. Built only by the two functions below. Each
1062
- step is separate so `createBrowser` owns a provider session id from the moment
1063
- it exists, including when its CDP connect then fails.
1063
+ The provider half of the host port. Each step is separate so `createBrowser`
1064
+ owns a provider session id from the moment it exists, including when its CDP
1065
+ connect then fails.
1064
1066
 
1065
1067
  ---
1066
1068
 
@@ -1338,10 +1340,11 @@ function createBrowser(host, limits): Browser;
1338
1340
 
1339
1341
  `@fungi.computer/oyster/browser`: the one agent browser for the Hub and a
1340
1342
  standalone Oyster. A host builds the port from a provider (`browserRunSessions`
1341
- or `bindingSessions`, plus Browser Run Quick Reads when it has them), its
1342
- required metering owner and an event sink; `createBrowser` owns lease ownership,
1343
- egress policy and billing order; `browserCapabilitySource` is the agent's
1344
- `cloudBrowser` tool. Raw CDP stays internal to this directory.
1343
+ or `bindingSessions` or `onboxBrowserSessions` on staging, plus Browser Run
1344
+ Quick Reads when it has them), its required metering owner and an event sink;
1345
+ `createBrowser` owns lease ownership, egress policy and billing order;
1346
+ `browserCapabilitySource` is the agent's `cloudBrowser` tool. Raw CDP stays
1347
+ internal to this directory.
1345
1348
 
1346
1349
  #### Parameters
1347
1350
 
@@ -1438,3 +1441,24 @@ A standalone Oyster's own Workers BROWSER binding.
1438
1441
  #### Returns
1439
1442
 
1440
1443
  [`BrowserSessions`](#browsersessions)
1444
+
1445
+ ---
1446
+
1447
+ ### onboxBrowserSessions()
1448
+
1449
+ ```ts
1450
+ function onboxBrowserSessions(origin, fetcher?): BrowserSessions;
1451
+ ```
1452
+
1453
+ Staging uses Chromium's native CDP browser contexts; no session store.
1454
+
1455
+ #### Parameters
1456
+
1457
+ | Parameter | Type |
1458
+ | --------- | ------------------------- |
1459
+ | `origin` | `string` |
1460
+ | `fetcher` | [`HttpFetch`](#httpfetch) |
1461
+
1462
+ #### Returns
1463
+
1464
+ [`BrowserSessions`](#browsersessions)
package/docs/api/keys.md CHANGED
@@ -14,7 +14,7 @@
14
14
 
15
15
  ```ts
16
16
  type OysterKeyCapability =
17
- "apps.read" | "apps.use" | "computer.use" | "machine.enroll";
17
+ "apps.read" | "apps.use" | "agents.use" | "computer.use" | "machine.enroll";
18
18
  ```
19
19
 
20
20
  Every capability an Oyster key may carry.
@@ -88,8 +88,9 @@ const oysterKeyVocabulary: GrantVocabulary<OysterKeyCapability>;
88
88
  `apps.use` also reaches `apps.read` (one level, per `defineGrants`'s own
89
89
  `implies`): a key that may change an App's data may also read it. `computer.use`
90
90
  has no narrower capability under it; the Computer is on or off, host-wide.
91
- `machine.enroll` admits enrollment only; it implies no App or Computer access.
92
- The enrollment owner, not this vocabulary, owns one-use consumption.
91
+ `agents.use` selects Agent ids independently of App grants. `machine.enroll`
92
+ admits enrollment only; it implies no App or Computer access. The enrollment
93
+ owner, not this vocabulary, owns one-use consumption.
93
94
 
94
95
  ---
95
96
 
@@ -159,6 +160,27 @@ name for MCP) to reach the full `AppToolAccess`.
159
160
 
160
161
  ---
161
162
 
163
+ ### keyAgentAccess()
164
+
165
+ ```ts
166
+ function keyAgentAccess(key, agentId): boolean;
167
+ ```
168
+
169
+ Agent access is scoped to the granted Agent ids, never inferred from App grants.
170
+
171
+ #### Parameters
172
+
173
+ | Parameter | Type |
174
+ | --------- | --------------------------------- |
175
+ | `key` | [`LiveOysterKey`](#liveoysterkey) |
176
+ | `agentId` | `string` |
177
+
178
+ #### Returns
179
+
180
+ `boolean`
181
+
182
+ ---
183
+
162
184
  ### keyAllowsComputerUse()
163
185
 
164
186
  ```ts
@@ -253,6 +253,7 @@ approvalGet(
253
253
  | "outcome-unknown"
254
254
  | "identity-conflict"
255
255
  | null;
256
+ denialNote?: string;
256
257
  requestedAt: number;
257
258
  expiresAt: number;
258
259
  updatedAt: number;
@@ -289,9 +290,9 @@ Read one member-scoped view; the native home resolves membership again.
289
290
  `"not-found"` \| `"not-eligible"` \| `"already-answered"` \| `"stale-revision"`
290
291
  \| `"declined"` \| `"fenced"` \| `"not-granted"` \| `"read-only"` \|
291
292
  `"in-flight"` \| `"outcome-unknown"` \| `"identity-conflict"` \| `null`;
292
- `requestedAt`: `number`; `expiresAt`: `number`; `updatedAt`: `number`;
293
- `revision`: `number`; \}; `canAnswer`: `boolean`; `canSetAutoApprove`:
294
- `boolean`; \} \| `null`\>
293
+ `denialNote?`: `string`; `requestedAt`: `number`; `expiresAt`: `number`;
294
+ `updatedAt`: `number`; `revision`: `number`; \}; `canAnswer`: `boolean`;
295
+ `canSetAutoApprove`: `boolean`; \} \| `null`\>
295
296
 
296
297
  ##### approvalAnswer()
297
298
 
@@ -353,6 +354,7 @@ approvalAnswer(
353
354
  | "outcome-unknown"
354
355
  | "identity-conflict"
355
356
  | null;
357
+ denialNote?: string;
356
358
  requestedAt: number;
357
359
  expiresAt: number;
358
360
  updatedAt: number;
@@ -377,6 +379,7 @@ approvalAnswer(
377
379
  | "in-flight"
378
380
  | "outcome-unknown"
379
381
  | "identity-conflict";
382
+ denialNote?: string;
380
383
  }>>;
381
384
  ```
382
385
 
@@ -388,10 +391,11 @@ Persist an eligible answer and attempt its native decision work once.
388
391
  | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
389
392
  | `pin` | `Readonly`\<\{ `tenantId`: `string`; `origin`: `string`; \}\> |
390
393
  | `actor` | \| \{ `kind`: `"member"`; `memberId`: `string`; \} \| \{ `kind`: `"agent"`; `agentId`: `string`; \} \| \{ `kind`: `"team-key"`; `keyId`: `string`; \} |
391
- | `input` | \{ `recordId`: `string`; `decision`: `"approve"` \| `"deny"`; `expectedRevision`: `number`; \} |
394
+ | `input` | \{ `recordId`: `string`; `decision`: `"approve"` \| `"deny"`; `expectedRevision`: `number`; `denialNote?`: `string`; \} |
392
395
  | `input.recordId` | `string` |
393
396
  | `input.decision` | `"approve"` \| `"deny"` |
394
397
  | `input.expectedRevision` | `number` |
398
+ | `input.denialNote?` | `string` |
395
399
 
396
400
  ###### Returns
397
401
 
@@ -408,13 +412,14 @@ Persist an eligible answer and attempt its native decision work once.
408
412
  `reason`: \| `"pending"` \| `"expired"` \| `"not-found"` \| `"not-eligible"` \|
409
413
  `"already-answered"` \| `"stale-revision"` \| `"declined"` \| `"fenced"` \|
410
414
  `"not-granted"` \| `"read-only"` \| `"in-flight"` \| `"outcome-unknown"` \|
411
- `"identity-conflict"` \| `null`; `requestedAt`: `number`; `expiresAt`: `number`;
412
- `updatedAt`: `number`; `revision`: `number`; \}; `canAnswer`: `boolean`;
413
- `canSetAutoApprove`: `boolean`; \}; \}\> \| `Readonly`\<\{ `kind`: `"refused"`;
414
- `reason`: \| `"pending"` \| `"expired"` \| `"not-found"` \| `"not-eligible"` \|
415
- `"already-answered"` \| `"stale-revision"` \| `"declined"` \| `"fenced"` \|
416
- `"not-granted"` \| `"read-only"` \| `"in-flight"` \| `"outcome-unknown"` \|
417
- `"identity-conflict"`; \}\>\>
415
+ `"identity-conflict"` \| `null`; `denialNote?`: `string`; `requestedAt`:
416
+ `number`; `expiresAt`: `number`; `updatedAt`: `number`; `revision`: `number`;
417
+ \}; `canAnswer`: `boolean`; `canSetAutoApprove`: `boolean`; \}; \}\> \|
418
+ `Readonly`\<\{ `kind`: `"refused"`; `reason`: \| `"pending"` \| `"expired"` \|
419
+ `"not-found"` \| `"not-eligible"` \| `"already-answered"` \| `"stale-revision"`
420
+ \| `"declined"` \| `"fenced"` \| `"not-granted"` \| `"read-only"` \|
421
+ `"in-flight"` \| `"outcome-unknown"` \| `"identity-conflict"`; `denialNote?`:
422
+ `string`; \}\>\>
418
423
 
419
424
  ##### approvalSetAutoApprove()
420
425
 
@@ -439,6 +444,7 @@ approvalSetAutoApprove(
439
444
  | "in-flight"
440
445
  | "outcome-unknown"
441
446
  | "identity-conflict";
447
+ denialNote?: string;
442
448
  }>
443
449
  | Readonly<{
444
450
  kind: "updated";
@@ -478,9 +484,10 @@ it.
478
484
  `"expired"` \| `"not-found"` \| `"not-eligible"` \| `"already-answered"` \|
479
485
  `"stale-revision"` \| `"declined"` \| `"fenced"` \| `"not-granted"` \|
480
486
  `"read-only"` \| `"in-flight"` \| `"outcome-unknown"` \| `"identity-conflict"`;
481
- \}\> \| `Readonly`\<\{ `kind`: `"updated"`; `rule`: \{ `scope`: \{ `agentId`:
482
- `string` \| `null`; \}; `family`: \{ `module`: `string` \| `null`; `access`:
483
- `"write"` \| `"external"`; \}; `enabled`: `boolean`; \}; \}\>\>
487
+ `denialNote?`: `string`; \}\> \| `Readonly`\<\{ `kind`: `"updated"`; `rule`: \{
488
+ `scope`: \{ `agentId`: `string` \| `null`; \}; `family`: \{ `module`: `string`
489
+ \| `null`; `access`: `"write"` \| `"external"`; \}; `enabled`: `boolean`; \};
490
+ \}\>\>
484
491
 
485
492
  ##### installationOperation()
486
493
 
@@ -10,6 +10,29 @@ Run every command exactly as written, from the directory each step names.
10
10
  Compare its output with the expected output before you go on. When a step fails,
11
11
  read the "If it fails" notes under it before trying anything else.
12
12
 
13
+ ## Before you start
14
+
15
+ - **Cloudflare plan:** SQLite-backed Durable Objects are available on Workers
16
+ Free; they do not by themselves require Paid. Cloudflare's SQLite-backed
17
+ Durable Objects limits list 30 seconds of CPU per request by default,
18
+ configurable to 5 minutes. See Cloudflare's
19
+ [Durable Objects limits](https://developers.cloudflare.com/durable-objects/platform/limits/)
20
+ and
21
+ [Durable Objects pricing](https://developers.cloudflare.com/durable-objects/platform/pricing/).
22
+ - **Agent chat and AI Gateway:** By default, Agent chat uses the Workers AI
23
+ binding; it needs no separate provider key or Gateway setup. Prints uses your
24
+ account's default AI Gateway (created automatically); set `AI_GATEWAY_ID` to
25
+ use another. The routed models include `openai/gpt-image-2` and video IDs
26
+ `bytedance/seedance-2.0`, `bytedance/seedance-2.0-fast`,
27
+ `bytedance/seedance-2.0-mini`, `google/veo-3.1`, `google/veo-3.1-fast` and
28
+ `minimax/h3-max`.
29
+
30
+ - **Deployment plan:** Step 7 needs Workers Paid because this deployment uses
31
+ Containers for the Computer image; Cloudflare lists Containers as available on
32
+ the
33
+ [Workers Paid plan](https://developers.cloudflare.com/containers/platform/).
34
+ SQLite-backed Durable Objects alone do not require Paid.
35
+
13
36
  ## What Oyster is
14
37
 
15
38
  - Oyster hosts Apps on one Cloudflare Worker, or on celld, a Workers-compatible