@fungi.computer/oyster 0.0.0-stage → 0.1.0

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 (339) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1134 -2
  3. package/dist/agents.d.ts +114 -0
  4. package/dist/agents.js +322 -0
  5. package/dist/app-builds.d.ts +210 -0
  6. package/dist/app-builds.js +499 -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 +31 -0
  10. package/dist/app-sources.js +370 -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/artifact-writer.d.ts +40 -0
  28. package/dist/build/artifact-writer.js +181 -0
  29. package/dist/build/build-jobs.d.ts +128 -0
  30. package/dist/build/build-jobs.js +203 -0
  31. package/dist/build/build-policy.d.ts +10 -0
  32. package/dist/build/build-policy.js +10 -0
  33. package/dist/build/build-step.d.ts +37 -0
  34. package/dist/build/build-step.js +28 -0
  35. package/dist/build/bundled-html.d.ts +7 -0
  36. package/dist/build/bundled-html.js +64 -0
  37. package/dist/build/candidate-builder.d.ts +68 -0
  38. package/dist/build/candidate-builder.js +91 -0
  39. package/dist/build/candidate-cli.d.ts +1 -0
  40. package/dist/build/candidate-cli.js +119 -0
  41. package/dist/build/candidate-cli.mjs +28170 -0
  42. package/dist/build/content-objects.d.ts +37 -0
  43. package/dist/build/content-objects.js +53 -0
  44. package/dist/build/definition-emission.d.ts +34 -0
  45. package/dist/build/definition-emission.js +201 -0
  46. package/dist/build/effect.d.ts +249 -0
  47. package/dist/build/effect.js +436 -0
  48. package/dist/build/git-environment.d.ts +9 -0
  49. package/dist/build/git-environment.js +60 -0
  50. package/dist/build/html-recipe.d.ts +20 -0
  51. package/dist/build/html-recipe.js +51 -0
  52. package/dist/build/local-build.d.ts +67 -0
  53. package/dist/build/local-build.js +1128 -0
  54. package/dist/build/snapshot-package-store.d.ts +12 -0
  55. package/dist/build/snapshot-package-store.js +36 -0
  56. package/dist/build/snapshot-program-scripts.d.ts +6 -0
  57. package/dist/build/snapshot-program-scripts.js +2 -0
  58. package/dist/build/snapshot-programs/snapshot-path.d.ts +2 -0
  59. package/dist/build/snapshot-programs/snapshot-path.js +6 -0
  60. package/dist/build/submission-contract.d.ts +828 -0
  61. package/dist/build/submission-contract.js +180 -0
  62. package/dist/build/workspace-build.d.ts +55 -0
  63. package/dist/build/workspace-build.js +200 -0
  64. package/dist/build-contract.d.ts +7 -0
  65. package/dist/build-contract.js +7 -0
  66. package/dist/build-egress.d.ts +62 -0
  67. package/dist/build-egress.js +148 -0
  68. package/dist/build-jobs.d.ts +2 -0
  69. package/dist/build-jobs.js +2 -0
  70. package/dist/build.d.ts +12 -0
  71. package/dist/build.js +14 -0
  72. package/dist/capability-descriptor.d.ts +46 -0
  73. package/dist/capability-descriptor.js +51 -0
  74. package/dist/catalog-frame.d.ts +39 -0
  75. package/dist/catalog-frame.js +72 -0
  76. package/dist/cause-class.d.ts +19 -0
  77. package/dist/cause-class.js +54 -0
  78. package/dist/cloudflare.d.ts +123 -0
  79. package/dist/cloudflare.js +391 -0
  80. package/dist/computer-terminal.d.ts +38 -0
  81. package/dist/computer-terminal.js +410 -0
  82. package/dist/computer.d.ts +60 -0
  83. package/dist/computer.js +33 -0
  84. package/dist/event-recipients.d.ts +57 -0
  85. package/dist/event-recipients.js +144 -0
  86. package/dist/facet-invocation.d.ts +4 -0
  87. package/dist/facet-invocation.js +45 -0
  88. package/dist/files.d.ts +42 -0
  89. package/dist/files.js +123 -0
  90. package/dist/frame-binding-wire-script.d.ts +2 -0
  91. package/dist/frame-binding-wire-script.js +1 -0
  92. package/dist/frame-binding-wire.d.ts +28 -0
  93. package/dist/frame-binding-wire.js +54 -0
  94. package/dist/git-admission.d.ts +52 -0
  95. package/dist/git-admission.js +188 -0
  96. package/dist/git-auth.d.ts +11 -0
  97. package/dist/git-auth.js +27 -0
  98. package/dist/git-door.d.ts +35 -0
  99. package/dist/git-door.js +108 -0
  100. package/dist/git-journal.d.ts +31 -0
  101. package/dist/git-journal.js +188 -0
  102. package/dist/git.d.ts +31 -0
  103. package/dist/git.js +64 -0
  104. package/dist/guest-policy.d.ts +9 -0
  105. package/dist/guest-policy.js +13 -0
  106. package/dist/host.d.ts +244 -0
  107. package/dist/host.js +1043 -0
  108. package/dist/index.d.ts +166 -0
  109. package/dist/index.js +320 -0
  110. package/dist/installation-consent.d.ts +79 -0
  111. package/dist/installation-consent.js +82 -0
  112. package/dist/internal/background-origins.d.ts +3 -0
  113. package/dist/internal/background-origins.js +8 -0
  114. package/dist/internal/delivery.d.ts +12 -0
  115. package/dist/internal/delivery.js +167 -0
  116. package/dist/internal/host-gate.d.ts +14 -0
  117. package/dist/internal/host-gate.js +62 -0
  118. package/dist/internal/registry-installations.d.ts +6 -0
  119. package/dist/internal/registry-installations.js +12 -0
  120. package/dist/invocation.d.ts +251 -0
  121. package/dist/invocation.js +377 -0
  122. package/dist/keys.d.ts +48 -0
  123. package/dist/keys.js +52 -0
  124. package/dist/mcp-owner.d.ts +27 -0
  125. package/dist/mcp-owner.js +155 -0
  126. package/dist/mcp.d.ts +89 -0
  127. package/dist/mcp.js +429 -0
  128. package/dist/media.d.ts +23 -0
  129. package/dist/media.js +12 -0
  130. package/dist/pack.d.ts +79 -0
  131. package/dist/pack.js +151 -0
  132. package/dist/page/capabilities.d.ts +23 -0
  133. package/dist/page/capabilities.js +256 -0
  134. package/dist/page/computer-handles.d.ts +79 -0
  135. package/dist/page/computer-handles.js +44 -0
  136. package/dist/page/gill-observation.d.ts +4 -0
  137. package/dist/page/gill-observation.js +31 -0
  138. package/dist/page/lifetime.d.ts +6 -0
  139. package/dist/page/lifetime.js +19 -0
  140. package/dist/page/mount.d.ts +163 -0
  141. package/dist/page/mount.js +390 -0
  142. package/dist/page/storage.d.ts +3 -0
  143. package/dist/page/storage.js +82 -0
  144. package/dist/page/terminal.d.ts +5 -0
  145. package/dist/page/terminal.js +7 -0
  146. package/dist/page/window-launch.d.ts +18 -0
  147. package/dist/page/window-launch.js +11 -0
  148. package/dist/page-script.d.ts +14 -0
  149. package/dist/page-script.js +5 -0
  150. package/dist/r2-files.d.ts +3 -0
  151. package/dist/r2-files.js +79 -0
  152. package/dist/release.d.ts +83 -0
  153. package/dist/release.js +167 -0
  154. package/dist/root-data.d.ts +86 -0
  155. package/dist/root-data.js +69 -0
  156. package/dist/shell/app-directory.d.ts +542 -0
  157. package/dist/shell/app-directory.js +199 -0
  158. package/dist/shell/browser-actions.d.ts +7 -0
  159. package/dist/shell/browser-actions.js +56 -0
  160. package/dist/shell/command-host.d.ts +112 -0
  161. package/dist/shell/command-host.js +502 -0
  162. package/dist/shell/commands.d.ts +25 -0
  163. package/dist/shell/commands.js +78 -0
  164. package/dist/shell/index.d.ts +8 -0
  165. package/dist/shell/index.js +8 -0
  166. package/dist/shell/keys.d.ts +41 -0
  167. package/dist/shell/keys.js +134 -0
  168. package/dist/shell/shell-module.d.ts +55 -0
  169. package/dist/shell/shell-module.js +86 -0
  170. package/dist/shell/window-manager.d.ts +124 -0
  171. package/dist/shell/window-manager.js +711 -0
  172. package/dist/shell/workbench-contract.d.ts +68 -0
  173. package/dist/shell/workbench-contract.js +1 -0
  174. package/dist/shell/workbench-directory.d.ts +332 -0
  175. package/dist/shell/workbench-directory.js +35 -0
  176. package/dist/shell/workbench-host-types.d.ts +96 -0
  177. package/dist/shell/workbench-host-types.js +1 -0
  178. package/dist/shell/workbench-host.d.ts +34 -0
  179. package/dist/shell/workbench-host.js +347 -0
  180. package/dist/shell/workbench-model.d.ts +192 -0
  181. package/dist/shell/workbench-model.js +53 -0
  182. package/dist/shell/workbench.d.ts +4 -0
  183. package/dist/shell/workbench.js +2 -0
  184. package/dist/source/fork.d.ts +27 -0
  185. package/dist/source/fork.js +46 -0
  186. package/dist/source/index.d.ts +10 -0
  187. package/dist/source/index.js +10 -0
  188. package/dist/source/seed-receipt.d.ts +53 -0
  189. package/dist/source/seed-receipt.js +18 -0
  190. package/dist/source/team-source.d.ts +32 -0
  191. package/dist/source/team-source.js +42 -0
  192. package/dist/source/template-push.d.ts +33 -0
  193. package/dist/source/template-push.js +223 -0
  194. package/dist/source/template-repository.d.ts +4 -0
  195. package/dist/source/template-repository.js +21 -0
  196. package/dist/source/template-seed.d.ts +68 -0
  197. package/dist/source/template-seed.js +80 -0
  198. package/dist/standalone/agent-directory.d.ts +4 -0
  199. package/dist/standalone/agent-directory.js +4 -0
  200. package/dist/standalone/agents.d.ts +220 -0
  201. package/dist/standalone/agents.js +795 -0
  202. package/dist/standalone/app-builds-binding.d.ts +36 -0
  203. package/dist/standalone/app-builds-binding.js +119 -0
  204. package/dist/standalone/app-set.d.ts +91 -0
  205. package/dist/standalone/app-set.js +395 -0
  206. package/dist/standalone/app-source.d.ts +41 -0
  207. package/dist/standalone/app-source.js +59 -0
  208. package/dist/standalone/builds.d.ts +18 -0
  209. package/dist/standalone/builds.js +101 -0
  210. package/dist/standalone/computer-egress.d.ts +37 -0
  211. package/dist/standalone/computer-egress.js +111 -0
  212. package/dist/standalone/computer.d.ts +122 -0
  213. package/dist/standalone/computer.js +714 -0
  214. package/dist/standalone/install-apps-cli.d.ts +2 -0
  215. package/dist/standalone/install-apps-cli.js +126 -0
  216. package/dist/standalone/instance.d.ts +35 -0
  217. package/dist/standalone/instance.js +74 -0
  218. package/dist/standalone/media.d.ts +76 -0
  219. package/dist/standalone/media.js +227 -0
  220. package/dist/standalone/preview.d.ts +51 -0
  221. package/dist/standalone/preview.js +167 -0
  222. package/dist/standalone/registry.d.ts +102 -0
  223. package/dist/standalone/registry.js +193 -0
  224. package/dist/standalone/tenancy.d.ts +42 -0
  225. package/dist/standalone/tenancy.js +241 -0
  226. package/dist/standalone/worker.d.ts +55 -0
  227. package/dist/standalone/worker.js +147 -0
  228. package/dist/standalone/workflow-authority-readers.d.ts +16 -0
  229. package/dist/standalone/workflow-authority-readers.js +35 -0
  230. package/dist/standalone/workflow-binding.d.ts +216 -0
  231. package/dist/standalone/workflow-binding.js +217 -0
  232. package/dist/standalone/workflow-events.d.ts +46 -0
  233. package/dist/standalone/workflow-events.js +39 -0
  234. package/dist/standalone/workflow-git-capture.d.ts +7 -0
  235. package/dist/standalone/workflow-git-capture.js +62 -0
  236. package/dist/standalone/workflow-host.d.ts +931 -0
  237. package/dist/standalone/workflow-host.js +751 -0
  238. package/dist/standalone/workflow-node-capture.d.ts +13 -0
  239. package/dist/standalone/workflow-node-capture.js +72 -0
  240. package/dist/standalone/workflow-root-authority.d.ts +27 -0
  241. package/dist/standalone/workflow-root-authority.js +87 -0
  242. package/dist/standalone/workflow-schedules.d.ts +52 -0
  243. package/dist/standalone/workflow-schedules.js +40 -0
  244. package/dist/standalone/workflow-trigger-host.d.ts +1694 -0
  245. package/dist/standalone/workflow-trigger-host.js +52 -0
  246. package/dist/standalone/workflows.d.ts +657 -0
  247. package/dist/standalone/workflows.js +554 -0
  248. package/dist/storage-frame.d.ts +64 -0
  249. package/dist/storage-frame.js +28 -0
  250. package/dist/tenancy/auth0.d.ts +64 -0
  251. package/dist/tenancy/auth0.js +198 -0
  252. package/dist/tenancy/cloudflare/rate-limit.d.ts +8 -0
  253. package/dist/tenancy/cloudflare/rate-limit.js +19 -0
  254. package/dist/tenancy/cloudflare/registry.d.ts +50 -0
  255. package/dist/tenancy/cloudflare/registry.js +301 -0
  256. package/dist/tenancy/cloudflare.d.ts +2 -0
  257. package/dist/tenancy/cloudflare.js +2 -0
  258. package/dist/tenancy/cold-directory.d.ts +13 -0
  259. package/dist/tenancy/cold-directory.js +43 -0
  260. package/dist/tenancy/contracts.d.ts +195 -0
  261. package/dist/tenancy/contracts.js +1 -0
  262. package/dist/tenancy/index.d.ts +19 -0
  263. package/dist/tenancy/index.js +156 -0
  264. package/dist/tenancy/owner-key.d.ts +26 -0
  265. package/dist/tenancy/owner-key.js +63 -0
  266. package/dist/tenancy/oyster.d.ts +4 -0
  267. package/dist/tenancy/oyster.js +25 -0
  268. package/dist/tenancy/registry-record.d.ts +7 -0
  269. package/dist/tenancy/registry-record.js +44 -0
  270. package/dist/tenancy/registry.d.ts +27 -0
  271. package/dist/tenancy/registry.js +138 -0
  272. package/dist/tenancy/resolve.d.ts +7 -0
  273. package/dist/tenancy/resolve.js +64 -0
  274. package/dist/tenant.d.ts +81 -0
  275. package/dist/tenant.js +71 -0
  276. package/dist/testing/capabilities.d.ts +29 -0
  277. package/dist/testing/capabilities.js +130 -0
  278. package/dist/testing/catalog.d.ts +3 -0
  279. package/dist/testing/catalog.js +53 -0
  280. package/dist/testing/index.d.ts +10 -0
  281. package/dist/testing/index.js +10 -0
  282. package/dist/testing/installation.d.ts +11 -0
  283. package/dist/testing/installation.js +25 -0
  284. package/dist/testing/native-worker.d.ts +21 -0
  285. package/dist/testing/native-worker.js +106 -0
  286. package/dist/testing/worker.d.ts +18 -0
  287. package/dist/testing/worker.js +67 -0
  288. package/dist/testing/workers-runtime.d.ts +24 -0
  289. package/dist/testing/workers-runtime.js +33 -0
  290. package/dist/webhook-delivery.d.ts +14 -0
  291. package/dist/webhook-delivery.js +17 -0
  292. package/dist/workers-git.d.ts +285 -0
  293. package/dist/workers-git.js +1174 -0
  294. package/dist/workers.d.ts +352 -0
  295. package/dist/workers.js +868 -0
  296. package/docs/api/README.md +82 -0
  297. package/docs/api/agents.md +332 -0
  298. package/docs/api/app-storage.md +209 -0
  299. package/docs/api/apps.md +1375 -0
  300. package/docs/api/auth.md +63 -0
  301. package/docs/api/build-contract.md +1304 -0
  302. package/docs/api/build.md +1712 -0
  303. package/docs/api/cloudflare.md +421 -0
  304. package/docs/api/computer.md +203 -0
  305. package/docs/api/files.md +117 -0
  306. package/docs/api/git.md +470 -0
  307. package/docs/api/host.md +1328 -0
  308. package/docs/api/index.md +430 -0
  309. package/docs/api/invocation.md +738 -0
  310. package/docs/api/keys.md +175 -0
  311. package/docs/api/mcp.md +301 -0
  312. package/docs/api/media.md +40 -0
  313. package/docs/api/modules.md +50 -0
  314. package/docs/api/pack.md +141 -0
  315. package/docs/api/page/mount.md +298 -0
  316. package/docs/api/root-data.md +111 -0
  317. package/docs/api/shell/app-directory.md +599 -0
  318. package/docs/api/shell/command-host.md +224 -0
  319. package/docs/api/shell/commands.md +197 -0
  320. package/docs/api/shell/keys.md +183 -0
  321. package/docs/api/shell/shell-module.md +204 -0
  322. package/docs/api/shell/window-manager.md +267 -0
  323. package/docs/api/shell/workbench-directory.md +119 -0
  324. package/docs/api/shell/workbench.md +205 -0
  325. package/docs/api/shell.md +1915 -0
  326. package/docs/api/tenancy/auth0.md +110 -0
  327. package/docs/api/tenancy/cloudflare.md +221 -0
  328. package/docs/api/tenancy/cold-directory.md +46 -0
  329. package/docs/api/tenancy/owner-key.md +63 -0
  330. package/docs/api/tenancy/registry.md +77 -0
  331. package/docs/api/tenancy/resolve.md +69 -0
  332. package/docs/api/tenancy.md +430 -0
  333. package/docs/api/tenant.md +194 -0
  334. package/docs/api/workers-git.md +1646 -0
  335. package/docs/api/workers.md +1691 -0
  336. package/docs/site/agent-quickstart.mdx +346 -0
  337. package/examples/standalone/worker.ts +7 -0
  338. package/examples/standalone/wrangler.jsonc +82 -0
  339. package/package.json +300 -3
