@fungi.computer/oyster 0.0.1 → 0.2.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 (195) hide show
  1. package/README.md +228 -176
  2. package/dist/agent-avatars.d.ts +103 -0
  3. package/dist/agent-avatars.js +66 -0
  4. package/dist/agents.d.ts +12 -2
  5. package/dist/agents.js +39 -2
  6. package/dist/app-builds.d.ts +76 -17
  7. package/dist/app-builds.js +207 -63
  8. package/dist/app-sources.d.ts +7 -22
  9. package/dist/app-sources.js +49 -14
  10. package/dist/apps/gate.d.ts +7 -7
  11. package/dist/apps/gate.js +6 -6
  12. package/dist/apps/installation.d.ts +35 -35
  13. package/dist/apps/installation.js +20 -19
  14. package/dist/build/artifact-writer.d.ts +40 -0
  15. package/dist/build/artifact-writer.js +181 -0
  16. package/dist/build/build-jobs.js +7 -1
  17. package/dist/build/build-policy.d.ts +10 -0
  18. package/dist/build/build-policy.js +10 -0
  19. package/dist/build/build-step.d.ts +12 -1
  20. package/dist/build/build-step.js +9 -1
  21. package/dist/build/candidate-builder.d.ts +68 -0
  22. package/dist/build/candidate-builder.js +91 -0
  23. package/dist/build/candidate-cli.d.ts +1 -0
  24. package/dist/build/candidate-cli.js +119 -0
  25. package/dist/build/candidate-cli.mjs +28220 -0
  26. package/dist/build/content-objects.d.ts +37 -0
  27. package/dist/build/content-objects.js +53 -0
  28. package/dist/build/effect.d.ts +18 -0
  29. package/dist/build/effect.js +52 -12
  30. package/dist/build/git-environment.d.ts +3 -0
  31. package/dist/build/git-environment.js +21 -9
  32. package/dist/build/local-build.d.ts +13 -3
  33. package/dist/build/local-build.js +149 -99
  34. package/dist/build/snapshot-package-store.d.ts +10 -0
  35. package/dist/build/snapshot-package-store.js +15 -0
  36. package/dist/build/snapshot-program-scripts.js +1 -1
  37. package/dist/build/snapshot-programs/snapshot-path.d.ts +2 -0
  38. package/dist/build/snapshot-programs/snapshot-path.js +6 -0
  39. package/dist/build/submission-contract.d.ts +461 -17
  40. package/dist/build/submission-contract.js +136 -26
  41. package/dist/build/workspace-build.d.ts +7 -2
  42. package/dist/build/workspace-build.js +55 -11
  43. package/dist/build-contract.d.ts +3 -0
  44. package/dist/build-contract.js +3 -0
  45. package/dist/build-egress.d.ts +62 -0
  46. package/dist/build-egress.js +148 -0
  47. package/dist/build.d.ts +1 -0
  48. package/dist/build.js +1 -0
  49. package/dist/cloudflare.js +4 -4
  50. package/dist/frame-binding-wire-script.js +1 -1
  51. package/dist/frame-binding-wire.d.ts +1 -1
  52. package/dist/frame-binding-wire.js +1 -1
  53. package/dist/git-journal.js +4 -4
  54. package/dist/index.d.ts +1 -0
  55. package/dist/index.js +1 -0
  56. package/dist/installation-consent.d.ts +3 -1
  57. package/dist/installation-consent.js +2 -0
  58. package/dist/mcp-owner.js +20 -8
  59. package/dist/pack.d.ts +6 -3
  60. package/dist/pack.js +28 -11
  61. package/dist/page/gill-observation.d.ts +4 -0
  62. package/dist/page/gill-observation.js +31 -0
  63. package/dist/page/mount.d.ts +4 -8
  64. package/dist/page/window-launch.d.ts +18 -0
  65. package/dist/page/window-launch.js +11 -0
  66. package/dist/page-script.js +4 -4
  67. package/dist/release.d.ts +11 -0
  68. package/dist/release.js +4 -1
  69. package/dist/shell/app-directory.d.ts +50 -45
  70. package/dist/shell/app-directory.js +19 -18
  71. package/dist/shell/shell-module.d.ts +26 -2
  72. package/dist/shell/shell-module.js +51 -2
  73. package/dist/shell/window-manager.d.ts +5 -1
  74. package/dist/shell/window-manager.js +21 -2
  75. package/dist/shell/workbench-directory.d.ts +332 -0
  76. package/dist/shell/workbench-directory.js +35 -0
  77. package/dist/shell/workbench-host-types.d.ts +1 -1
  78. package/dist/shell/workbench-host.js +5 -0
  79. package/dist/shell/workbench-model.d.ts +3 -1
  80. package/dist/source/index.d.ts +6 -7
  81. package/dist/source/index.js +6 -7
  82. package/dist/source/template-push.d.ts +33 -0
  83. package/dist/source/template-push.js +223 -0
  84. package/dist/source/template-repository.d.ts +2 -0
  85. package/dist/source/template-repository.js +13 -0
  86. package/dist/source/{template-write.d.ts → template-seed.d.ts} +19 -20
  87. package/dist/source/template-seed.js +80 -0
  88. package/dist/standalone/agent-directory.d.ts +4 -0
  89. package/dist/standalone/agent-directory.js +4 -0
  90. package/dist/standalone/agents.d.ts +227 -0
  91. package/dist/standalone/agents.js +834 -0
  92. package/dist/standalone/app-builds-binding.d.ts +36 -0
  93. package/dist/standalone/app-builds-binding.js +119 -0
  94. package/dist/standalone/app-set.d.ts +91 -0
  95. package/dist/standalone/app-set.js +395 -0
  96. package/dist/standalone/app-source.d.ts +41 -0
  97. package/dist/standalone/app-source.js +59 -0
  98. package/dist/standalone/builds.d.ts +18 -0
  99. package/dist/standalone/builds.js +101 -0
  100. package/dist/standalone/computer-egress.d.ts +37 -0
  101. package/dist/standalone/computer-egress.js +111 -0
  102. package/dist/standalone/computer.d.ts +122 -0
  103. package/dist/standalone/computer.js +714 -0
  104. package/dist/standalone/install-apps-cli.d.ts +2 -0
  105. package/dist/standalone/install-apps-cli.js +126 -0
  106. package/dist/standalone/instance.d.ts +35 -0
  107. package/dist/standalone/instance.js +74 -0
  108. package/dist/standalone/media.d.ts +76 -0
  109. package/dist/standalone/media.js +227 -0
  110. package/dist/standalone/preview.d.ts +51 -0
  111. package/dist/standalone/preview.js +167 -0
  112. package/dist/standalone/registry.d.ts +102 -0
  113. package/dist/standalone/registry.js +193 -0
  114. package/dist/standalone/tenancy.d.ts +42 -0
  115. package/dist/standalone/tenancy.js +241 -0
  116. package/dist/standalone/worker.d.ts +55 -0
  117. package/dist/standalone/worker.js +147 -0
  118. package/dist/standalone/workflow-authority-readers.d.ts +16 -0
  119. package/dist/standalone/workflow-authority-readers.js +35 -0
  120. package/dist/standalone/workflow-binding.d.ts +216 -0
  121. package/dist/standalone/workflow-binding.js +217 -0
  122. package/dist/standalone/workflow-events.d.ts +46 -0
  123. package/dist/standalone/workflow-events.js +39 -0
  124. package/dist/standalone/workflow-git-capture.d.ts +7 -0
  125. package/dist/standalone/workflow-git-capture.js +62 -0
  126. package/dist/standalone/workflow-host.d.ts +955 -0
  127. package/dist/standalone/workflow-host.js +778 -0
  128. package/dist/standalone/workflow-node-capture.d.ts +13 -0
  129. package/dist/standalone/workflow-node-capture.js +72 -0
  130. package/dist/standalone/workflow-root-authority.d.ts +27 -0
  131. package/dist/standalone/workflow-root-authority.js +87 -0
  132. package/dist/standalone/workflow-schedules.d.ts +52 -0
  133. package/dist/standalone/workflow-schedules.js +40 -0
  134. package/dist/standalone/workflow-trigger-host.d.ts +1716 -0
  135. package/dist/standalone/workflow-trigger-host.js +52 -0
  136. package/dist/standalone/workflows.d.ts +713 -0
  137. package/dist/standalone/workflows.js +554 -0
  138. package/dist/tenancy/auth0.d.ts +64 -0
  139. package/dist/tenancy/auth0.js +198 -0
  140. package/dist/tenancy/cloudflare/rate-limit.d.ts +8 -0
  141. package/dist/tenancy/cloudflare/rate-limit.js +19 -0
  142. package/dist/tenancy/cloudflare/registry.d.ts +50 -0
  143. package/dist/tenancy/cloudflare/registry.js +301 -0
  144. package/dist/tenancy/cloudflare.d.ts +2 -0
  145. package/dist/tenancy/cloudflare.js +2 -0
  146. package/dist/tenancy/cold-directory.d.ts +13 -0
  147. package/dist/tenancy/cold-directory.js +43 -0
  148. package/dist/tenancy/contracts.d.ts +195 -0
  149. package/dist/tenancy/contracts.js +1 -0
  150. package/dist/tenancy/index.d.ts +19 -0
  151. package/dist/tenancy/index.js +156 -0
  152. package/dist/tenancy/owner-key.d.ts +26 -0
  153. package/dist/tenancy/owner-key.js +63 -0
  154. package/dist/tenancy/oyster.d.ts +4 -0
  155. package/dist/tenancy/oyster.js +25 -0
  156. package/dist/tenancy/registry-record.d.ts +7 -0
  157. package/dist/tenancy/registry-record.js +44 -0
  158. package/dist/tenancy/registry.d.ts +27 -0
  159. package/dist/tenancy/registry.js +138 -0
  160. package/dist/tenancy/resolve.d.ts +7 -0
  161. package/dist/tenancy/resolve.js +64 -0
  162. package/dist/workers-git.js +1 -4
  163. package/dist/workers.d.ts +2 -2
  164. package/dist/workers.js +3 -1
  165. package/docs/api/build-contract.md +829 -32
  166. package/docs/api/build.md +253 -17
  167. package/docs/api/modules.md +40 -32
  168. package/docs/api/pack.md +6 -3
  169. package/docs/api/shell/app-directory.md +267 -0
  170. package/docs/api/shell/shell-module.md +64 -3
  171. package/docs/api/shell/workbench-directory.md +119 -0
  172. package/docs/api/shell/workbench.md +19 -0
  173. package/docs/api/shell.md +127 -0
  174. package/docs/api/tenancy/auth0.md +110 -0
  175. package/docs/api/tenancy/cloudflare.md +221 -0
  176. package/docs/api/tenancy/cold-directory.md +46 -0
  177. package/docs/api/tenancy/owner-key.md +63 -0
  178. package/docs/api/tenancy/registry.md +77 -0
  179. package/docs/api/tenancy/resolve.md +69 -0
  180. package/docs/api/tenancy.md +430 -0
  181. package/docs/api/workers-git.md +140 -21
  182. package/docs/api/workers.md +15 -2
  183. package/docs/site/agent-quickstart.mdx +52 -55
  184. package/examples/standalone/worker.ts +7 -14
  185. package/examples/standalone/wrangler.jsonc +67 -6
  186. package/package.json +78 -25
  187. package/dist/build/r2-artifact-writer.d.ts +0 -31
  188. package/dist/build/r2-artifact-writer.js +0 -179
  189. package/dist/source/template-write.js +0 -344
  190. package/docs/media/architecture.mmd +0 -32
  191. package/docs/media/architecture.png +0 -0
  192. package/docs/media/bonsai-network.png +0 -0
  193. package/docs/media/bonsai-repository.png +0 -0
  194. package/examples/git/worker.ts +0 -39
  195. package/examples/git/wrangler.jsonc +0 -32
