@fungi.computer/oyster 0.0.0-stage → 0.0.1
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/LICENSE +21 -0
- package/README.md +1082 -2
- package/dist/agents.d.ts +114 -0
- package/dist/agents.js +322 -0
- package/dist/app-builds.d.ts +151 -0
- package/dist/app-builds.js +355 -0
- package/dist/app-sandbox.d.ts +2 -0
- package/dist/app-sandbox.js +2 -0
- package/dist/app-sources.d.ts +46 -0
- package/dist/app-sources.js +335 -0
- package/dist/app-storage.d.ts +141 -0
- package/dist/app-storage.js +281 -0
- package/dist/apps/gate.d.ts +105 -0
- package/dist/apps/gate.js +273 -0
- package/dist/apps/index.d.ts +5 -0
- package/dist/apps/index.js +5 -0
- package/dist/apps/installation.d.ts +248 -0
- package/dist/apps/installation.js +307 -0
- package/dist/apps/operations.d.ts +8 -0
- package/dist/apps/operations.js +98 -0
- package/dist/apps/tools.d.ts +33 -0
- package/dist/apps/tools.js +17 -0
- package/dist/auth.d.ts +11 -0
- package/dist/auth.js +93 -0
- package/dist/backend-request.d.ts +3 -0
- package/dist/backend-request.js +53 -0
- package/dist/build/build-jobs.d.ts +128 -0
- package/dist/build/build-jobs.js +197 -0
- package/dist/build/build-step.d.ts +26 -0
- package/dist/build/build-step.js +20 -0
- package/dist/build/bundled-html.d.ts +7 -0
- package/dist/build/bundled-html.js +64 -0
- package/dist/build/definition-emission.d.ts +34 -0
- package/dist/build/definition-emission.js +201 -0
- package/dist/build/effect.d.ts +231 -0
- package/dist/build/effect.js +396 -0
- package/dist/build/git-environment.d.ts +6 -0
- package/dist/build/git-environment.js +48 -0
- package/dist/build/html-recipe.d.ts +20 -0
- package/dist/build/html-recipe.js +51 -0
- package/dist/build/local-build.d.ts +57 -0
- package/dist/build/local-build.js +1078 -0
- package/dist/build/r2-artifact-writer.d.ts +31 -0
- package/dist/build/r2-artifact-writer.js +179 -0
- package/dist/build/snapshot-package-store.d.ts +2 -0
- package/dist/build/snapshot-package-store.js +21 -0
- package/dist/build/snapshot-program-scripts.d.ts +6 -0
- package/dist/build/snapshot-program-scripts.js +2 -0
- package/dist/build/submission-contract.d.ts +384 -0
- package/dist/build/submission-contract.js +70 -0
- package/dist/build/workspace-build.d.ts +50 -0
- package/dist/build/workspace-build.js +156 -0
- package/dist/build-contract.d.ts +4 -0
- package/dist/build-contract.js +4 -0
- package/dist/build-jobs.d.ts +2 -0
- package/dist/build-jobs.js +2 -0
- package/dist/build.d.ts +11 -0
- package/dist/build.js +13 -0
- package/dist/capability-descriptor.d.ts +46 -0
- package/dist/capability-descriptor.js +51 -0
- package/dist/catalog-frame.d.ts +39 -0
- package/dist/catalog-frame.js +72 -0
- package/dist/cause-class.d.ts +19 -0
- package/dist/cause-class.js +54 -0
- package/dist/cloudflare.d.ts +123 -0
- package/dist/cloudflare.js +391 -0
- package/dist/computer-terminal.d.ts +38 -0
- package/dist/computer-terminal.js +410 -0
- package/dist/computer.d.ts +60 -0
- package/dist/computer.js +33 -0
- package/dist/event-recipients.d.ts +57 -0
- package/dist/event-recipients.js +144 -0
- package/dist/facet-invocation.d.ts +4 -0
- package/dist/facet-invocation.js +45 -0
- package/dist/files.d.ts +42 -0
- package/dist/files.js +123 -0
- package/dist/frame-binding-wire-script.d.ts +2 -0
- package/dist/frame-binding-wire-script.js +1 -0
- package/dist/frame-binding-wire.d.ts +28 -0
- package/dist/frame-binding-wire.js +54 -0
- package/dist/git-admission.d.ts +52 -0
- package/dist/git-admission.js +188 -0
- package/dist/git-auth.d.ts +11 -0
- package/dist/git-auth.js +27 -0
- package/dist/git-door.d.ts +35 -0
- package/dist/git-door.js +108 -0
- package/dist/git-journal.d.ts +31 -0
- package/dist/git-journal.js +188 -0
- package/dist/git.d.ts +31 -0
- package/dist/git.js +64 -0
- package/dist/guest-policy.d.ts +9 -0
- package/dist/guest-policy.js +13 -0
- package/dist/host.d.ts +244 -0
- package/dist/host.js +1043 -0
- package/dist/index.d.ts +166 -0
- package/dist/index.js +320 -0
- package/dist/installation-consent.d.ts +79 -0
- package/dist/installation-consent.js +82 -0
- package/dist/internal/background-origins.d.ts +3 -0
- package/dist/internal/background-origins.js +8 -0
- package/dist/internal/delivery.d.ts +12 -0
- package/dist/internal/delivery.js +167 -0
- package/dist/internal/host-gate.d.ts +14 -0
- package/dist/internal/host-gate.js +62 -0
- package/dist/internal/registry-installations.d.ts +6 -0
- package/dist/internal/registry-installations.js +12 -0
- package/dist/invocation.d.ts +251 -0
- package/dist/invocation.js +377 -0
- package/dist/keys.d.ts +48 -0
- package/dist/keys.js +52 -0
- package/dist/mcp-owner.d.ts +27 -0
- package/dist/mcp-owner.js +143 -0
- package/dist/mcp.d.ts +89 -0
- package/dist/mcp.js +429 -0
- package/dist/media.d.ts +23 -0
- package/dist/media.js +12 -0
- package/dist/pack.d.ts +76 -0
- package/dist/pack.js +134 -0
- package/dist/page/capabilities.d.ts +23 -0
- package/dist/page/capabilities.js +256 -0
- package/dist/page/computer-handles.d.ts +79 -0
- package/dist/page/computer-handles.js +44 -0
- package/dist/page/lifetime.d.ts +6 -0
- package/dist/page/lifetime.js +19 -0
- package/dist/page/mount.d.ts +163 -0
- package/dist/page/mount.js +390 -0
- package/dist/page/storage.d.ts +3 -0
- package/dist/page/storage.js +82 -0
- package/dist/page/terminal.d.ts +5 -0
- package/dist/page/terminal.js +7 -0
- package/dist/page-script.d.ts +14 -0
- package/dist/page-script.js +5 -0
- package/dist/r2-files.d.ts +3 -0
- package/dist/r2-files.js +79 -0
- package/dist/release.d.ts +72 -0
- package/dist/release.js +164 -0
- package/dist/root-data.d.ts +86 -0
- package/dist/root-data.js +69 -0
- package/dist/shell/app-directory.d.ts +537 -0
- package/dist/shell/app-directory.js +195 -0
- package/dist/shell/browser-actions.d.ts +7 -0
- package/dist/shell/browser-actions.js +56 -0
- package/dist/shell/command-host.d.ts +112 -0
- package/dist/shell/command-host.js +502 -0
- package/dist/shell/commands.d.ts +25 -0
- package/dist/shell/commands.js +78 -0
- package/dist/shell/index.d.ts +8 -0
- package/dist/shell/index.js +8 -0
- package/dist/shell/keys.d.ts +41 -0
- package/dist/shell/keys.js +134 -0
- package/dist/shell/shell-module.d.ts +31 -0
- package/dist/shell/shell-module.js +37 -0
- package/dist/shell/window-manager.d.ts +124 -0
- package/dist/shell/window-manager.js +711 -0
- package/dist/shell/workbench-contract.d.ts +68 -0
- package/dist/shell/workbench-contract.js +1 -0
- package/dist/shell/workbench-host-types.d.ts +96 -0
- package/dist/shell/workbench-host-types.js +1 -0
- package/dist/shell/workbench-host.d.ts +34 -0
- package/dist/shell/workbench-host.js +347 -0
- package/dist/shell/workbench-model.d.ts +192 -0
- package/dist/shell/workbench-model.js +53 -0
- package/dist/shell/workbench.d.ts +4 -0
- package/dist/shell/workbench.js +2 -0
- package/dist/source/fork.d.ts +27 -0
- package/dist/source/fork.js +46 -0
- package/dist/source/index.d.ts +11 -0
- package/dist/source/index.js +11 -0
- package/dist/source/seed-receipt.d.ts +53 -0
- package/dist/source/seed-receipt.js +18 -0
- package/dist/source/team-source.d.ts +32 -0
- package/dist/source/team-source.js +42 -0
- package/dist/source/template-repository.d.ts +2 -0
- package/dist/source/template-repository.js +8 -0
- package/dist/source/template-write.d.ts +69 -0
- package/dist/source/template-write.js +344 -0
- package/dist/storage-frame.d.ts +64 -0
- package/dist/storage-frame.js +28 -0
- package/dist/tenant.d.ts +81 -0
- package/dist/tenant.js +71 -0
- package/dist/testing/capabilities.d.ts +29 -0
- package/dist/testing/capabilities.js +130 -0
- package/dist/testing/catalog.d.ts +3 -0
- package/dist/testing/catalog.js +53 -0
- package/dist/testing/index.d.ts +10 -0
- package/dist/testing/index.js +10 -0
- package/dist/testing/installation.d.ts +11 -0
- package/dist/testing/installation.js +25 -0
- package/dist/testing/native-worker.d.ts +21 -0
- package/dist/testing/native-worker.js +106 -0
- package/dist/testing/worker.d.ts +18 -0
- package/dist/testing/worker.js +67 -0
- package/dist/testing/workers-runtime.d.ts +24 -0
- package/dist/testing/workers-runtime.js +33 -0
- package/dist/webhook-delivery.d.ts +14 -0
- package/dist/webhook-delivery.js +17 -0
- package/dist/workers-git.d.ts +285 -0
- package/dist/workers-git.js +1174 -0
- package/dist/workers.d.ts +352 -0
- package/dist/workers.js +866 -0
- package/docs/api/README.md +82 -0
- package/docs/api/agents.md +332 -0
- package/docs/api/app-storage.md +209 -0
- package/docs/api/apps.md +1375 -0
- package/docs/api/auth.md +63 -0
- package/docs/api/build-contract.md +507 -0
- package/docs/api/build.md +1476 -0
- package/docs/api/cloudflare.md +421 -0
- package/docs/api/computer.md +203 -0
- package/docs/api/files.md +117 -0
- package/docs/api/git.md +470 -0
- package/docs/api/host.md +1328 -0
- package/docs/api/index.md +430 -0
- package/docs/api/invocation.md +738 -0
- package/docs/api/keys.md +175 -0
- package/docs/api/mcp.md +301 -0
- package/docs/api/media.md +40 -0
- package/docs/api/modules.md +42 -0
- package/docs/api/pack.md +138 -0
- package/docs/api/page/mount.md +298 -0
- package/docs/api/root-data.md +111 -0
- package/docs/api/shell/app-directory.md +332 -0
- package/docs/api/shell/command-host.md +224 -0
- package/docs/api/shell/commands.md +197 -0
- package/docs/api/shell/keys.md +183 -0
- package/docs/api/shell/shell-module.md +143 -0
- package/docs/api/shell/window-manager.md +267 -0
- package/docs/api/shell/workbench.md +186 -0
- package/docs/api/shell.md +1788 -0
- package/docs/api/tenant.md +194 -0
- package/docs/api/workers-git.md +1527 -0
- package/docs/api/workers.md +1678 -0
- package/docs/media/architecture.mmd +32 -0
- package/docs/media/architecture.png +0 -0
- package/docs/media/bonsai-network.png +0 -0
- package/docs/media/bonsai-repository.png +0 -0
- package/docs/site/agent-quickstart.mdx +349 -0
- package/examples/git/worker.ts +39 -0
- package/examples/git/wrangler.jsonc +32 -0
- package/examples/standalone/worker.ts +14 -0
- package/examples/standalone/wrangler.jsonc +21 -0
- package/package.json +249 -3
package/README.md
CHANGED
|
@@ -1,3 +1,1083 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Oyster (`@fungi.computer/oyster`)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Oyster is an open-source host for Apps: deploy one Worker to your Cloudflare
|
|
4
|
+
account and install Apps into it, such as a git platform, workflows, terminals,
|
|
5
|
+
files and agents, each built from a git commit and each with its own database.
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
Install it from npm as `@fungi.computer/oyster`, copy one example Worker and
|
|
10
|
+
deploy. Try a live Oyster first at <https://oyster.shiit.app/>: it runs Bonsai,
|
|
11
|
+
Fungi's git platform for many agents at once, and
|
|
12
|
+
[CONTEST.md](https://github.com/fungi-computer/oyster/blob/main/CONTEST.md) is
|
|
13
|
+
the five-minute tour and the comparison with GitHub.
|
|
14
|
+
|
|
15
|
+
An Oyster is one Worker. Every App installed on it is an immutable Release built
|
|
16
|
+
from an exact git commit. Its UI runs in a sandboxed frame, its backend runs in
|
|
17
|
+
its own Durable Object with its own SQLite, and it receives only the bindings it
|
|
18
|
+
declares. Agents reach every installed App through one MCP endpoint. The same
|
|
19
|
+
Worker runs on Cloudflare Workers and on celld, a Workers-compatible runtime you
|
|
20
|
+
can host yourself.
|
|
21
|
+
|
|
22
|
+
The live demo opens into Tourtois, a desktop with a launcher, windows and
|
|
23
|
+
settings, and runs Fungi's own App set:
|
|
24
|
+
|
|
25
|
+
- **Bonsai** hosts git repositories on Cloudflare Artifacts. Each agent gets its
|
|
26
|
+
own fork and pushes with ordinary `git`.
|
|
27
|
+
- **Workflows** runs typed graphs of App methods on Cloudflare Workflows,
|
|
28
|
+
started by a push, a webhook or a schedule.
|
|
29
|
+
- **Shed** opens a terminal on a Computer, a Linux workspace in a Cloudflare
|
|
30
|
+
Container.
|
|
31
|
+
- **Inkcap** edits files on a Computer, **Grove** holds conversations with
|
|
32
|
+
agents and **Prints** generates images and video with Workers AI.
|
|
33
|
+
|
|
34
|
+
Those Apps belong to [Fungi](https://fungi.computer/) and are not part of this
|
|
35
|
+
package. Your own Oyster installs the Apps you pack from your own git
|
|
36
|
+
repositories.
|
|
37
|
+
|
|
38
|
+
## Part of Fungi
|
|
39
|
+
|
|
40
|
+
[Fungi](https://fungi.computer/) runs AI Agents on their own Computers, inside
|
|
41
|
+
Teams that share Agents, Computers, Apps and a bill. Oyster is its App host,
|
|
42
|
+
released on its own, and it is built from 21 more Fungi packages released with
|
|
43
|
+
it. Each one is MIT-licensed, has its own repository at
|
|
44
|
+
`github.com/fungi-computer/<name>`, installs from npm as
|
|
45
|
+
`@fungi.computer/<name>`, and works without the rest. Installing Oyster brings
|
|
46
|
+
in the ones it runs on.
|
|
47
|
+
|
|
48
|
+
Apps and hosting:
|
|
49
|
+
|
|
50
|
+
- [Agar](https://github.com/fungi-computer/agar): the App contract. Typed
|
|
51
|
+
backend methods, host capabilities and the connection between an App's
|
|
52
|
+
frontend and its host.
|
|
53
|
+
- [Puffball](https://github.com/fungi-computer/puffball): a catalog of web Apps
|
|
54
|
+
and their immutable Releases, which serves only promoted Releases and checks
|
|
55
|
+
every artifact against its recorded digest.
|
|
56
|
+
- [Gills](https://github.com/fungi-computer/gills): compact plain-text App views
|
|
57
|
+
shared by browser and terminal hosts.
|
|
58
|
+
- [Whistle](https://github.com/fungi-computer/whistle): one command definition
|
|
59
|
+
becomes key bindings, a command palette, slash commands, help, a menu and an
|
|
60
|
+
agent tool, all running through one typed `execute` door.
|
|
61
|
+
|
|
62
|
+
Agents and tools:
|
|
63
|
+
|
|
64
|
+
- [Shiitake](https://github.com/fungi-computer/shiitake): durable Agent
|
|
65
|
+
conversations. A Session holds accepted work, messages and observations; the
|
|
66
|
+
host supplies storage, models and tools.
|
|
67
|
+
- [Mule](https://github.com/fungi-computer/mule): runs Pi's model and tool loop
|
|
68
|
+
inside Effect as an awaited, cancellable run.
|
|
69
|
+
- [Mycelium](https://github.com/fungi-computer/mycelium): gives an Agent typed
|
|
70
|
+
operations through one `execute` tool, with a capability snapshot acquired and
|
|
71
|
+
released per run.
|
|
72
|
+
- [Knapsack](https://github.com/fungi-computer/knapsack): connects an Agent to
|
|
73
|
+
Computers, files and remote MCP servers as Mycelium capability sources.
|
|
74
|
+
- [Forage](https://github.com/fungi-computer/forage): lists, searches and
|
|
75
|
+
describes a catalog of operations, so a caller or a model finds the right one.
|
|
76
|
+
- [BirdDog](https://github.com/fungi-computer/bird-dog): durable mail between
|
|
77
|
+
agent sessions, with replies retried from an outbox until they are accepted.
|
|
78
|
+
|
|
79
|
+
Durable work, git and keys:
|
|
80
|
+
|
|
81
|
+
- [Watchdog](https://github.com/fungi-computer/watchdog): a small durable job
|
|
82
|
+
engine on SQLite, D1 or PostgreSQL. Each job settles with exactly one recorded
|
|
83
|
+
outcome, even across restarts.
|
|
84
|
+
- [Carrier](https://github.com/fungi-computer/carrier): durable email delivery
|
|
85
|
+
whose receipts record only what the provider said, and ordered envelopes
|
|
86
|
+
between Durable Objects.
|
|
87
|
+
- [Burrow](https://github.com/fungi-computer/burrow): the repository owner for
|
|
88
|
+
Cloudflare Artifacts, serving only Git's smart-HTTP operations and never
|
|
89
|
+
returning a credential.
|
|
90
|
+
- [Ear Tag](https://github.com/fungi-computer/ear-tag): bearer keys and their
|
|
91
|
+
hashes, a typed grant vocabulary, and a tenant-scoped key store with
|
|
92
|
+
revocation, expiry and a per-key rate limit.
|
|
93
|
+
- [Cairn](https://github.com/fungi-computer/cairn): typed, opaque identifiers,
|
|
94
|
+
temporary keys and candidate word names, with their parsers.
|
|
95
|
+
|
|
96
|
+
Interface:
|
|
97
|
+
|
|
98
|
+
- [Stipe](https://github.com/fungi-computer/stipe): a CSS-first theme layer for
|
|
99
|
+
Tailwind CSS v4 and daisyUI 5, with spacing, fonts and material tokens.
|
|
100
|
+
- [Caps](https://github.com/fungi-computer/caps): accessible React primitives
|
|
101
|
+
and blocks, from buttons and dialogs to a chat composer and window frames,
|
|
102
|
+
themed by Stipe.
|
|
103
|
+
- [Maosaico](https://github.com/fungi-computer/maosaico): arranges React panes
|
|
104
|
+
inside a measured container, with floating frames and optional tiling.
|
|
105
|
+
- [Sigils](https://github.com/fungi-computer/sigils): Fungi's decorative SVG
|
|
106
|
+
marks and their gallery.
|
|
107
|
+
- [Sprite](https://github.com/fungi-computer/sprite): shows an animated
|
|
108
|
+
character through a renderer the host installs.
|
|
109
|
+
- [Sprite Zdog](https://github.com/fungi-computer/sprite-zdog): a Canvas
|
|
110
|
+
renderer for Zdog characters, with built-in characters and authoring tools for
|
|
111
|
+
Sprite.
|
|
112
|
+
|
|
113
|
+
## Hand this to your agent
|
|
114
|
+
|
|
115
|
+
Paste this prompt into a coding agent (Claude Code, Codex or any other) to have
|
|
116
|
+
it set up a local Oyster, install an App, connect over MCP and deploy:
|
|
117
|
+
|
|
118
|
+
```text
|
|
119
|
+
Set up Oyster for me. Install it from npm as @fungi.computer/oyster and follow
|
|
120
|
+
https://github.com/fungi-computer/oyster/blob/main/docs/site/agent-quickstart.mdx
|
|
121
|
+
step by step. Run each command exactly as written, compare its output with the
|
|
122
|
+
expected output, and use the "If it fails" notes before improvising. Stop and
|
|
123
|
+
ask me before step 7 (deploy).
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The guide ships in the package as `docs/site/agent-quickstart.mdx`. It needs
|
|
127
|
+
Node 24, pnpm and Git; a Cloudflare account matters only for the deploy step.
|
|
128
|
+
|
|
129
|
+
## Why Oyster
|
|
130
|
+
|
|
131
|
+
**Apps are Releases with provenance.** `packRelease` builds an App from one
|
|
132
|
+
commit and records it as `sourceRevision: git:<sha>`. Every artifact is
|
|
133
|
+
addressed by its SHA-256 digest, and Oyster verifies each digest before it
|
|
134
|
+
installs or replaces a Release. Replacing a Release keeps the App's data;
|
|
135
|
+
deleting it purges the data and leaves a tombstone.
|
|
136
|
+
|
|
137
|
+
**Every App is isolated.** An App's backend loads through a Worker Loader into
|
|
138
|
+
its own Durable Object facet with its own SQLite database. It has no ambient
|
|
139
|
+
network access. It receives only the bindings its Release declares (`GIT`,
|
|
140
|
+
`EVENTS`, Computer, agent, workflow, media), and Oyster checks that declaration
|
|
141
|
+
again on every request. App pages are served with a deny-by-default Content
|
|
142
|
+
Security Policy inside a sandboxed frame.
|
|
143
|
+
|
|
144
|
+
**Git is a binding.** An App that declares `git.repository` gets `env.GIT`:
|
|
145
|
+
create, fork, log, read and merge on Cloudflare Artifacts, scoped to its
|
|
146
|
+
installation. People and agents push with ordinary `git` through Oyster's git
|
|
147
|
+
door using their own keys; the host keeps every provider credential. Each
|
|
148
|
+
accepted push reaches the App's `onGitPush` handler and can start a Workflow or
|
|
149
|
+
a signed webhook.
|
|
150
|
+
|
|
151
|
+
**Agents are first-class callers.** `POST /mcp` serves one MCP endpoint for the
|
|
152
|
+
whole Oyster. Every installed App's methods appear as typed `env.<APP>` modules
|
|
153
|
+
that an agent discovers and calls from code; a dotted method is a nested
|
|
154
|
+
property, e.g. `env.BONSAI.repos.list({})`. Writes need explicit approval. Each
|
|
155
|
+
agent has its own key, so every push and call is attributed.
|
|
156
|
+
|
|
157
|
+
**Workflows compose Apps.** Workflows stores each graph in git, so a commit is a
|
|
158
|
+
version. Each graph compiles to a plan with a digest and runs on Cloudflare
|
|
159
|
+
Workflows. Steps call App methods and can run a sandboxed Code node or a step in
|
|
160
|
+
a Computer.
|
|
161
|
+
|
|
162
|
+
**Computers are ordinary capabilities.** A Computer is a durable workspace with
|
|
163
|
+
a Container backing. Shed opens its terminal, Inkcap edits its files and port
|
|
164
|
+
previews serve what it runs on signed hostnames. Apps reach it only through the
|
|
165
|
+
`computer.*` capability they declare.
|
|
166
|
+
|
|
167
|
+
**The desktop is a trusted shell.** Tourtois is installed like any other
|
|
168
|
+
Release, but your Oyster renders its home installation as the top-level page,
|
|
169
|
+
with every permission you have as owner. Each App it opens still runs in its own
|
|
170
|
+
sandboxed frame inside that App's window. Pick a shell you trust: it can do
|
|
171
|
+
anything you can.
|
|
172
|
+
|
|
173
|
+
## Architecture
|
|
174
|
+
|
|
175
|
+

|
|
176
|
+
|
|
177
|
+
| Piece | Owns |
|
|
178
|
+
| ----------------- | ----------------------------------------------------------------------------------------- |
|
|
179
|
+
| Router | Sign-in, `/apps/<installation>/` pages, App methods, owner `/api/*` routes and `/mcp`. |
|
|
180
|
+
| `OysterRegistry` | The installation list: which Release each installation runs, and its visibility. |
|
|
181
|
+
| R2 | Release artifacts, stored by digest. |
|
|
182
|
+
| App Instance | One Durable Object per installation: lifecycle, owner state and the backend facet. |
|
|
183
|
+
| Backend facet | The App's own code and SQLite database, loaded through the Worker Loader. |
|
|
184
|
+
| git door | Smart-HTTP `clone`, `fetch` and `push`, admitted per key and forwarded to Artifacts. |
|
|
185
|
+
| Tourtois | The trusted shell: launcher, windows, menus and settings, rendered as the top-level page. |
|
|
186
|
+
| Platform bindings | Workflows, Computers, Agents and Workers AI, offered only to Apps that declare them. |
|
|
187
|
+
|
|
188
|
+
The diagram source is
|
|
189
|
+
[`docs/media/architecture.mmd`](docs/media/architecture.mmd).
|
|
190
|
+
|
|
191
|
+
## Quick start: deploy your own Oyster
|
|
192
|
+
|
|
193
|
+
You need a Cloudflare account on Workers Paid, Node 24 and pnpm (npm works the
|
|
194
|
+
same way). Start a project, add Oyster and Wrangler, and copy the standalone
|
|
195
|
+
example Worker out of the package:
|
|
196
|
+
|
|
197
|
+
```sh
|
|
198
|
+
mkdir my-oyster && cd my-oyster
|
|
199
|
+
pnpm add @fungi.computer/oyster
|
|
200
|
+
pnpm add -D wrangler
|
|
201
|
+
cp node_modules/@fungi.computer/oyster/examples/standalone/{worker.ts,wrangler.jsonc} .
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
pnpm 11 may end an `add` with `ERR_PNPM_IGNORED_BUILDS` for packages such as
|
|
205
|
+
`esbuild` or `workerd`; they are installed, and Oyster needs none of their
|
|
206
|
+
install scripts.
|
|
207
|
+
|
|
208
|
+
`worker.ts` is the whole Worker: the default handler plus four classes.
|
|
209
|
+
`OysterRegistry`, `OysterInstance` and `OysterMcp` are the Durable Objects its
|
|
210
|
+
`wrangler.jsonc` binds; `OysterBinding` is the one entrypoint every App binding
|
|
211
|
+
in every invocation frame is minted from (`env.APP_STORAGE`, `env.BACKGROUND`,
|
|
212
|
+
`env.APPS` and the rest).
|
|
213
|
+
|
|
214
|
+
```ts title="worker.ts"
|
|
215
|
+
import { createOysterWorker } from "@fungi.computer/oyster/workers";
|
|
216
|
+
|
|
217
|
+
export {
|
|
218
|
+
OysterBinding,
|
|
219
|
+
OysterInstance,
|
|
220
|
+
OysterRegistry,
|
|
221
|
+
OysterMcp,
|
|
222
|
+
} from "@fungi.computer/oyster/workers";
|
|
223
|
+
|
|
224
|
+
export default createOysterWorker();
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
`wrangler.jsonc` names the Worker `oyster` and its Release bucket
|
|
228
|
+
`oyster-releases`. Create the bucket and deploy. Your Worker's address is
|
|
229
|
+
`https://oyster.<your-subdomain>.workers.dev`; your workers.dev subdomain is on
|
|
230
|
+
the Workers & Pages overview in the Cloudflare dashboard.
|
|
231
|
+
|
|
232
|
+
```sh
|
|
233
|
+
npx wrangler login
|
|
234
|
+
npx wrangler r2 bucket create oyster-releases
|
|
235
|
+
export OYSTER_URL=https://oyster.<your-subdomain>.workers.dev
|
|
236
|
+
npx wrangler deploy --var OYSTER_ORIGIN:$OYSTER_URL
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Set `OYSTER_ORIGIN` in `wrangler.jsonc` instead if you deploy again later; every
|
|
240
|
+
deploy replaces the Worker's variables.
|
|
241
|
+
|
|
242
|
+
Choose an owner key and store it as a secret:
|
|
243
|
+
|
|
244
|
+
```sh
|
|
245
|
+
export OYSTER_OWNER_KEY=$(openssl rand -hex 24)
|
|
246
|
+
printf '%s' "$OYSTER_OWNER_KEY" | npx wrangler secret put OYSTER_OWNER_KEY
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Open `$OYSTER_URL/login` and sign in with the owner key. Your Oyster starts with
|
|
250
|
+
no Apps; "Pack an App" under Run your own Oyster builds one from a git commit
|
|
251
|
+
and installs it, and the
|
|
252
|
+
[agent quick start](https://github.com/fungi-computer/oyster/blob/main/docs/site/agent-quickstart.mdx)
|
|
253
|
+
walks through a first App step by step.
|
|
254
|
+
|
|
255
|
+
### Add Git
|
|
256
|
+
|
|
257
|
+
For git, copy `examples/git` instead of `examples/standalone`:
|
|
258
|
+
|
|
259
|
+
```sh
|
|
260
|
+
cp node_modules/@fungi.computer/oyster/examples/git/{worker.ts,wrangler.jsonc} .
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
It is the same Oyster with Oyster's git door: people and agents `clone`, `fetch`
|
|
264
|
+
and `push` App repositories with ordinary `git` at
|
|
265
|
+
`/git/<installation>/<repo>.git`. Its `wrangler.jsonc` names the Worker
|
|
266
|
+
`oyster-git` and adds the `ARTIFACTS` binding and the `KEYS` and `WEBHOOKS`
|
|
267
|
+
Durable Objects. Repositories live in Cloudflare Artifacts, so the account needs
|
|
268
|
+
Artifacts enabled. Deploy it the same way, with `OYSTER_URL` set to
|
|
269
|
+
`https://oyster-git.<your-subdomain>.workers.dev`, then give each agent its own
|
|
270
|
+
key; Oyster shows each key once:
|
|
271
|
+
|
|
272
|
+
```sh
|
|
273
|
+
curl -s -X POST -H "Authorization: Bearer $OYSTER_OWNER_KEY" \
|
|
274
|
+
-H 'content-type: application/json' \
|
|
275
|
+
-d '{"name":"morel","grants":[{"capability":"apps.use","targets":{"kind":"all"}}]}' \
|
|
276
|
+
$OYSTER_URL/api/keys
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
The answer's `plaintext` field, starting `oyk_`, signs in to `git` and to
|
|
280
|
+
`/mcp`. That key can read and write every App on this Oyster, including Apps
|
|
281
|
+
installed later, without asking each time; anyone holding it can act as the
|
|
282
|
+
owner in those Apps, and revoking it stops it immediately.
|
|
283
|
+
|
|
284
|
+
Any MCP client can call every installed App with a key. For Claude Code:
|
|
285
|
+
|
|
286
|
+
```sh
|
|
287
|
+
claude mcp add --transport http oyster "$OYSTER_URL/mcp" \
|
|
288
|
+
--header "Authorization: Bearer <the agent's key>"
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### What the live demo runs
|
|
292
|
+
|
|
293
|
+
<https://oyster.shiit.app/> is this package's git root with more filled in: it
|
|
294
|
+
installs Fungi's App set (Tourtois, Bonsai, Shed, Inkcap, Workflows, Prints and
|
|
295
|
+
Grove), selects Tourtois as its home installation, and composes Computers,
|
|
296
|
+
Agents and media. Its `koi-pond` repository is a small canvas game three coding
|
|
297
|
+
agents built together, each in its own fork with its own key, with Bonsai
|
|
298
|
+
drawing a lane per agent as they pushed and merged.
|
|
299
|
+
[CONTEST.md](https://github.com/fungi-computer/oyster/blob/main/CONTEST.md)
|
|
300
|
+
tours it.
|
|
301
|
+
|
|
302
|
+
## Install or set up
|
|
303
|
+
|
|
304
|
+
```sh
|
|
305
|
+
pnpm add @fungi.computer/oyster
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Oyster is ESM only and runs on Node 24 or newer, Cloudflare Workers and celld.
|
|
309
|
+
React and React DOM 18.3 or 19.2 are peer dependencies; the host renders its
|
|
310
|
+
pages with them. Wrangler is an optional peer dependency: add it as a dev
|
|
311
|
+
dependency to run or deploy the example Workers, which ship in the package under
|
|
312
|
+
`examples/`. The generated [API reference](docs/api/README.md) ships in the
|
|
313
|
+
package too.
|
|
314
|
+
|
|
315
|
+
Oyster's runtime dependencies are other Fungi packages, among them
|
|
316
|
+
[Agar](https://github.com/fungi-computer/agar) for the App contract,
|
|
317
|
+
[Puffball](https://github.com/fungi-computer/puffball) for Releases and
|
|
318
|
+
[Mycelium](https://github.com/fungi-computer/mycelium) for the MCP `execute`
|
|
319
|
+
door. "Part of Fungi" above lists them all.
|
|
320
|
+
|
|
321
|
+
## Run your own Oyster
|
|
322
|
+
|
|
323
|
+
Agent runtime composition belongs to the deployable host. The live demo's root
|
|
324
|
+
composes stock [Shiitake](https://github.com/fungi-computer/shiitake) Durable
|
|
325
|
+
Objects and its Agent fleet, with `oysterAppMethods` supplying the installed App
|
|
326
|
+
catalog. Oyster exports that catalog and its capability slots without an Agent
|
|
327
|
+
runtime dependency.
|
|
328
|
+
|
|
329
|
+
`@fungi.computer/oyster/workers` is a complete single-owner App server; its
|
|
330
|
+
Worker entry is the `worker.ts` in the quick start.
|
|
331
|
+
`examples/standalone/wrangler.jsonc` declares its bindings: a Worker Loader, an
|
|
332
|
+
R2 bucket for Releases and three SQLite Durable Objects (`REGISTRY`, `INSTANCES`
|
|
333
|
+
and `MCP`). Set the owner key as a secret (`OYSTER_OWNER_KEY`, 24+ characters)
|
|
334
|
+
and deploy with `wrangler deploy`; the same config runs under `celld dev`. Sign
|
|
335
|
+
in at `/login`.
|
|
336
|
+
|
|
337
|
+
Pack and install an App with `@fungi.computer/oyster/pack`: `packRelease` turns
|
|
338
|
+
an exact Git commit and a canonical build recipe into a Release with immutable
|
|
339
|
+
artifacts, and `installRelease` uploads it. Each installed App gets:
|
|
340
|
+
|
|
341
|
+
- its UI at `/apps/<installation>/`, in a sandboxed frame connected through
|
|
342
|
+
Agar's `app.backend` capability;
|
|
343
|
+
- its methods at `POST /apps/<installation>/methods/<name>` (JSON in and out);
|
|
344
|
+
- its own SQLite state in its Instance's `app` facet.
|
|
345
|
+
|
|
346
|
+
`createOyster(host)` from `@fungi.computer/oyster/host` is the same server over
|
|
347
|
+
your own adapters (`auth`, `artifacts`, `installations`, `instances`, `mcp`,
|
|
348
|
+
`capabilities`).
|
|
349
|
+
|
|
350
|
+
### Add Git to your host
|
|
351
|
+
|
|
352
|
+
Git is one optional field of the typed host: `host.git`. When it is present,
|
|
353
|
+
`createOyster` mounts the git door at `/git/<installation>/<repo>.git`, and
|
|
354
|
+
people and agents `clone`, `fetch` and `push` with ordinary git. Nothing else in
|
|
355
|
+
the host changes.
|
|
356
|
+
|
|
357
|
+
There are two ways to get it:
|
|
358
|
+
|
|
359
|
+
- **Use one Git root.** `oysterGitRoot(options)` from
|
|
360
|
+
`@fungi.computer/oyster/workers-git` is the whole composition: the git door
|
|
361
|
+
plus tenancy, Agents and their owner-only approval routes, workspace and
|
|
362
|
+
capability slots, the page script and the home App. Its `hostFor(env, scope)`
|
|
363
|
+
is the only way a tenant host is built. Pass the root to
|
|
364
|
+
`createOysterGitWorker` for HTTP and to `createOysterGitMcpOwner` for MCP, and
|
|
365
|
+
return `root.hostFor(this.env, scope)` from your `OysterGitInstance` subclass,
|
|
366
|
+
so HTTP, MCP and background wakes share one composition. `tenancy` is
|
|
367
|
+
required; a single-owner root passes `ownerOrKeyTenancy`, so the owner key and
|
|
368
|
+
named keys sign git in. `examples/git/worker.ts` is that root in one short
|
|
369
|
+
file, and the live demo's root is a real multi-door caller. Its
|
|
370
|
+
`wrangler.jsonc` adds the `ARTIFACTS` binding and the `KEYS` and `WEBHOOKS`
|
|
371
|
+
Durable Objects.
|
|
372
|
+
- **Set `host.git` on your own host.** Keep your own `createOyster(host)`
|
|
373
|
+
composition and set `git: oysterWorkerGit(env, { scope })` and
|
|
374
|
+
`ownerRoutes: gitAdminRoutes(env, scope)` for `/api/keys` and `/api/webhooks`,
|
|
375
|
+
with the tenant `scope` your host selected.
|
|
376
|
+
|
|
377
|
+
The git root, from Workers:
|
|
378
|
+
|
|
379
|
+
```ts title="worker.ts"
|
|
380
|
+
import type { TenantScope } from "@fungi.computer/oyster/tenant";
|
|
381
|
+
import {
|
|
382
|
+
createOysterGitMcpOwner,
|
|
383
|
+
createOysterGitWorker,
|
|
384
|
+
OysterGitInstance,
|
|
385
|
+
oysterGitRoot,
|
|
386
|
+
ownerOrKeyTenancy,
|
|
387
|
+
type OysterGitWorkerEnv,
|
|
388
|
+
} from "@fungi.computer/oyster/workers-git";
|
|
389
|
+
|
|
390
|
+
const root = oysterGitRoot<OysterGitWorkerEnv>({ tenancy: ownerOrKeyTenancy });
|
|
391
|
+
|
|
392
|
+
export class OysterMcp extends createOysterGitMcpOwner(root) {}
|
|
393
|
+
|
|
394
|
+
export class OysterInstance extends OysterGitInstance {
|
|
395
|
+
protected hostFor(scope: TenantScope) {
|
|
396
|
+
return root.hostFor(this.env, scope);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export default createOysterGitWorker(root);
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
`examples/git/worker.ts` adds the Durable Object exports and the git
|
|
404
|
+
`OysterBinding` entrypoint, whose table also serves `env.GIT` and `env.EVENTS`.
|
|
405
|
+
The package's `test:git-door` check runs that exact file on workerd against real
|
|
406
|
+
bare repositories: the owner pushes with `git`, a named agent key's push is
|
|
407
|
+
refused by the App's policy with Git's own `ng` report, both keys clone, and the
|
|
408
|
+
accepted push reaches the App's `onGitPush`.
|
|
409
|
+
|
|
410
|
+
Each layer owns one decision:
|
|
411
|
+
|
|
412
|
+
```mermaid
|
|
413
|
+
flowchart LR
|
|
414
|
+
client["git client<br/>clone · fetch · push"]
|
|
415
|
+
auth["Authentication<br/>owner key · named key<br/>Workflow Git pass header"]
|
|
416
|
+
admission["Git admission<br/>enabled installation · git.repository<br/>tenant scope · Workflow branch grant"]
|
|
417
|
+
policy["App push policy<br/>authorizeGitPush, every ref"]
|
|
418
|
+
burrow["Burrow<br/>Artifacts provider and repository<br/>host-held credentials"]
|
|
419
|
+
events["App events<br/>journal → onGitPush → env.EVENTS<br/>webhooks · Workflow triggers"]
|
|
420
|
+
client --> auth --> admission
|
|
421
|
+
admission -- "push: ask first" --> policy
|
|
422
|
+
admission -- "admitted bytes" --> burrow
|
|
423
|
+
burrow -- "accepted refs" --> events
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
- **Authentication** is the host's own `auth`. The owner key is a Bearer token;
|
|
427
|
+
a named key may also be the Basic password from a git credential helper. A
|
|
428
|
+
Workflow Git pass (a `wgp.` Bearer header, never in a URL) goes only to
|
|
429
|
+
`host.git.workflowPass`; without that port it admits nothing. Anonymous
|
|
430
|
+
requests read only public installations and never push.
|
|
431
|
+
- **Git admission** reopens the live installation before every provider call: it
|
|
432
|
+
must be enabled and its Release must declare `git.repository`. The tenant
|
|
433
|
+
scope prefixes every repository name (`<installation>--<repo>` for the default
|
|
434
|
+
tenant), so an installation reaches only its own repositories. A Workflow
|
|
435
|
+
writer may push only the branches its grant names.
|
|
436
|
+
- **The App's push policy** answers `authorizeGitPush` for every ref update
|
|
437
|
+
before any bytes reach the provider. One refusal rejects the whole push with
|
|
438
|
+
Git's native `ng` report. Unsupported receive-pack commands, including signed
|
|
439
|
+
push certificates, fail closed.
|
|
440
|
+
- **Burrow** owns the provider and the repository. The host's Artifacts adapter
|
|
441
|
+
mints short-lived credentials through it and forwards the admitted smart-HTTP
|
|
442
|
+
exchange (up to 64 MiB per request); the App sees repository names, git
|
|
443
|
+
objects and its own events, never a credential or remote.
|
|
444
|
+
- **App events** start from the accepted-ref journal. Delivery calls the
|
|
445
|
+
selected Instance's `onGitPush` when the Release declares
|
|
446
|
+
`receives: ["git.push"]` and `git.repository`. The App emits typed events
|
|
447
|
+
through `env.EVENTS`: Oyster delivers them as signed Standard Webhooks, and a
|
|
448
|
+
host with Workflows (the Hub, or the live demo's root) starts the Workflow
|
|
449
|
+
triggers armed on that event.
|
|
450
|
+
|
|
451
|
+
Agent approval stays owner-direct in both compositions: the approval routes are
|
|
452
|
+
owner routes, refuse App-origin requests, and are never an `OysterCapability` an
|
|
453
|
+
App can be granted.
|
|
454
|
+
|
|
455
|
+
#### Git bindings, journal and lower-level ports
|
|
456
|
+
|
|
457
|
+
A Release's `backend.bindings` determines whether its App gets `env.GIT`
|
|
458
|
+
(Cloudflare Artifacts, scoped to its installation; credentials stay in the host)
|
|
459
|
+
or `env.EVENTS` (delivered as Standard Webhooks to subscriptions the owner
|
|
460
|
+
creates at `/api/webhooks`). Each binding enforces its declared operations.
|
|
461
|
+
`await env.GIT.cloneUrl(repo)` returns the host-issued clone URL; an undeclared
|
|
462
|
+
binding grants nothing. Declaration changes invalidate the loader entry even
|
|
463
|
+
when the backend bytes stay the same. Agents push with keys from `/api/keys`.
|
|
464
|
+
|
|
465
|
+
Hosts compose one `createAdmittedGit` with an installation-scoped store, live
|
|
466
|
+
host grants, the App's typed `authorizeGitPush` policy and an accepted-ref
|
|
467
|
+
journal. `GitWriter`, `GitPushQuestion`, `GitPushVerdict` and accepted facts
|
|
468
|
+
come from `@fungi.computer/agar/git`; the shared wire parser is in
|
|
469
|
+
`agar/git-wire`.
|
|
470
|
+
|
|
471
|
+
An App's own `GIT` binding uses `admitted.binding(onBehalfOf)`. Its writer is
|
|
472
|
+
`installation`, with the host-attested caller retained in `onBehalfOf`. These
|
|
473
|
+
writes skip the App's external-push policy, retain live host admission and enter
|
|
474
|
+
the same accepted-ref journal. The journal records only refs accepted by the
|
|
475
|
+
provider and commits before the exchange is acknowledged. Redelivery keeps the
|
|
476
|
+
complete original fact and `eventId`; handlers record that ID with their
|
|
477
|
+
effects.
|
|
478
|
+
|
|
479
|
+
`createGitPushJournal` composes the standalone Instance's SQLite owner with
|
|
480
|
+
Carrier delivery and an alarm hook. Its accepted fact, ref tips and Carrier send
|
|
481
|
+
commit together; Carrier retains delivery attempts across restarts and retries.
|
|
482
|
+
The Hub composes its ref journal and `TeamPost.sendGitPush` in one host
|
|
483
|
+
transaction. Neither HTTP routing nor the provider adapter creates a second push
|
|
484
|
+
fact.
|
|
485
|
+
|
|
486
|
+
`createGitDoor` from `@fungi.computer/oyster/git` accepts the host's
|
|
487
|
+
`authenticate`, `isPublic` and `git` ports. `authenticate` receives the request
|
|
488
|
+
and the exact installation, repository and read/write scope it names. The last
|
|
489
|
+
returns an `AdmittedGit`; the door forwards reads to `.read` and pushes to
|
|
490
|
+
`.push`. Provider credentials stay inside the storage adapter. `createOyster`
|
|
491
|
+
builds these ports from `host.auth`, `host.installations` and `host.git`, so
|
|
492
|
+
most hosts set `host.git` and never call `createGitDoor` directly.
|
|
493
|
+
|
|
494
|
+
A host that mints Workflow Git passes supplies `workflowGitPass` in
|
|
495
|
+
`oysterGitRoot` or `oysterWorkerGit` options. A pass travels only as an
|
|
496
|
+
`Authorization: Bearer` header starting with `workflowGitPassPrefix` (`wgp.`) on
|
|
497
|
+
the ordinary credential-free door URL; it is never part of a URL. That header is
|
|
498
|
+
admitted only by the port, for one repository and scope, as the arming member
|
|
499
|
+
under the Workflow's branch grant, and never as a session or key. Pushes outside
|
|
500
|
+
those branches are refused before the App's push policy runs.
|
|
501
|
+
|
|
502
|
+
### Provide App capabilities
|
|
503
|
+
|
|
504
|
+
Each `OysterCapability` supplies its Agar `id` and `version`, a mount `base`,
|
|
505
|
+
relative Hono `routes`, an `admits(principal, installation)` decision, and
|
|
506
|
+
`methods`. Each method descriptor has `kind`, `method` (HTTP method), and `path`
|
|
507
|
+
(relative route). `CapabilityMethodKind` is `json | transport | read | write`:
|
|
508
|
+
JSON calls return decoded data, transports return Fetch handles, and
|
|
509
|
+
reads/writes return Agar's stream handles.
|
|
510
|
+
|
|
511
|
+
Method paths may contain named parameters, such as `:agentId/sessions`. Oyster
|
|
512
|
+
uses `path-to-regexp` to compile input fields into those parameters; remaining
|
|
513
|
+
scalar fields become query parameters for GET and handle calls. Other JSON calls
|
|
514
|
+
keep their remaining fields in the request body. Transport handles append their
|
|
515
|
+
Fetch pathname below that resolved route and may declare `method: "ANY"` to
|
|
516
|
+
retain the request's HTTP method. JSON results, including Agar's
|
|
517
|
+
`{ kind: "pending", retryAfterMs }`, reach the App unchanged; the page does not
|
|
518
|
+
poll on the App's behalf.
|
|
519
|
+
|
|
520
|
+
For example, a directory method uses
|
|
521
|
+
`{ kind: "json", method: "GET", path: "list" }`; a file read uses
|
|
522
|
+
`{ kind: "read", method: "GET", path: "content" }`. `fileCapabilities(source)`
|
|
523
|
+
from `@fungi.computer/oyster/files` supplies the three file contracts over the
|
|
524
|
+
existing `/files/` GET, PUT and POST routes.
|
|
525
|
+
|
|
526
|
+
The page offers a capability only when the installed Release declares its ID and
|
|
527
|
+
`admits` allows the principal. Visitors receive none. Every route request also
|
|
528
|
+
rechecks that declaration and admission using the `X-Oyster-Installation` header
|
|
529
|
+
set by the page; a read grant does not authorize the write route sharing its
|
|
530
|
+
file mount.
|
|
531
|
+
|
|
532
|
+
Consent approves capabilities, like OAuth scopes. An App upgrade may add a
|
|
533
|
+
method that uses an already-approved capability without asking again. A new
|
|
534
|
+
capability always needs approval. Release selection intersects stored operation
|
|
535
|
+
and resource approvals; it never adds a grant. Every Worker call receives only
|
|
536
|
+
the bindings allowed by that method's declared `requires`, rather than all the
|
|
537
|
+
App's approved capabilities.
|
|
538
|
+
|
|
539
|
+
The `agent.manage` consent wording is **Can create Agents; write abilities need
|
|
540
|
+
your approval.**
|
|
541
|
+
|
|
542
|
+
The `apps.call` permission also shows the declared target Apps and methods. Its
|
|
543
|
+
route approvals retain those exact pairs: adding or renaming a target method
|
|
544
|
+
needs owner approval. On Oyster these page calls run as the signed-in member
|
|
545
|
+
through the target App's ordinary method gate; Agents, keys and visitors cannot
|
|
546
|
+
use this path. The target receives its own grants, never the caller App's
|
|
547
|
+
grants. The Hub shows the same consent list but does not offer this page
|
|
548
|
+
capability.
|
|
549
|
+
|
|
550
|
+
### Connect an agent over MCP
|
|
551
|
+
|
|
552
|
+
Connect your MCP client to `https://<your-oyster>/mcp` with
|
|
553
|
+
`Authorization: Bearer <key>`. Installed App methods are available through
|
|
554
|
+
`execute` as `env.<APP>`; a dotted method is a nested property, e.g.
|
|
555
|
+
`env.BONSAI.repos.list({})`; discover them with `forage.search` and
|
|
556
|
+
`forage.describe`. A key's or Agent's App writes run under the owner's stored
|
|
557
|
+
approval of its App tools (recorded when the owner creates the key), so a
|
|
558
|
+
headless client such as `claude -p` can write. That approval already covers
|
|
559
|
+
every App on this Oyster, including ones installed after the key existed, with
|
|
560
|
+
no per-call prompt; anyone holding the key can act as the owner in those Apps,
|
|
561
|
+
and revoking the key is what stops it. Every other write, including the owner's
|
|
562
|
+
own calls, requires a client that supports form elicitation and explicit
|
|
563
|
+
approval; anonymous callers are refused.
|
|
564
|
+
|
|
565
|
+
When the host has a Computer, the catalog also holds it as
|
|
566
|
+
`env.computers.workspace`, e.g.
|
|
567
|
+
`env.computers.workspace.exec({ command: "ls", cwd: "/workspace" })`. Its
|
|
568
|
+
operations are the ones every Computer advertises (`read`, `list`, `write`,
|
|
569
|
+
`mkdir`, `bash`, `exec` and the process tools), and each call asks for approval.
|
|
570
|
+
The host supplies `computer(principal)` on its workspace: it returns the
|
|
571
|
+
Computer's calls for a caller with Computer access and `null` otherwise. The
|
|
572
|
+
standalone host gives an owner, key or member the same Computer grant its
|
|
573
|
+
`/computer` routes use; visitors and Agents get none. The Computer accepts every
|
|
574
|
+
call against that grant. `oysterComputerTools(host, principal)` from
|
|
575
|
+
`@fungi.computer/oyster/mcp` returns this Knapsack source and its `grants`.
|
|
576
|
+
|
|
577
|
+
Workers route every MCP request to the tenant's `MCP` Durable Object. Export
|
|
578
|
+
`OysterMcp` from `oyster/workers` for the default host, or derive it from
|
|
579
|
+
`createOysterGitMcpOwner(root)` with the same root passed to
|
|
580
|
+
`createOysterGitWorker(root)`. Declare the `MCP` binding and SQLite migration as
|
|
581
|
+
shown in the standalone example, and configure `OYSTER_ORIGIN`.
|
|
582
|
+
|
|
583
|
+
The owner authenticates every request. SDK session IDs, event data and closure
|
|
584
|
+
belong to that owner; deleting a session releases its catalog connection. If an
|
|
585
|
+
owner restarts and cannot restore its live SDK transport, the old ID
|
|
586
|
+
returns 404. Close the client's old transport and initialize a fresh session.
|
|
587
|
+
Pending execution and elicitation are never replayed. Node hosts retain their
|
|
588
|
+
explicit MCP adapter and call its `close()` when shutting down.
|
|
589
|
+
|
|
590
|
+
Agent and key adapters use `await oysterAppMethods(host, principal, access)`
|
|
591
|
+
from `@fungi.computer/oyster/mcp`. Pass the result in Mycelium's `sources`; its
|
|
592
|
+
`grants` names each captured module at the level (`read`/`write`) the caller
|
|
593
|
+
actually holds for it, so Mycelium itself refuses a write call up front when a
|
|
594
|
+
module's own grant is `read`. For an Agent, `access` is
|
|
595
|
+
`{kind: "agent", policy}`, the host's one stored, blanket
|
|
596
|
+
`{level, revision, grantedBy}` approval; missing it grants no tool access. For a
|
|
597
|
+
named key, `access` is `{kind: "key", keyId, byInstallation}`, one
|
|
598
|
+
`{level, revision, grantedBy}` per installation the key's own Ear Tag grants
|
|
599
|
+
admit (`keyAppAccess`); an installation the key has no grant for is absent from
|
|
600
|
+
`byInstallation` and never listed at all, not merely refused. A key's grants are
|
|
601
|
+
set once, at `POST /api/keys`, and never edited: to change them the owner
|
|
602
|
+
revokes the key (`POST /api/keys/revoke`) and issues a new one. A module grant
|
|
603
|
+
does not approve an App operation. The gate also intersects the installation's
|
|
604
|
+
explicit approvals with its verified Release; `external` methods are refused
|
|
605
|
+
with `approval_required`. MCP uses this same projection. Capture again when tool
|
|
606
|
+
policy, installation authority, a key's own grants or a Release changes; the
|
|
607
|
+
source represents one immutable catalog snapshot.
|
|
608
|
+
|
|
609
|
+
### Approve an Agent's App tools
|
|
610
|
+
|
|
611
|
+
An App with `agent.manage` can create Agents and request tool-policy changes.
|
|
612
|
+
The host records the admitted installation; those requests remain pending and
|
|
613
|
+
have no `grantedBy`. Only direct owner approval grants App tools. The standalone
|
|
614
|
+
owner key resolves through the seeded tenancy registry: `grantedBy` is the
|
|
615
|
+
canonical stored member ID, while `OYSTER_OWNER_NAME` is an identity label. The
|
|
616
|
+
stored approval pins that member's `membershipVersion`; demotion and
|
|
617
|
+
re-promotion, removal and re-addition, or suspension and reactivation require
|
|
618
|
+
new approval. Old policy rows also require re-approval.
|
|
619
|
+
|
|
620
|
+
The authenticated owner reads `GET /api/agent-policies/:agentId` for the current
|
|
621
|
+
`expectedRevision`, then sends `PUT` to the same path with
|
|
622
|
+
`{"level":"write","expectedRevision":"<the returned revision>"}` (or `null` when
|
|
623
|
+
no policy exists). These host-owned routes reject an App installation header.
|
|
624
|
+
The Agent must have its host-minted directory/tenant pin; another tenant or an
|
|
625
|
+
unpinned Agent refuses. A stale revision returns 409, an unknown Agent returns
|
|
626
|
+
404, and malformed approval input returns 400.
|
|
627
|
+
|
|
628
|
+
### Connect an Agent to a remote MCP server
|
|
629
|
+
|
|
630
|
+
The owner connects any Streamable HTTP MCP server to one Agent. The Agent's
|
|
631
|
+
codemode then sees its tools as `env.mcp.<name>.<tool>()`, beside the installed
|
|
632
|
+
Apps; Knapsack's `mcpServers` connection owns the client. On a standalone Oyster
|
|
633
|
+
the Agent's own Durable Object keeps the endpoint and an optional bearer token,
|
|
634
|
+
and no route returns the token.
|
|
635
|
+
|
|
636
|
+
`GET /api/agent-mcp-servers` lists every Agent with its servers (`serverId`,
|
|
637
|
+
`name`, `url`, `hasBearer`). `POST /api/agent-mcp-servers/:agentId` with
|
|
638
|
+
`{"name":"linear","url":"https://mcp.example.com/mcp","bearer":null}` connects
|
|
639
|
+
one; the name is lowercase letters, digits and `_`, and the endpoint must be
|
|
640
|
+
`https:` with no credentials or fragment. A repeated name or more than 16
|
|
641
|
+
servers returns 409. `DELETE /api/agent-mcp-servers/:agentId/:serverId`
|
|
642
|
+
disconnects one. These routes are owner-direct and reject an App installation
|
|
643
|
+
header. A server that is down or refuses the token is left out of that run and
|
|
644
|
+
the others stay available. OAuth sign-in is not offered yet.
|
|
645
|
+
|
|
646
|
+
### Pack an App
|
|
647
|
+
|
|
648
|
+
`packRelease` composes `oyster/build`'s source capture, candidate preparer,
|
|
649
|
+
compiler and local artifact writer. Pass `sourceDirectory` as the repository
|
|
650
|
+
root, an explicit `sourceRevision` (`git:<full commit SHA>`), and `recipe`.
|
|
651
|
+
Every entry is relative to that captured repository root. Dirty files and
|
|
652
|
+
external build outputs are not inputs. The result retains `release.json` plus
|
|
653
|
+
`artifacts/`, ready for `installRelease`. This example packs the
|
|
654
|
+
[Notes example App](https://github.com/fungi-computer/oyster/tree/main/examples/notes-app)
|
|
655
|
+
kept at the root of its own git repository, `./notes-app`:
|
|
656
|
+
|
|
657
|
+
```ts
|
|
658
|
+
import { execFileSync } from "node:child_process";
|
|
659
|
+
import { packRelease } from "@fungi.computer/oyster/pack";
|
|
660
|
+
|
|
661
|
+
const commit = execFileSync("git", ["-C", "./notes-app", "rev-parse", "HEAD"], {
|
|
662
|
+
encoding: "utf8",
|
|
663
|
+
}).trim();
|
|
664
|
+
|
|
665
|
+
await packRelease({
|
|
666
|
+
appId: "app_notes",
|
|
667
|
+
sourceDirectory: "./notes-app",
|
|
668
|
+
sourceRevision: `git:${commit}`,
|
|
669
|
+
recipe: {
|
|
670
|
+
entry: "ui/index.html",
|
|
671
|
+
assets: {
|
|
672
|
+
maxManifestBytes: 1_048_576,
|
|
673
|
+
maxObjects: 1024,
|
|
674
|
+
maxObjectBytes: 16_777_216,
|
|
675
|
+
maxTotalBytes: 67_108_864,
|
|
676
|
+
},
|
|
677
|
+
assetBase: "./",
|
|
678
|
+
maxOutputBytes: 67_108_864,
|
|
679
|
+
backend: {
|
|
680
|
+
entry: "backend/index.ts",
|
|
681
|
+
exportName: "Notes",
|
|
682
|
+
compatibilityDate: "2026-09-04",
|
|
683
|
+
runtime: { kind: "durable-facet", stateSchema: 1 },
|
|
684
|
+
},
|
|
685
|
+
},
|
|
686
|
+
outDirectory: "./notes-release",
|
|
687
|
+
});
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
The canonical compiler reads the nearest package's `fungi.app` declaration,
|
|
691
|
+
including capabilities and commands for UI-only Apps. Backend Apps declare
|
|
692
|
+
`fungi.backendDefinition`; the emitter derives the API and declaration from that
|
|
693
|
+
definition and `fungi.app` in the same build that produces the Worker.
|
|
694
|
+
Handwritten package API methods are refused. Closed source HTML recipes
|
|
695
|
+
(including Notes, Bonsai and Inkcap) build captured source.
|
|
696
|
+
`kind: "bundled-html"` instead accepts committed, ready assets such as
|
|
697
|
+
`bundle/index.html`, with an explicit asset policy.
|
|
698
|
+
|
|
699
|
+
`installFromSource` accepts `appId`, `recipe`,
|
|
700
|
+
`source: {remote, token, sourceRevision}` and the destination Oyster's `url`,
|
|
701
|
+
`key` and `installationId`. It uses the maintained Artifacts Git capture, the
|
|
702
|
+
same build/writer composition, then `installRelease`, and cleans its temporary
|
|
703
|
+
output. Source credentials and the destination owner key remain separate.
|
|
704
|
+
`installRelease` uploads a packed Release and makes one
|
|
705
|
+
`POST /api/installations` naming the installation. The host's one
|
|
706
|
+
install-or-select owner installs it (201) or, when the installation already
|
|
707
|
+
exists, switches it to that Release (200); the App keeps its state, and every
|
|
708
|
+
other refusal is returned unchanged.
|
|
709
|
+
|
|
710
|
+
## Build a shell
|
|
711
|
+
|
|
712
|
+
`@fungi.computer/oyster/shell` supplies the client side of hosting Apps. It can
|
|
713
|
+
be imported independently of the Workers server and Node packing helpers. The
|
|
714
|
+
same exports are available through focused entries:
|
|
715
|
+
|
|
716
|
+
| Entry | Contract |
|
|
717
|
+
| ------------------------ | ----------------------------------------------------------------------------------- |
|
|
718
|
+
| `./shell/workbench` | Workbench host contract, shared child-window host, Gill layout and arranger shapes. |
|
|
719
|
+
| `./shell/command-host` | A Whistle graph that admits mounted commands against verified Release declarations. |
|
|
720
|
+
| `./shell/keys` | DOM key dispatch and shared chord matching. |
|
|
721
|
+
| `./shell/commands` | Window, App picker and command palette declarations over host callbacks. |
|
|
722
|
+
| `./shell/window-manager` | Window generations, visibility, navigation, cancellation and disposal. |
|
|
723
|
+
| `./shell/app-directory` | App addresses, reusable directory rows and catalog metadata. |
|
|
724
|
+
|
|
725
|
+
`./shell/shell-module` is the contract between a host and the shell it runs, and
|
|
726
|
+
is imported on its own rather than through `./shell`. A shell Release exports
|
|
727
|
+
`mountShell(root, input)` (`MountShell`); `input` (`ShellMountInput`) carries
|
|
728
|
+
the workbench handle, the shell installation's `AppStorage` and
|
|
729
|
+
`methods.call(method, input)` for its declared backend methods, which takes the
|
|
730
|
+
input as JSON text and resolves with the decoded JSON result.
|
|
731
|
+
`importShellModule(moduleUrl, hostOrigin)` imports the picked shell's module
|
|
732
|
+
from the host's own origin, checks that it exports `mountShell`, and rejects
|
|
733
|
+
with `ShellModuleError` otherwise. The host never bundles a shell.
|
|
734
|
+
|
|
735
|
+
Supply your own mount adapter to `createWindowManager`. It receives the
|
|
736
|
+
container, launch descriptor, cancellation signal and navigation callbacks; its
|
|
737
|
+
returned handle owns visibility, reconfiguration and disposal. The manager
|
|
738
|
+
disposes stale mounts and serializes changes to each window. It neither picks
|
|
739
|
+
Apps nor grants capabilities.
|
|
740
|
+
|
|
741
|
+
`createAppSurfaceCommandHost` owns command contribution and focus. Give
|
|
742
|
+
`mount(id, declarations)` the verified Release's declarations and pass the
|
|
743
|
+
returned handlers to Agar's `connectAppHost`. A mounted App cannot introduce an
|
|
744
|
+
undeclared command or claim the reserved shell chord. Dispose its command mount
|
|
745
|
+
when its iframe closes, and dispose the command host with the shell.
|
|
746
|
+
`installShellKeys` attaches DOM listeners only when called; its return value
|
|
747
|
+
removes them. Terminal shells can use the command and window owners without a
|
|
748
|
+
DOM.
|
|
749
|
+
|
|
750
|
+
Directory metadata is not launch authority. A host extends the public row schema
|
|
751
|
+
with its own permission model and admits each mount itself. Fungi's Team
|
|
752
|
+
directory, Team settings capability and signed Hub/host protocol live in its
|
|
753
|
+
private consumers; the public shell imports none of them.
|
|
754
|
+
|
|
755
|
+
`@fungi.computer/oyster/page` mounts one installed App's page and connects its
|
|
756
|
+
sandboxed frame with that installation's admitted slots. That page is only the
|
|
757
|
+
framed App: menus, the command palette, status bars and every other piece of
|
|
758
|
+
desktop chrome belong to the shell.
|
|
759
|
+
|
|
760
|
+
### The shell is trusted
|
|
761
|
+
|
|
762
|
+
A shell is not a framed App. When an owner opens the home installation, the page
|
|
763
|
+
is served with an empty `#oyster-shell` root and the host's page script renders
|
|
764
|
+
the shell there, in the top-level document. The shell runs with the host's
|
|
765
|
+
escalated permissions: it reads the installation directory and opens, arranges
|
|
766
|
+
and closes every App. Choose a shell you trust, as you would choose the host
|
|
767
|
+
itself.
|
|
768
|
+
|
|
769
|
+
Child Apps stay isolated. `WorkbenchHost.mount(container, launch, events)`
|
|
770
|
+
mounts each App in its own sandboxed, cross-origin frame (`appSandbox`), inside
|
|
771
|
+
the `container` slot the shell hands it. The shell keeps one stable slot per
|
|
772
|
+
window and moves that window with transforms, so the frame moves, clips to the
|
|
773
|
+
window's rounded corners and stacks below shell menus in the same rendered
|
|
774
|
+
frame. Slots are never re-parented, and a minimized window keeps its frame
|
|
775
|
+
mounted, so restoring it never reloads the App. A click inside a frame never
|
|
776
|
+
reaches the shell; the App's own focus report raises its window. Every child
|
|
777
|
+
keeps its own configuration and capabilities; nothing granted to the shell is
|
|
778
|
+
lent to it. Read the home installation's own handles with
|
|
779
|
+
`oysterShellHandles(readOysterPageConfig())`, and connect each child frame with
|
|
780
|
+
`connectOysterApp`.
|
|
781
|
+
|
|
782
|
+
The standalone root chooses its home by installation ID. Oyster core imports no
|
|
783
|
+
home App: it serves the selected enabled installation, or its installation list
|
|
784
|
+
when no home is available. `@fungi.computer/oyster/root-data` adapts the
|
|
785
|
+
existing installation and verified Release owners into shared workbench metadata
|
|
786
|
+
and opens the existing SQLite App-storage owner with fresh admission. The
|
|
787
|
+
deployable Bonsai root mounts Tourtois's own `PlatformControlsView`, offering
|
|
788
|
+
desktop and App settings. Unsupported hosted administration and platform
|
|
789
|
+
destinations are not offered; Computer inventory and Gill resource reads
|
|
790
|
+
currently refuse.
|
|
791
|
+
|
|
792
|
+
The workbench host handle offers `memberAvatar()` as a read-only projection of
|
|
793
|
+
the signed-in member's catalog-validated avatar preference. Consumers refresh
|
|
794
|
+
that read when host settings change. Account writes remain Hub-owned.
|
|
795
|
+
|
|
796
|
+
## Core concepts
|
|
797
|
+
|
|
798
|
+
### Owner state and selection
|
|
799
|
+
|
|
800
|
+
The host calls `apply` with the complete owner-issued state, then `fetch` or
|
|
801
|
+
`call` with the admitted selection. Both identify the Instance, owner, App,
|
|
802
|
+
Release and authority version. Oyster persists the version the owner supplied;
|
|
803
|
+
it never increments another owner's authority or trusts a guest's claim to it.
|
|
804
|
+
|
|
805
|
+
| Owner state | Runtime behavior |
|
|
806
|
+
| ----------- | --------------------------------------------------------------- |
|
|
807
|
+
| `enabled` | Resolve and activate the eligible backend. |
|
|
808
|
+
| `disabled` | Fence new requests and abort connections while retaining state. |
|
|
809
|
+
| `deleted` | Record deletion, abort, purge state and retain a tombstone. |
|
|
810
|
+
|
|
811
|
+
An exact owner-state retry returns its recorded result. Changed input at the
|
|
812
|
+
same version conflicts. Older versions are stale. Disable or delete can be the
|
|
813
|
+
first state delivered. Re-enable requires a later owner operation. A same-schema
|
|
814
|
+
Release replacement keeps backend state. A changed schema fails with
|
|
815
|
+
`state_schema_migration_unsupported`; Oyster invents no migration policy.
|
|
816
|
+
|
|
817
|
+
### Ports
|
|
818
|
+
|
|
819
|
+
`createAppInstance` is the Effect core. Its host supplies `read`, `write`,
|
|
820
|
+
`resolve`, `abort`, `purge` and `connect` for one stable Instance ID. `resolve`
|
|
821
|
+
returns the selected Release's verified durable backend: runtime profile,
|
|
822
|
+
state-schema version, compatibility date, export name and bundled ESM under an
|
|
823
|
+
immutable digest. Admission takes the lifecycle lock while acquiring a
|
|
824
|
+
connection; waiting on App code does not hold that lock, so disable and delete
|
|
825
|
+
can abort in-flight work.
|
|
826
|
+
|
|
827
|
+
### Scoped App storage
|
|
828
|
+
|
|
829
|
+
`@fungi.computer/oyster/app-storage` exports `createAppStorage`, the SQLite
|
|
830
|
+
owner for declared App keys. Supply the database connection and an atomic
|
|
831
|
+
`commit` callback. `open(authorize)` binds a member view;
|
|
832
|
+
`openInstallation(authorize)` binds lifecycle work to the shared team layer.
|
|
833
|
+
Each operation rechecks live admission. Member-scoped lifecycle reads and writes
|
|
834
|
+
reject with `owner_required`.
|
|
835
|
+
|
|
836
|
+
The standalone backend receives `env.APP_STORAGE` only for operations admitted
|
|
837
|
+
by its verified declaration and approved grants. Export `OysterBinding`, the one
|
|
838
|
+
guest binding entrypoint, beside `OysterInstance` and `OysterRegistry` from your
|
|
839
|
+
Worker entry. A root that omits it refuses every backend frame with
|
|
840
|
+
`binding_unavailable`. Backend calls admit only the current instance owner;
|
|
841
|
+
other callers receive `owner_required`. Reads, writes and resets use the same
|
|
842
|
+
storage owner and invalidation stream as the UI. On the Hub, the live owner
|
|
843
|
+
admission also allows an owner's attached Agent to edit a declared Team layer;
|
|
844
|
+
losing owner authority refuses the next mutation. Storage events carry the key,
|
|
845
|
+
scope and revision; they do not identify an Agent.
|
|
846
|
+
|
|
847
|
+
Writes and resets check the expected revision, declared scopes, size and quotas.
|
|
848
|
+
After commit, `events(key)` publishes bounded invalidations; its first `null`
|
|
849
|
+
value marks an armed subscription. Installation and membership deletion use
|
|
850
|
+
`purgeAppStorageInCommit` and `purgeAppMemberStorageInCommit` inside the
|
|
851
|
+
caller's transaction. `appStorageSchemaStatements` retains the existing SQLite
|
|
852
|
+
table names.
|
|
853
|
+
|
|
854
|
+
### Durable Object composition
|
|
855
|
+
|
|
856
|
+
`createCloudflareAppInstance` keeps supervisor state in a SQLite Durable Object
|
|
857
|
+
and runs the backend in a named `app` facet loaded through the Worker Loader.
|
|
858
|
+
Supervisor metadata and the backend's database are separate, and a new Release
|
|
859
|
+
keeps the facet's database. The loader key includes the Instance ID and artifact
|
|
860
|
+
digest, so separate Instances of one Release never share guest module scope.
|
|
861
|
+
Guest code receives only the `bindings()` its host builds, no ambient outbound
|
|
862
|
+
access, and requests carry only `Content-Type` and the host-admitted actor,
|
|
863
|
+
Computer and resource headers.
|
|
864
|
+
|
|
865
|
+
## API reference
|
|
866
|
+
|
|
867
|
+
The core is `@fungi.computer/oyster`; the Durable Object adapter is
|
|
868
|
+
`@fungi.computer/oyster/cloudflare`. The generated
|
|
869
|
+
[API reference](docs/api/README.md) is included in the tarball.
|
|
870
|
+
|
|
871
|
+
## Examples
|
|
872
|
+
|
|
873
|
+
`examples/standalone` and `examples/git` ship in the npm package, ready to copy
|
|
874
|
+
as in the quick start. The generated API reference and the in-memory example
|
|
875
|
+
below ship with them. The rest live in the
|
|
876
|
+
[Oyster repository](https://github.com/fungi-computer/oyster):
|
|
877
|
+
|
|
878
|
+
- `examples/standalone` is the complete single-owner Oyster from the quick
|
|
879
|
+
start.
|
|
880
|
+
- `examples/git` adds the git door to an Oyster host on Workers, as described in
|
|
881
|
+
"Add Git to your host" under Run your own Oyster.
|
|
882
|
+
- `examples/notes-app` is a full App: UI, backend methods and SQLite. Copy it to
|
|
883
|
+
start an App with a backend. `test/standalone.native.mjs` packs it, installs
|
|
884
|
+
it on the standalone Oyster and drives its UI in a browser, on workerd and on
|
|
885
|
+
celld.
|
|
886
|
+
- `examples/notes.js` is a small backend with its own SQLite and no external
|
|
887
|
+
grants. It reads or writes one note.
|
|
888
|
+
- `src/testing/worker.ts` with `wrangler.test.jsonc` hosts Instances in a
|
|
889
|
+
Durable Object with a Worker Loader. The same pre-bundled Worker runs under
|
|
890
|
+
`wrangler dev` (workerd) and `celld dev`.
|
|
891
|
+
- `test/core.mjs` exercises replay, state transitions and owner fences in plain
|
|
892
|
+
Node.
|
|
893
|
+
- <https://oyster.shiit.app/> is the complete deployment: the git root with
|
|
894
|
+
Fungi's App set, Workflows, Computers, Agents and media in one Worker.
|
|
895
|
+
|
|
896
|
+
### Drive one Instance in memory
|
|
897
|
+
|
|
898
|
+
This self-contained example drives one Instance through in-memory ports. A
|
|
899
|
+
disabled Instance never resolves or connects a backend, so no Release is needed.
|
|
900
|
+
The request runs in a frame from `createInvocationGate`, the same gate the
|
|
901
|
+
standalone Instances and the Hub open for every call; the example is
|
|
902
|
+
`docs/home.ts`, which the package docs check typechecks.
|
|
903
|
+
|
|
904
|
+
```ts
|
|
905
|
+
import * as Effect from "effect/Effect";
|
|
906
|
+
import * as Schema from "effect/Schema";
|
|
907
|
+
import {
|
|
908
|
+
AppInstanceError,
|
|
909
|
+
createAppInstance,
|
|
910
|
+
instanceIds,
|
|
911
|
+
ownerStateSchema,
|
|
912
|
+
type Instance,
|
|
913
|
+
} from "@fungi.computer/oyster";
|
|
914
|
+
import {
|
|
915
|
+
APP_INVOCATION_CEILING_MS,
|
|
916
|
+
createInvocationGate,
|
|
917
|
+
invocationFactsSchema,
|
|
918
|
+
} from "@fungi.computer/oyster/invocation";
|
|
919
|
+
|
|
920
|
+
const instanceId = instanceIds.instance.generate();
|
|
921
|
+
let stored: Instance | null = null;
|
|
922
|
+
const instance = createAppInstance({
|
|
923
|
+
instanceId,
|
|
924
|
+
read: () => Effect.succeed(stored),
|
|
925
|
+
write: (record) =>
|
|
926
|
+
Effect.sync(() => {
|
|
927
|
+
stored = record;
|
|
928
|
+
}),
|
|
929
|
+
resolve: () => Effect.fail(new Error("No Releases in this example")),
|
|
930
|
+
abort: () => Effect.void,
|
|
931
|
+
purge: () => Effect.void,
|
|
932
|
+
connect: () => Effect.fail(new Error("No backend in this example")),
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
const selection = {
|
|
936
|
+
instanceId,
|
|
937
|
+
owner: { kind: "installation", id: "installation_example" },
|
|
938
|
+
appId: "app_example",
|
|
939
|
+
releaseId: "rel_example",
|
|
940
|
+
authorityVersion: 2,
|
|
941
|
+
} as const;
|
|
942
|
+
const disabled = Schema.decodeUnknownSync(ownerStateSchema)({
|
|
943
|
+
...selection,
|
|
944
|
+
status: "disabled",
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
const receipt = await Effect.runPromise(instance.apply(disabled));
|
|
948
|
+
console.log(receipt.status); // "disabled"
|
|
949
|
+
|
|
950
|
+
// A request runs inside an invocation frame: the caller facts the host admitted,
|
|
951
|
+
// a deadline, and an abort signal the gate owns.
|
|
952
|
+
const gate = createInvocationGate<null>({
|
|
953
|
+
abortGuest: () => Promise.resolve(),
|
|
954
|
+
close: () => Promise.resolve(),
|
|
955
|
+
});
|
|
956
|
+
const facts = Schema.decodeUnknownSync(invocationFactsSchema)({
|
|
957
|
+
instanceId,
|
|
958
|
+
selection,
|
|
959
|
+
artifactDigest: `sha256:${"0".repeat(64)}`,
|
|
960
|
+
subject: {
|
|
961
|
+
kind: "call",
|
|
962
|
+
context: {
|
|
963
|
+
actor: { kind: "member", memberId: "member_example" },
|
|
964
|
+
computerId: null,
|
|
965
|
+
},
|
|
966
|
+
},
|
|
967
|
+
grants: [],
|
|
968
|
+
ancestry: [],
|
|
969
|
+
deadlineAt: Date.now() + APP_INVOCATION_CEILING_MS,
|
|
970
|
+
});
|
|
971
|
+
const refused = await gate.run(facts, null, (frame) =>
|
|
972
|
+
Effect.runPromise(
|
|
973
|
+
Effect.result(instance.fetch(frame, new Request("https://app.test/"))),
|
|
974
|
+
),
|
|
975
|
+
);
|
|
976
|
+
if (refused._tag === "Failure" && refused.failure instanceof AppInstanceError)
|
|
977
|
+
console.log(refused.failure.code); // "instance_inactive"
|
|
978
|
+
```
|
|
979
|
+
|
|
980
|
+
## Limits and errors
|
|
981
|
+
|
|
982
|
+
Typed `AppInstanceError` codes distinguish stale authority, mismatched identity,
|
|
983
|
+
inactive or deleted state and unsupported schema replacement. The host decides
|
|
984
|
+
their HTTP presentation and retry behavior. Rejected admission is not a
|
|
985
|
+
successful empty backend response.
|
|
986
|
+
|
|
987
|
+
Deletion persists `deleting` before purge and a `deleted` tombstone afterwards.
|
|
988
|
+
A later access resumes an interrupted purge; this is access-driven recovery, not
|
|
989
|
+
an autonomous scheduler. Recreating a facet cannot bypass the tombstone.
|
|
990
|
+
|
|
991
|
+
Oyster is not admission. It does not decide who may install, enable or call an
|
|
992
|
+
App; the host must obtain that authority from its owner.
|
|
993
|
+
|
|
994
|
+
## Build App Releases
|
|
995
|
+
|
|
996
|
+
`@fungi.computer/oyster/build` owns the Node source capture, pinned compiler,
|
|
997
|
+
declaration emission and local artifact writer used by the private Publisher.
|
|
998
|
+
`createLocalCommitSourceCapture` captures one Git commit into a scoped snapshot;
|
|
999
|
+
`createAppCandidatePreparer` builds it with `createLocalBuildExecutor` and keeps
|
|
1000
|
+
that commit in the candidate's `sourceRevision`. `commitPreparedCandidate`
|
|
1001
|
+
persists its output through the supplied writer and Release catalog.
|
|
1002
|
+
|
|
1003
|
+
Workers use `@fungi.computer/oyster/build-contract` for the same recipe schemas,
|
|
1004
|
+
normalization, digest and provider-neutral Effect orchestration. That entry has
|
|
1005
|
+
no filesystem, subprocess or compiler imports. Builds execute in Node within the
|
|
1006
|
+
host's trusted build environment; they do not execute inside Oyster request
|
|
1007
|
+
handlers.
|
|
1008
|
+
|
|
1009
|
+
Recipe paths are relative to the captured Git repository root. For an App in
|
|
1010
|
+
`apps/notes`, use `apps/notes/ui/index.html` and `apps/notes/backend/index.ts`;
|
|
1011
|
+
capture does not rebase paths to the App directory. The nearest `package.json`
|
|
1012
|
+
above the entry supplies `fungi.app`, plus `fungi.backendDefinition` for an API
|
|
1013
|
+
derived from the compiled backend.
|
|
1014
|
+
|
|
1015
|
+
A captured UI bundle uses `kind: "bundled-html"`, an `entry` ending in
|
|
1016
|
+
`/index.html`, `assetBase: "./"`, and an explicit `assets` policy with
|
|
1017
|
+
`maxManifestBytes`, `maxObjects`, `maxObjectBytes`, and `maxTotalBytes`. Set
|
|
1018
|
+
`backend: null` for UI only, or supply a repository-relative `backend.entry`
|
|
1019
|
+
with the backend runtime, export name, and compatibility date. The compiler
|
|
1020
|
+
joins both outputs from that same snapshot. A backend-only recipe uses its
|
|
1021
|
+
Worker entry as `entry` and omits `backend.entry`. Existing first-party HTML
|
|
1022
|
+
recipes retain their pinned steps and digests.
|
|
1023
|
+
|
|
1024
|
+
For UI source, omit `kind` and use a closed HTML recipe entry, such as
|
|
1025
|
+
`ui/index.html` for Notes at its repository root. It requires `assets` and
|
|
1026
|
+
`assetBase`, and accepts `backend.entry` in the same recipe. The compiler
|
|
1027
|
+
installs the captured lockfile, emits the catalog's ordered workspace TypeScript
|
|
1028
|
+
projects, compiles declared stylesheets, and builds the source HTML with Vite.
|
|
1029
|
+
Notes uses plain TypeScript; Bonsai uses React and Tailwind. Neither reads
|
|
1030
|
+
prebuilt workspace `dist` directories or an external UI output directory.
|
|
1031
|
+
|
|
1032
|
+
`createLocalArtifactWriter(directory, recipe.assets)` writes the App root and
|
|
1033
|
+
all referenced UI/backend objects. It uses the supplied asset bounds and
|
|
1034
|
+
maintained MIME metadata. Bundled UI plus a backend may hydrate a captured
|
|
1035
|
+
`pnpm-lock.yaml` with the pinned `pnpm@11.24.0`, frozen lockfile and no
|
|
1036
|
+
lifecycle scripts or pnpmfile; it does not run arbitrary package build scripts.
|
|
1037
|
+
|
|
1038
|
+
For Artifacts, `createArtifactsSourceCapture({ remote, token, sourceRevision })`
|
|
1039
|
+
accepts the host's Git URL and a `git:<commit>` revision. Native Git receives
|
|
1040
|
+
the credential through its environment, refuses redirects, and captures that
|
|
1041
|
+
exact commit even if the branch advances. A `git.push` Workflow can prepare the
|
|
1042
|
+
candidate in Node, commit it through the artifact writer and Release catalog,
|
|
1043
|
+
then upload/install the Release through `oyster/pack`.
|
|
1044
|
+
|
|
1045
|
+
## Contribute and license
|
|
1046
|
+
|
|
1047
|
+
Oyster is developed inside Fungi's own workspace with the rest of the family and
|
|
1048
|
+
released to this repository with its history. There,
|
|
1049
|
+
`pnpm --filter @fungi.computer/oyster test` runs the lifecycle checks,
|
|
1050
|
+
`pnpm --filter @fungi.computer/oyster build` compiles the package, and
|
|
1051
|
+
`test:standalone` and `test:standalone:celld` run a real Oyster under workerd
|
|
1052
|
+
and under celld, install the Notes example App, call its methods and drive its
|
|
1053
|
+
UI in a browser. The package uses the MIT license in [`LICENSE`](LICENSE).
|
|
1054
|
+
|
|
1055
|
+
## Hosting many tenants
|
|
1056
|
+
|
|
1057
|
+
Pass your own `OysterTenancy` from `@fungi.computer/oyster/tenant` as the
|
|
1058
|
+
`tenancy` of `oysterGitRoot({ tenancy })`. Its `resolve(request)` selects one
|
|
1059
|
+
tenant from the request origin before sign-in and returns that tenant's
|
|
1060
|
+
`TenantScope` (built with `tenantScope(tenantId, origin)`) and authentication,
|
|
1061
|
+
or `null` to refuse an unknown origin. Oyster uses the scope for registry,
|
|
1062
|
+
workspace, keys, webhooks and repository names, and pins each Instance to its
|
|
1063
|
+
selected tenant. Canonical member roles remain distinct from owner authority.
|
|
1064
|
+
Public App reads remain available; unknown origins and policy refusals fail
|
|
1065
|
+
closed. `ownerOrKeyTenancy` is the single-tenant case.
|
|
1066
|
+
|
|
1067
|
+
## Approve an installed App's permissions
|
|
1068
|
+
|
|
1069
|
+
After selecting a new Release, the owner can open **App access** beside the App
|
|
1070
|
+
on the standalone owner home, or visit
|
|
1071
|
+
`/api/installations/<installationId>/grants` directly. Review the requested
|
|
1072
|
+
capabilities and choose **Save permissions**. Existing resource restrictions are
|
|
1073
|
+
preserved; a newly selected capability is approved within this Oyster's scope.
|
|
1074
|
+
This updates the installation without reinstalling the App or changing its data.
|
|
1075
|
+
|
|
1076
|
+
The page sends an owner-only `PUT` to the same path, carrying the captured
|
|
1077
|
+
`authorityVersion`, Release `artifactDigest`, and the maintained
|
|
1078
|
+
`{kind:"set-grants",grants}` command. Agents, keys, visitors and App-origin
|
|
1079
|
+
requests cannot approve. A stale Release/version or concurrent approval returns
|
|
1080
|
+
`409`; reload the page to review current permissions. The required
|
|
1081
|
+
`InstallationStore.compareAndPut` port returns a serializable commit/refusal
|
|
1082
|
+
result across Durable Object RPC. The registry checks the expected authority
|
|
1083
|
+
version in its existing commit transaction.
|