@fungi.computer/oyster 0.5.0 → 0.6.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 (57) hide show
  1. package/README.md +46 -8
  2. package/dist/agents.js +4 -0
  3. package/dist/auth.d.ts +24 -1
  4. package/dist/auth.js +38 -0
  5. package/dist/boot-host-script.js +1 -1
  6. package/dist/build/candidate-cli.mjs +8 -1
  7. package/dist/channels/binding.d.ts +8 -1
  8. package/dist/channels/binding.js +12 -0
  9. package/dist/channels/conversations.d.ts +16 -5
  10. package/dist/channels/visitor-channel.d.ts +53 -0
  11. package/dist/channels/visitor-channel.js +124 -0
  12. package/dist/channels/visitor-edge.d.ts +115 -0
  13. package/dist/channels/visitor-edge.js +582 -0
  14. package/dist/channels/visitor-request.d.ts +7 -0
  15. package/dist/channels/visitor-request.js +17 -0
  16. package/dist/channels/visitor-stream-slots.d.ts +2 -0
  17. package/dist/channels/visitor-stream-slots.js +33 -0
  18. package/dist/channels/visitors.d.ts +47 -0
  19. package/dist/channels/visitors.js +181 -0
  20. package/dist/cloudflare.d.ts +8 -2
  21. package/dist/cloudflare.js +27 -1
  22. package/dist/computer-target.d.ts +5 -0
  23. package/dist/computer-target.js +11 -0
  24. package/dist/host.d.ts +6 -3
  25. package/dist/host.js +75 -13
  26. package/dist/internal/host-gate.d.ts +6 -4
  27. package/dist/internal/host-gate.js +14 -7
  28. package/dist/invocation.d.ts +11 -4
  29. package/dist/invocation.js +134 -91
  30. package/dist/page/capabilities.js +1 -1
  31. package/dist/page/child-selection.d.ts +27 -0
  32. package/dist/page/child-selection.js +38 -0
  33. package/dist/page/mount.js +1 -1
  34. package/dist/page-script.js +4 -4
  35. package/dist/release.d.ts +13 -0
  36. package/dist/release.js +10 -0
  37. package/dist/request-origin.d.ts +4 -0
  38. package/dist/request-origin.js +10 -0
  39. package/dist/standalone/agents.js +11 -11
  40. package/dist/standalone/channel-session-door.d.ts +2 -2
  41. package/dist/standalone/channel-session-door.js +35 -5
  42. package/dist/standalone/computer-fleet.js +7 -1
  43. package/dist/standalone/tenancy.d.ts +3 -0
  44. package/dist/standalone/tenancy.js +7 -2
  45. package/dist/turnstile.d.ts +27 -0
  46. package/dist/turnstile.js +51 -0
  47. package/dist/workers-git.js +20 -1
  48. package/dist/workers.js +15 -12
  49. package/docs/api/auth.md +55 -0
  50. package/docs/api/build-contract.md +2 -2
  51. package/docs/api/cloudflare.md +26 -1
  52. package/docs/api/host.md +22 -13
  53. package/docs/api/invocation.md +7 -3
  54. package/docs/api/workers.md +17 -16
  55. package/docs/site/agent-quickstart.mdx +6 -2
  56. package/examples/standalone/wrangler.celld.jsonc +66 -0
  57. package/package.json +40 -16
package/README.md CHANGED
@@ -62,6 +62,10 @@ Workers AI, Workflows and media.
62
62
 
63
63
  ## Quick start: deploy your own Oyster
64
64
 