@@ -5,43 +5,72 @@
5
5
  * A build runs the shared job lifecycle (`build/build-jobs.ts`) and the fixed
6
6
  * captured builder command on one Computer. The Computer holds no key: its
7
7
  * clone is admitted at egress for exactly the job's one repository (read
8
- * only), and its candidate upload for exactly the job. The candidate is
9
- * written to the Release bucket by the shared R2 writer, verified like any
10
- * installed Release, and recorded here; installing it is a separate owner act.
8
+ * only), and its candidate upload for exactly the job. A source is either a
9
+ * repository in an installation's Git (a fork, through the Git door) or the
10
+ * App's own published template, built only at a recorded seed revision of
11
+ * its pinned recipe (no installed App is needed). The build streams each
12
+ * built object into the host's artifact store itself, through the egress, and
13
+ * then submits a small manifest naming them. This owner only records facts:
14
+ * it checks each named object's stored size and digest, writes the asset
15
+ * manifest and App root in Oyster's one immutable artifact representation
16
+ * (the same objects a packed upload writes), verifies the Release like any
17
+ * installed one, and records it; installing it is a separate owner act.
11
18
  */
12
- import * as Effect from "effect/Effect";
13
19
  import * as Schema from "effect/Schema";
14
20
  import { parseArtifactRef, parseReleaseId, } from "@fungi.computer/puffball";
