@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
@@ -0,0 +1,344 @@
1
+ var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
2
+ if (value !== null && value !== void 0) {
3
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
4
+ var dispose, inner;
5
+ if (async) {
6
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
7
+ dispose = value[Symbol.asyncDispose];
8
+ }
9
+ if (dispose === void 0) {
10
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
11
+ dispose = value[Symbol.dispose];
12
+ if (async) inner = dispose;
13
+ }
14
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
15
+ if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
16
+ env.stack.push({ value: value, dispose: dispose, async: async });
17
+ }
18
+ else if (async) {
19
+ env.stack.push({ async: true });
20
+ }
21
+ return value;
22
+ };
23
+ var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
24
+ return function (env) {
25
+ function fail(e) {
26
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
27
+ env.hasError = true;
28
+ }
29
+ var r, s = 0;
30
+ function next() {
31
+ while (r = env.stack.pop()) {
32
+ try {
33
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
34
+ if (r.dispose) {
35
+ var result = r.dispose.call(r.value);
36
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
37
+ }
38
+ else s |= 1;
39
+ }
40
+ catch (e) {
41
+ fail(e);
42
+ }
43
+ }
44
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
45
+ if (env.hasError) throw env.error;
46
+ }
47
+ return next();
48
+ };
49
+ })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
50
+ var e = new Error(message);
51
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
52
+ });
53
+ import { memfs } from "memfs";
54
+ import { dirname, join } from "node:path";
55
+ import * as git from "isomorphic-git";
56
+ import * as Schema from "effect/Schema";
57
+ import { parseAppId, parseSourceRevision } from "@fungi.computer/puffball";
58
+ import { appIdSchema, sha256DigestSchema, sourceRevisionSchema, } from "@fungi.computer/puffball/effect";
59
+ import { capturedPinnedRecipeSchema, digestPinnedRecipe, } from "../build-contract.js";
60
+ import { templateRepositoryForApp } from "./template-repository.js";
61
+ function logEvent(operation, cause) {
62
+ console.error(JSON.stringify({
63
+ source: "app-publisher",
64
+ operation,
65
+ failureKind: cause instanceof Error ? "Error" : "NonErrorThrown",
66
+ }));
67
+ }
68
+ class TemplateRequestTooLarge extends Error {
69
+ }
70
+ /** A bounded operator source upload; decoding happens only at the Publisher boundary. */
71
+ export const TEMPLATE_WRITE_MAX_REQUEST_BYTES = 24 * 1024 * 1024;
72
+ const templatePathSchema = Schema.String.pipe(Schema.check(Schema.makeFilter((path) => path.length > 0 &&
73
+ path.length <= 4096 &&
74
+ !path.includes("\\") &&
75
+ !path.includes("\0") &&
76
+ path
77
+ .split("/")
78
+ .every((part) => part !== "" &&
79
+ part !== "." &&
80
+ part !== ".." &&
81
+ part.toLowerCase() !== ".git"))));
82
+ export const templateWriteInputSchema = Schema.Struct({
83
+ appId: appIdSchema,
84
+ sourceRevision: sourceRevisionSchema,
85
+ sourceTreeDigest: sha256DigestSchema,
86
+ recipe: capturedPinnedRecipeSchema,
87
+ files: Schema.Array(Schema.Struct({
88
+ path: templatePathSchema,
89
+ contents: Schema.Uint8ArrayFromBase64,
90
+ })).pipe(Schema.check(Schema.makeFilter((files) => files.length > 0 && files.length <= 10_000))),
91
+ });
92
+ /** Isomorphic Git owns the protocol; the existing Artifacts owner owns every
93
+ * credential and revokes it when the response stream closes. */
94
+ function templateHttp(transport, repository, signal) {
95
+ const readers = new Set();
96
+ const close = async (reader) => {
97
+ if (!readers.delete(reader))
98
+ return;
99
+ try {
100
+ await reader.cancel();
101
+ }
102
+ finally {
103
+ reader.releaseLock();
104
+ }
105
+ };
106
+ return {
107
+ async [Symbol.asyncDispose]() {
108
+ const results = await Promise.allSettled([...readers].map(close));
109
+ if (results.some((result) => result.status === "rejected"))
110
+ throw new Error("Template Git response cleanup failed");
111
+ },
112
+ async request(input) {
113
+ signal.throwIfAborted();
114
+ const chunks = input.body;
115
+ const iterator = chunks === undefined
116
+ ? undefined
117
+ : (async function* () {
118
+ yield* chunks;
119
+ })();
120
+ const body = iterator === undefined
121
+ ? null
122
+ : new ReadableStream({
123
+ async pull(controller) {
124
+ const item = await iterator.next();
125
+ if (item.done)
126
+ controller.close();
127
+ else
128
+ controller.enqueue(item.value);
129
+ },
130
+ async cancel() {
131
+ await iterator.return(undefined);
132
+ },
133
+ });
134
+ const init = {
135
+ method: input.method ?? "GET",
136
+ headers: new Headers(input.headers),
137
+ body,
138
+ signal,
139
+ };
140
+ if (body !== null)
141
+ Object.assign(init, { duplex: "half" });
142
+ const response = await transport.exchangeTemplateGit(repository, new Request(input.url, init));
143
+ // Own the reader before handing the iterable to Git, including when the
144
+ // client never starts iteration or abandons it without calling return().
145
+ const reader = response.body?.getReader();
146
+ if (reader !== undefined)
147
+ readers.add(reader);
148
+ async function* responseChunks() {
149
+ if (reader === undefined)
150
+ return;
151
+ try {
152
+ for (;;) {
153
+ signal.throwIfAborted();
154
+ const item = await reader.read();
155
+ if (item.done)
156
+ return;
157
+ yield item.value;
158
+ }
159
+ }
160
+ finally {
161
+ await close(reader);
162
+ }
163
+ }
164
+ return {
165
+ url: input.url,
166
+ method: input.method,
167
+ statusCode: response.status,
168
+ statusMessage: response.statusText,
169
+ headers: Object.fromEntries(response.headers),
170
+ body: responseChunks(),
171
+ };
172
+ },
173
+ };
174
+ }
175
+ /** Each request owns an in-memory Git working tree. Uploaded source never
176
+ * executes here and callers supply no provider token; the maintained Git client
177
+ * writes and compares trees. */
178
+ async function writeTemplate(transport, input, repository, signal) {
179
+ const env_1 = { stack: [], error: void 0, hasError: false };
180
+ try {
181
+ const { fs } = memfs();
182
+ const dir = "/template";
183
+ await git.init({ fs, dir, defaultBranch: "main" });
184
+ const paths = new Set();
185
+ for (const file of input.files) {
186
+ signal.throwIfAborted();
187
+ if (paths.has(file.path))
188
+ throw new Error("Duplicate template source path");
189
+ paths.add(file.path);
190
+ const target = join(dir, file.path);
191
+ await fs.promises.mkdir(dirname(target), { recursive: true });
192
+ await fs.promises.writeFile(target, file.contents);
193
+ }
194
+ await git.add({ fs, dir, filepath: [...paths], force: true });
195
+ const oid = await git.commit({
196
+ fs,
197
+ dir,
198
+ author: {
199
+ name: "Botanical App Source",
200
+ email: "app-source@botanical.invalid",
201
+ timestamp: 0,
202
+ timezoneOffset: 0,
203
+ },
204
+ message: `Seed ${input.appId} from ${input.sourceRevision}`,
205
+ });
206
+ const http = __addDisposableResource(env_1, templateHttp(transport, repository, signal), true);
207
+ const url = `https://git.team.internal/${repository}.git`;
208
+ await git.addRemote({ fs, dir, remote: "origin", url });
209
+ const refs = await git.listServerRefs({
210
+ http,
211
+ url,
212
+ prefix: "refs/heads/main",
213
+ });
214
+ const main = refs.find((ref) => ref.ref === "refs/heads/main");
215
+ if (main !== undefined) {
216
+ await git.fetch({
217
+ fs,
218
+ http,
219
+ dir,
220
+ url,
221
+ ref: "main",
222
+ singleBranch: true,
223
+ depth: 1,
224
+ tags: false,
225
+ });
226
+ const expected = await git.readCommit({ fs, dir, oid });
227
+ const actual = await git.readCommit({ fs, dir, oid: main.oid });
228
+ if (expected.commit.tree !== actual.commit.tree)
229
+ return null;
230
+ }
231
+ if (main === undefined) {
232
+ // isomorphic-git's pack writer sends whole objects, preserving Artifacts
233
+ // native forks' requirement that seed packs contain no delta objects.
234
+ const pushed = await git.push({
235
+ fs,
236
+ http,
237
+ dir,
238
+ url,
239
+ ref: "main",
240
+ remoteRef: "main",
241
+ signal,
242
+ });
243
+ if (!pushed.ok)
244
+ throw new Error("Template Git push refused");
245
+ }
246
+ // A successful push or matching fetched tree is not sufficient: retain the
247
+ // former Publisher receipt check that main still names this exact commit.
248
+ const expectedHead = main?.oid ?? oid;
249
+ const confirmed = await git.listServerRefs({
250
+ http,
251
+ url,
252
+ prefix: "refs/heads/main",
253
+ });
254
+ return confirmed.find((ref) => ref.ref === "refs/heads/main")?.oid ===
255
+ expectedHead
256
+ ? expectedHead
257
+ : null;
258
+ }
259
+ catch (e_1) {
260
+ env_1.error = e_1;
261
+ env_1.hasError = true;
262
+ }
263
+ finally {
264
+ const result_1 = __disposeResources(env_1);
265
+ if (result_1)
266
+ await result_1;
267
+ }
268
+ }
269
+ /** The only operator template-write result contains provenance, never custody. */
270
+ export async function seedPublisherTemplate(receipts, transport, rawAppId, request) {
271
+ try {
272
+ const appId = parseAppId(rawAppId);
273
+ const length = Number(request.headers.get("content-length") ?? "0");
274
+ if (!Number.isSafeInteger(length) ||
275
+ length < 0 ||
276
+ length > TEMPLATE_WRITE_MAX_REQUEST_BYTES)
277
+ return { kind: "too-large" };
278
+ const signal = AbortSignal.any([
279
+ request.signal,
280
+ AbortSignal.timeout(240_000),
281
+ ]);
282
+ let bytes = 0;
283
+ const bounded = request.body?.pipeThrough(new TransformStream({
284
+ transform(chunk, controller) {
285
+ bytes += chunk.byteLength;
286
+ if (bytes > TEMPLATE_WRITE_MAX_REQUEST_BYTES)
287
+ throw new TemplateRequestTooLarge();
288
+ controller.enqueue(chunk);
289
+ },
290
+ }), { signal });
291
+ let input;
292
+ try {
293
+ const body = await new Response(bounded).json();
294
+ input = Schema.decodeUnknownSync(templateWriteInputSchema, {
295
+ onExcessProperty: "error",
296
+ })(body);
297
+ }
298
+ catch (cause) {
299
+ logEvent("operator.template.decode", cause);
300
+ return {
301
+ kind: cause instanceof TemplateRequestTooLarge ? "too-large" : "invalid",
302
+ };
303
+ }
304
+ if (input.appId !== appId)
305
+ return { kind: "conflict" };
306
+ const repository = templateRepositoryForApp(appId);
307
+ await transport.ensureRepository(repository);
308
+ const oid = await writeTemplate(transport, input, repository, signal);
309
+ if (oid === null)
310
+ return { kind: "conflict" };
311
+ const seedRevision = parseSourceRevision(`git:${oid}`);
312
+ const receipt = {
313
+ appId,
314
+ sourceRevision: input.sourceRevision,
315
+ sourceTreeDigest: input.sourceTreeDigest,
316
+ seedRevision,
317
+ recipe: input.recipe,
318
+ buildRecipeDigest: digestPinnedRecipe(input.recipe),
319
+ };
320
+ const existing = await receipts.read(appId, seedRevision);
321
+ if (existing !== null &&
322
+ (existing.sourceRevision !== receipt.sourceRevision ||
323
+ existing.sourceTreeDigest !== receipt.sourceTreeDigest ||
324
+ existing.buildRecipeDigest !== receipt.buildRecipeDigest))
325
+ return { kind: "conflict" };
326
+ // Recording still uses the write-once, digest-checked Publisher receipt owner.
327
+ await receipts.record(receipt);
328
+ return {
329
+ kind: existing === null ? "recorded" : "existing",
330
+ receipt: {
331
+ appId,
332
+ repository,
333
+ sourceRevision: receipt.sourceRevision,
334
+ sourceTreeDigest: receipt.sourceTreeDigest,
335
+ seedRevision,
336
+ buildRecipeDigest: receipt.buildRecipeDigest,
337
+ },
338
+ };
339
+ }
340
+ catch (cause) {
341
+ logEvent("operator.template.write", cause);
342
+ return { kind: "unavailable" };
343
+ }
344
+ }
@@ -0,0 +1,64 @@
1
+ import * as Schema from "effect/Schema";
2
+ /** The storage codecs and operation checks used by both sides of the native frame. */
3
+ export declare const storageFrameMethods: {
4
+ APP_STORAGE: {
5
+ read: {
6
+ decodeArgs: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => readonly [string];
7
+ decodeResult: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => {
8
+ readonly kind: "accepted";
9
+ readonly value: {
10
+ readonly value: Schema.Json;
11
+ readonly source: "team" | "member" | "none";
12
+ readonly team: {
13
+ readonly value: Schema.Json;
14
+ readonly revision: number;
15
+ } | null;
16
+ readonly member: {
17
+ readonly value: Schema.Json;
18
+ readonly revision: number;
19
+ } | null;
20
+ readonly canWriteTeam: boolean;
21
+ };
22
+ } | {
23
+ readonly kind: "rejected";
24
+ readonly code: "unavailable" | "invalid" | "undeclared_key" | "scope_not_declared" | "owner_required" | "too_large" | "quota_exceeded" | "conflict";
25
+ };
26
+ requiredOperations: () => string[];
27
+ };
28
+ write: {
29
+ decodeArgs: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => readonly [{
30
+ readonly value: Schema.Json;
31
+ readonly key: string;
32
+ readonly scope: "team" | "member";
33
+ readonly basedOn: number | null;
34
+ }];
35
+ decodeResult: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => {
36
+ readonly kind: "accepted";
37
+ readonly value: {
38
+ readonly revision: number;
39
+ };
40
+ } | {
41
+ readonly kind: "rejected";
42
+ readonly code: "unavailable" | "invalid" | "undeclared_key" | "scope_not_declared" | "owner_required" | "too_large" | "quota_exceeded" | "conflict";
43
+ };
44
+ requiredOperations: () => string[];
45
+ };
46
+ reset: {
47
+ decodeArgs: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => readonly [{
48
+ readonly key: string;
49
+ readonly scope: "team" | "member";
50
+ readonly basedOn: number | null;
51
+ }];
52
+ decodeResult: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => {
53
+ readonly kind: "accepted";
54
+ readonly value: {
55
+ readonly revision: number;
56
+ };
57
+ } | {
58
+ readonly kind: "rejected";
59
+ readonly code: "unavailable" | "invalid" | "undeclared_key" | "scope_not_declared" | "owner_required" | "too_large" | "quota_exceeded" | "conflict";
60
+ };
61
+ requiredOperations: () => string[];
62
+ };
63
+ };
64
+ };
@@ -0,0 +1,28 @@
1
+ import * as Schema from "effect/Schema";
2
+ import { appStorageKeyNameSchema, appStorageWriteSchema, appStorageResetSchema, appStorageReadResultSchema, appStorageMutationResultSchema, } from "@fungi.computer/agar/storage";
3
+ /** The storage codecs and operation checks used by both sides of the native frame. */
4
+ export const storageFrameMethods = {
5
+ APP_STORAGE: {
6
+ read: {
7
+ decodeArgs: Schema.decodeUnknownSync(Schema.Tuple([appStorageKeyNameSchema]), { onExcessProperty: "error" }),
8
+ decodeResult: Schema.decodeUnknownSync(appStorageReadResultSchema, {
9
+ onExcessProperty: "error",
10
+ }),
11
+ requiredOperations: () => ["read"],
12
+ },
13
+ write: {
14
+ decodeArgs: Schema.decodeUnknownSync(Schema.Tuple([appStorageWriteSchema]), { onExcessProperty: "error" }),
15
+ decodeResult: Schema.decodeUnknownSync(appStorageMutationResultSchema, {
16
+ onExcessProperty: "error",
17
+ }),
18
+ requiredOperations: () => ["write"],
19
+ },
20
+ reset: {
21
+ decodeArgs: Schema.decodeUnknownSync(Schema.Tuple([appStorageResetSchema]), { onExcessProperty: "error" }),
22
+ decodeResult: Schema.decodeUnknownSync(appStorageMutationResultSchema, {
23
+ onExcessProperty: "error",
24
+ }),
25
+ requiredOperations: () => ["reset"],
26
+ },
27
+ },
28
+ };
@@ -0,0 +1,81 @@
1
+ import type { HostActor } from "@fungi.computer/puffball/app-method-runtime";
2
+ import * as Schema from "effect/Schema";
3
+ import type { OysterAuth } from "./host.js";
4
+ /**
5
+ * A host-minted tenant slug: 2–40 lowercase ASCII letters or digits, with
6
+ * single interior hyphens. The repository separator `--` is forbidden.
7
+ * Hosts resolve it before authentication; App-controlled inputs never select it.
8
+ */
9
+ export declare const tenantIdSchema: Schema.brand<Schema.String, "OysterTenantId">;
10
+ /** A validated host-selected tenant identity. */
11
+ export type TenantId = typeof tenantIdSchema.Type;
12
+ /** A canonical host actor with a tenant role only when the actor is a member. */
13
+ export type TenantPrincipal = Readonly<{
14
+ /** Stored member identity resolved by the tenant registry. */
15
+ actor: Extract<HostActor, {
16
+ kind: "member";
17
+ }>;
18
+ /** The member's admitted role; membership does not imply ownership. */
19
+ role: "owner" | "member";
20
+ /** The host attests this member is the tenant's only member. */
21
+ soleMember?: true;
22
+ }> | Readonly<{
23
+ /** An Agent or Team key retains its canonical actor identity. */
24
+ actor: Exclude<HostActor, {
25
+ kind: "member";
26
+ }>;
27
+ }>;
28
+ /** The single-tenant Oyster identity whose durable names remain unchanged. */
29
+ export declare const defaultTenant: TenantId;
30
+ /** Resolves a request before authentication; null means refuse, never fallback. */
31
+ export type TenantResolver = Readonly<{
32
+ /** Return the host-selected tenant and canonical origin, or refuse this URL. */
33
+ resolve(request: Request): Promise<Readonly<{
34
+ tenantId: TenantId;
35
+ origin: string;
36
+ }> | null>;
37
+ }>;
38
+ /** The only source of durable names for a standalone Oyster tenant. */
39
+ export type TenantScope = Readonly<{
40
+ /** Host-selected tenant identity; never taken from App input. */
41
+ tenantId: TenantId;
42
+ /** Canonical HTTP(S) origin for this tenant's public routes. */
43
+ origin: string;
44
+ /** Name of a tenant-owned Durable Object within its binding namespace. */
45
+ durableName(kind: "registry" | "keys" | "webhooks" | "git-push" | "agents" | "mcp"): string;
46
+ /** Name within the Computer binding; nondefault Computers await host support. */
47
+ computerName(computerId: string): string;
48
+ /** R2 prefix for mutable workspace files; immutable release bytes may be shared. */
49
+ workspacePrefix: string;
50
+ /**
51
+ * Provider prefix for an installation slug without `--`.
52
+ * The Git adapter must validate the final prefix + repo against the provider's
53
+ * 128-character limit; a prefix alone cannot establish that a repo name fits.
54
+ */
55
+ repositoryPrefix(installationId: string): string;
56
+ }>;
57
+ /**
58
+ * Name tenant-owned stores without changing any default-tenant name. The `t.`
59
+ * repository namespace cannot be a default installation slug, which forbids dots.
60
+ * Hub supplies its own Team-scoped ports and does not use this naming contract.
61
+ */
62
+ export declare function tenantScope(tenantId: TenantId, origin: string): TenantScope;
63
+ /**
64
+ * Admit defaultTenant only at the configured origin. Scheme, host and port must
65
+ * match after URL canonicalization; unknown origins never fall back to default.
66
+ */
67
+ export declare function singleTenant(origin: string): TenantResolver;
68
+ /** A host-selected scope paired with its existing authentication port. */
69
+ export type ResolvedTenant = Readonly<{
70
+ /** Canonical origin and durable names selected before authentication. */
71
+ scope: TenantScope;
72
+ /** Authentication bound to this tenant; resolution does not invoke it. */
73
+ auth: OysterAuth;
74
+ }>;
75
+ /** Resolve an origin-scoped host; null refuses the request without a fallback. */
76
+ export type OysterTenancy = Readonly<{
77
+ /** Select one tenant before authentication; unknown origins return null. */
78
+ resolve(request: Request): Promise<ResolvedTenant | null>;
79
+ }>;
80
+ /** Preserve the default tenant's names and auth only at the configured origin. */
81
+ export declare function implicitTenancy(origin: string, auth: OysterAuth): OysterTenancy;
package/dist/tenant.js ADDED
@@ -0,0 +1,71 @@
1
+ import * as Schema from "effect/Schema";
2
+ /**
3
+ * A host-minted tenant slug: 2–40 lowercase ASCII letters or digits, with
4
+ * single interior hyphens. The repository separator `--` is forbidden.
5
+ * Hosts resolve it before authentication; App-controlled inputs never select it.
6
+ */
7
+ export const tenantIdSchema = Schema.String.check(Schema.isPattern(/^[a-z0-9](?:[a-z0-9]|-(?!-)){0,38}[a-z0-9]$/u)).pipe(Schema.brand("OysterTenantId"));
8
+ /** The single-tenant Oyster identity whose durable names remain unchanged. */
9
+ export const defaultTenant = Schema.decodeUnknownSync(tenantIdSchema)("default");
10
+ function configuredOrigin(origin) {
11
+ const url = new URL(origin);
12
+ if (!["https:", "http:"].includes(url.protocol) ||
13
+ url.username !== "" ||
14
+ url.password !== "" ||
15
+ url.pathname !== "/" ||
16
+ url.search !== "" ||
17
+ url.hash !== "") {
18
+ throw new TypeError("Configure an HTTP(S) origin without credentials or a path");
19
+ }
20
+ return url;
21
+ }
22
+ const installationIdSchema = Schema.String.check(Schema.isPattern(/^[a-z][a-z0-9-]{0,62}$/u), Schema.isPattern(/^(?!.*--)/u));
23
+ /**
24
+ * Name tenant-owned stores without changing any default-tenant name. The `t.`
25
+ * repository namespace cannot be a default installation slug, which forbids dots.
26
+ * Hub supplies its own Team-scoped ports and does not use this naming contract.
27
+ */
28
+ export function tenantScope(tenantId, origin) {
29
+ const canonicalOrigin = configuredOrigin(origin).origin;
30
+ const isDefault = tenantId === defaultTenant;
31
+ const durablePrefix = isDefault ? "" : `t.${tenantId}.`;
32
+ return {
33
+ tenantId,
34
+ origin: canonicalOrigin,
35
+ durableName: (kind) => `${durablePrefix}${kind}`,
36
+ computerName: (computerId) => `${durablePrefix}${computerId}`,
37
+ workspacePrefix: isDefault
38
+ ? "workspace/"
39
+ : `tenants/${tenantId}/workspace/`,
40
+ repositoryPrefix(installationId) {
41
+ const installation = Schema.decodeUnknownSync(installationIdSchema)(installationId);
42
+ return isDefault
43
+ ? `${installation}--`
44
+ : `t.${tenantId}--${installation}--`;
45
+ },
46
+ };
47
+ }
48
+ /**
49
+ * Admit defaultTenant only at the configured origin. Scheme, host and port must
50
+ * match after URL canonicalization; unknown origins never fall back to default.
51
+ */
52
+ export function singleTenant(origin) {
53
+ const canonicalOrigin = configuredOrigin(origin).origin;
54
+ return {
55
+ resolve: (request) => Promise.resolve(new URL(request.url).origin === canonicalOrigin
56
+ ? { tenantId: defaultTenant, origin: canonicalOrigin }
57
+ : null),
58
+ };
59
+ }
60
+ /** Preserve the default tenant's names and auth only at the configured origin. */
61
+ export function implicitTenancy(origin, auth) {
62
+ const resolver = singleTenant(origin);
63
+ const scope = tenantScope(defaultTenant, origin);
64
+ return {
65
+ async resolve(request) {
66
+ return (await resolver.resolve(request)) === null
67
+ ? null
68
+ : { scope, auth };
69
+ },
70
+ };
71
+ }
@@ -0,0 +1,29 @@
1
+ import { Hono } from "hono";
2
+ import { type AppCapabilityId } from "@fungi.computer/puffball/app";
3
+ import { type OysterAgents } from "../agents.js";
4
+ import { type FileSource } from "../files.js";
5
+ import { type OysterAuthResult, type OysterCapability } from "../host.js";
6
+ /** How one capability fixture host admits its single installation. */
7
+ export type CapabilityFixtureOptions = Readonly<{
8
+ /** `false` publishes a Release that declares no capability. */
9
+ declared?: boolean;
10
+ /** Capabilities the Release declares; defaults to `computer.list`. */
11
+ capabilities?: readonly AppCapabilityId[];
12
+ /** Adapters the host grants; defaults to a `computer.list` probe. */
13
+ adapters?: readonly OysterCapability[];
14
+ /** Grant the file adapters over this source instead of the probe. */
15
+ fileSource?: FileSource;
16
+ /** `false` clears the owner's approval after install. */
17
+ admitted?: boolean;
18
+ /** Serve a public installation to an anonymous visitor. */
19
+ visitor?: boolean;
20
+ authenticate?: (request: Request) => Promise<OysterAuthResult>;
21
+ agents?: OysterAgents;
22
+ }>;
23
+ /** One Oyster host with a single installation whose capability grants the options shape. */
24
+ export declare function capabilityFixture(options?: CapabilityFixtureOptions): Promise<{
25
+ app: Hono<import("../host.js").OysterEnv, import("hono/types").BlankSchema, "/">;
26
+ page: () => Response | Promise<Response>;
27
+ request: () => Response | Promise<Response>;
28
+ removeDeclaration(): Promise<void>;
29
+ }>;