@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.
Files changed (242) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1082 -2
  3. package/dist/agents.d.ts +114 -0
  4. package/dist/agents.js +322 -0
  5. package/dist/app-builds.d.ts +151 -0
  6. package/dist/app-builds.js +355 -0
  7. package/dist/app-sandbox.d.ts +2 -0
  8. package/dist/app-sandbox.js +2 -0
  9. package/dist/app-sources.d.ts +46 -0
  10. package/dist/app-sources.js +335 -0
  11. package/dist/app-storage.d.ts +141 -0
  12. package/dist/app-storage.js +281 -0
  13. package/dist/apps/gate.d.ts +105 -0
  14. package/dist/apps/gate.js +273 -0
  15. package/dist/apps/index.d.ts +5 -0
  16. package/dist/apps/index.js +5 -0
  17. package/dist/apps/installation.d.ts +248 -0
  18. package/dist/apps/installation.js +307 -0
  19. package/dist/apps/operations.d.ts +8 -0
  20. package/dist/apps/operations.js +98 -0
  21. package/dist/apps/tools.d.ts +33 -0
  22. package/dist/apps/tools.js +17 -0
  23. package/dist/auth.d.ts +11 -0
  24. package/dist/auth.js +93 -0
  25. package/dist/backend-request.d.ts +3 -0
  26. package/dist/backend-request.js +53 -0
  27. package/dist/build/build-jobs.d.ts +128 -0
  28. package/dist/build/build-jobs.js +197 -0
  29. package/dist/build/build-step.d.ts +26 -0
  30. package/dist/build/build-step.js +20 -0
  31. package/dist/build/bundled-html.d.ts +7 -0
  32. package/dist/build/bundled-html.js +64 -0
  33. package/dist/build/definition-emission.d.ts +34 -0
  34. package/dist/build/definition-emission.js +201 -0
  35. package/dist/build/effect.d.ts +231 -0
  36. package/dist/build/effect.js +396 -0
  37. package/dist/build/git-environment.d.ts +6 -0
  38. package/dist/build/git-environment.js +48 -0
  39. package/dist/build/html-recipe.d.ts +20 -0
  40. package/dist/build/html-recipe.js +51 -0
  41. package/dist/build/local-build.d.ts +57 -0
  42. package/dist/build/local-build.js +1078 -0
  43. package/dist/build/r2-artifact-writer.d.ts +31 -0
  44. package/dist/build/r2-artifact-writer.js +179 -0
  45. package/dist/build/snapshot-package-store.d.ts +2 -0
  46. package/dist/build/snapshot-package-store.js +21 -0
  47. package/dist/build/snapshot-program-scripts.d.ts +6 -0
  48. package/dist/build/snapshot-program-scripts.js +2 -0
  49. package/dist/build/submission-contract.d.ts +384 -0
  50. package/dist/build/submission-contract.js +70 -0
  51. package/dist/build/workspace-build.d.ts +50 -0
  52. package/dist/build/workspace-build.js +156 -0
  53. package/dist/build-contract.d.ts +4 -0
  54. package/dist/build-contract.js +4 -0
  55. package/dist/build-jobs.d.ts +2 -0
  56. package/dist/build-jobs.js +2 -0
  57. package/dist/build.d.ts +11 -0
  58. package/dist/build.js +13 -0
  59. package/dist/capability-descriptor.d.ts +46 -0
  60. package/dist/capability-descriptor.js +51 -0
  61. package/dist/catalog-frame.d.ts +39 -0
  62. package/dist/catalog-frame.js +72 -0
  63. package/dist/cause-class.d.ts +19 -0
  64. package/dist/cause-class.js +54 -0
  65. package/dist/cloudflare.d.ts +123 -0
  66. package/dist/cloudflare.js +391 -0
  67. package/dist/computer-terminal.d.ts +38 -0
  68. package/dist/computer-terminal.js +410 -0
  69. package/dist/computer.d.ts +60 -0
  70. package/dist/computer.js +33 -0
  71. package/dist/event-recipients.d.ts +57 -0
  72. package/dist/event-recipients.js +144 -0
  73. package/dist/facet-invocation.d.ts +4 -0
  74. package/dist/facet-invocation.js +45 -0
  75. package/dist/files.d.ts +42 -0
  76. package/dist/files.js +123 -0
  77. package/dist/frame-binding-wire-script.d.ts +2 -0
  78. package/dist/frame-binding-wire-script.js +1 -0
  79. package/dist/frame-binding-wire.d.ts +28 -0
  80. package/dist/frame-binding-wire.js +54 -0
  81. package/dist/git-admission.d.ts +52 -0
  82. package/dist/git-admission.js +188 -0
  83. package/dist/git-auth.d.ts +11 -0
  84. package/dist/git-auth.js +27 -0
  85. package/dist/git-door.d.ts +35 -0
  86. package/dist/git-door.js +108 -0
  87. package/dist/git-journal.d.ts +31 -0
  88. package/dist/git-journal.js +188 -0
  89. package/dist/git.d.ts +31 -0
  90. package/dist/git.js +64 -0
  91. package/dist/guest-policy.d.ts +9 -0
  92. package/dist/guest-policy.js +13 -0
  93. package/dist/host.d.ts +244 -0
  94. package/dist/host.js +1043 -0
  95. package/dist/index.d.ts +166 -0
  96. package/dist/index.js +320 -0
  97. package/dist/installation-consent.d.ts +79 -0
  98. package/dist/installation-consent.js +82 -0
  99. package/dist/internal/background-origins.d.ts +3 -0
  100. package/dist/internal/background-origins.js +8 -0
  101. package/dist/internal/delivery.d.ts +12 -0
  102. package/dist/internal/delivery.js +167 -0
  103. package/dist/internal/host-gate.d.ts +14 -0
  104. package/dist/internal/host-gate.js +62 -0
  105. package/dist/internal/registry-installations.d.ts +6 -0
  106. package/dist/internal/registry-installations.js +12 -0
  107. package/dist/invocation.d.ts +251 -0
  108. package/dist/invocation.js +377 -0
  109. package/dist/keys.d.ts +48 -0
  110. package/dist/keys.js +52 -0
  111. package/dist/mcp-owner.d.ts +27 -0
  112. package/dist/mcp-owner.js +143 -0
  113. package/dist/mcp.d.ts +89 -0
  114. package/dist/mcp.js +429 -0
  115. package/dist/media.d.ts +23 -0
  116. package/dist/media.js +12 -0
  117. package/dist/pack.d.ts +76 -0
  118. package/dist/pack.js +134 -0
  119. package/dist/page/capabilities.d.ts +23 -0
  120. package/dist/page/capabilities.js +256 -0
  121. package/dist/page/computer-handles.d.ts +79 -0
  122. package/dist/page/computer-handles.js +44 -0
  123. package/dist/page/lifetime.d.ts +6 -0
  124. package/dist/page/lifetime.js +19 -0
  125. package/dist/page/mount.d.ts +163 -0
  126. package/dist/page/mount.js +390 -0
  127. package/dist/page/storage.d.ts +3 -0
  128. package/dist/page/storage.js +82 -0
  129. package/dist/page/terminal.d.ts +5 -0
  130. package/dist/page/terminal.js +7 -0
  131. package/dist/page-script.d.ts +14 -0
  132. package/dist/page-script.js +5 -0
  133. package/dist/r2-files.d.ts +3 -0
  134. package/dist/r2-files.js +79 -0
  135. package/dist/release.d.ts +72 -0
  136. package/dist/release.js +164 -0
  137. package/dist/root-data.d.ts +86 -0
  138. package/dist/root-data.js +69 -0
  139. package/dist/shell/app-directory.d.ts +537 -0
  140. package/dist/shell/app-directory.js +195 -0
  141. package/dist/shell/browser-actions.d.ts +7 -0
  142. package/dist/shell/browser-actions.js +56 -0
  143. package/dist/shell/command-host.d.ts +112 -0
  144. package/dist/shell/command-host.js +502 -0
  145. package/dist/shell/commands.d.ts +25 -0
  146. package/dist/shell/commands.js +78 -0
  147. package/dist/shell/index.d.ts +8 -0
  148. package/dist/shell/index.js +8 -0
  149. package/dist/shell/keys.d.ts +41 -0
  150. package/dist/shell/keys.js +134 -0
  151. package/dist/shell/shell-module.d.ts +31 -0
  152. package/dist/shell/shell-module.js +37 -0
  153. package/dist/shell/window-manager.d.ts +124 -0
  154. package/dist/shell/window-manager.js +711 -0
  155. package/dist/shell/workbench-contract.d.ts +68 -0
  156. package/dist/shell/workbench-contract.js +1 -0
  157. package/dist/shell/workbench-host-types.d.ts +96 -0
  158. package/dist/shell/workbench-host-types.js +1 -0
  159. package/dist/shell/workbench-host.d.ts +34 -0
  160. package/dist/shell/workbench-host.js +347 -0
  161. package/dist/shell/workbench-model.d.ts +192 -0
  162. package/dist/shell/workbench-model.js +53 -0
  163. package/dist/shell/workbench.d.ts +4 -0
  164. package/dist/shell/workbench.js +2 -0
  165. package/dist/source/fork.d.ts +27 -0
  166. package/dist/source/fork.js +46 -0
  167. package/dist/source/index.d.ts +11 -0
  168. package/dist/source/index.js +11 -0
  169. package/dist/source/seed-receipt.d.ts +53 -0
  170. package/dist/source/seed-receipt.js +18 -0
  171. package/dist/source/team-source.d.ts +32 -0
  172. package/dist/source/team-source.js +42 -0
  173. package/dist/source/template-repository.d.ts +2 -0
  174. package/dist/source/template-repository.js +8 -0
  175. package/dist/source/template-write.d.ts +69 -0
  176. package/dist/source/template-write.js +344 -0
  177. package/dist/storage-frame.d.ts +64 -0
  178. package/dist/storage-frame.js +28 -0
  179. package/dist/tenant.d.ts +81 -0
  180. package/dist/tenant.js +71 -0
  181. package/dist/testing/capabilities.d.ts +29 -0
  182. package/dist/testing/capabilities.js +130 -0
  183. package/dist/testing/catalog.d.ts +3 -0
  184. package/dist/testing/catalog.js +53 -0
  185. package/dist/testing/index.d.ts +10 -0
  186. package/dist/testing/index.js +10 -0
  187. package/dist/testing/installation.d.ts +11 -0
  188. package/dist/testing/installation.js +25 -0
  189. package/dist/testing/native-worker.d.ts +21 -0
  190. package/dist/testing/native-worker.js +106 -0
  191. package/dist/testing/worker.d.ts +18 -0
  192. package/dist/testing/worker.js +67 -0
  193. package/dist/testing/workers-runtime.d.ts +24 -0
  194. package/dist/testing/workers-runtime.js +33 -0
  195. package/dist/webhook-delivery.d.ts +14 -0
  196. package/dist/webhook-delivery.js +17 -0
  197. package/dist/workers-git.d.ts +285 -0
  198. package/dist/workers-git.js +1174 -0
  199. package/dist/workers.d.ts +352 -0
  200. package/dist/workers.js +866 -0
  201. package/docs/api/README.md +82 -0
  202. package/docs/api/agents.md +332 -0
  203. package/docs/api/app-storage.md +209 -0
  204. package/docs/api/apps.md +1375 -0
  205. package/docs/api/auth.md +63 -0
  206. package/docs/api/build-contract.md +507 -0
  207. package/docs/api/build.md +1476 -0
  208. package/docs/api/cloudflare.md +421 -0
  209. package/docs/api/computer.md +203 -0
  210. package/docs/api/files.md +117 -0
  211. package/docs/api/git.md +470 -0
  212. package/docs/api/host.md +1328 -0
  213. package/docs/api/index.md +430 -0
  214. package/docs/api/invocation.md +738 -0
  215. package/docs/api/keys.md +175 -0
  216. package/docs/api/mcp.md +301 -0
  217. package/docs/api/media.md +40 -0
  218. package/docs/api/modules.md +42 -0
  219. package/docs/api/pack.md +138 -0
  220. package/docs/api/page/mount.md +298 -0
  221. package/docs/api/root-data.md +111 -0
  222. package/docs/api/shell/app-directory.md +332 -0
  223. package/docs/api/shell/command-host.md +224 -0
  224. package/docs/api/shell/commands.md +197 -0
  225. package/docs/api/shell/keys.md +183 -0
  226. package/docs/api/shell/shell-module.md +143 -0
  227. package/docs/api/shell/window-manager.md +267 -0
  228. package/docs/api/shell/workbench.md +186 -0
  229. package/docs/api/shell.md +1788 -0
  230. package/docs/api/tenant.md +194 -0
  231. package/docs/api/workers-git.md +1527 -0
  232. package/docs/api/workers.md +1678 -0
  233. package/docs/media/architecture.mmd +32 -0
  234. package/docs/media/architecture.png +0 -0
  235. package/docs/media/bonsai-network.png +0 -0
  236. package/docs/media/bonsai-repository.png +0 -0
  237. package/docs/site/agent-quickstart.mdx +349 -0
  238. package/examples/git/worker.ts +39 -0
  239. package/examples/git/wrangler.jsonc +32 -0
  240. package/examples/standalone/worker.ts +14 -0
  241. package/examples/standalone/wrangler.jsonc +21 -0
  242. package/package.json +249 -3
package/README.md CHANGED
@@ -1,3 +1,1083 @@
1
- # Temporary Holding Version
1
+ # Oyster (`@fungi.computer/oyster`)
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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
+ ![Bonsai running on Oyster: three agents work in their own forks while the network graph shows their commits folding back into main](docs/media/bonsai-network.png)
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
+ ![Oyster architecture: one Worker routes browsers, git clients and MCP agents to a registry, per-App Durable Objects, the git door and the Tourtois home, over R2, Artifacts and platform bindings](docs/media/architecture.png)
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.