15
21
  import { appIdSchema, releaseSchema, sha256DigestSchema, sourceRevisionSchema, } from "@fungi.computer/puffball/effect";
16
- import { capturedPinnedRecipeSchema, commitPreparedCandidate, digestPinnedRecipe, } from "./build/effect.js";
17
- import { encodeTeamAppCandidateSubmission, } from "./build/submission-contract.js";
22
+ import { capturedPinnedRecipeSchema, digestPinnedRecipe, fitsPinnedRecipe, pinnedRecipeObjectBound, } from "./build/effect.js";
23
+ import { candidateManifestObjects, decodeCandidateBackend, digestTeamAppCandidateManifest, } from "./build/submission-contract.js";
24
+ import { contentObjectRef } from "./build/content-objects.js";
18
25
  import { createTeamBuildPublisher, teamAppBuildCommand, } from "./build/build-jobs.js";
19
- import { createR2ArtifactWriter, } from "./build/r2-artifact-writer.js";
26
+ import { writeArtifactRoot, } from "./build/artifact-writer.js";
20
27
  import { buildGitPassPrefix } from "./git-auth.js";
21
- import { verifyRelease } from "./release.js";
22
- /** A Computer's whole-build budget: a cold standard-2 build measured 185-270 s. */
28
+ import { defaultAssetPolicy, readVerified, verifyRelease, } from "./release.js";
29
+ import { appForTemplateRepository, templateRepositoryForApp, } from "./source/template-repository.js";
30
+ /** A Computer's whole-build budget: a cold build on a 6 GiB Computer measured 185-270 s. */
23
31
  export const oysterAppBuildTimeoutMs = 900_000;
24
32
  /** The job, its clone admission and its upload grant end together. */
25
33
  const jobTtlMs = oysterAppBuildTimeoutMs + 5 * 60_000;
26
34
  /** Host names the Computer's egress serves for a build; never public hosts. */
27
35
  export const oysterBuildGitHost = "git.team.internal";
28
36
  export const oysterBuildReportHost = "build.team.internal";
29
- /** An App's registered build source: one repository and its pinned recipe. */
37
+ /** Where an App's source is cloned from. */
38
+ export const oysterBuildLocationSchema = Schema.Union([
39
+ Schema.Struct({
40
+ kind: Schema.Literal("installation"),
41
+ /** The installation whose Git holds the repository (Bonsai for a fork). */
42
+ installationId: Schema.String.check(Schema.isPattern(/^[a-zA-Z0-9_-]{1,256}$/u)),
43
+ repository: Schema.String.check(Schema.isPattern(/^[a-zA-Z0-9._-]{1,128}$/u)),
44
+ }),
45
+ /** The App's own published template (`templateRepositoryForApp`). */
46
+ Schema.Struct({ kind: Schema.Literal("template") }),
47
+ ]);
48
+ /** An App's registered build source: where it is cloned and its pinned recipe. */
30
49
  export const oysterBuildSourceSchema = Schema.Struct({
31
50
  appId: appIdSchema,
32
- /** The installation whose Git holds the repository (Bonsai for a fork). */
33
- installationId: Schema.String.check(Schema.isPattern(/^[a-zA-Z0-9_-]{1,256}$/u)),
34
- repository: Schema.String.check(Schema.isPattern(/^[a-zA-Z0-9._-]{1,128}$/u)),
51
+ location: oysterBuildLocationSchema,
35
52
  recipe: capturedPinnedRecipeSchema,
36
53
  buildRecipeDigest: sha256DigestSchema,
37
54
  });