65
+ Deciding what to run?
66
+ [Is Oyster for you?](https://docs.fungi.computer/is-oyster-for-you/) compares
67
+ agent Computers and App builds with Cloudflare Workers Builds.
68
+
65
69
  You need:
66
70
 
67
71
  - a Cloudflare account on Workers Paid, with Containers, Artifacts, Workers AI,
@@ -69,11 +73,12 @@ You need:
69
73
  - Docker, which Wrangler uses to build the Computer's container image;
70
74
  - Node 24, pnpm 11 or newer and Git.
71
75
 
72
- The first install takes about 15 to 20 minutes, most of it the seven App builds
73
- on the Computer, one after another. Start a project, add Oyster, its Computer
74
- and Wrangler, and copy the example Worker out of the package. The local pnpm
75
- policy allows esbuild and workerd to install their native binaries and disables
76
- unneeded optional dependency scripts, so a fresh install exits successfully:
76
+ The first install takes about 15 to 20 minutes, mostly building the seven Apps
77
+ on the Computer. By default, Oyster builds two Apps at a time. Start a project,
78
+ add Oyster, its Computer and Wrangler, and copy the example Worker out of the
79
+ package. The local pnpm policy allows esbuild and workerd to install their
80
+ native binaries and disables unneeded optional dependency scripts, so a fresh
81
+ install exits successfully:
77
82
 
78
83
  ```sh
79
84
  mkdir my-oyster && cd my-oyster
@@ -462,10 +467,43 @@ the Computer. `examples/standalone/wrangler.jsonc` declares every binding it
462
467
  uses. Set the owner key as a secret (`OYSTER_OWNER_KEY`, 24+ characters) and
463
468
  deploy with `wrangler deploy`. Sign in at `/login`.
464
469
 
470
+ Hosts may supply the optional `OYSTER_KEY_SESSIONS` binding, implementing
471
+ `OysterKeySessionResolver` from `@fungi.computer/oyster/auth`. Its
472
+ `resolve(value)` returns `{ keyId }` for a live host-owned session, or `null`.
473
+ The host mints and expires opaque sessions and sets the `keySessionCookieName`
474
+ cookie; it is not a named key's bearer secret. Hosts should use `Path=/`,
475
+ `HttpOnly`, `Secure` and `SameSite=Lax` (or stricter) as defense in depth.
476
+ Oyster independently checks request provenance. Cookie-only GET/HEAD navigation
477
+ is allowed. Other methods require `Origin` to be present and equal the request
478
+ URL's origin, and refuse `Sec-Fetch-Site: cross-site`. Missing `Origin` is
479
+ refused, using the same strict predicate as the channel's POST routes. Oyster
480
+ stores no browser sessions. `keySessionAuth` wraps ordinary `ownerOrKeyAuth` at
481
+ the standalone tenancy composition root. Each cookie admission reopens the live
482
+ key and checks its canonical owner; every authenticated door still uses that
483
+ key's current grants. App pages refuse keys without App access, even when the
484
+ App is public to visitors. Explicit header authentication is unchanged, and a
485
+ failed Authorization header cannot fall back to a named-key cookie. The owner
486
+ cookie and owner-only management API are unchanged; without the binding
487
+ named-key cookies admit nobody.
488
+
489
+ The celld example is a limited trial, not the full Cloudflare setup above. It
490
+ requires access to Fungi's private celld fork and Botanical source repository.
491
+ Use `examples/standalone/wrangler.celld.jsonc` beside `worker.ts`, not the
492
+ Cloudflare-only config. The published `0.5.0` tarball does not yet contain this
493
+ celld example; copy it from your authorized Botanical source checkout until a
494
+ package including it is released.
495
+
465
496
  The layers it is built from stay exported for hosts that compose their own:
466
- `@fungi.computer/oyster/workers` (Releases, Instances, MCP and App storage) and
467
- `@fungi.computer/oyster/workers-git` (the git door), with `oysterAppMethods`
468
- supplying the installed App catalog to an Agent runtime.
497
+ `@fungi.computer/oyster/turnstile` owns Cloudflare challenge configuration and
498
+ token verification. Hosts supply their origin, verification timeout and safe
499
+ error reporter; they retain their own challenged routes and HTTP responses.
500
+ Configuration keeps the explicit `TURNSTILE=off` opt-out and fails closed on
501
+ HTTPS hosts without both keys. Hub retains its five-second verification policy.
502
+
503
+ Other host building blocks: `@fungi.computer/oyster/workers` (Releases,
504
+ Instances, MCP and App storage) and `@fungi.computer/oyster/workers-git` (the
505
+ git door), with `oysterAppMethods` supplying the installed App catalog to an
506
+ Agent runtime.
469
507
 
470
508
  Pack and install an App with `@fungi.computer/oyster/pack`: `packRelease` turns
471
509
  an exact Git commit and a canonical build recipe into a Release with immutable
package/dist/agents.js CHANGED
@@ -12,6 +12,7 @@ import * as Schema from "effect/Schema";
12
12
  import { z } from "zod";
13
13
  import { oysterPrincipalIsOwner, } from "./host.js";
14
14
  import { agentAvatarSchema } from "./agent-avatars.js";
15
+ import { computerTargetParam } from "./computer-target.js";
15
16
  import { agentAvatar, agentList, agentManage, agentRename, agentSessionRead, agentSessionWrite, agentToolPolicy, } from "@fungi.computer/agar/agents";
16
17
  /** At most this many MCP servers per Agent. */
17
18
  export const maxAgentMcpServers = 16;
@@ -246,6 +247,9 @@ function createAgentRoutes(agents, liveKey) {
246
247
  if (length > maxAgentRequestBytes)
247
248
  return refusal(413, "Request too large");
248
249
  const target = new URL(`https://agent.internal${path}${url.search}`);
250
+ // A desk window's Computer routing hint is the host's to admit; the
251
+ // Agent's Session routes decode a closed query that refuses it.
252
+ target.searchParams.delete(computerTargetParam);
249
253
  const response = await agents.fetch(agentId, new Request(target, incoming));
250
254
  return response ?? refusal(404, "No such agent");
251
255
  });
package/dist/auth.d.ts CHANGED
@@ -1,7 +1,24 @@
1
- import type { OysterAuth } from "./host.js";
1
+ import type { OysterAuth, OysterHost } from "./host.js";
2
2
  import type { OysterKeyStore } from "./keys.js";
