@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.
- package/README.md +46 -8
- package/dist/agents.js +4 -0
- package/dist/auth.d.ts +24 -1
- package/dist/auth.js +38 -0
- package/dist/boot-host-script.js +1 -1
- package/dist/build/candidate-cli.mjs +8 -1
- package/dist/channels/binding.d.ts +8 -1
- package/dist/channels/binding.js +12 -0
- package/dist/channels/conversations.d.ts +16 -5
- package/dist/channels/visitor-channel.d.ts +53 -0
- package/dist/channels/visitor-channel.js +124 -0
- package/dist/channels/visitor-edge.d.ts +115 -0
- package/dist/channels/visitor-edge.js +582 -0
- package/dist/channels/visitor-request.d.ts +7 -0
- package/dist/channels/visitor-request.js +17 -0
- package/dist/channels/visitor-stream-slots.d.ts +2 -0
- package/dist/channels/visitor-stream-slots.js +33 -0
- package/dist/channels/visitors.d.ts +47 -0
- package/dist/channels/visitors.js +181 -0
- package/dist/cloudflare.d.ts +8 -2
- package/dist/cloudflare.js +27 -1
- package/dist/computer-target.d.ts +5 -0
- package/dist/computer-target.js +11 -0
- package/dist/host.d.ts +6 -3
- package/dist/host.js +75 -13
- package/dist/internal/host-gate.d.ts +6 -4
- package/dist/internal/host-gate.js +14 -7
- package/dist/invocation.d.ts +11 -4
- package/dist/invocation.js +134 -91
- package/dist/page/capabilities.js +1 -1
- package/dist/page/child-selection.d.ts +27 -0
- package/dist/page/child-selection.js +38 -0
- package/dist/page/mount.js +1 -1
- package/dist/page-script.js +4 -4
- package/dist/release.d.ts +13 -0
- package/dist/release.js +10 -0
- package/dist/request-origin.d.ts +4 -0
- package/dist/request-origin.js +10 -0
- package/dist/standalone/agents.js +11 -11
- package/dist/standalone/channel-session-door.d.ts +2 -2
- package/dist/standalone/channel-session-door.js +35 -5
- package/dist/standalone/computer-fleet.js +7 -1
- package/dist/standalone/tenancy.d.ts +3 -0
- package/dist/standalone/tenancy.js +7 -2
- package/dist/turnstile.d.ts +27 -0
- package/dist/turnstile.js +51 -0
- package/dist/workers-git.js +20 -1
- package/dist/workers.js +15 -12
- package/docs/api/auth.md +55 -0
- package/docs/api/build-contract.md +2 -2
- package/docs/api/cloudflare.md +26 -1
- package/docs/api/host.md +22 -13
- package/docs/api/invocation.md +7 -3
- package/docs/api/workers.md +17 -16
- package/docs/site/agent-quickstart.mdx +6 -2
- package/examples/standalone/wrangler.celld.jsonc +66 -0
- 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,
|
|
73
|
-
on the Computer,
|
|
74
|
-
and Wrangler, and copy the example Worker out of the
|
|
75
|
-
policy allows esbuild and workerd to install their
|
|
76
|
-
unneeded optional dependency scripts, so a fresh
|
|
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/
|
|
467
|
-
|
|
468
|
-
|
|
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
|
+
}
|