55
+ /** One Computer as the build owner reaches it. */
56
+ /** The Computer established no terminal result for a build command, for
57
+ * example because its connection to the container dropped mid-command. */
58
+ class BuildOutcomeUnknown extends Error {
59
+ constructor() {
60
+ super("Build outcome unknown");
61
+ }
62
+ }
63
+ /** The repository a job clones: its installation repository, or its App's template. */
64
+ const cloneRepository = (job) => job.location.kind === "installation"
65
+ ? job.location.repository
66
+ : templateRepositoryForApp(job.app_id);
38
67
  const jobRow = Schema.Struct({
39
68
  job_id: Schema.String,
40
69
  app_id: Schema.String,
41
70
  release_id: Schema.String,
42
71
  source_revision: Schema.String,
43
- installation_id: Schema.String,
44
- repository: Schema.String,
72
+ /** The job's clone location, as its source named it when the job started. */
73
+ location: Schema.fromJsonString(oysterBuildLocationSchema),
45
74
  recipe: Schema.String,
46
75
  recipe_digest: Schema.String,
47
76
  requested_by: Schema.String,
@@ -61,6 +90,7 @@ const releaseRow = Schema.Struct({ release: Schema.String });
61
90
  const sourceCodec = Schema.fromJsonString(oysterBuildSourceSchema);
62
91
  const releaseCodec = Schema.fromJsonString(releaseSchema);
63
92
  const recipeCodec = Schema.fromJsonString(capturedPinnedRecipeSchema);
93
+ const locationCodec = Schema.fromJsonString(oysterBuildLocationSchema);
64
94
  /** Unguessable hex identity from the platform CSPRNG. */
65
95
  function randomHex(bytes) {
66
96
  return Array.from(crypto.getRandomValues(new Uint8Array(bytes)), (byte) => byte.toString(16).padStart(2, "0")).join("");
@@ -77,24 +107,75 @@ function sameDigest(left, right) {
77
107
  difference |= left.charCodeAt(index) ^ right.charCodeAt(index);
78
108
  return difference === 0;
79
109
  }
80
- /** The SQLite tables one Oyster build owner keeps. */
110
+ /**
111
+ * The SQLite tables one Oyster build owner keeps. Creation and any carrying
112
+ * forward of retained facts run as one transaction of the owner's storage, so
113
+ * an interrupted conversion leaves the facts as they were for a retry.
114
+ */
81
115
  export function migrateOysterAppBuilds(owner) {
82
- owner.exec("CREATE TABLE IF NOT EXISTS oyster_build_sources (app_id TEXT PRIMARY KEY, source TEXT NOT NULL) STRICT");
83
- owner.exec(`CREATE TABLE IF NOT EXISTS oyster_build_jobs (
116
+ const { sql } = owner;
117
+ owner.transactionSync(() => {
118
+ sql.exec("CREATE TABLE IF NOT EXISTS oyster_build_sources (app_id TEXT PRIMARY KEY, source TEXT NOT NULL) STRICT");
119
+ // Sources recorded before the location union named only an installation
120
+ // repository; carry each forward once, in place.
121
+ sql.exec(`UPDATE oyster_build_sources SET source = json_set(
122
+ json_remove(source, '$.installationId', '$.repository'), '$.location',
123
+ json_object('kind', 'installation',
124
+ 'installationId', json_extract(source, '$.installationId'),
125
+ 'repository', json_extract(source, '$.repository')))
126
+ WHERE json_type(source, '$.location') IS NULL`);
127
+ const jobColumns = `
84
128
  job_id TEXT PRIMARY KEY, app_id TEXT NOT NULL, release_id TEXT NOT NULL UNIQUE,
85
- source_revision TEXT NOT NULL, installation_id TEXT NOT NULL, repository TEXT NOT NULL,
129
+ source_revision TEXT NOT NULL, location TEXT NOT NULL,
86
130
  recipe TEXT NOT NULL, recipe_digest TEXT NOT NULL, requested_by TEXT NOT NULL,
87
131
  credential_id TEXT NOT NULL, upload_grant_digest TEXT NOT NULL,
88
132
  status TEXT NOT NULL, artifact_digest TEXT, artifact_bytes INTEGER, detail TEXT,
89
- issued_at INTEGER NOT NULL, expires_at INTEGER NOT NULL, executable_until INTEGER NOT NULL
90
- ) STRICT`);
91
- owner.exec("CREATE TABLE IF NOT EXISTS oyster_build_releases (release_id TEXT PRIMARY KEY, job_id TEXT NOT NULL UNIQUE, release TEXT NOT NULL) STRICT");
133
+ issued_at INTEGER NOT NULL, expires_at INTEGER NOT NULL, executable_until INTEGER NOT NULL`;
134
+ sql.exec(`CREATE TABLE IF NOT EXISTS oyster_build_jobs (${jobColumns}) STRICT`);
135
+ // Jobs recorded before the location union named an installation repository
136
+ // in two columns; carry each into the one location column, once.
137
+ if (sql
138
+ .exec("SELECT name FROM pragma_table_info('oyster_build_jobs') WHERE name='installation_id'")
139
+ .toArray().length > 0) {
140
+ sql.exec(`CREATE TABLE oyster_build_jobs_current (${jobColumns}) STRICT`);
141
+ sql.exec(`INSERT INTO oyster_build_jobs_current SELECT job_id, app_id, release_id,
142
+ source_revision,
143
+ json_object('kind', 'installation', 'installationId', installation_id,
144
+ 'repository', repository),
145
+ recipe, recipe_digest, requested_by, credential_id, upload_grant_digest,
146
+ status, artifact_digest, artifact_bytes, detail, issued_at, expires_at,
147
+ executable_until
148
+ FROM oyster_build_jobs`);
149
+ sql.exec("DROP TABLE oyster_build_jobs");
150
+ sql.exec("ALTER TABLE oyster_build_jobs_current RENAME TO oyster_build_jobs");
151
+ }
152
+ sql.exec("CREATE TABLE IF NOT EXISTS oyster_build_releases (release_id TEXT PRIMARY KEY, job_id TEXT NOT NULL UNIQUE, release TEXT NOT NULL) STRICT");
153
+ });
92
154
  }
93
- /** The build owner over one tenant's SQLite, Computer and Release bucket. */
155
+ /** The build owner over one tenant's SQLite, Computer and artifact store. */
94
156
  export function createOysterAppBuilds(options) {
95
157
  const { owner } = options;
96
158
  const now = options.now ?? Date.now;
97
- migrateOysterAppBuilds(owner.sql);
159
+ // The host store overwrites on put, so write-once is checked here first.
160
+ const storeObjects = {
161
+ async create(artifactRef, bytes) {
162
+ if ((await options.artifacts.get(artifactRef)) !== null)
163
+ return "exists";
164
+ await options.artifacts.put(artifactRef, bytes);
165
+ return "created";
166
+ },
167
+ read: (artifactRef) => options.artifacts.get(artifactRef),
168
+ };
169
+ migrateOysterAppBuilds(owner);
170
+ // A job outlives the owner instance that started it: the Computer keeps
171
+ // building and its own reports (objects, candidate, status) settle the
172
+ // job, so an owner restart must not fail it. A job that reaches its expiry
173
+ // without a result can never settle, so it fails then instead of reading
174
+ // as running for as long as anyone asks.
175
+ const expire = () => owner.sql.exec("UPDATE oyster_build_jobs SET status='failed', detail=? WHERE status='running' AND expires_at<=?", "Build expired without a result", now());
176
+ expire();
177
+ const decodeRecipe = (recipe) => Schema.decodeUnknownSync(recipeCodec)(recipe);
178
+ const grantAdmits = async (job, grant) => sameDigest(job.upload_grant_digest, await sha256Hex(new TextEncoder().encode(grant)));
98
179
  const readJob = (jobId) => {
99
180
  const [row] = owner.sql
100
181
  .exec("SELECT * FROM oyster_build_jobs WHERE job_id=?", jobId)
@@ -115,9 +196,11 @@ export function createOysterAppBuilds(options) {
115
196
  computerId: options.computer.name,
116
197
  credentialId: job.credential_id,
117
198
  issuedAt: job.issued_at,
118
- repository: job.repository,
199
+ repository: cloneRepository(job),
119
200
  expiresAt: job.expires_at,
120
- remote: `https://${oysterBuildGitHost}/git/${job.installation_id}/${job.repository}.git`,
201
+ remote: job.location.kind === "installation"
202
+ ? `https://${oysterBuildGitHost}/git/${job.location.installationId}/${job.location.repository}.git`
203
+ : `https://${oysterBuildGitHost}/${cloneRepository(job)}.git`,
121
204
  reportUrl: `https://${oysterBuildReportHost}/jobs/${job.job_id}`,
122
205
  status: job.status,
123
206
  artifact: job.artifact_digest === null || job.artifact_bytes === null
@@ -165,11 +248,16 @@ export function createOysterAppBuilds(options) {
165
248
  owner.sql.exec("UPDATE oyster_build_jobs SET executable_until=? WHERE job_id=?", decision.status === "allowed" ? decision.expiresAt : 0, decision.jobId);
166
249
  return Promise.resolve();
167
250
  },
168
- execute: (input) => options.computer.port.runBuild({
169
- jobId: input.jobId,
170
- command: input.command,
171
- timeoutMs: input.timeoutMs,
172
- }),
251
+ execute: async (input) => {
252
+ const result = await options.computer.port.runBuild({
253
+ jobId: input.jobId,
254
+ command: input.command,
255
+ timeoutMs: input.timeoutMs,
256
+ });
257
+ if (result.output === undefined || result.output === null)
258
+ throw new BuildOutcomeUnknown();
259
+ return result;
260
+ },
173
261
  deliverTeamGitEvents: () => Promise.resolve(0),
174
262
  }),
175
263
  });
