@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,391 @@
1
+ import { appMethodReplySchema } from "@fungi.computer/agar/backend";
2
+ import { nativeGitFrameMethods } from "@fungi.computer/agar/git-rpc";
3
+ import { rpcHandleSchema, rpcMethod } from "@fungi.computer/agar/rpc";
4
+ import { facetInvocationModule, facetFrameBindingModule, } from "./facet-invocation.js";
5
+ import { createFrameBindingWire, } from "./frame-binding-wire.js";
6
+ import { backgroundSyncRefusalSchema, claimedJobAdmissionSchema, } from "@fungi.computer/puffball/background-replay";
7
+ import { causeClass, failureDiagnostic, guestFailure } from "./cause-class.js";
8
+ import { appGitPushFactSchema, gitPushVerdictSchema, } from "@fungi.computer/agar/git";
9
+ import { WorkerEntrypoint } from "cloudflare:workers";
10
+ import { guestLimits } from "./guest-policy.js";
11
+ import { appBackendBindingKinds, } from "@fungi.computer/puffball/app";
12
+ import { isAppApiMethodName } from "@fungi.computer/puffball/app-method-runtime";
13
+ import { createInvocationGate, invocationFrameIdSchema, } from "./invocation.js";
14
+ import * as Effect from "effect/Effect";
15
+ import * as Schema from "effect/Schema";
16
+ import { z } from "zod";
17
+ import { createAppInstance, AppInstanceError, instanceIdSchema, instanceSchema, appInstanceErrorCodeSchema, } from "./index.js";
18
+ const bindingWire = createFrameBindingWire(appInstanceErrorCodeSchema.literals);
19
+ /** Encode supervisor refusals before native RPC can discard Error properties. */
20
+ export async function captureFrameBindingReply(run) {
21
+ try {
22
+ return { kind: "result", value: await run() };
23
+ }
24
+ catch (cause) {
25
+ if (cause instanceof AppInstanceError)
26
+ return { kind: "local_refusal", code: cause.code };
27
+ throw cause;
28
+ }
29
+ }
30
+ /** Retained shard reply ceiling; replies settle before the frame aborts its facet. */
31
+ export const MAX_APP_REPLY_BYTES = 240 * 1024;
32
+ // Match the retained signed shard request and reply body ceilings.
33
+ const requestBodyCeiling = {
34
+ bytes: 64 * 1024,
35
+ message: "App request too large",
36
+ };
37
+ const replyBodyCeiling = {
38
+ bytes: MAX_APP_REPLY_BYTES,
39
+ message: "App result too large",
40
+ };
41
+ async function readHttpBody(body, ceiling) {
42
+ if (body === null)
43
+ return null;
44
+ const reader = body.getReader();
45
+ const chunks = [];
46
+ let size = 0;
47
+ try {
48
+ for (;;) {
49
+ const next = await reader.read();
50
+ if (next.done)
51
+ break;
52
+ const chunk = z.instanceof(Uint8Array).parse(next.value);
53
+ if (chunk.byteLength === 0)
54
+ continue;
55
+ size += chunk.byteLength;
56
+ if (size > ceiling.bytes)
57
+ throw new Error(ceiling.message);
58
+ chunks.push(chunk);
59
+ }
60
+ }
61
+ catch (cause) {
62
+ await reader.cancel(cause);
63
+ throw cause;
64
+ }
65
+ finally {
66
+ reader.releaseLock();
67
+ }
68
+ const bytes = new Uint8Array(size);
69
+ let offset = 0;
70
+ for (const chunk of chunks) {
71
+ bytes.set(chunk, offset);
72
+ offset += chunk.byteLength;
73
+ }
74
+ return bytes;
75
+ }
76
+ /** Shared replay-only port codecs. Origin input is verified by the host and fails with a typed refusal. */
77
+ export const backgroundFrameMethods = {
78
+ BACKGROUND: {
79
+ admit: {
80
+ /** Malformed arity supplies invalid origin evidence for the host's typed refusal. */
81
+ decodeArgs: (raw) => {
82
+ const parsed = z.tuple([z.unknown()]).safeParse(raw);
83
+ return parsed.success ? parsed.data : [undefined];
84
+ },
85
+ /** Preserve the host's closed admitted/refused result. */
86
+ decodeResult: (raw) => claimedJobAdmissionSchema.parse(raw),
87
+ /** Only the host-minted replay grant supplies this operation. */
88
+ requiredOperations: () => ["admit"],
89
+ },
90
+ },
91
+ };
92
+ /** The shared RPC boundary decoder for host-minted invocation binding props. */
93
+ export const frameBindingPropsSchema = Schema.Struct({
94
+ instanceId: instanceIdSchema,
95
+ frameId: invocationFrameIdSchema,
96
+ binding: Schema.String.pipe(Schema.refine((value) => Object.hasOwn(appBackendBindingKinds, value))),
97
+ });
98
+ /** Decode a host routing packet or refuse it before any Instance lookup. */
99
+ export function decodeFrameBindingProps(raw) {
100
+ try {
101
+ return Schema.decodeUnknownSync(frameBindingPropsSchema, {
102
+ onExcessProperty: "error",
103
+ })(raw);
104
+ }
105
+ catch (cause) {
106
+ throw new AppInstanceError("revoked", { cause });
107
+ }
108
+ }
109
+ /** One native binding factory for both Hub and standalone, preserving RPC values. */
110
+ export function frameBindingEntrypoint(methods, dispatch) {
111
+ return class FrameBinding extends WorkerEntrypoint {
112
+ static {
113
+ const names = new Set([
114
+ ...Object.values(methods).flatMap((binding) => Object.keys(binding)),
115
+ ...Object.values(appBackendBindingKinds).flatMap((binding) => Object.keys(binding.operations)),
116
+ ...Object.keys(nativeGitFrameMethods),
117
+ ]);
118
+ for (const method of names) {
119
+ Object.defineProperty(this.prototype, method, {
120
+ value: async function (...raw) {
121
+ const checked = await captureFrameBindingReply(async () => {
122
+ const props = decodeFrameBindingProps(this.ctx.props);
123
+ const descriptor = methods[props.binding]?.[method];
124
+ let args;
125
+ try {
126
+ args =
127
+ descriptor === undefined ? raw : descriptor.decodeArgs(raw);
128
+ }
129
+ catch (cause) {
130
+ throw new AppInstanceError("invalid_invocation_facts", {
131
+ cause,
132
+ });
133
+ }
134
+ return {
135
+ descriptor,
136
+ reply: await dispatch(this.env, props, method, args),
137
+ };
138
+ });
139
+ if (checked.kind === "local_refusal")
140
+ return checked;
141
+ const { descriptor, reply } = checked.value;
142
+ const decoded = bindingWire.decode(reply);
143
+ if (decoded.kind === "local_refusal")
144
+ return decoded;
145
+ if (descriptor === undefined)
146
+ throw new AppInstanceError("invalid_invocation_facts");
147
+ return {
148
+ kind: "result",
149
+ value: descriptor.decodeResult(decoded.value),
150
+ };
151
+ },
152
+ });
153
+ }
154
+ }
155
+ };
156
+ }
157
+ const facetHttpReplySchema = z.strictObject({
158
+ status: z.number().int(),
159
+ statusText: z.string(),
160
+ headers: z.array(z.tuple([z.string(), z.string()])),
161
+ body: z.instanceof(ReadableStream).nullable(),
162
+ });
163
+ // App methods return JSON; HTTP carries binary bytes and lifecycle hooks return void.
164
+ // Binding operation results keep their separate, operation-specific decoders.
165
+ const facetHttpInputSchema = z.strictObject({
166
+ url: z.string(),
167
+ method: z.string(),
168
+ headers: z.array(z.tuple([z.string(), z.string()])),
169
+ body: z.instanceof(Uint8Array).nullable(),
170
+ });
171
+ const facetInvocationSchema = rpcHandleSchema({
172
+ invoke: rpcMethod(z.union([z.json(), z.undefined()])).input(z.tuple([z.string(), z.array(z.unknown()), z.unknown()])),
173
+ http: rpcMethod(facetHttpReplySchema).input(z.tuple([facetHttpInputSchema, z.unknown()])),
174
+ });
175
+ /** End the native App facet before shared frame admission releases its permit. */
176
+ export function createCloudflareInvocationGate(ctx, close) {
177
+ return createInvocationGate({
178
+ abortGuest: () => {
179
+ ctx.facets.abort("app", new AppInstanceError("revoked"));
180
+ return Promise.resolve();
181
+ },
182
+ close,
183
+ });
184
+ }
185
+ /**
186
+ * Run an App Instance inside a SQLite-backed Durable Object on workerd, celld
187
+ * or Cloudflare. Supervisor state lives in the Durable Object's own SQLite; the
188
+ * App backend runs in a named `app` facet loaded through the Worker Loader, so
189
+ * its database survives Release replacement and stays separate from the
190
+ * supervisor. Guest code gets only host gatekeeper bindings and no ambient outbound access.
191
+ *
192
+ * @param ctx - The Durable Object's name, storage and facets.
193
+ * @param loader - The Worker Loader binding; without it backend execution is unavailable.
194
+ * @param resolve - Resolves a Release ID to its verified durable backend.
195
+ * @param guestEnv - Mints loopbacks bound to the captured invocation frame.
196
+ * @param limits - Guest CPU and subrequest budgets; omitted fields use safe defaults.
197
+ */
198
+ export function createCloudflareAppInstance(ctx, loader, resolve, guestEnv, limits) {
199
+ const budget = guestLimits(limits);
200
+ const sql = ctx.storage.sql;
201
+ sql.exec("CREATE TABLE IF NOT EXISTS app_instance (id INTEGER PRIMARY KEY CHECK(id=1), record TEXT NOT NULL)");
202
+ const attempt = (run) => Effect.try({
203
+ try: run,
204
+ catch: (error) => error instanceof Error ? error : new Error("Instance storage failure"),
205
+ });
206
+ const instanceId = ctx.id.name;
207
+ if (!instanceId)
208
+ throw new Error("Named Instance required");
209
+ // A guest isolate whose invocation failed (or whose earlier frame was
210
+ // canceled mid-call) may stay unusable inside the Loader's per-isolate memo.
211
+ // Each such failure retires the guest: the facet is aborted and the next
212
+ // frame loads a fresh isolate under a loader id named by the retiring frame.
213
+ // Empty until a failure: a healthy Instance keeps its original loader id.
214
+ let retiredBy = "";
215
+ const retireGuest = (frame) => {
216
+ retiredBy = `:${frame.frameId}`;
217
+ ctx.facets.abort("app", new AppInstanceError("revoked"));
218
+ };
219
+ /**
220
+ * A failure that came back through the App facet keeps its identity, retires
221
+ * the guest, and its possibly guest-authored text never reaches host logs.
222
+ * Host refusal codes stay host-owned and keep the guest.
223
+ */
224
+ const facetFailure = (frame, stage, cause, fallback) => {
225
+ if (cause instanceof AppInstanceError) {
226
+ if (stage === "backend_execution")
227
+ console.error("AppInstance failure", {
228
+ stage,
229
+ code: cause.code,
230
+ causeClass: causeClass(cause),
231
+ });
232
+ return cause;
233
+ }
234
+ const failure = cause instanceof Error ? guestFailure(cause) : fallback();
235
+ retireGuest(frame);
236
+ console.error("AppInstance failure", {
237
+ stage,
238
+ code: "failed",
239
+ ...failureDiagnostic(cause),
240
+ recycled: true,
241
+ });
242
+ return failure;
243
+ };
244
+ return createAppInstance({
245
+ instanceId,
246
+ read: () => attempt(() => {
247
+ const stored = sql
248
+ .exec("SELECT record FROM app_instance WHERE id=1")
249
+ .toArray()[0];
250
+ const row = stored === undefined
251
+ ? null
252
+ : Schema.decodeUnknownSync(Schema.Struct({ record: Schema.String }))(stored);
253
+ return row
254
+ ? Schema.decodeUnknownSync(Schema.fromJsonString(instanceSchema))(row.record)
255
+ : null;
256
+ }),
257
+ write: (value) => attempt(() => {
258
+ sql.exec("INSERT INTO app_instance(id,record) VALUES(1,?) ON CONFLICT(id) DO UPDATE SET record=excluded.record", Schema.encodeUnknownSync(Schema.fromJsonString(instanceSchema))(value));
259
+ }),
260
+ resolve,
261
+ abort: () => attempt(() => {
262
+ ctx.facets.abort("app", new Error("Instance authority changed"));
263
+ }),
264
+ purge: () => attempt(() => {
265
+ ctx.facets.delete("app");
266
+ }),
267
+ connect: (artifact, frame) => attempt(() => {
268
+ if (!loader)
269
+ throw new Error("App backend execution unavailable");
270
+ let stub;
271
+ try {
272
+ // The suffix retires a failed guest isolate (see retireGuest).
273
+ const key = `${String(budget.cpuMs)}:${String(budget.subRequests)}${retiredBy}`;
274
+ const loaderId = `${instanceId}:${artifact.digest}:${key}`;
275
+ stub = facetInvocationSchema.parse(ctx.facets.get("app", () => ({
276
+ // Worker Loader memoizes `get(id)` per host isolate, and Instances
277
+ // of every Team share host isolates. A digest-only id would hand
278
+ // one Instance's guest module scope to another Instance of the
279
+ // same Release, so the id is scoped to this Instance.
280
+ class: loader
281
+ .get(loaderId, () => ({
282
+ compatibilityDate: artifact.backend.compatibilityDate,
283
+ mainModule: "invocation.js",
284
+ modules: {
285
+ "backend.js": artifact.backend.code,
286
+ "invocation.js": facetInvocationModule(artifact.backend.exportName),
287
+ "frame-binding-wire.js": facetFrameBindingModule,
288
+ },
289
+ env: {},
290
+ globalOutbound: null,
291
+ limits: budget,
292
+ }))
293
+ .getDurableObjectClass("AppInvocation"),
294
+ })));
295
+ }
296
+ catch (error) {
297
+ // The Loader and facet are host platform: their text is host-owned.
298
+ retireGuest(frame);
299
+ console.error("AppInstance failure", {
300
+ stage: "artifact_resolve",
301
+ code: "failed",
302
+ ...failureDiagnostic(error),
303
+ recycled: true,
304
+ });
305
+ throw error;
306
+ }
307
+ // A host that cannot mint its binding stubs refuses the frame by code;
308
+ // it never hands the guest a partial env or an untyped failure.
309
+ let bindings;
310
+ try {
311
+ bindings = guestEnv(frame);
312
+ }
313
+ catch (cause) {
314
+ if (cause instanceof AppInstanceError)
315
+ throw cause;
316
+ throw new AppInstanceError("binding_unavailable", { cause });
317
+ }
318
+ // A Gill enters through its one reserved generated entry; every
319
+ // other entry is a declared method answering with an AppMethodReply.
320
+ const invokeEntry = (entry, input) => Effect.tryPromise({
321
+ try: async () => {
322
+ if (frame.facts.subject.kind !== "call")
323
+ throw new AppInstanceError("invalid_invocation_facts");
324
+ if (entry !== "gill" && !isAppApiMethodName(entry))
325
+ throw new AppInstanceError("invalid_invocation_facts");
326
+ const result = Schema.decodeUnknownSync(Schema.Json)(await stub.invoke(entry, [input, frame.facts.subject.context], bindings));
327
+ if (new TextEncoder().encode(JSON.stringify(result)).byteLength >
328
+ MAX_APP_REPLY_BYTES)
329
+ throw new Error("App result too large");
330
+ if (frame.signal.aborted)
331
+ throw new AppInstanceError("revoked");
332
+ return result;
333
+ },
334
+ catch: (cause) => facetFailure(frame, "backend_execution", cause, () => new Error("App method failed")),
335
+ });
336
+ return {
337
+ fetch: (request) => Effect.tryPromise({
338
+ try: async () => {
339
+ const reply = facetHttpReplySchema.parse(await stub.http({
340
+ url: request.url,
341
+ method: request.method,
342
+ headers: [...request.headers],
343
+ body: await readHttpBody(request.body, requestBodyCeiling),
344
+ }, bindings));
345
+ const body = await readHttpBody(reply.body, replyBodyCeiling);
346
+ if (frame.signal.aborted)
347
+ throw new AppInstanceError("revoked");
348
+ return new Response(body, {
349
+ status: reply.status,
350
+ statusText: reply.statusText,
351
+ headers: reply.headers,
352
+ });
353
+ },
354
+ catch: (error) => facetFailure(frame, "backend_execution", error, () => new Error("App execution failed")),
355
+ }),
356
+ authorizeGitPush: (question) => Effect.tryPromise({
357
+ try: async () => {
358
+ const verdict = gitPushVerdictSchema.parse(await stub.invoke("authorizeGitPush", [question], bindings));
359
+ if (frame.signal.aborted)
360
+ throw new AppInstanceError("revoked");
361
+ return verdict;
362
+ },
363
+ catch: (cause) => facetFailure(frame, "git_push", cause, () => new Error("App Git push authorization failed", { cause })),
364
+ }),
365
+ syncRefused: (refusal) => Effect.tryPromise({
366
+ try: async () => {
367
+ z.void().parse(await stub.invoke("syncRefused", [backgroundSyncRefusalSchema.parse(refusal)], bindings));
368
+ if (frame.signal.aborted)
369
+ throw new AppInstanceError("revoked");
370
+ },
371
+ catch: (cause) => facetFailure(frame, "background_refusal", cause, () => new Error("App background refusal handler failed", {
372
+ cause,
373
+ })),
374
+ }),
375
+ onGitPush: (fact) => Effect.tryPromise({
376
+ try: async () => {
377
+ z.void().parse(await stub.invoke("onGitPush", [appGitPushFactSchema.parse(fact)], bindings));
378
+ if (frame.signal.aborted)
379
+ throw new AppInstanceError("revoked");
380
+ },
381
+ catch: (cause) => facetFailure(frame, "git_push", cause, () => new Error("App Git push handler failed", { cause })),
382
+ }),
383
+ call: (name, input) => invokeEntry(name, input).pipe(Effect.flatMap((raw) => Effect.try({
384
+ try: () => appMethodReplySchema.parse(raw),
385
+ catch: (cause) => new Error("App method reply invalid", { cause }),
386
+ }))),
387
+ gill: (cargo) => invokeEntry("gill", cargo),
388
+ };
389
+ }),
390
+ });
391
+ }
@@ -0,0 +1,38 @@
1
+ import type { AppComputerTerminal, ComputerTerminalSize } from "@fungi.computer/agar/computer";
2
+ /** Current host admission for the Computer captured when the terminal opened. */
3
+ export type ComputerTerminalSelection = Readonly<{
4
+ computerId: string | null;
5
+ terminalUrl: string | null;
6
+ }>;
7
+ /** Host admission on connection and lease renewal; unleased input rechecks it. */
8
+ export type ComputerTerminalAccess = ((signal: AbortSignal) => Promise<ComputerTerminalSelection>) & {
9
+ /** Current admission deadline, when the host uses expiring leases. */
10
+ leaseExpiresAt?: () => number | null;
11
+ };
12
+ /** Host transport construction; the shared protocol owns PTY session identity. */
13
+ export type ComputerTerminalConnect = (terminalUrl: string, input: ComputerTerminalSize & Readonly<{
14
+ session: string;
15
+ }>) => WebSocket;
16
+ /** Host policy and browser lifetime surrounding the retained PTY protocol. */
17
+ export type ComputerTerminalHostOptions = Readonly<{
18
+ terminalUrl: string;
19
+ computerId: string | null;
20
+ hostWindow: Readonly<{
21
+ /** Schedule a reconnect or admission lease check. */
22
+ setTimeout: (callback: () => void, delayMs: number) => number;
23
+ /** Cancel a pending reconnect or admission lease check. */
24
+ clearTimeout: (timer: number) => void;
25
+ }>;
26
+ ensureAccess: ComputerTerminalAccess;
27
+ connect: ComputerTerminalConnect;
28
+ mountSignal: AbortSignal | undefined;
29
+ }>;
30
+ /** Terminal operations and lifetime for one admitted App window. */
31
+ export type ComputerTerminalHost = {
32
+ /** Open an admitted PTY and retain its session across transport reconnects. */
33
+ open(size: ComputerTerminalSize): Promise<AppComputerTerminal>;
34
+ /** Close every terminal when the App window's host lifetime ends. */
35
+ [Symbol.dispose](): void;
36
+ };
37
+ /** Construct the shared terminal owner with host policy and transport edges. */
38
+ export declare const ComputerTerminalHost: new (options: ComputerTerminalHostOptions) => ComputerTerminalHost;