@@ -0,0 +1,1174 @@
1
+ import * as z from "zod";
2
+ import { defaultTenant, implicitTenancy, tenantScope, } from "./tenant.js";
3
+ import { createOysterMcpOwner } from "./mcp-owner.js";
4
+ import { createAdmittedGit, createInstallationGitAdmission, } from "./git-admission.js";
5
+ import { resolveBackend } from "./release.js";
6
+ /**
7
+ * Cloudflare adapters for git Apps on a standalone Oyster: the GIT binding
8
+ * over Artifacts, named keys for agents, and signed webhook delivery for App
9
+ * events. Every credential and secret stays in these host objects.
10
+ */
11
+ import { DurableObject } from "cloudflare:workers";
12
+ import { nativeGitFrameMethods, invokeNativeGit, } from "@fungi.computer/agar/git-rpc";
13
+ import { APP_INVOCATION_CEILING_MS, invocationFacts, invocationCallerContext, } from "./invocation.js";
14
+ import { AppInstanceError, selectionOf } from "./index.js";
15
+ import { ownerOrKeyAuth } from "./auth.js";
16
+ import { causeClass } from "./cause-class.js";
17
+ import { Hono } from "hono";
18
+ import * as Schema from "effect/Schema";
19
+ import { defineIdFamily } from "@fungi.computer/cairn";
20
+ import {} from "@fungi.computer/agar/git";
21
+ import { parseAppMethodCallContext, workflowAttestationSchema, } from "@fungi.computer/puffball/app-method-runtime";
22
+ import { lsRefsRequest, parseLsRefs, } from "./git.js";
23
+ import { createKeyStore, } from "@fungi.computer/ear-tag/keys";
24
+ import { oysterKeyVocabulary, } from "./keys.js";
25
+ import { agentCapabilities, agentApprovalRoutes, agentMcpServerRoutes, } from "./agents.js";
26
+ import { createArtifactsRestNamespace, } from "@fungi.computer/burrow";
27
+ import { attemptWebhook } from "./webhook-delivery.js";
28
+ import { createRecipientDeliveries, } from "./event-recipients.js";
29
+ import { createOyster, oysterPrincipalIsOwner, } from "./host.js";
30
+ import { OysterInstance, oysterBindingEntrypoint, oysterBindings, oysterWorkerHost, r2FileSource, r2ArtifactStore, } from "./workers.js";
31
+ import { registryInstallations } from "./internal/registry-installations.js";
32
+ const infoSchema = Schema.Struct({
33
+ name: Schema.String,
34
+ defaultBranch: Schema.String,
35
+ remote: Schema.String,
36
+ lastPushAt: Schema.optional(Schema.NullOr(Schema.String)),
37
+ source: Schema.optional(Schema.NullOr(Schema.String)),
38
+ });
39
+ const pageSchema = Schema.Struct({
40
+ repos: Schema.Array(Schema.Struct({ name: Schema.String, status: Schema.String })),
41
+ cursor: Schema.optional(Schema.String),
42
+ });
43
+ const plaintextSchema = Schema.Struct({ plaintext: Schema.NonEmptyString });
44
+ const tokenSchema = Schema.Struct({
45
+ ...plaintextSchema.fields,
46
+ expiresAt: Schema.DateFromString,
47
+ });
48
+ class CredentialRejected extends Error {
49
+ }
50
+ const codeSchema = Schema.Struct({ code: Schema.String });
51
+ function errorCode(error) {
52
+ const parsed = Schema.decodeUnknownOption(codeSchema)(error);
53
+ return parsed._tag === "Some" ? parsed.value.code : null;
54
+ }
55
+ /** A provider failure as host diagnostics may name it: its code, never its text. */
56
+ function providerCause(cause) {
57
+ return ((cause instanceof Error ? errorCode(cause) : null) ?? causeClass(cause));
58
+ }
59
+ function appCommit(commit) {
60
+ return {
61
+ ...commit,
62
+ author: { ...commit.author },
63
+ committer: { ...commit.committer },
64
+ parents: [...commit.parents],
65
+ };
66
+ }
67
+ const READY_TIMEOUT_MS = 60_000;
68
+ const TOKEN_TTL_SECONDS = 600;
69
+ const TOKEN_REUSE_MS = 8 * 60_000;
70
+ const TOKEN_EXPIRY_MARGIN_MS = 30_000;
71
+ // Eviction only causes another mint; it never limits repository work.
72
+ const TOKEN_CACHE_ENTRIES = 256;
73
+ const NAME = /^[a-zA-Z0-9._-]{1,100}$/u;
74
+ function wait(ms) {
75
+ return new Promise((resolve) => setTimeout(resolve, ms));
76
+ }
77
+ /** The GIT binding over Artifacts for one installation (`prefix` scopes its names).
78
+ * Credentials remain host-held and are reused until thirty seconds before the
79
+ * provider expiry or eight minutes, whichever comes first. Every operation still
80
+ * checks its captured grant. Provider 401/403 discards the credential for remint.
81
+ */
82
+ export function createArtifactsGit(binding, prefix, grant) {
83
+ // This public adapter owns a fresh cache; only the private Instance composition
84
+ // shares values between adapters, with a verified approval partition.
85
+ return artifactsGit(binding, prefix, grant, new Map(), null);
86
+ }
87
+ // Share only credential values. Every adapter retains its own admission callback
88
+ // and in-flight mint promises; neither survives its invocation frame.
89
+ function artifactsGit(binding, prefix, grant, tokens, approval) {
90
+ const credentialKey = (name, scope) => JSON.stringify(approval === null
91
+ ? [prefix, name, scope]
92
+ : [prefix, name, scope, approval.authorityVersion, approval.releaseId]);
93
+ const requireOperation = (operation) => {
94
+ if (!grant.operations.includes(operation))
95
+ throw new Error(`Git operation denied: ${operation}`);
96
+ grant.beforeProvider?.();
97
+ };
98
+ const provider = (name) => {
99
+ if (!NAME.test(name))
100
+ throw new Error("Invalid repository name");
101
+ const composed = `${prefix}${name}`;
102
+ if (composed.length > 128)
103
+ throw new Error("Invalid repository name");
104
+ return composed;
105
+ };
106
+ const local = (name) => name.startsWith(prefix) ? name.slice(prefix.length) : null;
107
+ const toInfo = (raw) => {
108
+ const info = Schema.decodeUnknownSync(infoSchema)(raw);
109
+ const source = info.source?.replace(/^artifacts:[^/]+\//u, "") ?? null;
110
+ return {
111
+ name: local(info.name) ?? info.name,
112
+ defaultBranch: info.defaultBranch,
113
+ lastPushAt: info.lastPushAt ?? null,
114
+ source: source === null ? null : local(source),
115
+ };
116
+ };
117
+ const minting = new Map();
118
+ const retrying = async (attempt) => {
119
+ for (let tries = 1;; tries += 1) {
120
+ try {
121
+ return await attempt();
122
+ }
123
+ catch (error) {
124
+ if (tries >= 4 ||
125
+ error instanceof CredentialRejected ||
126
+ Schema.isSchemaError(error) ||
127
+ (error instanceof Error &&
128
+ ["NOT_FOUND", "INVALID_RESPONSE"].includes(errorCode(error) ?? "")))
129
+ throw error;
130
+ await wait(250 * tries);
131
+ }
132
+ }
133
+ };
134
+ const status = async (name) => {
135
+ let cursor;
136
+ do {
137
+ const page = Schema.decodeUnknownSync(pageSchema)(await binding.list(cursor === undefined ? { limit: 200 } : { limit: 200, cursor }));
138
+ const found = page.repos.find((repo) => repo.name === name);
139
+ if (found)
140
+ return found.status;
141
+ cursor = page.cursor;
142
+ } while (cursor !== undefined);
143
+ return "missing";
144
+ };
145
+ const whenReady = async (name) => {
146
+ const deadline = Date.now() + READY_TIMEOUT_MS;
147
+ while (Date.now() < deadline) {
148
+ if ((await status(name)) === "ready") {
149
+ try {
150
+ return toInfo(await (await binding.get(name)).info());
151
+ }
152
+ catch (error) {
153
+ const code = error instanceof Error ? errorCode(error) : null;
154
+ if (code !== "FORK_IN_PROGRESS" && code !== "IMPORT_IN_PROGRESS")
155
+ throw error;
156
+ }
157
+ }
158
+ await wait(400);
159
+ }
160
+ throw new Error("Repository did not become ready");
161
+ };
162
+ const credential = async (name, scope) => {
163
+ const target = provider(name);
164
+ grant.beforeProvider?.();
165
+ const key = credentialKey(name, scope);
166
+ const cached = tokens.get(key);
167
+ if (cached && cached.until > Date.now())
168
+ return { remote: cached.remote, token: cached.token };
169
+ if (cached !== undefined)
170
+ tokens.delete(key);
171
+ let pending = minting.get(key);
172
+ if (pending === undefined) {
173
+ pending = retrying(async () => {
174
+ grant.beforeProvider?.();
175
+ const startedAt = Date.now();
176
+ const repo = await binding.get(target);
177
+ grant.beforeProvider?.();
178
+ const info = Schema.decodeUnknownSync(infoSchema)(await repo.info());
179
+ grant.beforeProvider?.();
180
+ const rawToken = await repo.createToken(scope, TOKEN_TTL_SECONDS);
181
+ grant.beforeProvider?.();
182
+ const now = Date.now();
183
+ let issued;
184
+ let until;
185
+ try {
186
+ issued = Schema.decodeUnknownSync(tokenSchema)(rawToken);
187
+ until = Math.min(issued.expiresAt.getTime() - TOKEN_EXPIRY_MARGIN_MS, startedAt + TOKEN_REUSE_MS);
188
+ if (until <= now)
189
+ throw new CredentialRejected("Artifacts credential expires too soon");
190
+ }
191
+ catch (cause) {
192
+ const rejected = new CredentialRejected("Artifacts credential rejected", { cause });
193
+ const returned = Schema.decodeUnknownOption(plaintextSchema)(rawToken);
194
+ if (returned._tag === "Some") {
195
+ try {
196
+ await repo.revokeToken(returned.value.plaintext);
197
+ }
198
+ catch (cleanupCause) {
199
+ // Best effort: keep only safe cause classes, never provider text/token data.
200
+ rejected.cause = {
201
+ rejection: causeClass(cause),
202
+ cleanup: causeClass(cleanupCause),
203
+ };
204
+ throw rejected;
205
+ }
206
+ }
207
+ throw rejected;
208
+ }
209
+ for (const [cachedKey, value] of tokens)
210
+ if (value.until <= now)
211
+ tokens.delete(cachedKey);
212
+ if (!tokens.has(key) && tokens.size >= TOKEN_CACHE_ENTRIES) {
213
+ const oldest = tokens.keys().next();
214
+ if (!oldest.done)
215
+ tokens.delete(oldest.value);
216
+ }
217
+ tokens.set(key, {
218
+ remote: info.remote,
219
+ token: issued.plaintext,
220
+ until,
221
+ });
222
+ return { remote: info.remote, token: issued.plaintext };
223
+ });
224
+ minting.set(key, pending);
225
+ }
226
+ try {
227
+ const result = await pending;
228
+ grant.beforeProvider?.();
229
+ return result;
230
+ }
231
+ finally {
232
+ if (minting.get(key) === pending)
233
+ minting.delete(key);
234
+ }
235
+ };
236
+ const exchange = async (name, request) => {
237
+ let access;
238
+ try {
239
+ access = await credential(name, request.service === "receive-pack" ? "write" : "read");
240
+ }
241
+ catch (error) {
242
+ if (error instanceof Error && errorCode(error) === "NOT_FOUND")
243
+ return {
244
+ status: 404,
245
+ headers: { "content-type": "text/plain; charset=utf-8" },
246
+ body: new TextEncoder().encode("Repository not found"),
247
+ };
248
+ throw error;
249
+ }
250
+ const { remote, token } = access;
251
+ const key = credentialKey(name, request.service === "receive-pack" ? "write" : "read");
252
+ const target = request.phase === "advertise"
253
+ ? `${remote}/info/refs?service=git-${request.service}`
254
+ : `${remote}/git-${request.service}`;
255
+ const incoming = new Headers(request.headers);
256
+ const headers = new Headers({ Authorization: `Bearer ${token}` });
257
+ for (const header of ["content-type", "accept", "git-protocol"]) {
258
+ const value = incoming.get(header);
259
+ if (value !== null)
260
+ headers.set(header, value);
261
+ }
262
+ grant.beforeProvider?.();
263
+ const response = await fetch(target, {
264
+ method: request.phase === "advertise" ? "GET" : "POST",
265
+ headers,
266
+ body: request.body === null ? null : new Uint8Array(request.body),
267
+ redirect: "manual",
268
+ });
269
+ if ((response.status === 401 || response.status === 403) &&
270
+ tokens.get(key)?.token === token)
271
+ tokens.delete(key);
272
+ if (response.status >= 300 && response.status < 400) {
273
+ await response.body?.cancel();
274
+ throw new Error("Artifacts Git redirect refused");
275
+ }
276
+ return {
277
+ status: response.status,
278
+ headers: Object.fromEntries(response.headers),
279
+ body: new Uint8Array(await response.arrayBuffer()),
280
+ };
281
+ };
282
+ const finishCreated = async (name, initialToken) => {
283
+ try {
284
+ await (await binding.get(name)).revokeToken(initialToken);
285
+ }
286
+ catch (revokeCause) {
287
+ // Provider codes are a closed provider vocabulary; never copy provider text.
288
+ const revoke = providerCause(revokeCause);
289
+ try {
290
+ await binding.delete(name);
291
+ }
292
+ catch (deleteCause) {
293
+ console.error("Artifacts repository activation cleanup failed", {
294
+ repository: name,
295
+ revoke,
296
+ delete: providerCause(deleteCause),
297
+ });
298
+ throw new Error(`Artifacts initial token cleanup refused repository activation (revoke ${revoke}, delete ${providerCause(deleteCause)})`, { cause: deleteCause });
299
+ }
300
+ throw new Error(`Artifacts initial token cleanup refused repository activation (revoke ${revoke})`, { cause: revokeCause });
301
+ }
302
+ };
303
+ return {
304
+ async create(name, opts) {
305
+ requireOperation("create");
306
+ const target = provider(name);
307
+ const created = await binding.create(target, {
308
+ description: opts?.description ?? "",
309
+ readOnly: false,
310
+ setDefaultBranch: opts?.defaultBranch ?? "main",
311
+ });
312
+ await finishCreated(target, created.token);
313
+ return toInfo(created);
314
+ },
315
+ async fork(source, target, opts) {
316
+ requireOperation("fork");
317
+ const destination = provider(target);
318
+ const repo = await binding.get(provider(source));
319
+ grant.beforeProvider?.();
320
+ let created;
321
+ try {
322
+ created = await repo.fork(destination, {
323
+ description: opts?.description ?? "",
324
+ readOnly: false,
325
+ defaultBranchOnly: false,
326
+ });
327
+ }
328
+ catch (forkCause) {
329
+ // A destination that already exists (an earlier attempt may have
330
+ // created it before failing) answers once ready; its `source` tells
331
+ // the caller whether it is the fork it asked for.
332
+ if ((await status(destination)) === "missing")
333
+ throw forkCause;
334
+ const existing = await whenReady(destination);
335
+ console.warn("Artifacts fork destination already exists", {
336
+ repository: destination,
337
+ source: existing.source,
338
+ fork: providerCause(forkCause),
339
+ });
340
+ return existing;
341
+ }
342
+ // Artifacts forks asynchronously: the destination refuses token and
343
+ // read operations until the copy is ready, so wait before cleanup.
344
+ const ready = await whenReady(destination);
345
+ await finishCreated(destination, created.token);
346
+ return ready;
347
+ },
348
+ async info(name) {
349
+ requireOperation("read");
350
+ try {
351
+ const repo = await binding.get(provider(name));
352
+ grant.beforeProvider?.();
353
+ return toInfo(await repo.info());
354
+ }
355
+ catch (error) {
356
+ if (error instanceof Error && errorCode(error) === "NOT_FOUND")
357
+ return null;
358
+ throw error;
359
+ }
360
+ },
361
+ async delete(name) {
362
+ requireOperation("write");
363
+ tokens.delete(credentialKey(name, "read"));
364
+ tokens.delete(credentialKey(name, "write"));
365
+ return binding.delete(provider(name));
366
+ },
367
+ async log(name, opts) {
368
+ requireOperation("read");
369
+ const repo = await binding.get(provider(name));
370
+ grant.beforeProvider?.();
371
+ const commits = await repo.log({
372
+ ref: opts.ref,
373
+ limit: opts.limit ?? 50,
374
+ offset: opts.offset ?? 0,
375
+ });
376
+ return commits.map(appCommit);
377
+ },
378
+ async readCommit(name, hash) {
379
+ requireOperation("read");
380
+ const repo = await binding.get(provider(name));
381
+ grant.beforeProvider?.();
382
+ const commit = await repo.readCommit(hash);
383
+ return commit === null ? null : appCommit(commit);
384
+ },
385
+ async readTree(name, hash) {
386
+ requireOperation("read");
387
+ const repo = await binding.get(provider(name));
388
+ grant.beforeProvider?.();
389
+ const entries = await repo.readTree(hash);
390
+ return entries === null ? null : [...entries];
391
+ },
392
+ async readBlob(name, hash) {
393
+ requireOperation("read");
394
+ const repo = await binding.get(provider(name));
395
+ grant.beforeProvider?.();
396
+ const blob = await repo.readBlob(hash);
397
+ return blob === null ? null : new Uint8Array(await blob.arrayBuffer());
398
+ },
399
+ async refs(name) {
400
+ requireOperation("read");
401
+ const response = await exchange(name, {
402
+ service: "upload-pack",
403
+ phase: "exchange",
404
+ headers: {
405
+ "content-type": "application/x-git-upload-pack-request",
406
+ "git-protocol": "version=2",
407
+ },
408
+ body: lsRefsRequest(),
409
+ });
410
+ if (response.status !== 200)
411
+ throw new Error(`ls-refs failed (${String(response.status)})`);
412
+ return parseLsRefs(response.body);
413
+ },
414
+ async exchange(name, request) {
415
+ requireOperation("exchange");
416
+ return exchange(name, request);
417
+ },
418
+ async credential(name, scope) {
419
+ requireOperation(scope);
420
+ return credential(name, scope);
421
+ },
422
+ };
423
+ }
424
+ const webhookIds = defineIdFamily({
425
+ family: "oyster-webhooks",
426
+ version: 1,
427
+ kinds: { subscription: { prefix: "whk" } },
428
+ });
429
+ /**
430
+ * Named keys an owner issues directly: an Ear Tag key store scoped to this
431
+ * tenant. A key's id is the name the owner chose for it, so forks and other
432
+ * name-derived identities (`service.ts:95`) stay stable across a reissue.
433
+ * Plaintext keys are never stored.
434
+ */
435
+ export class OysterKeys extends DurableObject {
436
+ #store;
437
+ constructor(ctx, env) {
438
+ super(ctx, env);
439
+ this.#store = createKeyStore({
440
+ sql: ctx.storage.sql,
441
+ commit: (body) => Promise.resolve(ctx.storage.transactionSync(body)),
442
+ vocabulary: oysterKeyVocabulary,
443
+ prefix: "oyk",
444
+ // The DO trusts only its Worker caller, which checks the owner on
445
+ // every path (gitAdminRoutes, ownerOrKeyAuth) before it ever calls
446
+ // here, so every actor this store admits is already the owner.
447
+ authorizeIssuer: () => Promise.resolve({ kind: "unrestricted" }),
448
+ });
449
+ }
450
+ /**
451
+ * Issue a key named `name`, its id too. Refuses `conflict` once a name has
452
+ * been used, including by a now-revoked key: a name is never reused.
453
+ */
454
+ issue(owner, input) {
455
+ return this.#store.issue(owner, {
456
+ name: input.name,
457
+ keyId: input.name,
458
+ grants: input.grants,
459
+ expiresAt: input.expiresAt,
460
+ });
461
+ }
462
+ /** Revoke a key by id; idempotent on repeat. */
463
+ revoke(owner, requestId, keyId) {
464
+ return this.#store.revoke(owner, { requestId, keyId });
465
+ }
466
+ /** Every key this tenant has issued, newest first. */
467
+ list(owner) {
468
+ return this.#store.list(owner);
469
+ }
470
+ /** Verify a bearer secret and return its current principal. */
471
+ verify(secret) {
472
+ return this.#store.verify(secret);
473
+ }
474
+ /** The live facts behind one key id, for the host's `keys.live` port; `null` for an unknown, revoked, expired or suspended key. */
475
+ async live(keyId) {
476
+ const result = await this.#store.read("host", keyId);
477
+ if (result.kind !== "ok")
478
+ return null;
479
+ return {
480
+ keyId: result.value.keyId,
481
+ owner: result.value.owner,
482
+ grantVersion: result.value.grantVersion,
483
+ grants: result.value.scope.grants,
484
+ };
485
+ }
486
+ }
487
+ const RETRY_DELAYS_MS = [10_000, 60_000, 300_000, 1_800_000, 7_200_000];
488
+ const subscriptionRow = Schema.Struct({
489
+ id: Schema.String,
490
+ installation_id: Schema.String,
491
+ url: Schema.String,
492
+ types: Schema.String,
493
+ created_at: Schema.Number,
494
+ });
495
+ const deliveryRow = Schema.Struct({
496
+ id: Schema.String,
497
+ subscription: Schema.String,
498
+ type: Schema.String,
499
+ state: Schema.Literals(["pending", "delivered", "failed"]),
500
+ attempts: Schema.Number,
501
+ last_status: Schema.NullOr(Schema.Number),
502
+ updated_at: Schema.Number,
503
+ });
504
+ const typesSchema = Schema.fromJsonString(Schema.Array(Schema.String));
505
+ function toSubscription(row) {
506
+ const decoded = Schema.decodeUnknownSync(subscriptionRow)(row);
507
+ return {
508
+ id: decoded.id,
509
+ installationId: decoded.installation_id,
510
+ url: decoded.url,
511
+ types: Schema.decodeUnknownSync(typesSchema)(decoded.types),
512
+ createdAt: decoded.created_at,
513
+ };
514
+ }
515
+ /**
516
+ * The tenant's App event delivery owner: webhook subscriptions with signed,
517
+ * retried deliveries, and host recipients with durable per-recipient records.
518
+ * An emit commits here and returns; every delivery happens on the alarm.
519
+ */
520
+ export class OysterWebhooks extends DurableObject {
521
+ #recipients;
522
+ constructor(ctx, env) {
523
+ super(ctx, env);
524
+ ctx.storage.sql.exec(`
525
+ CREATE TABLE IF NOT EXISTS oyster_subscriptions (
526
+ id TEXT PRIMARY KEY, installation_id TEXT NOT NULL, url TEXT NOT NULL, secret TEXT NOT NULL,
527
+ types TEXT NOT NULL, created_at INTEGER NOT NULL) STRICT;
528
+ CREATE TABLE IF NOT EXISTS oyster_deliveries (
529
+ id TEXT PRIMARY KEY, subscription TEXT NOT NULL, type TEXT NOT NULL, body TEXT NOT NULL,
530
+ state TEXT NOT NULL, attempts INTEGER NOT NULL, next_at INTEGER NOT NULL,
531
+ last_status INTEGER, updated_at INTEGER NOT NULL) STRICT;
532
+ CREATE INDEX IF NOT EXISTS oyster_deliveries_due ON oyster_deliveries(state, next_at);
533
+ `);
534
+ this.#recipients = createRecipientDeliveries({
535
+ sql: ctx.storage.sql,
536
+ now: () => Date.now(),
537
+ wake: (at) => this.wakeBy(at),
538
+ deliver: (recipient, event) => this.deliverToRecipient({ recipient, event }),
539
+ });
540
+ }
541
+ /**
542
+ * A root that names host recipients delivers to them here. This host has
543
+ * none, so a row that names one fails rather than claiming delivery.
544
+ */
545
+ deliverToRecipient(delivery) {
546
+ return Promise.reject(new Error(`This host has no event recipient ${delivery.recipient}`));
547
+ }
548
+ /** Arm the one alarm no later than `at`; webhooks and recipients share it. */
549
+ async wakeBy(at) {
550
+ const current = await this.ctx.storage.getAlarm();
551
+ if (current === null || at < current)
552
+ await this.ctx.storage.setAlarm(at);
553
+ }
554
+ /** Recent deliveries to one host recipient, newest first. */
555
+ recipientDeliveries(recipient) {
556
+ return this.#recipients.deliveries(recipient);
557
+ }
558
+ /** Subscribe `url` to an installation's events; returns the secret once. */
559
+ subscribe(input) {
560
+ const secret = input.secret ??
561
+ `whsec_${btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(24))))}`;
562
+ const subscription = {
563
+ id: webhookIds.subscription.generate(),
564
+ installationId: input.installationId,
565
+ url: input.url,
566
+ types: [...input.types],
567
+ createdAt: Date.now(),
568
+ };
569
+ this.ctx.storage.sql.exec("INSERT INTO oyster_subscriptions (id, installation_id, url, secret, types, created_at) VALUES (?, ?, ?, ?, ?, ?)", subscription.id, subscription.installationId, subscription.url, secret, JSON.stringify(subscription.types), subscription.createdAt);
570
+ return { subscription, secret };
571
+ }
572
+ /** Every subscription. */
573
+ subscriptions() {
574
+ return this.ctx.storage.sql
575
+ .exec("SELECT id, installation_id, url, types, created_at FROM oyster_subscriptions ORDER BY created_at")
576
+ .toArray()
577
+ .map(toSubscription);
578
+ }
579
+ /** Remove a subscription and its deliveries. */
580
+ unsubscribe(id) {
581
+ const removed = this.ctx.storage.sql.exec("DELETE FROM oyster_subscriptions WHERE id = ?", id).rowsWritten > 0;
582
+ this.ctx.storage.sql.exec("DELETE FROM oyster_deliveries WHERE subscription = ?", id);
583
+ return removed;
584
+ }
585
+ /** Recent deliveries for one subscription, newest first. */
586
+ deliveries(subscription) {
587
+ return this.ctx.storage.sql
588
+ .exec("SELECT id, subscription, type, state, attempts, last_status, updated_at FROM oyster_deliveries WHERE subscription = ? ORDER BY updated_at DESC LIMIT 50", subscription)
589
+ .toArray()
590
+ .map((row) => {
591
+ const decoded = Schema.decodeUnknownSync(deliveryRow)(row);
592
+ return {
593
+ id: decoded.id,
594
+ subscription: decoded.subscription,
595
+ type: decoded.type,
596
+ state: decoded.state,
597
+ attempts: decoded.attempts,
598
+ lastStatus: decoded.last_status,
599
+ updatedAt: decoded.updated_at,
600
+ };
601
+ });
602
+ }
603
+ /**
604
+ * Accept an emitted event: queue it for every matching subscription and
605
+ * every named host recipient (idempotent per event id). Nothing is delivered
606
+ * before this returns, so no recipient can fail the emitter's write.
607
+ */
608
+ async enqueue(source, payload, eventId, recipients) {
609
+ const { installationId } = source;
610
+ const now = Date.now();
611
+ let queued = this.#recipients.enqueue({ source, type: payload.type, eventId, payload }, recipients);
612
+ for (const subscription of this.subscriptions()) {
613
+ if (subscription.installationId !== installationId)
614
+ continue;
615
+ if (subscription.types.length > 0 &&
616
+ !subscription.types.includes(payload.type))
617
+ continue;
618
+ queued += this.ctx.storage.sql.exec("INSERT OR IGNORE INTO oyster_deliveries (id, subscription, type, body, state, attempts, next_at, last_status, updated_at) VALUES (?, ?, ?, ?, 'pending', 0, ?, NULL, ?)", `${subscription.id}.${eventId}`, subscription.id, payload.type, JSON.stringify(payload), now, now).rowsWritten;
619
+ }
620
+ if (queued > 0)
621
+ await this.wakeBy(now);
622
+ return queued;
623
+ }
624
+ /** Deliver due webhooks and recipient rows, then schedule the next retry. */
625
+ async alarm() {
626
+ await this.#recipients.drain();
627
+ const now = Date.now();
628
+ const due = this.ctx.storage.sql
629
+ .exec("SELECT id, subscription, type, body, attempts FROM oyster_deliveries WHERE state = 'pending' AND next_at <= ? ORDER BY next_at LIMIT 25", now)
630
+ .toArray();
631
+ await Promise.all(due.map(async (delivery) => {
632
+ const target = this.ctx.storage.sql
633
+ .exec("SELECT url, secret FROM oyster_subscriptions WHERE id = ?", delivery.subscription)
634
+ .toArray()[0];
635
+ let status = null;
636
+ if (target !== undefined) {
637
+ const attempt = await attemptWebhook(target, delivery);
638
+ status = attempt.kind === "response" ? attempt.status : null;
639
+ }
640
+ const attempts = delivery.attempts + 1;
641
+ const delivered = status !== null && status >= 200 && status < 300;
642
+ this.ctx.storage.sql.exec("UPDATE oyster_deliveries SET state = ?, attempts = ?, next_at = ?, last_status = ?, updated_at = ? WHERE id = ?", delivered
643
+ ? "delivered"
644
+ : target === undefined || attempts > RETRY_DELAYS_MS.length
645
+ ? "failed"
646
+ : "pending", attempts, now + (RETRY_DELAYS_MS[attempts - 1] ?? 0), status, Date.now(), delivery.id);
647
+ }));
648
+ const next = this.ctx.storage.sql
649
+ .exec("SELECT MIN(next_at) AS at FROM oyster_deliveries WHERE state = 'pending'")
650
+ .one().at;
651
+ if (next !== null)
652
+ await this.wakeBy(Math.max(next, Date.now() + 250));
653
+ }
654
+ }
655
+ /** The Artifacts namespace: the binding when present, else the REST API. */
656
+ export function artifactsOf(env) {
657
+ if (env.ARTIFACTS !== undefined)
658
+ return env.ARTIFACTS;
659
+ if (env.ARTIFACTS_ACCOUNT_ID === undefined ||
660
+ env.ARTIFACTS_NAMESPACE === undefined ||
661
+ env.ARTIFACTS_API_TOKEN === undefined)
662
+ throw new Error("Configure the ARTIFACTS binding or ARTIFACTS_ACCOUNT_ID, ARTIFACTS_NAMESPACE and ARTIFACTS_API_TOKEN");
663
+ return createArtifactsRestNamespace({
664
+ accountId: env.ARTIFACTS_ACCOUNT_ID,
665
+ namespace: env.ARTIFACTS_NAMESPACE,
666
+ apiToken: env.ARTIFACTS_API_TOKEN,
667
+ });
668
+ }
669
+ const eventArgs = Schema.Tuple([
670
+ Schema.String,
671
+ Schema.Struct({
672
+ type: Schema.String,
673
+ timestamp: Schema.String,
674
+ data: Schema.Json,
675
+ }),
676
+ Schema.String,
677
+ ]);
678
+ const emitMethod = {
679
+ decodeArgs: Schema.decodeUnknownSync(eventArgs, {
680
+ onExcessProperty: "error",
681
+ }),
682
+ decodeResult: Schema.decodeUnknownSync(Schema.Undefined),
683
+ requiredOperations: () => ["emit"],
684
+ };
685
+ // Admission and in-flight provider work remain frame-local.
686
+ const gitSessions = new WeakMap();
687
+ // Keyed by the Instance's stable binding context, so physical credentials span
688
+ // frames only within one Instance. Values are host-held, expiry-bounded and
689
+ // partitioned by tenant/installation prefix, repository, read/write scope,
690
+ // authority version and Release; every frame keeps its own admission.
691
+ const gitCredentials = new WeakMap();
692
+ /**
693
+ * EVENTS: commit the emit to this tenant's delivery owner and return. Webhook
694
+ * subscribers and the host recipients a root names are delivered afterwards,
695
+ * so a subscriber outage never fails the emitting App's write.
696
+ */
697
+ export function eventsBindingFor(recipients) {
698
+ return {
699
+ methods: { emit: emitMethod },
700
+ async dispatch(context, frame, _props, _method, args) {
701
+ frame.signal.throwIfAborted();
702
+ const [type, payload, eventId] = emitMethod.decodeArgs(args);
703
+ if (payload.type !== type)
704
+ throw new Error("Event type does not match its payload");
705
+ const { owner, appId, releaseId } = frame.facts.selection;
706
+ const named = await recipients(context, frame);
707
+ frame.signal.throwIfAborted();
708
+ await context.env.WEBHOOKS.getByName(context.scope().durableName("webhooks")).enqueue({ installationId: owner.id, appId, releaseId }, payload, eventId, named);
709
+ frame.signal.throwIfAborted();
710
+ return undefined;
711
+ },
712
+ };
713
+ }
714
+ /** EVENTS for a host with webhook subscribers only. */
715
+ export const eventsBinding = eventsBindingFor(() => Promise.resolve([]));
716
+ /** The plain host's bindings plus native GIT over Artifacts and signed EVENTS. */
717
+ export const gitBindings = {
718
+ ...oysterBindings,
719
+ GIT: {
720
+ methods: nativeGitFrameMethods,
721
+ async dispatch(context, frame, _props, method, args) {
722
+ frame.signal.throwIfAborted();
723
+ const scope = context.scope();
724
+ let credentials = gitCredentials.get(context);
725
+ if (credentials === undefined) {
726
+ credentials = new Map();
727
+ gitCredentials.set(context, credentials);
728
+ }
729
+ let gitStore = gitSessions.get(frame);
730
+ if (gitStore === undefined) {
731
+ gitStore = artifactsGit(artifactsOf(context.env), scope.repositoryPrefix(frame.facts.selection.owner.id), {
732
+ operations: ["create", "fork", "read", "write", "exchange"],
733
+ beforeProvider: () => {
734
+ frame.signal.throwIfAborted();
735
+ },
736
+ }, credentials, frame.facts.selection);
737
+ gitSessions.set(frame, gitStore);
738
+ frame.signal.addEventListener("abort", () => gitSessions.delete(frame), { once: true });
739
+ }
740
+ const installation = await registryInstallations(context.env, scope).get(frame.facts.selection.owner.id);
741
+ frame.signal.throwIfAborted();
742
+ if (installation === null)
743
+ throw new AppInstanceError("revoked");
744
+ const git = await oysterWorkerGit(context.env, { scope }, { invocation: frame, store: gitStore }).git(installation);
745
+ frame.signal.throwIfAborted();
746
+ const result = await invokeNativeGit(git.binding(frame.facts.subject.kind === "call"
747
+ ? frame.facts.subject.context.actor
748
+ : null), method, args);
749
+ frame.signal.throwIfAborted();
750
+ return result;
751
+ },
752
+ },
753
+ EVENTS: eventsBinding,
754
+ };
755
+ /** A git Worker's one guest binding entrypoint, over `gitBindings`. */
756
+ export class OysterBinding extends oysterBindingEntrypoint(gitBindings) {
757
+ }
758
+ const subscribeSchema = Schema.Struct({
759
+ installationId: Schema.String,
760
+ url: Schema.String.check(Schema.isStartingWith("https://")),
761
+ types: Schema.optional(Schema.Array(Schema.String)),
762
+ secret: Schema.optional(Schema.String.check(Schema.isMinLength(16))),
763
+ });
764
+ const keyNamePattern = /^[a-z0-9][a-z0-9-]{0,39}$/u;
765
+ /** `invocation.ts:486` uses `team-key:visitor` for anonymous readers; a key can never claim that name. */
766
+ const reservedKeyName = "visitor";
767
+ const issueKeySchema = Schema.Struct({
768
+ name: Schema.String.check(Schema.isPattern(keyNamePattern)),
769
+ grants: Schema.Unknown,
770
+ expiresAt: Schema.optional(Schema.Number),
771
+ });
772
+ const revokeKeySchema = Schema.Struct({
773
+ keyId: Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(256)),
774
+ requestId: Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_.:-]{1,112}$/u)),
775
+ });
776
+ const oysterGrantsArraySchema = z
777
+ .array(oysterKeyVocabulary.grantSchema)
778
+ .min(1)
779
+ .max(64);
780
+ function keySummary(record) {
781
+ return {
782
+ keyId: record.keyId,
783
+ name: record.name,
784
+ grants: record.scope?.grants ?? [],
785
+ expiresAt: record.expiresAt,
786
+ createdAt: record.createdAt,
787
+ lastUsedAt: record.lastUsedAt,
788
+ revokedAt: record.revokedAt,
789
+ };
790
+ }
791
+ /** Owner routes for keys and webhooks; `createOyster` mounts them owner-only. */
792
+ export function gitAdminRoutes(env, scope) {
793
+ const routes = new Hono();
794
+ const keys = () => env.KEYS.getByName(scope.durableName("keys"));
795
+ routes.get("/api/keys", async (context) => {
796
+ const owner = context.get("principal");
797
+ if (!oysterPrincipalIsOwner(owner))
798
+ return Response.json({ error: "Owner only" }, { status: 403 });
799
+ const listed = await keys().list(owner.actor.memberId);
800
+ if (listed.kind === "error")
801
+ return Response.json({ error: listed.error.code }, { status: 500 });
802
+ return Response.json({ keys: listed.value.map(keySummary) });
803
+ });
804
+ routes.post("/api/keys", async (context) => {
805
+ const body = Schema.decodeUnknownOption(issueKeySchema)(await context.req.json());
806
+ if (body._tag === "None")
807
+ return Response.json({ error: "Invalid key" }, { status: 400 });
808
+ if (body.value.name === reservedKeyName)
809
+ return Response.json({ error: "Invalid key name" }, { status: 400 });
810
+ const owner = context.get("principal");
811
+ if (!oysterPrincipalIsOwner(owner))
812
+ return Response.json({ error: "Owner only" }, { status: 403 });
813
+ const grants = oysterGrantsArraySchema.safeParse(body.value.grants);
814
+ if (!grants.success)
815
+ return Response.json({ error: "Invalid grants" }, { status: 400 });
816
+ const issued = await keys().issue(owner.actor.memberId, {
817
+ name: body.value.name,
818
+ grants: grants.data,
819
+ expiresAt: body.value.expiresAt,
820
+ });
821
+ if (issued.kind === "error")
822
+ return Response.json({ error: issued.error.code }, { status: issued.error.code === "conflict" ? 409 : 400 });
823
+ return Response.json({ plaintext: issued.value.key, key: keySummary(issued.value.record) }, { status: 201 });
824
+ });
825
+ routes.post("/api/keys/revoke", async (context) => {
826
+ const body = Schema.decodeUnknownOption(revokeKeySchema)(await context.req.json());
827
+ if (body._tag === "None")
828
+ return Response.json({ error: "Invalid request" }, { status: 400 });
829
+ const owner = context.get("principal");
830
+ if (!oysterPrincipalIsOwner(owner))
831
+ return Response.json({ error: "Owner only" }, { status: 403 });
832
+ const revoked = await keys().revoke(owner.actor.memberId, body.value.requestId, body.value.keyId);
833
+ if (revoked.kind === "error")
834
+ return Response.json({ error: revoked.error.code }, { status: revoked.error.code === "denied" ? 404 : 400 });
835
+ return Response.json({ key: keySummary(revoked.value) });
836
+ });
837
+ routes.post("/api/webhooks", async (context) => {
838
+ const body = Schema.decodeUnknownOption(subscribeSchema)(await context.req.json());
839
+ if (body._tag === "None")
840
+ return Response.json({ error: "Invalid subscription" }, { status: 400 });
841
+ return Response.json(await env.WEBHOOKS.getByName(scope.durableName("webhooks")).subscribe({
842
+ ...body.value,
843
+ types: body.value.types ?? [],
844
+ }), { status: 201 });
845
+ });
846
+ routes.get("/api/webhooks", async () => Response.json(await env.WEBHOOKS.getByName(scope.durableName("webhooks")).subscriptions()));
847
+ routes.get("/api/webhooks/:id/deliveries", async (context) => Response.json(await env.WEBHOOKS.getByName(scope.durableName("webhooks")).deliveries(context.req.param("id"))));
848
+ routes.delete("/api/webhooks/:id", async (context) => (await env.WEBHOOKS.getByName(scope.durableName("webhooks")).unsubscribe(context.req.param("id")))
849
+ ? new Response(null, { status: 204 })
850
+ : Response.json({ error: "Not found" }, { status: 404 }));
851
+ return routes;
852
+ }
853
+ /**
854
+ * An App Instance whose backend may also receive declared Git and Events
855
+ * bindings. Background wakes readmit members through the deployable root's
856
+ * `hostFor`, the same composition HTTP and MCP use.
857
+ */
858
+ export class OysterGitInstance extends OysterInstance {
859
+ bindings = gitBindings;
860
+ }
861
+ /** Compose a Git Worker's root from its one options value. */
862
+ export function oysterGitRoot(options) {
863
+ const environments = new WeakMap();
864
+ const stateOf = (env) => {
865
+ let state = environments.get(env);
866
+ if (state === undefined) {
867
+ state = { tenancy: options.tenancy(env), admissions: new WeakMap() };
868
+ environments.set(env, state);
869
+ }
870
+ return state;
871
+ };
872
+ return {
873
+ async admit(env, request) {
874
+ const state = stateOf(env);
875
+ const resolved = await state.tenancy.resolve(request);
876
+ if (resolved !== null)
877
+ state.admissions.set(request, resolved);
878
+ return resolved;
879
+ },
880
+ hostFor(env, scope) {
881
+ const state = stateOf(env);
882
+ return oysterGitWorkerHost(env, options, {
883
+ scope,
884
+ auth: pinnedTenantAuth(state.tenancy, scope, state.admissions),
885
+ });
886
+ },
887
+ };
888
+ }
889
+ /** The single-owner root's tenancy: the owner key or a named key, at the configured origin only. */
890
+ export function ownerOrKeyTenancy(env) {
891
+ const scope = tenantScope(defaultTenant, env.OYSTER_ORIGIN);
892
+ return implicitTenancy(scope.origin, ownerOrKeyAuth(env.OYSTER_OWNER_KEY, env.OYSTER_OWNER_NAME ?? "owner", () => env.KEYS.getByName(scope.durableName("keys"))));
893
+ }
894
+ /** The standalone Worker's `fetch` handler with the git door and owner routes. */
895
+ export function createOysterGitWorker(root) {
896
+ // Bound frontend host state; SDK sessions belong to the tenant MCP owner.
897
+ const maxCachedHosts = 32;
898
+ const environments = new WeakMap();
899
+ return {
900
+ async fetch(request, env, ctx) {
901
+ let hosts = environments.get(env);
902
+ if (hosts === undefined) {
903
+ hosts = new Map();
904
+ environments.set(env, hosts);
905
+ }
906
+ const resolved = await root.admit(env, request);
907
+ if (resolved === null)
908
+ return new Response("Not found", { status: 404 });
909
+ const { scope } = resolved;
910
+ const key = `${scope.tenantId}:${scope.origin}`;
911
+ let entry = hosts.get(key);
912
+ let cache = true;
913
+ if (entry === undefined) {
914
+ if (hosts.size >= maxCachedHosts) {
915
+ const idle = [...hosts].find(([, item]) => item.active === 0);
916
+ if (idle === undefined)
917
+ cache = false;
918
+ else {
919
+ hosts.delete(idle[0]);
920
+ }
921
+ }
922
+ entry = { app: createOyster(root.hostFor(env, scope)), active: 0 };
923
+ if (cache)
924
+ hosts.set(key, entry);
925
+ }
926
+ else {
927
+ hosts.delete(key);
928
+ hosts.set(key, entry);
929
+ }
930
+ entry.active += 1;
931
+ try {
932
+ return await entry.app.fetch(request, env, ctx);
933
+ }
934
+ finally {
935
+ entry.active -= 1;
936
+ }
937
+ },
938
+ };
939
+ }
940
+ /**
941
+ * `host.git` for an Oyster Worker: the git door's writer projection and the
942
+ * admitted, installation-scoped Artifacts path with the App's push policy and
943
+ * the Instance journal. Set it on your own `createOyster` host; the full
944
+ * standalone composition (`oysterGitWorkerHost`) uses this same builder.
945
+ */
946
+ export function oysterWorkerGit(env, options, frame) {
947
+ const { scope } = options;
948
+ const installations = registryInstallations(env, scope);
949
+ const git = {
950
+ writerOf: (who) => {
951
+ const context = invocationCallerContext(who, null);
952
+ return context.actor.kind === "member" && context.role !== undefined
953
+ ? { kind: "actor", actor: context.actor, role: context.role }
954
+ : { kind: "actor", actor: context.actor };
955
+ },
956
+ git: (installation) => {
957
+ if (installation.instanceId === null)
958
+ throw new AppInstanceError("instance_inactive");
959
+ const stub = env.INSTANCES.getByName(installation.instanceId);
960
+ const invocation = frame?.invocation;
961
+ const beforeProvider = () => {
962
+ invocation?.signal.throwIfAborted();
963
+ };
964
+ const backendFor = async (current) => {
965
+ beforeProvider();
966
+ if (invocation !== undefined) {
967
+ const selected = invocation.facts.selection;
968
+ const captured = invocation.host.artifact;
969
+ if (selected.owner.kind !== "installation" ||
970
+ selected.owner.id !== current.installationId ||
971
+ selected.instanceId !== current.instanceId ||
972
+ selected.authorityVersion !== current.authorityVersion ||
973
+ selected.appId !== current.release.appId ||
974
+ selected.releaseId !== current.release.releaseId ||
975
+ captured.release.appId !== current.release.appId ||
976
+ captured.release.releaseId !== current.release.releaseId ||
977
+ captured.release.artifact.artifactRef !==
978
+ current.release.artifact.artifactRef ||
979
+ captured.release.artifact.artifactDigest !==
980
+ current.release.artifact.artifactDigest ||
981
+ captured.digest !== invocation.facts.artifactDigest)
982
+ throw new AppInstanceError("revoked");
983
+ return { ...captured, declaration: invocation.host.declaration };
984
+ }
985
+ return resolveBackend(r2ArtifactStore(env.RELEASES), current.release);
986
+ };
987
+ const currentInstallation = async () => {
988
+ beforeProvider();
989
+ const current = await installations.get(installation.installationId);
990
+ beforeProvider();
991
+ if (current?.status !== "enabled" ||
992
+ current.instanceId !== installation.instanceId ||
993
+ current.authorityVersion !== installation.authorityVersion ||
994
+ current.release.appId !== installation.release.appId ||
995
+ current.release.releaseId !== installation.release.releaseId ||
996
+ current.release.artifact.artifactRef !==
997
+ installation.release.artifact.artifactRef ||
998
+ current.release.artifact.artifactDigest !==
999
+ installation.release.artifact.artifactDigest)
1000
+ return null;
1001
+ return current;
1002
+ };
1003
+ const raw = frame?.store ??
1004
+ createArtifactsGit(artifactsOf(env), scope.repositoryPrefix(installation.installationId), {
1005
+ operations: ["create", "fork", "read", "write", "exchange"],
1006
+ beforeProvider,
1007
+ });
1008
+ return Promise.resolve(createAdmittedGit({
1009
+ installationId: installation.installationId,
1010
+ beforeProvider,
1011
+ store: {
1012
+ ...raw,
1013
+ cloneUrl: (name) => Promise.resolve(`${scope.origin}/git/${installation.installationId}/${name}.git`),
1014
+ },
1015
+ grants: {
1016
+ admits: createInstallationGitAdmission({
1017
+ currentInstallation,
1018
+ declaration: async (current) => (await backendFor(current))?.declaration ?? null,
1019
+ }),
1020
+ },
1021
+ policy: {
1022
+ authorize: async (question) => {
1023
+ const current = await currentInstallation();
1024
+ if (current?.instanceId == null)
1025
+ throw new AppInstanceError("revoked");
1026
+ const backend = await backendFor(current);
1027
+ if (backend === null)
1028
+ throw new AppInstanceError("revoked");
1029
+ const deadlineAt = Math.min(Date.now() + APP_INVOCATION_CEILING_MS, invocation?.facts.deadlineAt ?? Infinity);
1030
+ const context = {
1031
+ computerId: null,
1032
+ actor: question.writer.actor,
1033
+ };
1034
+ if (question.writer.role !== undefined)
1035
+ context.role = question.writer.role;
1036
+ if (question.writer.workflow !== undefined)
1037
+ context.workflow = workflowAttestationSchema.parse(question.writer.workflow);
1038
+ const facts = invocationFacts.fromCaller({
1039
+ instanceId: current.instanceId,
1040
+ selection: selectionOf(current),
1041
+ artifactDigest: backend.digest,
1042
+ grants: [],
1043
+ ancestry: [],
1044
+ }, parseAppMethodCallContext(context), { deadlineAt, grantExpiresAt: deadlineAt });
1045
+ return stub.authorizeGitPush(facts, question, undefined, scope.tenantId);
1046
+ },
1047
+ },
1048
+ journal: {
1049
+ tip: (name, ref) => stub.gitTip(installation.installationId, name, ref, scope.tenantId),
1050
+ record: async (fact) => {
1051
+ await stub.recordGitPush(installation.installationId, fact, scope.tenantId);
1052
+ },
1053
+ },
1054
+ }));
1055
+ },
1056
+ };
1057
+ const { workflowGitPass, buildGitPass } = options;
1058
+ let admitted = git;
1059
+ if (workflowGitPass !== undefined)
1060
+ admitted = {
1061
+ ...admitted,
1062
+ workflowPass: (pass, target) => workflowGitPass(env, scope, pass, target),
1063
+ };
1064
+ if (buildGitPass !== undefined)
1065
+ admitted = {
1066
+ ...admitted,
1067
+ buildPass: (pass, target) => buildGitPass(env, scope, pass, target),
1068
+ };
1069
+ return admitted;
1070
+ }
1071
+ /**
1072
+ * One selected tenant's whole host from a root's options. Roots reach it through
1073
+ * `oysterGitRoot(options).hostFor`; a cold directory passes its own resolved tenant.
1074
+ */
1075
+ export function oysterGitWorkerHost(env, options, selected) {
1076
+ const { scope, auth } = selected;
1077
+ // The one path to a key's liveness: the stored owner id is re-checked
1078
+ // against the live, current owner on every read (S3 inherits this port,
1079
+ // so this is the one place that check needs to live), the same discipline
1080
+ // ownerOrKeyAuth already applies at authentication. `host.keys.live` below
1081
+ // and the workspace/capabilities callbacks above it both reach a key only
1082
+ // through this one closure, never `env.KEYS` directly.
1083
+ const keysLive = async (keyId) => {
1084
+ const key = await env.KEYS.getByName(scope.durableName("keys")).live(keyId);
1085
+ if (key === null)
1086
+ return null;
1087
+ const current = await auth.resolve({
1088
+ kind: "member",
1089
+ memberId: key.owner,
1090
+ });
1091
+ return current !== null && oysterPrincipalIsOwner(current) ? key : null;
1092
+ };
1093
+ const workspace = options.workspace?.(env, keysLive) ?? {
1094
+ computerId: scope.computerName("workspace"),
1095
+ files: r2FileSource(env.RELEASES, scope.workspacePrefix),
1096
+ };
1097
+ const agents = options.agents?.(env, scope);
1098
+ let policyOptions = {
1099
+ workspace,
1100
+ scope,
1101
+ };
1102
+ if (agents !== undefined)
1103
+ policyOptions = {
1104
+ ...policyOptions,
1105
+ appToolPolicy: (principal) => agents.appToolPolicy(principal),
1106
+ };
1107
+ const base = oysterWorkerHost(env, auth, policyOptions);
1108
+ const capabilities = [
1109
+ ...(base.capabilities ?? []),
1110
+ ...(agents === undefined ? [] : agentCapabilities(agents)),
1111
+ ...(options.capabilities?.(env, workspace, keysLive) ?? []),
1112
+ ];
1113
+ let gitOptions = { scope };
1114
+ if (options.workflowGitPass !== undefined)
1115
+ gitOptions = { ...gitOptions, workflowGitPass: options.workflowGitPass };
1116
+ if (options.buildGitPass !== undefined)
1117
+ gitOptions = { ...gitOptions, buildGitPass: options.buildGitPass };
1118
+ const git = oysterWorkerGit(env, gitOptions);
1119
+ const ownerRoutes = gitAdminRoutes(env, scope);
1120
+ const hostOwnerRoutes = options.ownerRoutes?.(env, scope);
1121
+ if (hostOwnerRoutes !== undefined)
1122
+ ownerRoutes.route("/", hostOwnerRoutes);
1123
+ if (agents !== undefined) {
1124
+ ownerRoutes.route("/", agentApprovalRoutes(agents));
1125
+ ownerRoutes.route("/", agentMcpServerRoutes(agents));
1126
+ }
1127
+ let host = {
1128
+ ...base,
1129
+ capabilities,
1130
+ git,
1131
+ ownerRoutes,
1132
+ keys: { live: keysLive },
1133
+ };
1134
+ if (options.pageScript !== undefined)
1135
+ host = { ...host, pageScript: options.pageScript };
1136
+ if (options.homeInstallationId !== undefined)
1137
+ host = { ...host, homeInstallationId: options.homeInstallationId(env) };
1138
+ return host;
1139
+ }
1140
+ /**
1141
+ * Authenticate against the pinned tenant. A request admitted at the Worker's
1142
+ * top reuses that one admission; any other entry resolves its own.
1143
+ */
1144
+ function pinnedTenantAuth(tenancy, scope, admissions) {
1145
+ const admitted = async (incoming) => admissions.get(incoming) ?? (await tenancy.resolve(incoming));
1146
+ return {
1147
+ resolve: async (actor) => {
1148
+ const current = await tenancy.resolve(new Request(scope.origin));
1149
+ if (current?.scope.tenantId !== scope.tenantId ||
1150
+ current.scope.origin !== scope.origin)
1151
+ return null;
1152
+ return current.auth.resolve(actor);
1153
+ },
1154
+ authenticate: async (incoming) => {
1155
+ const current = await admitted(incoming);
1156
+ if (current?.scope.tenantId !== scope.tenantId ||
1157
+ current.scope.origin !== scope.origin)
1158
+ return { kind: "refused", status: 404 };
1159
+ return current.auth.authenticate(incoming);
1160
+ },
1161
+ login: async (incoming) => {
1162
+ const current = await admitted(incoming);
1163
+ if (current?.scope.tenantId !== scope.tenantId ||
1164
+ current.scope.origin !== scope.origin)
1165
+ return new Response("Not found", { status: 404 });
1166
+ return (current.auth.login?.(incoming) ??
1167
+ new Response("Sign-in is not available", { status: 404 }));
1168
+ },
1169
+ };
1170
+ }
1171
+ /** Deploy the MCP owner from the same root as createOysterGitWorker. */
1172
+ export function createOysterGitMcpOwner(root) {
1173
+ return createOysterMcpOwner((env, scope) => root.hostFor(env, scope));
1174
+ }