@@ -190,7 +278,10 @@ export function createOysterAppBuilds(options) {
190
278
  });
191
279
  }
192
280
  catch (cause) {
193
- settle(jobId, "failed", cause instanceof Error ? cause.message : "Build failed");
281
+ // Without a terminal result the build may still be running on the
282
+ // Computer; its own reports settle the job, or its expiry fails it.
283
+ if (!(cause instanceof BuildOutcomeUnknown))
284
+ settle(jobId, "failed", cause instanceof Error ? cause.message : "Build failed");
194
285
  }
195
286
  return view(required(jobId));
196
287
  };
@@ -214,6 +305,8 @@ export function createOysterAppBuilds(options) {
214
305
  readSource,
215
306
  /**
216
307
  * Mint one build job for an App's registered source at one exact commit.
308
+ * A template source builds only a seed revision recorded for its pinned
309
+ * recipe.
217
310
  * Answers the job and its one run, which the caller keeps alive (the
218
311
  * upload grant exists only inside that run's fixed command).
219
312
  */
@@ -222,19 +315,25 @@ export function createOysterAppBuilds(options) {
222
315
  const source = readSource(input.appId);
223
316
  if (source === null)
224
317
  throw new Error("App has no build source");
318
+ if (source.location.kind === "template") {
319
+ const receipt = await options.templates.read(source.appId, sourceRevision);
320
+ if (receipt?.buildRecipeDigest !== source.buildRecipeDigest)
321
+ throw new Error("Revision is not a seed of this App's template");
322
+ }
225
323
  const jobId = `build_${randomHex(16)}`;
226
324
  const releaseId = parseReleaseId(`rel_${sourceRevision.slice("git:".length, "git:".length + 12)}${randomHex(4)}`);
227
325
  const uploadGrant = randomHex(32);
228
326
  const issuedAt = now();
229
- owner.sql.exec(`INSERT INTO oyster_build_jobs(job_id, app_id, release_id, source_revision, installation_id,
230
- repository, recipe, recipe_digest, requested_by, credential_id, upload_grant_digest,
327
+ owner.sql.exec(`INSERT INTO oyster_build_jobs(job_id, app_id, release_id, source_revision, location,
328
+ recipe, recipe_digest, requested_by, credential_id, upload_grant_digest,
231
329
  status, artifact_digest, artifact_bytes, detail, issued_at, expires_at, executable_until)
232
- VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'running', NULL, NULL, NULL, ?, ?, 0)`, jobId, source.appId, releaseId, sourceRevision, source.installationId, source.repository, Schema.encodeUnknownSync(recipeCodec)(source.recipe), source.buildRecipeDigest, input.requestedBy, randomHex(32), await sha256Hex(new TextEncoder().encode(uploadGrant)), issuedAt, issuedAt + jobTtlMs);
330
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'running', NULL, NULL, NULL, ?, ?, 0)`, jobId, source.appId, releaseId, sourceRevision, Schema.encodeUnknownSync(locationCodec)(source.location), Schema.encodeUnknownSync(recipeCodec)(source.recipe), source.buildRecipeDigest, input.requestedBy, randomHex(32), await sha256Hex(new TextEncoder().encode(uploadGrant)), issuedAt, issuedAt + jobTtlMs);
233
331
  // The grant leaves this owner only inside the fixed command.
234
332
  const command = teamAppBuildCommand({ appId: source.appId, sourceRevision, source }, uploadGrant);
235
333
  return { build: view(required(jobId)), run: () => run(jobId, command) };
236
334
  },
237
335
  read(jobId) {
336
+ expire();
238
337
  const job = readJob(jobId);
239
338
  return job === null ? null : view(job);
240
339
  },
@@ -270,7 +369,9 @@ export function createOysterAppBuilds(options) {
270
369
  target.scope !== "read")
271
370
  return null;
272
371
  const job = owner.sql
273
- .exec("SELECT * FROM oyster_build_jobs WHERE installation_id=? AND repository=? AND status='running' AND expires_at>? ORDER BY issued_at DESC LIMIT 1", target.installationId, target.repository, now())
372
+ .exec(`SELECT * FROM oyster_build_jobs WHERE json_extract(location, '$.kind')='installation'
373
+ AND json_extract(location, '$.installationId')=? AND json_extract(location, '$.repository')=?
374
+ AND status='running' AND expires_at>? ORDER BY issued_at DESC LIMIT 1`, target.installationId, target.repository, now())
274
375
  .toArray()
275
376
  .map((row) => decodeJob(row))[0];
276
377
  return job === undefined
@@ -284,12 +385,29 @@ export function createOysterAppBuilds(options) {
284
385
  return null;
285
386
  const job = readJob(match[1] ?? "");
286
387
  return live(job) &&
388
+ job.location.kind === "installation" &&
287
389
  sameDigest(job.credential_id, match[2] ?? "") &&
288
- job.installation_id === target.installationId &&
289
- job.repository === target.repository
390
+ job.location.installationId === target.installationId &&
391
+ job.location.repository === target.repository
290
392
  ? { requestedBy: job.requested_by }
291
393
  : null;
292
394
  },
395
+ /**
396
+ * May this Computer's clone read this template repository now? Only while
397
+ * a live template job on this Computer names it.
398
+ */
399
+ admitTemplateClone(computerObjectId, repository) {
400
+ if (computerObjectId !== options.computer.objectId)
401
+ return false;
402
+ // The template repository names exactly one App (templateRepositoryForApp).
403
+ const appId = appForTemplateRepository(repository);
404
+ if (appId === null)
405
+ return false;
406
+ return (owner.sql
407
+ .exec(`SELECT 1 FROM oyster_build_jobs WHERE app_id=? AND json_extract(location, '$.kind')='template'
408
+ AND status='running' AND expires_at>? LIMIT 1`, appId, now())
409
+ .toArray().length > 0);
410
+ },
293
411
  /** A reported artifact or status for a live job. */
