@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,130 @@
1
+ import { Hono } from "hono";
2
+ import * as Schema from "effect/Schema";
3
+ import { releaseSchema } from "@fungi.computer/puffball/effect";
4
+ import { parseArtifactRef, parseSha256Digest } from "@fungi.computer/puffball";
5
+ import { createAppBuild, emptyAppDeclaration, } from "@fungi.computer/puffball/app";
6
+ import { computerList } from "@fungi.computer/agar/computer";
7
+ import { agentApprovalRoutes } from "../agents.js";
8
+ import { fileCapabilities } from "../files.js";
9
+ import { createOyster, } from "../host.js";
10
+ import { instanceIds } from "../index.js";
11
+ import { sha256, verifyRelease } from "../release.js";
12
+ import { approvedInstallation } from "./installation.js";
13
+ /** One Oyster host with a single installation whose capability grants the options shape. */
14
+ export async function capabilityFixture(options = {}) {
15
+ const artifacts = new Map();
16
+ async function put(bytes) {
17
+ const digest = parseSha256Digest(await sha256(bytes));
18
+ const artifactRef = parseArtifactRef(`artifact:sha256-${digest.slice(7)}`);
19
+ artifacts.set(artifactRef, bytes);
20
+ return { artifactRef, digest };
21
+ }
22
+ const encode = (text) => new TextEncoder().encode(text);
23
+ const uiBytes = encode("<!doctype html><title>Capability fixture</title>");
24
+ const ui = {
25
+ ...(await put(uiBytes)),
26
+ version: "fixture",
27
+ byteLength: uiBytes.length,
28
+ mediaType: "text/html; charset=utf-8",
29
+ };
30
+ async function release(releaseId, capabilities) {
31
+ const root = await put(encode(JSON.stringify(createAppBuild(ui, null, { capabilities, handlers: [], roles: [] }))));
32
+ return Schema.decodeUnknownSync(releaseSchema)({
33
+ releaseId,
34
+ appId: "app_capability",
35
+ artifact: {
36
+ artifactRef: root.artifactRef,
37
+ artifactDigest: root.digest,
38
+ provenance: {
39
+ sourceRevision: `git:${"a".repeat(40)}`,
40
+ sourceTreeDigest: root.digest,
41
+ buildRecipeDigest: root.digest,
42
+ },
43
+ },
44
+ });
45
+ }
46
+ const routes = new Hono().post("/list", (context) => context.json({ entries: [] }));
47
+ const capabilities = options.adapters ??
48
+ (options.fileSource
49
+ ? fileCapabilities(options.fileSource)
50
+ : [
51
+ {
52
+ id: computerList.id,
53
+ permission: "computer.list",
54
+ version: computerList.version,
55
+ base: "/probe/",
56
+ routes,
57
+ methods: { list: { kind: "json", method: "POST", path: "list" } },
58
+ },
59
+ ]);
60
+ const declaredRelease = await release("rel_declared", options.declared === false
61
+ ? []
62
+ : (options.capabilities ?? ["computer.list"]));
63
+ const declaration = (await verifyRelease({
64
+ get: (ref) => Promise.resolve(artifacts.get(ref) ?? null),
65
+ put: () => Promise.reject(new Error("Fixture artifacts are read-only")),
66
+ }, declaredRelease)).declaration;
67
+ let installation = await approvedInstallation({
68
+ installationId: "capability-fixture",
69
+ instanceId: instanceIds.instance.generate(),
70
+ release: declaredRelease,
71
+ declaration: declaration ?? emptyAppDeclaration,
72
+ descriptors: capabilities,
73
+ });
74
+ if (options.admitted === false)
75
+ installation = { ...installation, approvedGrants: [], approval: null };
76
+ // An anonymous reader reaches only a public App, as the host's visitor.
77
+ if (options.visitor)
78
+ installation = { ...installation, visibility: "public" };
79
+ const readOnly = () => Promise.reject(new Error("Fixture installations are read-only"));
80
+ const base = {
81
+ auth: {
82
+ resolve: () => Promise.reject(new Error("Capability fixture resolves no actor")),
83
+ authenticate: options.authenticate ??
84
+ ((request) => Promise.resolve(options.visitor || request.headers.get("x-who") === "visitor"
85
+ ? { kind: "refused", status: 401 }
86
+ : {
87
+ kind: "admitted",
88
+ principal: {
89
+ actor: { kind: "member", memberId: "owner" },
90
+ role: "owner",
91
+ },
92
+ })),
93
+ },
94
+ artifacts: {
95
+ get: (ref) => Promise.resolve(artifacts.get(ref) ?? null),
96
+ put: () => Promise.reject(new Error("Fixture artifacts are read-only")),
97
+ },
98
+ installations: {
99
+ list: () => Promise.resolve([installation]),
100
+ get: (id) => Promise.resolve(id === installation.installationId ? installation : null),
101
+ put: readOnly,
102
+ compareAndPut: readOnly,
103
+ },
104
+ instances: {
105
+ get: () => {
106
+ throw new Error("Capability routes must not execute an App backend");
107
+ },
108
+ },
109
+ capabilities,
110
+ };
111
+ const host = options.agents === undefined
112
+ ? base
113
+ : { ...base, ownerRoutes: agentApprovalRoutes(options.agents) };
114
+ const app = createOyster(host);
115
+ return {
116
+ app,
117
+ page: () => app.request("https://oyster.test/apps/capability-fixture/"),
118
+ request: () => app.request("https://oyster.test/probe/list", {
119
+ method: "POST",
120
+ headers: { "X-Oyster-Installation": installation.installationId },
121
+ }),
122
+ async removeDeclaration() {
123
+ installation = {
124
+ ...installation,
125
+ release: await release("rel_undeclared", []),
126
+ authorityVersion: installation.authorityVersion + 1,
127
+ };
128
+ },
129
+ };
130
+ }
@@ -0,0 +1,3 @@
1
+ /** Digest-verifying local catalog shared by native App host proofs. */
2
+ export declare function createFixtureCatalog(encoded: string): import("@fungi.computer/puffball/effect").AppReleaseCore;
3
+ export declare function seedFixtureCatalog(encoded: string, catalog: ReturnType<typeof createFixtureCatalog>): Promise<void>;
@@ -0,0 +1,53 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Schema from "effect/Schema";
3
+ import { createAppReleaseCore, createDeterministicMemoryAppReleaseStore, adaptPromiseAppReleaseStore, releaseSchema, adaptPromiseArtifactReader, } from "@fungi.computer/puffball/effect";
4
+ import { parseSha256Digest } from "@fungi.computer/puffball";
5
+ import { appMediaType } from "@fungi.computer/puffball/app";
6
+ import { backendMediaType } from "@fungi.computer/puffball/backend";
7
+ // The Publisher's artifact store: every Release root and child object by ref.
8
+ const fixtureSchema = Schema.Struct({
9
+ releases: Schema.Array(releaseSchema),
10
+ objects: Schema.Record(Schema.String, Schema.Struct({
11
+ mediaType: Schema.Literals([
12
+ appMediaType,
13
+ backendMediaType,
14
+ "text/html; charset=utf-8",
15
+ ]),
16
+ body: Schema.String,
17
+ })),
18
+ });
19
+ const hex = (bytes) => Array.from(new Uint8Array(bytes), (b) => b.toString(16).padStart(2, "0")).join("");
20
+ /** Digest-verifying local catalog shared by native App host proofs. */
21
+ export function createFixtureCatalog(encoded) {
22
+ const fixture = Schema.decodeUnknownSync(Schema.fromJsonString(fixtureSchema))(encoded);
23
+ return createAppReleaseCore({
24
+ store: adaptPromiseAppReleaseStore(createDeterministicMemoryAppReleaseStore()),
25
+ artifactReader: adaptPromiseArtifactReader({
26
+ read: async ({ artifactRef, expectedDigest }) => {
27
+ const object = fixture.objects[artifactRef];
28
+ if (!object)
29
+ return { kind: "missing" };
30
+ const bytes = new TextEncoder().encode(object.body);
31
+ const digest = parseSha256Digest(`sha256:${hex(await crypto.subtle.digest("SHA-256", bytes))}`);
32
+ if (digest !== expectedDigest)
33
+ return { kind: "digest-mismatch", actualDigest: digest };
34
+ return {
35
+ kind: "verified",
36
+ digest,
37
+ mediaType: object.mediaType,
38
+ bytes: { byteLength: bytes.length, copy: () => bytes.slice() },
39
+ byteLength: bytes.length,
40
+ };
41
+ },
42
+ }),
43
+ artifactPolicy: { maxBytes: 1024 * 1024 },
44
+ });
45
+ }
46
+ export async function seedFixtureCatalog(encoded, catalog) {
47
+ const fixture = Schema.decodeUnknownSync(Schema.fromJsonString(fixtureSchema))(encoded);
48
+ for (const release of fixture.releases) {
49
+ await Effect.runPromise(catalog.createApp({ appId: release.appId }));
50
+ await Effect.runPromise(catalog.createRelease(release));
51
+ await Effect.runPromise(catalog.verifyReleaseArtifact(release.releaseId));
52
+ }
53
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Test kit for Oyster hosts and Apps: fixture installations, a capability
3
+ * host, a digest-verifying release catalog and the frame binding wire.
4
+ * The `cloudflare:workers` stand-in lives at `./testing/workers-runtime`.
5
+ */
6
+ export { approvedInstallation } from "./installation.js";
7
+ export { capabilityFixture, type CapabilityFixtureOptions, } from "./capabilities.js";
8
+ export { createFixtureCatalog, seedFixtureCatalog } from "./catalog.js";
9
+ export { createFrameBindingWire, type FrameBindingReply, type FrameBindingWire, } from "../frame-binding-wire.js";
10
+ export { startNativeWorker, type NativeWorker, type NativeWorkerOptions, } from "./native-worker.js";
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Test kit for Oyster hosts and Apps: fixture installations, a capability
3
+ * host, a digest-verifying release catalog and the frame binding wire.
4
+ * The `cloudflare:workers` stand-in lives at `./testing/workers-runtime`.
5
+ */
6
+ export { approvedInstallation } from "./installation.js";
7
+ export { capabilityFixture, } from "./capabilities.js";
8
+ export { createFixtureCatalog, seedFixtureCatalog } from "./catalog.js";
9
+ export { createFrameBindingWire, } from "../frame-binding-wire.js";
10
+ export { startNativeWorker, } from "./native-worker.js";
@@ -0,0 +1,11 @@
1
+ import type { AppDeclaration } from "@fungi.computer/puffball/app";
2
+ import { type CapabilityOperationDescriptor, type Installation } from "../apps/index.js";
3
+ /** Build fixture state through the same approval owner used by installation routes. */
4
+ export declare function approvedInstallation(input: Readonly<{
5
+ release: Installation["release"];
6
+ declaration: AppDeclaration;
7
+ installationId: string;
8
+ instanceId: Installation["instanceId"];
9
+ descriptors?: readonly CapabilityOperationDescriptor[];
10
+ visibility?: Installation["visibility"];
11
+ }>): Promise<Installation>;
@@ -0,0 +1,25 @@
1
+ import * as Result from "effect/Result";
2
+ import { applyInstallationCommand, deriveDeclaredOperations, } from "../apps/index.js";
3
+ /** Build fixture state through the same approval owner used by installation routes. */
4
+ export async function approvedInstallation(input) {
5
+ const result = applyInstallationCommand(null, {
6
+ kind: "install",
7
+ release: input.release,
8
+ releasePin: "pinned",
9
+ grants: "declared",
10
+ }, {
11
+ by: { kind: "member", memberId: "fixture-owner" },
12
+ role: "owner",
13
+ expectedVersion: null,
14
+ declaration: input.declaration,
15
+ declaredOperations: await deriveDeclaredOperations(input.release, input.declaration, input.descriptors ?? []),
16
+ now: 1000,
17
+ identity: {
18
+ installationId: input.installationId,
19
+ instanceId: input.instanceId,
20
+ },
21
+ });
22
+ if (Result.isFailure(result))
23
+ throw new Error(result.failure.code);
24
+ return { ...result.success.next, visibility: input.visibility ?? "private" };
25
+ }
@@ -0,0 +1,21 @@
1
+ /** One `wrangler dev --local` launch: its config, vars and optional fixed port. */
2
+ export type NativeWorkerOptions = Readonly<{
3
+ /** The Worker's wrangler config. */
4
+ configPath: string;
5
+ /** Plain-text vars passed with `--var`. */
6
+ vars: Readonly<Record<string, string>>;
7
+ /** A fixed port; 0 (the default) reserves a free one. */
8
+ port?: number;
9
+ /** Append the Worker's output to this file, which must have mode 0600. */
10
+ logFile?: string;
11
+ }>;
12
+ /** A ready local Worker; `stop` ends it and removes its persisted state. */
13
+ export type NativeWorker = Readonly<{
14
+ port: number;
15
+ stop: () => Promise<void>;
16
+ }>;
17
+ /**
18
+ * Start `wrangler dev --local` on 127.0.0.1 with fresh persisted state and
19
+ * resolve once its root answers, or reject after 60 seconds.
20
+ */
21
+ export declare function startNativeWorker({ configPath, vars, port: requestedPort, logFile, }: NativeWorkerOptions): Promise<NativeWorker>;
@@ -0,0 +1,106 @@
1
+ import { spawn } from "node:child_process";
2
+ import { once } from "node:events";
3
+ import { closeSync, fstatSync, openSync } from "node:fs";
4
+ import { mkdtemp, rm } from "node:fs/promises";
5
+ import { createServer } from "node:http";
6
+ import { tmpdir } from "node:os";
7
+ import { createRequire } from "node:module";
8
+ import { dirname, join } from "node:path";
9
+ import process from "node:process";
10
+ import { setTimeout as delay } from "node:timers/promises";
11
+ import { z } from "zod";
12
+ // The consumer's own wrangler (an optional peer), resolved beside its config.
13
+ const wranglerBin = (configPath) => join(dirname(createRequire(configPath).resolve("wrangler/package.json")), "bin/wrangler.js");
14
+ /**
15
+ * Start `wrangler dev --local` on 127.0.0.1 with fresh persisted state and
16
+ * resolve once its root answers, or reject after 60 seconds.
17
+ */
18
+ export async function startNativeWorker({ configPath, vars, port: requestedPort = 0, logFile, }) {
19
+ const reservation = createServer();
20
+ reservation.listen(requestedPort, "127.0.0.1");
21
+ await once(reservation, "listening");
22
+ const { port } = z
23
+ .object({ port: z.number().int().positive() })
24
+ .parse(reservation.address());
25
+ await new Promise((resolve) => {
26
+ reservation.close(() => {
27
+ resolve();
28
+ });
29
+ });
30
+ const state = await mkdtemp(join(tmpdir(), "oyster-native-worker-"));
31
+ const logDescriptor = logFile === undefined ? undefined : openSync(logFile, "a", 0o600);
32
+ const child = (() => {
33
+ try {
34
+ if (logDescriptor !== undefined &&
35
+ (fstatSync(logDescriptor).mode & 0o777) !== 0o600)
36
+ throw new Error("Worker proof log must have mode 0600");
37
+ return spawn(process.execPath, [
38
+ wranglerBin(configPath),
39
+ "dev",
40
+ "--config",
41
+ configPath,
42
+ "--local",
43
+ "--ip",
44
+ "127.0.0.1",
45
+ "--port",
46
+ String(port),
47
+ "--persist-to",
48
+ state,
49
+ ...Object.entries(vars).flatMap(([name, value]) => [
50
+ "--var",
51
+ `${name}:${value}`,
52
+ ]),
53
+ ], {
54
+ stdio: logDescriptor === undefined
55
+ ? "ignore"
56
+ : ["ignore", logDescriptor, logDescriptor],
57
+ });
58
+ }
59
+ finally {
60
+ if (logDescriptor !== undefined)
61
+ closeSync(logDescriptor);
62
+ }
63
+ })();
64
+ const startup = { error: null };
65
+ child.on("error", (cause) => {
66
+ startup.error = cause;
67
+ });
68
+ const stop = async () => {
69
+ if (child.exitCode === null &&
70
+ child.signalCode === null &&
71
+ startup.error === null) {
72
+ const exited = once(child, "exit");
73
+ child.kill("SIGTERM");
74
+ await exited;
75
+ }
76
+ await rm(state, { recursive: true, force: true });
77
+ };
78
+ try {
79
+ const deadline = Date.now() + 60_000;
80
+ while (Date.now() < deadline) {
81
+ if (startup.error !== null)
82
+ throw new Error("Worker proof startup failed", {
83
+ cause: startup.error,
84
+ });
85
+ if (child.exitCode !== null || child.signalCode !== null)
86
+ throw new Error(`Worker proof exited before readiness (${String(child.exitCode ?? child.signalCode)})`);
87
+ try {
88
+ const response = await fetch(`http://127.0.0.1:${String(port)}/`, {
89
+ signal: AbortSignal.timeout(1000),
90
+ });
91
+ await response.body?.cancel();
92
+ return { port, stop };
93
+ }
94
+ catch (cause) {
95
+ if (Date.now() >= deadline)
96
+ throw new Error("Worker proof readiness timeout", { cause });
97
+ }
98
+ await delay(100);
99
+ }
100
+ throw new Error("Worker proof readiness timeout");
101
+ }
102
+ catch (cause) {
103
+ await stop();
104
+ throw cause;
105
+ }
106
+ }
@@ -0,0 +1,18 @@
1
+ import { DurableObject } from "cloudflare:workers";
2
+ export type Env = {
3
+ LOADER: WorkerLoader;
4
+ INSTANCES: DurableObjectNamespace<ConfiguredInstance>;
5
+ CATALOG_FIXTURE: string;
6
+ OWNER: string;
7
+ };
8
+ export declare class ConfiguredInstance extends DurableObject<Env> {
9
+ private readonly catalog;
10
+ private readonly runtime;
11
+ private readonly gate;
12
+ constructor(ctx: DurableObjectState, env: Env);
13
+ fetch(request: Request): Promise<Response>;
14
+ }
15
+ declare const _default: {
16
+ fetch(request: Request, env: Env): Response | Promise<Response>;
17
+ };
18
+ export default _default;
@@ -0,0 +1,67 @@
1
+ import { DurableObject } from "cloudflare:workers";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Schema from "effect/Schema";
4
+ import { resolveDurableBackend } from "@fungi.computer/puffball/backend";
5
+ import { createFixtureCatalog, seedFixtureCatalog } from "./catalog.js";
6
+ import { createCloudflareAppInstance, createCloudflareInvocationGate, } from "../cloudflare.js";
7
+ import { invocationFactsSchema, APP_INVOCATION_CEILING_MS, } from "../invocation.js";
8
+ import { selectionSchema, instanceIdSchema } from "../index.js";
9
+ // Configured test admission and catalog persistence stand-ins; not Team/Grant APIs.
10
+ export class ConfiguredInstance extends DurableObject {
11
+ catalog;
12
+ runtime;
13
+ gate;
14
+ constructor(ctx, env) {
15
+ super(ctx, env);
16
+ this.catalog = createFixtureCatalog(env.CATALOG_FIXTURE);
17
+ this.gate = createCloudflareInvocationGate(ctx, () => Promise.resolve());
18
+ void ctx.blockConcurrencyWhile(() => seedFixtureCatalog(env.CATALOG_FIXTURE, this.catalog));
19
+ this.runtime = createCloudflareAppInstance(ctx, env.LOADER, (id) => resolveDurableBackend(this.catalog, id).pipe(Effect.mapError((error) => new Error(String(error)))), () => ({}));
20
+ }
21
+ async fetch(request) {
22
+ const url = new URL(request.url);
23
+ try {
24
+ const authority = Schema.decodeUnknownSync(Schema.fromJsonString(selectionSchema))(request.headers.get("x-test-admission"));
25
+ if (authority.owner.kind !== "deployment" ||
26
+ authority.owner.id !== this.env.OWNER ||
27
+ authority.instanceId !== this.ctx.id.name)
28
+ return new Response("Denied", { status: 403 });
29
+ if (url.pathname.endsWith("/select"))
30
+ return Response.json(await Effect.runPromise(this.runtime.apply({ ...authority, status: "enabled" })));
31
+ if (url.pathname.endsWith("/disable"))
32
+ return Response.json(await Effect.runPromise(this.runtime.apply({ ...authority, status: "disabled" })));
33
+ if (url.pathname.endsWith("/delete"))
34
+ return Response.json(await Effect.runPromise(this.runtime.apply({ ...authority, status: "deleted" })));
35
+ const backend = await Effect.runPromise(resolveDurableBackend(this.catalog, authority.releaseId));
36
+ const facts = Schema.decodeUnknownSync(invocationFactsSchema)({
37
+ instanceId: authority.instanceId,
38
+ selection: authority,
39
+ artifactDigest: backend.digest,
40
+ subject: {
41
+ kind: "call",
42
+ context: {
43
+ computerId: null,
44
+ actor: { kind: "member", memberId: this.env.OWNER },
45
+ },
46
+ },
47
+ grants: [],
48
+ deadlineAt: Date.now() + APP_INVOCATION_CEILING_MS,
49
+ ancestry: [],
50
+ });
51
+ return await this.gate.run(facts, undefined, (frame) => Effect.runPromise(this.runtime.fetch(frame, request)));
52
+ }
53
+ catch {
54
+ return new Response("Unavailable or denied", { status: 409 });
55
+ }
56
+ }
57
+ }
58
+ const route = new URLPattern({ pathname: "/instances/:id/:operation" });
59
+ export default {
60
+ fetch(request, env) {
61
+ const match = route.exec(request.url);
62
+ if (!match)
63
+ return new Response("Not found", { status: 404 });
64
+ const id = Schema.decodeUnknownSync(instanceIdSchema)(match.pathname.groups.id);
65
+ return env.INSTANCES.getByName(id).fetch(request);
66
+ },
67
+ };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Node stand-in for `cloudflare:workers` and `cloudflare:workflows`: only the
3
+ * platform constructors are replaced, so adapters and SQLite run unchanged.
4
+ * Resolve it with `import.meta.resolve("@fungi.computer/oyster/testing/workers-runtime")`
5
+ * from a module hook.
6
+ */
7
+ export declare class DurableObject<Env = unknown> {
8
+ readonly ctx: DurableObjectState;
9
+ readonly env: Env;
10
+ constructor(ctx: DurableObjectState, env: Env);
11
+ }
12
+ export declare class WorkerEntrypoint<Env = unknown> {
13
+ readonly ctx: ExecutionContext;
14
+ readonly env: Env;
15
+ constructor(ctx: ExecutionContext, env: Env);
16
+ }
17
+ export declare class WorkflowEntrypoint<Env = unknown> {
18
+ readonly ctx: ExecutionContext;
19
+ readonly env: Env;
20
+ constructor(ctx: ExecutionContext, env: Env);
21
+ }
22
+ export declare class NonRetryableError extends Error {
23
+ }
24
+ export { RpcTarget } from "@fungi.computer/capnweb";
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Node stand-in for `cloudflare:workers` and `cloudflare:workflows`: only the
3
+ * platform constructors are replaced, so adapters and SQLite run unchanged.
4
+ * Resolve it with `import.meta.resolve("@fungi.computer/oyster/testing/workers-runtime")`
5
+ * from a module hook.
6
+ */
7
+ export class DurableObject {
8
+ ctx;
9
+ env;
10
+ constructor(ctx, env) {
11
+ this.ctx = ctx;
12
+ this.env = env;
13
+ }
14
+ }
15
+ export class WorkerEntrypoint {
16
+ ctx;
17
+ env;
18
+ constructor(ctx, env) {
19
+ this.ctx = ctx;
20
+ this.env = env;
21
+ }
22
+ }
23
+ export class WorkflowEntrypoint {
24
+ ctx;
25
+ env;
26
+ constructor(ctx, env) {
27
+ this.ctx = ctx;
28
+ this.env = env;
29
+ }
30
+ }
31
+ export class NonRetryableError extends Error {
32
+ }
33
+ export { RpcTarget } from "@fungi.computer/capnweb";
@@ -0,0 +1,14 @@
1
+ /** HTTP refusals and transport failures both remain eligible for owner retries. */
2
+ export declare function attemptWebhook(target: Readonly<{
3
+ url: string;
4
+ secret: string;
5
+ }>, delivery: Readonly<{
6
+ id: string;
7
+ type: string;
8
+ body: string;
9
+ }>): Promise<Readonly<{
10
+ kind: "response";
11
+ status: number;
12
+ }> | Readonly<{
13
+ kind: "transport-error";
14
+ }>>;
@@ -0,0 +1,17 @@
1
+ import { webhookHeaders } from "./git.js";
2
+ /** HTTP refusals and transport failures both remain eligible for owner retries. */
3
+ export async function attemptWebhook(target, delivery) {
4
+ try {
5
+ const response = await fetch(target.url, {
6
+ method: "POST",
7
+ headers: await webhookHeaders(target.secret, delivery.id, delivery.type, Math.floor(Date.now() / 1000), delivery.body),
8
+ body: delivery.body,
9
+ signal: AbortSignal.timeout(10_000),
10
+ });
11
+ await response.body?.cancel();
12
+ return { kind: "response", status: response.status };
13
+ }
14
+ catch {
15
+ return { kind: "transport-error" };
16
+ }
17
+ }