@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
package/dist/agents.d.ts CHANGED
@@ -7,6 +7,7 @@
7
7
  * transport (`/browser/…`), served by the adapter behind the same App grants.
8
8
  */
9
9
  import { Hono } from "hono";
10
+ import { type LiveOysterKey } from "./keys.js";
10
11
  import type { AppToolPolicy } from "./apps/tools.js";
11
12
  import * as Schema from "effect/Schema";
12
13
  import { type OysterCapability, type OysterEnv, type OysterPrincipal } from "./host.js";
@@ -125,7 +126,7 @@ export type OysterAgents = Readonly<{
125
126
  visit(agentId: string, sessionId: string): Promise<boolean>;
126
127
  }>;
127
128
  /** Agent adapters admitted against each App's declared capabilities. */
128
- export declare function agentCapabilities(agents: OysterAgents): readonly OysterCapability[];
129
+ export declare function agentCapabilities(agents: OysterAgents, liveKey: (keyId: string) => Promise<LiveOysterKey | null>): readonly OysterCapability[];
129
130
  /** Host-owned direct approval under the admitted owner; never an App capability. */
130
131
  export declare function agentApprovalRoutes(agents: OysterAgents): Hono<OysterEnv>;
131
132
  /**
package/dist/agents.js CHANGED
@@ -7,6 +7,7 @@
7
7
  * transport (`/browser/…`), served by the adapter behind the same App grants.
8
8
  */
9
9
  import { Hono } from "hono";
10
+ import { keyAgentAccess } from "./keys.js";
10
11
  import * as Schema from "effect/Schema";
11
12
  import { z } from "zod";
12
13
  import { oysterPrincipalIsOwner, } from "./host.js";
@@ -51,8 +52,8 @@ function refusal(status, error) {
51
52
  return Response.json({ error }, { status });
52
53
  }
53
54
  /** Agent adapters admitted against each App's declared capabilities. */
54
- export function agentCapabilities(agents) {
55
- const routes = createAgentRoutes(agents);
55
+ export function agentCapabilities(agents, liveKey) {
56
+ const routes = createAgentRoutes(agents, liveKey);
56
57
  const shared = { base: "/", routes };
57
58
  return [
58
59
  {
@@ -134,9 +135,41 @@ export function agentCapabilities(agents) {
134
135
  },
135
136
  ];
136
137
  }
137
- function createAgentRoutes(agents) {
138
+ function createAgentRoutes(agents, liveKey) {
138
139
  const routes = new Hono();
139
- routes.get("/agents", async () => Response.json(await agents.list()));
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
+ });
140
173
  routes.post("/agents", async (context) => {
141
174
  const { principal } = context.env;
142
175
  if (!oysterPrincipalIsOwner(principal))
@@ -216,19 +249,37 @@ function createAgentRoutes(agents) {
216
249
  const response = await agents.fetch(agentId, new Request(target, incoming));
217
250
  return response ?? refusal(404, "No such agent");
218
251
  });
219
- routes.get("/member/inbox", async () => {
252
+ routes.get("/member/inbox", async (context) => {
220
253
  const inbox = await agents.inbox();
254
+ const principal = context.env.principal;
255
+ if (oysterPrincipalIsOwner(principal))
256
+ return Response.json({
257
+ items: inbox.items,
258
+ next: null,
259
+ cursor: inbox.revision,
260
+ });
261
+ if (principal.actor.kind !== "team-key")
262
+ return refusal(403, "Agent access denied");
263
+ const key = await liveKey(principal.actor.keyId);
221
264
  return Response.json({
222
- items: inbox.items,
265
+ items: key === null
266
+ ? []
267
+ : inbox.items.filter((item) => "agentId" in item && keyAgentAccess(key, item.agentId)),
223
268
  next: null,
224
- cursor: inbox.revision,
269
+ // The global revision changes for ungranted Agents too. Key feeds have
270
+ // no scoped event stream, so expose neither that revision nor its events.
271
+ cursor: 0,
225
272
  });
226
273
  });
227
- routes.get("/member/inbox/notices", () => Response.json({ items: [], next: null }));
274
+ routes.get("/member/inbox/notices", (context) => oysterPrincipalIsOwner(context.env.principal)
275
+ ? Response.json({ items: [], next: null })
276
+ : refusal(403, "Agent access denied"));
228
277
  routes.post("/member/inbox/visits", async (context) => {
229
278
  const body = Schema.decodeUnknownOption(visitSchema)(await context.req.json());
230
279
  if (body._tag === "None")
231
280
  return refusal(400, "Invalid visit");
281
+ if (!(await access(context.env.principal, body.value.agentId)))
282
+ return refusal(403, "Agent access denied");
232
283
  return (await agents.visit(body.value.agentId, body.value.sessionId))
233
284
  ? new Response(null, { status: 204 })
234
285
  : refusal(404, "No such Session");
@@ -236,6 +287,10 @@ function createAgentRoutes(agents) {
236
287
  // The feed is small (one person's agents), so a change is announced as a
237
288
  // `reset` and the client re-reads the whole inbox.
238
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");
239
294
  const after = Number(context.req.query("after") ?? "0");
240
295
  const signal = context.req.raw.signal;
241
296
  const encoder = new TextEncoder();
@@ -36,6 +36,10 @@ export declare const gateRequestSchema: z.ZodLazy<z.ZodDiscriminatedUnion<[z.Zod
36
36
  kind: z.ZodLiteral<"capability-route">;
37
37
  capability: z.ZodPipe<z.ZodUnknown, z.ZodTransform<"approvals" | "agent.list" | "agent.manage" | "agent.rename" | "agent.avatar" | "agent.toolPolicy" | "agent.read" | "agent.send" | "computer.list" | "computer.readFile" | "computer.writeFile" | "computer.execute" | "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", unknown>>;
38
38
  operation: z.ZodString;
39
+ effect: z.ZodEnum<{
40
+ read: "read";
41
+ write: "write";
42
+ }>;
39
43
  }, z.core.$strict>, z.ZodObject<{
40
44
  kind: z.ZodLiteral<"binding">;
41
45
  binding: z.ZodPipe<z.ZodUnknown, z.ZodTransform<"BACKGROUND" | "GIT" | "EVENTS" | "APPS" | "WORKFLOWS" | "APP_STORAGE" | "MEDIA" | "COMPUTER" | "INBOX" | "AGENTS", unknown>>;
package/dist/apps/gate.js CHANGED
@@ -26,6 +26,7 @@ export const gateRequestSchema = z.lazy(() => z.discriminatedUnion("kind", [
26
26
  kind: z.literal("capability-route"),
27
27
  capability: ownerCodec(appCapabilityIdSchema),
28
28
  operation: z.string().min(1),
29
+ effect: z.enum(["read", "write"]),
29
30
  }),
30
31
  z.strictObject({
31
32
  kind: z.literal("binding"),
@@ -259,6 +260,8 @@ export function admits(facts, rawRequest) {
259
260
  case "capability-route": {
260
261
  if (visitor)
261
262
  return refusal("visitor_read_only");
263
+ if (request.effect === "write" && facts.access?.level === "read")
264
+ return refusal("level_too_low");
262
265
  if (!request.operation.startsWith("route:") ||
263
266
  !facts.declaration.capabilities.includes(request.capability) ||
264
267
  facts.declaredOperations.capabilities