3
3
  /** Cookie emitted by ownerKeyAuth, for composing credential-specific adapters. */
4
4
  export declare const ownerKeyCookieName = "oyster_session";
5
+ /** Host-minted opaque sessions for named keys, separate from the owner cookie. */
6
+ export declare const keySessionCookieName = "oyster_key_session";
7
+ /**
8
+ * The host owns session minting, hashing/storage and expiry.
9
+ * A session resolves only to a key id, never to owner or grant authority.
10
+ * Hosts should use Path=/, HttpOnly, Secure and SameSite=Lax (or stricter)
11
+ * as defense in depth; Oyster independently checks request provenance.
12
+ * Cookie-only admission allows GET/HEAD navigation. Every other method requires
13
+ * Origin to be present and equal the request URL's origin, and Sec-Fetch-Site
14
+ * not to be cross-site. Missing Origin is refused.
15
+ */
16
+ export type OysterKeySessionResolver = Readonly<{
17
+ /** Resolve an opaque host session to its named key, or null when unavailable. */
18
+ resolve: (value: string) => Promise<Readonly<{
19
+ keyId: string;
20
+ }> | null>;
21
+ }>;
5
22
  /**
6
23
  * Single-owner sign-in: a bearer token or a session cookie holding the
7
24
  * owner key's digest. The key itself is never stored in the browser.
@@ -9,3 +26,9 @@ export declare const ownerKeyCookieName = "oyster_session";
9
26
  export declare function ownerKeyAuth(ownerKey: string, ownerId?: string): OysterAuth;
10
27
  /** The owner key, or a named key admitted as its canonical Team-key actor. */
11
28
  export declare function ownerOrKeyAuth(ownerKey: string, ownerId: string, keys: () => Pick<OysterKeyStore, "verify">): OysterAuth;
29
+ /**
30
+ * Add host-owned browser sessions to ownerOrKeyAuth. Explicit credentials and
31
+ * non-401 refusals keep their ordinary result; ambient cookies cannot undo them.
32
+ * Every cookie admission rechecks the key's liveness and canonical owner.
33
+ */
34
+ export declare function keySessionAuth(auth: OysterAuth, ownerId: string, keys: () => NonNullable<OysterHost["keys"]>, sessions: OysterKeySessionResolver): OysterAuth;
package/dist/auth.js CHANGED
@@ -1,9 +1,13 @@
1
1
  import * as Option from "effect/Option";
2
2
  import * as Schema from "effect/Schema";
3
+ import { parse } from "hono/utils/cookie";
3
4
  import { presentedGitKey } from "./git-auth.js";
4
5
  import { sha256 } from "./release.js";
6
+ import { sameOriginRequest } from "./request-origin.js";
5
7
  /** Cookie emitted by ownerKeyAuth, for composing credential-specific adapters. */
6
8
  export const ownerKeyCookieName = "oyster_session";
9
+ /** Host-minted opaque sessions for named keys, separate from the owner cookie. */
10
+ export const keySessionCookieName = "oyster_key_session";
7
11
  const loginFormSchema = Schema.Struct({ key: Schema.String });
8
12
  /** Compare two secrets through their fixed-length digests, in constant time. */
9
13
  async function constantTimeEqual(a, b) {
@@ -91,3 +95,37 @@ export function ownerOrKeyAuth(ownerKey, ownerId, keys) {
91
95
  Promise.resolve(new Response("Sign-in is not available", { status: 404 })),
92
96
  };
93
97
  }
98
+ /**
99
+ * Add host-owned browser sessions to ownerOrKeyAuth. Explicit credentials and
100
+ * non-401 refusals keep their ordinary result; ambient cookies cannot undo them.
101
+ * Every cookie admission rechecks the key's liveness and canonical owner.
102
+ */
103
+ export function keySessionAuth(auth, ownerId, keys, sessions) {
104
+ return {
105
+ ...auth,
106
+ async authenticate(request) {
107
+ const result = await auth.authenticate(request);
108
+ if (result.kind === "admitted" ||
109
+ result.status !== 401 ||
110
+ request.headers.has("Authorization"))
111
+ return result;
112
+ if (request.method !== "GET" &&
113
+ request.method !== "HEAD" &&
114
+ !sameOriginRequest(request))
115
+ return result;
116
+ const value = parse(request.headers.get("Cookie") ?? "", keySessionCookieName)[keySessionCookieName];
117
+ if (value === undefined || value.length === 0)
118
+ return result;
119
+ const session = await sessions.resolve(value);
120
+ if (session === null)
121
+ return result;
122
+ const key = await keys().live(session.keyId);
123
+ return key !== null && key.owner === ownerId
124
+ ? {
125
+ kind: "admitted",
126
+ principal: { actor: { kind: "team-key", keyId: key.keyId } },
127
+ }
128
+ : result;
129
+ },
130
+ };
131
+ }