294
412
  record(event) {
295
413
  const job = readJob(event.jobId);
@@ -302,42 +420,68 @@ export function createOysterAppBuilds(options) {
302
420
  settle(event.jobId, event.status, event.detail);
303
421
  },
304
422
  /**
305
- * The Computer's candidate for a live job: checked against the job, written
306
- * by the shared R2 writer, verified as an installable Release and recorded.
423
+ * May this live job's build upload one object now, and how large? The
424
+ * grant is the one inside the job's fixed command; the bound is its
425
+ * recipe's (`pinnedRecipeObjectBound`).
307
426
  */
308
- async submitCandidate(jobId, candidate, grant, credentialId) {
427
+ async admitObjectUpload(jobId, computerObjectId, grant) {
428
+ const job = readJob(jobId);
429
+ if (computerObjectId !== options.computer.objectId ||
430
+ !live(job) ||
431
+ !(await grantAdmits(job, grant)))
432
+ return null;
433
+ return {
434
+ maxObjectBytes: pinnedRecipeObjectBound(decodeRecipe(job.recipe)),
435
+ };
436
+ },
437
+ /**
438
+ * The Computer's candidate manifest for a live job: checked against the
439
+ * job and its pinned recipe, each named object found stored with its
440
+ * size and verified digest (one at a time; only the backend descriptor
441
+ * is read), its asset manifest and App root written, verified as an
442
+ * installable Release and recorded. An over-policy candidate stores
443
+ * nothing.
444
+ */
445
+ async submitCandidate(jobId, manifest, grant, credentialId) {
309
446
  const job = readJob(jobId);
310
447
  if (!live(job) ||
311
448
  job.credential_id !== credentialId ||
312
- !sameDigest(job.upload_grant_digest, await sha256Hex(new TextEncoder().encode(grant))))
449
+ !(await grantAdmits(job, grant)))
313
450
  return new Response(null, { status: 403 });
314
- if (candidate.appId !== job.app_id ||
315
- candidate.provenance.sourceRevision !== job.source_revision ||
316
- candidate.provenance.buildRecipeDigest !== job.recipe_digest)
451
+ if (manifest.appId !== job.app_id ||
452
+ manifest.provenance.sourceRevision !== job.source_revision ||
453
+ manifest.provenance.buildRecipeDigest !== job.recipe_digest)
317
454
  return new Response(null, { status: 409 });
318
455
  if (readRelease(jobId) !== null)
319
456
  return new Response(null, { status: 409 });
320
- const recipe = Schema.decodeUnknownSync(recipeCodec)(job.recipe);
321
- const digest = await sha256Hex(new TextEncoder().encode(encodeTeamAppCandidateSubmission(candidate)));
322
- const writerOptions = {
323
- bucket: options.bucket,
324
- artifactRef: parseArtifactRef(`artifact:sha256-${digest}`),
325
- policy: {
326
- maxBytes: recipe.maxOutputBytes,
327
- maxManifestBytes: recipe.assets?.maxManifestBytes ?? 1024 * 1024,
328
- },
329
- };
330
- if (recipe.assets !== undefined)
331
- writerOptions.assets = recipe.assets;
457
+ const recipe = decodeRecipe(job.recipe);
332
458
  let release;
333
459
  try {
334
- release = await Effect.runPromise(commitPreparedCandidate({ ...candidate, releaseId: parseReleaseId(job.release_id) }, {
335
- artifactWriter: createR2ArtifactWriter(writerOptions),
336
- releaseCatalog: {
337
- createRelease: (value) => Promise.resolve(value),
460
+ if (!fitsPinnedRecipe(manifest.output, recipe))
461
+ throw new Error("Candidate exceeds its pinned recipe");
462
+ for (const object of candidateManifestObjects(manifest.output)) {
463
+ const head = await options.artifacts.head(contentObjectRef(object.digest));
464
+ if (head?.byteLength !== object.byteLength ||
465
+ head.digest !== object.digest)
466
+ throw new Error("Candidate object is not stored as named");
467
+ }
468
+ if (manifest.output.kind !== "ui")
469
+ decodeCandidateBackend(await readVerified(options.artifacts, contentObjectRef(manifest.output.backend.digest), manifest.output.backend.digest));
470
+ const root = await writeArtifactRoot(manifest.output, storeObjects, {
471
+ rootArtifactRef: parseArtifactRef(`artifact:sha256-${await digestTeamAppCandidateManifest(manifest)}`),
472
+ assetPolicy: recipe.assets,
473
+ });
474
+ release = Schema.decodeUnknownSync(releaseSchema)({
475
+ releaseId: job.release_id,
476
+ appId: manifest.appId,
477
+ artifact: {
478
+ artifactRef: root.artifactRef,
479
+ artifactDigest: root.artifactDigest,
480
+ provenance: manifest.provenance,
338
481
  },
339
- }));
340
- await verifyRelease(options.artifacts, release);
482
+ });
483
+ // Each object was checked by its stored digest above.
484
+ await verifyRelease(options.artifacts, release, defaultAssetPolicy, false);
341
485
  }
342
486
  catch {
343
487
  settle(jobId, "failed", "Candidate could not be stored as a Release");
@@ -22,25 +22,10 @@ export declare function createOysterArtifactsRepositoryMetadataStore(sql: Watchd
22
22
  * writes (`services/app-publisher/src/source-git.ts`), scoped to Oyster's own
23
23
  * Artifacts composition since a package cannot depend on that service. */
24
24
  export declare function exchangeOysterTemplateGit(artifacts: Pick<Artifacts, "exchangeGit">, remote: string, repository: string, request: Request, permission?: "read" | "write"): Promise<Response>;
25
- /** Seed Oyster's own template source from the operator owner route, over the
26
- * durable receipts and the Artifacts composition for this Oyster. */
27
- export declare function publishOysterAppSource(receipts: PublisherTemplateSeedReceipts, artifacts: Pick<Artifacts, "ensureRepository" | "exchangeGit">, rawAppId: string, request: Request): Promise<{
28
- readonly kind: "invalid" | "too-large";
29
- readonly receipt?: undefined;
30
- } | {
31
- readonly kind: "conflict";
32
- readonly receipt?: undefined;
33
- } | {
34
- readonly kind: "recorded" | "existing";
35
- readonly receipt: {
36
- readonly appId: import("@fungi.computer/puffball").AppId;
37
- readonly repository: string;
38
- readonly sourceRevision: import("@fungi.computer/puffball").SourceRevision;
39
- readonly sourceTreeDigest: import("@fungi.computer/puffball").Sha256Digest;
40
- readonly seedRevision: import("@fungi.computer/puffball").SourceRevision;
41
- readonly buildRecipeDigest: import("@fungi.computer/puffball").Sha256Digest;
42
- };
43
- } | {
44
- readonly kind: "unavailable";
45
- readonly receipt?: undefined;
46
- }>;
25
+ /** The operator's smart-HTTP door to one App's template repository: an
26
+ * ordinary `git push` (or the fetch before it) to
27
+ * `<base>/<suffix>` reaches `template-<app>` through the same admitted
28
+ * Artifacts exchange, which streams both bodies and holds every provider
29
+ * credential. An advertisement first opens the repository, so a fresh Oyster's
30
+ * first seed creates it. Admission (the owner) is the caller's. */
31
+ export declare function exchangeOysterTemplateSource(artifacts: Pick<Artifacts, "ensureRepository" | "exchangeGit">, rawAppId: string, suffix: string, request: Request): Promise<Response>;
@@ -4,7 +4,8 @@ import { appIdSchema, sha256DigestSchema, sourceRevisionSchema, } from "@fungi.c
4
4
  import { teamAppPublisherRegistrationSchema, } from "@fungi.computer/puffball/publisher";
5
5
  import { capturedPinnedRecipeSchema, digestPinnedRecipe, } from "./build-contract.js";
6
6
  import { sameReceipt, seedReceiptInputSchema, templateRepositoryForApp, } from "./source/index.js";
7
- import { seedPublisherTemplate, } from "./source/template-write.js";
7
+ import { smartHttpExchange } from "./git-door.js";
8
+ import { parseAppId } from "@fungi.computer/puffball";
8
9
  /**
9
10
  * Oyster's own storage for App source templates, Team-owned fork
10
11
  * registrations and the recorded Artifacts repository metadata their fork
@@ -207,20 +208,21 @@ export function createOysterArtifactsRepositoryMetadataStore(sql) {
207
208
  initialToken: decoded.initial_token,
208
209
  }));
209
210
  };
210
- const read = async (name) => {
211
- if ((await readPending(name)) !== null)
212
- return null;
211
+ /** The active row alone; activation checks its own promotion with this. */
212
+ const readActive = (name) => {
213
213
  const row = sql
214
214
  .exec(`SELECT repository_name, provider_id, default_branch, remote,
215
215
  parent_repository
216
216
  FROM oyster_artifacts_repositories WHERE repository_name = ?`, name)
217
217
  .toArray()[0];
218
- return row === undefined
218
+ return Promise.resolve(row === undefined
219
219
  ? null
220
220
  : metadataFromRow(Schema.decodeUnknownSync(repositoryRowSchema, {
221
221
  onExcessProperty: "error",
222
- })(row));
222
+ })(row)));
223
223
  };
224
+ /** Public reads hide a repository while its initial token is pending. */
225
+ const read = async (name) => (await readPending(name)) === null ? readActive(name) : null;
224
226
  return Object.freeze({
225
227
  read,
226
228
  readPending,
@@ -253,7 +255,7 @@ export function createOysterArtifactsRepositoryMetadataStore(sql) {
253
255
  FROM oyster_artifacts_repositories_pending
254
256
  WHERE repository_name = ? AND provider_id = ? AND default_branch = ?
255
257
  AND remote = ? AND parent_repository IS ? AND initial_token = ?`, metadata.name, metadata.id, metadata.defaultBranch, metadata.remote, metadata.parentRepository, initialToken);
256
- const active = await read(metadata.name);
258
+ const active = await readActive(metadata.name);
257
259
  if (active === null || !sameMetadata(active, metadata))
258
260
  throw new Error("Oyster Artifacts metadata conflicts");
259
261
  sql.exec(`DELETE FROM oyster_artifacts_repositories_pending
@@ -324,12 +326,45 @@ export async function exchangeOysterTemplateGit(artifacts, remote, repository, r
324
326
  headers: { "Content-Type": type, "Cache-Control": "no-store" },
325
327
  });
326
328
  }
327
- /** Seed Oyster's own template source from the operator owner route, over the
328
- * durable receipts and the Artifacts composition for this Oyster. */
329
- export function publishOysterAppSource(receipts, artifacts, rawAppId, request) {
330
- const transport = {
331
- ensureRepository: (name) => artifacts.ensureRepository(name),
332
- exchangeTemplateGit: (repository, templateRequest) => exchangeOysterTemplateGit(artifacts, `https://git.team.internal/${repository}.git`, repository, templateRequest, "write"),
329
+ /** The operator's smart-HTTP door to one App's template repository: an
330
+ * ordinary `git push` (or the fetch before it) to
331
+ * `<base>/<suffix>` reaches `template-<app>` through the same admitted
332
+ * Artifacts exchange, which streams both bodies and holds every provider
333
+ * credential. An advertisement first opens the repository, so a fresh Oyster's
334
+ * first seed creates it. Admission (the owner) is the caller's. */
335
+ export async function exchangeOysterTemplateSource(artifacts, rawAppId, suffix, request) {
336
+ let appId;
337
+ try {
338
+ appId = parseAppId(rawAppId);
339
+ }
340
+ catch {
341
+ return new Response("Not an App", { status: 404 });
342
+ }
343
+ const url = new URL(request.url);
344
+ const exchange = smartHttpExchange(request.method, url, suffix);
345
+ if (exchange === null)
346
+ return new Response("Not a git route", { status: 404 });
347
+ // The provider exchange carries no content coding; refuse rather than corrupt.
348
+ if (request.headers.has("content-encoding"))
349
+ return new Response("Compressed Git requests are not supported", {
350
+ status: 400,
351
+ });
352
+ const repository = templateRepositoryForApp(appId);
353
+ if (exchange.advertise)
354
+ await artifacts.ensureRepository(repository);
355
+ const headers = new Headers();
356
+ for (const name of ["content-type", "accept", "git-protocol"]) {
357
+ const value = request.headers.get(name);
358
+ if (value !== null)
359
+ headers.set(name, value);
360
+ }
361
+ const init = {
362
+ method: request.method,
363
+ headers,
364
+ body: exchange.advertise ? null : request.body,
365
+ signal: request.signal,
333
366
  };
334
- return seedPublisherTemplate(receipts, transport, rawAppId, request);
367
+ if (init.body !== null)
368
+ Object.assign(init, { duplex: "half" });
369
+ return exchangeOysterTemplateGit(artifacts, `https://git.team.internal/${repository}.git`, repository, new Request(`https://git.team.internal/${repository}.git/${suffix}${url.search}`, init), "write");
335
370
  }
@@ -10,7 +10,7 @@ export type HostPrincipal = Readonly<{
10
10
  role: "owner" | "member" | null;
11
11
  }>;
12
12
  /** Requests select existing SDK methods, opaque backend calls and derived operation identities. */
13
- export declare const gateRequestSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
13
+ export declare const gateRequestSchema: z.ZodLazy<z.ZodDiscriminatedUnion<[z.ZodObject<{
14
14
  kind: z.ZodLiteral<"method">;
15
15
  name: z.ZodString;
16
16
  input: z.ZodUnknown;
@@ -22,7 +22,7 @@ export declare const gateRequestSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
22
22
  }>;
23
23
  }, z.core.$strict>, z.ZodObject<{
24
24
  kind: z.ZodLiteral<"capability-route">;
25
- capability: z.ZodPipe<z.ZodUnknown, z.ZodTransform<"agent.list" | "agent.manage" | "agent.rename" | "agent.toolPolicy" | "agent.read" | "agent.send" | "computer.list" | "computer.readFile" | "computer.writeFile" | "computer.execute" | "llm.generation" | "media.generation" | "games:servers" | "git.repository" | "apps.open" | "apps.call" | "app.storage" | "app.events.emit" | "team.workbenchHost" | "marketplace.browse" | "marketplace.purchase" | "workflow.run" | "workflow.publish" | "workflow.arm" | "world" | "channels.read" | "channels.personal.manage", unknown>>;
25
+ capability: z.ZodPipe<z.ZodUnknown, z.ZodTransform<"agent.list" | "agent.manage" | "agent.rename" | "agent.avatar" | "agent.toolPolicy" | "agent.read" | "agent.send" | "computer.list" | "computer.readFile" | "computer.writeFile" | "computer.execute" | "llm.generation" | "media.generation" | "games:servers" | "git.repository" | "apps.open" | "apps.call" | "app.storage" | "app.events.emit" | "team.workbenchHost" | "marketplace.browse" | "marketplace.purchase" | "workflow.run" | "workflow.publish" | "workflow.arm" | "world" | "channels.read" | "channels.personal.manage", unknown>>;
26
26
  operation: z.ZodString;
27
27
  }, z.core.$strict>, z.ZodObject<{
28
28
  kind: z.ZodLiteral<"binding">;
@@ -32,7 +32,7 @@ export declare const gateRequestSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
32
32
  kind: z.ZodLiteral<"list-tools">;
33
33
  }, z.core.$strict>, z.ZodObject<{
34
34
  kind: z.ZodLiteral<"backend">;
35
- }, z.core.$strict>], "kind">;
35
+ }, z.core.$strict>], "kind">>;
36
36
  /** One decoded method, route, binding or catalog admission request. */
37
37
  export type GateRequest = z.output<typeof gateRequestSchema>;
38
38
  /** Immutable values the host already read; resource/grantor freshness remains host-owned. */
@@ -49,16 +49,16 @@ export type GateFacts = Readonly<{
49
49
  }>;
50
50
  }>;
51
51
  /** Binding grants have the exact S2 InvocationFacts shape and its maintained registry grammar. */
52
- export declare const invocationGrantsSchema: z.ZodReadonly<z.ZodArray<z.ZodObject<{
52
+ export declare const invocationGrantsSchema: z.ZodLazy<z.ZodReadonly<z.ZodArray<z.ZodObject<{
53
53
  binding: z.ZodPipe<z.ZodUnknown, z.ZodTransform<"BACKGROUND" | "GIT" | "EVENTS" | "APPS" | "WORKFLOWS" | "APP_STORAGE" | "MEDIA" | "COMPUTER" | "INBOX" | "AGENTS", unknown>>;
54
54
  operations: z.ZodReadonly<z.ZodArray<z.ZodString>>;
55
- }, z.core.$strict>>>;
55
+ }, z.core.$strict>>>>;
56
56
  /** The explicit binding ceiling passed to an invocation frame. */
57
57
  export type InvocationGrants = z.output<typeof invocationGrantsSchema>;
58
58
  /** A declared method from the canonical Puffball API grammar. */
59
59
  export type AppMethodDeclaration = AppApi["methods"][string];
60
60
  /** Ordered, closed refusal vocabulary; external methods always require approval. */
61
- export declare const gateRefusalSchema: z.ZodObject<{
61
+ export declare const gateRefusalSchema: z.ZodLazy<z.ZodObject<{
62
62
  kind: z.ZodLiteral<"refused">;
63
63
  code: z.ZodEnum<{
64
64
  unavailable: "unavailable";
@@ -71,7 +71,7 @@ export declare const gateRefusalSchema: z.ZodObject<{
71
71
  invalid_input: "invalid_input";
72
72
  visitor_read_only: "visitor_read_only";
73
73
  }>;
74
- }, z.core.$strict>;
74
+ }, z.core.$strict>>;
75
75
  /** Either an admitted scoped ceiling or one closed refusal code. */
76
76
  export type GateDecision = Readonly<{
77
77
  kind: "admitted";
package/dist/apps/gate.js CHANGED
@@ -8,7 +8,7 @@ import { appToolAccessSchema } from "./tools.js";
8
8
  import { ownerCodec, } from "./installation.js";
9
9
  const bindingNameSchema = ownerCodec(Schema.Literals(Object.values(appBackendBindingKinds).map((kind) => kind.name)));
10
10
  /** Requests select existing SDK methods, opaque backend calls and derived operation identities. */
11
- export const gateRequestSchema = z.discriminatedUnion("kind", [
11
+ export const gateRequestSchema = z.lazy(() => z.discriminatedUnion("kind", [
12
12
  z.strictObject({
13
13
  kind: z.literal("method"),
14
14
  name: z.string(),
@@ -27,9 +27,9 @@ export const gateRequestSchema = z.discriminatedUnion("kind", [
27
27
  }),
28
28
  z.strictObject({ kind: z.literal("list-tools") }),
29
29
  z.strictObject({ kind: z.literal("backend") }),
30
- ]);
30
+ ]));
31
31
  /** Binding grants have the exact S2 InvocationFacts shape and its maintained registry grammar. */
32
- export const invocationGrantsSchema = z
32
+ export const invocationGrantsSchema = z.lazy(() => z
33
33
  .array(z.strictObject({
34
34
  binding: bindingNameSchema,
35
35
  operations: z.array(z.string().min(1)).readonly(),
@@ -38,9 +38,9 @@ export const invocationGrantsSchema = z
38
38
  name: grant.binding,
39
39
  operations: grant.operations,
40
40
  }))), "Invalid declared binding operations")
41
- .readonly();
41
+ .readonly());
42
42
  /** Ordered, closed refusal vocabulary; external methods always require approval. */
43
- export const gateRefusalSchema = z.strictObject({
43
+ export const gateRefusalSchema = z.lazy(() => z.strictObject({
44
44
  kind: z.literal("refused"),
45
45
  code: z.enum([
46
46
  "unavailable",
@@ -53,7 +53,7 @@ export const gateRefusalSchema = z.strictObject({
53
53
  "invalid_input",
54
54
  "visitor_read_only",
55
55
  ]),
56
- });
56
+ }));
57
57
  /** Intersect stored approvals with the current verified Release without widening resource selectors. */
58
58
  export function grantsFor(installation, declaration, snapshot, capability) {
59
59
  if (installation.status !== "enabled" ||