@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
@@ -4,17 +4,23 @@ import { appDeclarationSchema } from "@fungi.computer/puffball/app";
4
4
  import { backendJsonSchema } from "@fungi.computer/puffball/backend";
5
5
  import * as Schema from "effect/Schema";
6
6
  import { isSigilName } from "@fungi.computer/sigils";
7
- /** The largest Team App candidate submission request body, in bytes. */
8
- export const TEAM_APP_SUBMISSION_MAX_REQUEST_BYTES = 24 * 1024 * 1024;
9
- /** One uploaded byte string; the enclosing HTTP request has a separate total bound. */
10
- const bytesSchema = Schema.Uint8ArrayFromBase64.pipe(Schema.check(Schema.makeFilter((bytes) => bytes.byteLength <= 16 * 1024 * 1024)));
11
- const backendBytesSchema = bytesSchema.pipe(Schema.check(Schema.makeFilter((bytes) => Schema.decodeUnknownResult(backendJsonSchema)(new TextDecoder().decode(bytes), { onExcessProperty: "error" })._tag === "Success")));
12
- const uiOutputSchema = Schema.Union([
7
+ /**
8
+ * The largest candidate manifest request body, in bytes. A manifest names
9
+ * objects; their bytes are uploaded one at a time, never inside it.
10
+ */
11
+ export const TEAM_APP_CANDIDATE_MANIFEST_MAX_BYTES = 1024 * 1024;
12
+ const byteLengthSchema = Schema.Number.pipe(Schema.check(Schema.isInt()), Schema.check(Schema.isGreaterThanOrEqualTo(0)), Schema.check(Schema.makeFilter(Number.isSafeInteger)));
13
+ /** One content-addressed object a build uploaded before its manifest. */
14
+ export const candidateObjectSchema = Schema.Struct({
15
+ digest: sha256DigestSchema,
16
+ byteLength: byteLengthSchema,
17
+ });
18
+ const uiManifestSchema = Schema.Union([
13
19
  Schema.Struct({
14
20
  mediaType: Schema.Literal("text/html; charset=utf-8"),
15
21
  content: Schema.Struct({
16
22
  kind: Schema.Literal("html"),
17
- bytes: bytesSchema,
23
+ ...candidateObjectSchema.fields,
18
24
  }),
19
25
  }),
20
26
  Schema.Struct({
@@ -22,7 +28,10 @@ const uiOutputSchema = Schema.Union([
22
28
  content: Schema.Struct({
23
29
  kind: Schema.Literal("assets"),
24
30
  base: assetBaseSchema,
25
- assets: Schema.Array(Schema.Struct({ path: assetPathSchema, bytes: bytesSchema })),
31
+ assets: Schema.Array(Schema.Struct({
32
+ path: assetPathSchema,
33
+ ...candidateObjectSchema.fields,
34
+ })),
26
35
  }),
27
36
  }),
28
37
  ]);
@@ -30,41 +39,142 @@ const uiOutputSchema = Schema.Union([
30
39
  const declaration = {
31
40
  declaration: Schema.optionalKey(appDeclarationSchema.check(Schema.makeFilter((declaration) => declaration.icon === undefined || isSigilName(declaration.icon), { message: "App icon must name a registered Sigil" }))),
32
41
  };
33
- const outputSchema = Schema.Union([
42
+ /** A build's output by reference: the same surfaces as its bytes, sized. */
43
+ export const candidateOutputManifestSchema = Schema.Union([
34
44
  Schema.Struct({
35
45
  kind: Schema.Literal("ui"),
36
- ui: uiOutputSchema,
46
+ ui: uiManifestSchema,
37
47
  ...declaration,
38
48
  }),
39
49
  Schema.Struct({
40
50
  kind: Schema.Literal("backend"),
41
- backend: backendBytesSchema,
51
+ backend: candidateObjectSchema,
42
52
  ...declaration,
43
53
  }),
44
54
  Schema.Struct({
45
55
  kind: Schema.Literal("app"),
46
- ui: uiOutputSchema,
47
- backend: backendBytesSchema,
56
+ ui: uiManifestSchema,
57
+ backend: candidateObjectSchema,
48
58
  ...declaration,
49
59
  }),
50
60
  ]);
51
- const candidateFields = {
61
+ const provenanceSchema = Schema.Struct({
62
+ sourceRevision: sourceRevisionSchema,
63
+ sourceTreeDigest: sha256DigestSchema,
64
+ buildRecipeDigest: sha256DigestSchema,
65
+ });
66
+ const manifestFields = {
52
67
  appId: appIdSchema,
53
- output: outputSchema,
54
- provenance: Schema.Struct({
55
- sourceRevision: sourceRevisionSchema,
56
- sourceTreeDigest: sha256DigestSchema,
57
- buildRecipeDigest: sha256DigestSchema,
58
- }),
68
+ output: candidateOutputManifestSchema,
69
+ provenance: provenanceSchema,
59
70
  };
60
- /** Exact submitted output and provenance before the Team assigns a stable Release ID. */
61
- export const teamAppCandidateSubmissionSchema = Schema.Struct(candidateFields);
71
+ /**
72
+ * A submitted Team App candidate: its output's objects by digest and size,
73
+ * and its provenance, before the Team assigns a stable Release ID.
74
+ */
75
+ export const teamAppCandidateManifestSchema = Schema.Struct(manifestFields);
62
76
  /** Publisher persistence input after TeamDO reserves retry-stable identities. */
77
+ export const preparedCandidateManifestSchema = Schema.Struct({
78
+ ...manifestFields,
79
+ releaseId: releaseIdSchema,
80
+ });
81
+ /** One operator-published byte string; its request has a separate total bound. */
82
+ const bytesSchema = Schema.Uint8ArrayFromBase64.pipe(Schema.check(Schema.makeFilter((bytes) => bytes.byteLength <= 16 * 1024 * 1024)));
83
+ const backendBytesSchema = bytesSchema.pipe(Schema.check(Schema.makeFilter((bytes) => Schema.decodeUnknownResult(backendJsonSchema)(new TextDecoder().decode(bytes), { onExcessProperty: "error" })._tag === "Success")));
84
+ const uiBytesSchema = Schema.Union([
85
+ Schema.Struct({
86
+ mediaType: Schema.Literal("text/html; charset=utf-8"),
87
+ content: Schema.Struct({
88
+ kind: Schema.Literal("html"),
89
+ bytes: bytesSchema,
90
+ }),
91
+ }),
92
+ Schema.Struct({
93
+ mediaType: Schema.Literal("application/vnd.fungi.app-assets+json"),
94
+ content: Schema.Struct({
95
+ kind: Schema.Literal("assets"),
96
+ base: assetBaseSchema,
97
+ assets: Schema.Array(Schema.Struct({ path: assetPathSchema, bytes: bytesSchema })),
98
+ }),
99
+ }),
100
+ ]);
101
+ /**
102
+ * A platform App candidate the trusted operator built and packed itself,
103
+ * with its bytes inline: the operator's platform publication request only.
104
+ * A Team candidate never carries bytes; it is a manifest.
105
+ */
63
106
  export const preparedCandidateSchema = Schema.Struct({
64
- ...candidateFields,
107
+ appId: appIdSchema,
65
108
  releaseId: releaseIdSchema,
109
+ output: Schema.Union([
110
+ Schema.Struct({
111
+ kind: Schema.Literal("ui"),
112
+ ui: uiBytesSchema,
113
+ ...declaration,
114
+ }),
115
+ Schema.Struct({
116
+ kind: Schema.Literal("backend"),
117
+ backend: backendBytesSchema,
118
+ ...declaration,
119
+ }),
120
+ Schema.Struct({
121
+ kind: Schema.Literal("app"),
122
+ ui: uiBytesSchema,
123
+ backend: backendBytesSchema,
124
+ ...declaration,
125
+ }),
126
+ ]),
127
+ provenance: provenanceSchema,
66
128
  });
67
- /** Canonical wire bytes make TeamDO's same-request digest independent of JSON spacing. */
68
- export function encodeTeamAppCandidateSubmission(input) {
69
- return JSON.stringify(Schema.encodeUnknownSync(teamAppCandidateSubmissionSchema)(input));
129
+ /** Canonical manifest bytes, so a digest is independent of JSON spacing. */
130
+ export function encodeTeamAppCandidateManifest(manifest) {
131
+ return JSON.stringify(Schema.encodeUnknownSync(teamAppCandidateManifestSchema)(manifest));
132
+ }
133
+ /** The hex SHA-256 of a manifest's canonical bytes: its App root's name. */
134
+ export async function digestTeamAppCandidateManifest(manifest) {
135
+ return Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(encodeTeamAppCandidateManifest(manifest)))), (byte) => byte.toString(16).padStart(2, "0")).join("");
136
+ }
137
+ /**
138
+ * The media type an uploaded object is stored with. A store that reads an
139
+ * HTML document or backend descriptor by its stored type (the Publisher's)
140
+ * needs it; an asset's type is its manifest's, so it is an octet stream.
141
+ */
142
+ export const candidateObjectMediaTypes = [
143
+ "text/html; charset=utf-8",
144
+ "application/vnd.fungi.app-backend+json",
145
+ "application/octet-stream",
146
+ ];
147
+ /** The stored media type a request names, if it is one an object may carry. */
148
+ export function candidateObjectMediaType(value) {
149
+ return candidateObjectMediaTypes.find((type) => type === value) ?? null;
150
+ }
151
+ /** Every distinct object a manifest names, each once, in manifest order. */
152
+ export function candidateManifestObjects(output) {
153
+ const objects = [];
154
+ const add = (object, mediaType) => {
155
+ const seen = objects.find(({ digest }) => digest === object.digest);
156
+ if (seen === undefined)
157
+ objects.push({
158
+ digest: object.digest,
159
+ byteLength: object.byteLength,
160
+ mediaType,
161
+ });
162
+ else if (seen.byteLength !== object.byteLength)
163
+ throw new Error("One object digest names two sizes");
164
+ };
165
+ if (output.kind !== "backend") {
166
+ const { content } = output.ui;
167
+ if (content.kind === "html")
168
+ add(content, "text/html; charset=utf-8");
169
+ else
170
+ for (const asset of content.assets)
171
+ add(asset, "application/octet-stream");
172
+ }
173
+ if (output.kind !== "ui")
174
+ add(output.backend, "application/vnd.fungi.app-backend+json");
175
+ return objects;
176
+ }
177
+ /** Decode a stored backend descriptor; a manifest's backend must be one. */
178
+ export function decodeCandidateBackend(bytes) {
179
+ return Schema.decodeUnknownSync(backendJsonSchema)(new TextDecoder("utf-8", { fatal: true, ignoreBOM: false }).decode(bytes), { onExcessProperty: "error" });
70
180
  }
@@ -6,8 +6,11 @@ import type { WorkspaceBuildStep } from "./build-step.js";
6
6
  * packages whose declared stylesheets compile first (`packageStylesheets`)
7
7
  * and its own `stylesheets`, compiled in place, in order. A workspace
8
8
  * package declares its build products: extra TypeScript configs compiled
9
- * after its `tsconfig.build.json`, and its stylesheet input and output.
10
- * Every path is relative to the manifest that declares it.
9
+ * after its `tsconfig.build.json`, and its stylesheet input and output; or,
10
+ * instead of TypeScript configs, the one `package.json` script that builds
11
+ * it (`script`), which only an admitted Computer build runs. Every path is
12
+ * relative to the manifest that declares it. Unknown build fields are
13
+ * refused, so a builder never silently ignores a policy it does not know.
11
14
  */
12
15
  export declare const workspaceBuildPolicySchema: Schema.Struct<{
13
16
  readonly react: Schema.optional<Schema.Boolean>;
@@ -15,6 +18,8 @@ export declare const workspaceBuildPolicySchema: Schema.Struct<{
15
18
  readonly packageStylesheets: Schema.optional<Schema.$Array<Schema.String>>;
16
19
  readonly stylesheets: Schema.optional<Schema.$Array<Schema.String>>;
17
20
  readonly typescript: Schema.optional<Schema.$Array<Schema.String>>;
21
+ /** A script name in this manifest's own `scripts`. */
22
+ readonly script: Schema.optional<Schema.NonEmptyString>;
18
23
  readonly stylesheet: Schema.optional<Schema.Struct<{
19
24
  readonly input: Schema.String;
20
25
  readonly output: Schema.String;
@@ -13,8 +13,11 @@ const manifestPathSchema = Schema.String.pipe(Schema.check(Schema.makeFilter((pa
13
13
  * packages whose declared stylesheets compile first (`packageStylesheets`)
14
14
  * and its own `stylesheets`, compiled in place, in order. A workspace
15
15
  * package declares its build products: extra TypeScript configs compiled
16
- * after its `tsconfig.build.json`, and its stylesheet input and output.
17
- * Every path is relative to the manifest that declares it.
16
+ * after its `tsconfig.build.json`, and its stylesheet input and output; or,
17
+ * instead of TypeScript configs, the one `package.json` script that builds
18
+ * it (`script`), which only an admitted Computer build runs. Every path is
19
+ * relative to the manifest that declares it. Unknown build fields are
20
+ * refused, so a builder never silently ignores a policy it does not know.
18
21
  */
19
22
  export const workspaceBuildPolicySchema = Schema.Struct({
20
23
  react: Schema.optional(Schema.Boolean),
@@ -22,13 +25,25 @@ export const workspaceBuildPolicySchema = Schema.Struct({
22
25
  packageStylesheets: Schema.optional(Schema.Array(Schema.String)),
23
26
  stylesheets: Schema.optional(Schema.Array(manifestPathSchema)),
24
27
  typescript: Schema.optional(Schema.Array(manifestPathSchema)),
28
+ /** A script name in this manifest's own `scripts`. */
29
+ script: Schema.optional(Schema.NonEmptyString),
25
30
  stylesheet: Schema.optional(Schema.Struct({ input: manifestPathSchema, output: manifestPathSchema })),
26
31
  });
27
32
  const manifestSchema = Schema.fromJsonString(Schema.Struct({
28
33
  name: Schema.optional(Schema.String),
29
34
  dependencies: Schema.optional(Schema.Record(Schema.String, Schema.String)),
30
- fungi: Schema.optional(Schema.Struct({ build: Schema.optional(workspaceBuildPolicySchema) })),
35
+ scripts: Schema.optional(Schema.Record(Schema.String, Schema.String)),
36
+ fungi: Schema.optional(Schema.Struct({ build: Schema.optional(Schema.Unknown) })),
31
37
  }));
38
+ /** A manifest's build policy; only `fungi.build` itself is held strict. */
39
+ function policyOf(manifest) {
40
+ const build = manifest.fungi?.build;
41
+ return build === undefined
42
+ ? {}
43
+ : Schema.decodeUnknownSync(workspaceBuildPolicySchema, {
44
+ onExcessProperty: "error",
45
+ })(build);
46
+ }
32
47
  /** One pnpm workspace project, as `pnpm ls -r --depth -1 --json` lists it. */
33
48
  const workspaceProjectsSchema = Schema.fromJsonString(Schema.Array(Schema.Struct({
34
49
  name: Schema.String.pipe(Schema.check(Schema.isMinLength(1))),
@@ -72,10 +87,13 @@ export async function resolveWorkspaceBuild(options) {
72
87
  return decoded;
73
88
  };
74
89
  const projects = new Map();
90
+ let rootProject = null;
75
91
  for (const project of Schema.decodeUnknownSync(workspaceProjectsSchema)(options.projects)) {
76
92
  const directory = within(root, project.path);
77
- if (directory === "")
93
+ if (directory === "") {
94
+ rootProject = project.name;
78
95
  continue;
96
+ }
79
97
  if (projects.has(project.name))
80
98
  throw new Error(`Workspace package name repeats: ${project.name}`);
81
99
  projects.set(project.name, directory);
@@ -83,15 +101,26 @@ export async function resolveWorkspaceBuild(options) {
83
101
  const owners = [...projects]
84
102
  .filter(([, directory]) => entry.startsWith(`${directory}/`))
85
103
  .sort(([, left], [, right]) => right.length - left.length);
86
- const owner = owners[0];
104
+ // A workspace member owns its entry; with none, an App's own repository
105
+ // (one package, the App itself at the root) owns it.
106
+ const owner = owners[0] ??
107
+ (projects.size === 0 && rootProject !== null
108
+ ? [rootProject, ""]
109
+ : undefined);
87
110
  if (!owner)
88
111
  throw new Error(`No workspace package owns ${entry}`);
89
112
  const [packageName, directory] = owner;
90
- let appDirectory = dirname(entry);
113
+ const parent = (path) => {
114
+ const up = dirname(path);
115
+ return up === "." ? "" : up;
116
+ };
117
+ let appDirectory = parent(entry);
91
118
  while (appDirectory !== directory &&
92
119
  !(await exists(join(root, appDirectory, "package.json"))))
93
- appDirectory = dirname(appDirectory);
94
- const policy = (await manifest(appDirectory)).fungi?.build ?? {};
120
+ appDirectory = parent(appDirectory);
121
+ const policy = policyOf(await manifest(appDirectory));
122
+ if (policy.script !== undefined)
123
+ throw new Error(`${appDirectory} is the App; its page builds with the recipe, not a script`);
95
124
  const dependencies = [];
96
125
  const visiting = new Set([directory]);
97
126
  const visit = async (from) => {
@@ -114,20 +143,35 @@ export async function resolveWorkspaceBuild(options) {
114
143
  await visit(directory);
115
144
  const steps = [];
116
145
  for (const dependency of dependencies) {
146
+ const declared = await manifest(dependency);
147
+ const own = policyOf(declared);
148
+ if (own.script !== undefined) {
149
+ if (own.typescript !== undefined)
150
+ throw new Error(`${dependency} declares both a build script and TypeScript configs`);
151
+ if (declared.scripts === undefined ||
152
+ !Object.hasOwn(declared.scripts, own.script))
153
+ throw new Error(`${dependency} has no script named ${own.script}`);
154
+ steps.push({ kind: "script", directory: dependency, script: own.script });
155
+ continue;
156
+ }
117
157
  if (await exists(join(root, dependency, "tsconfig.build.json")))
118
158
  steps.push({
119
159
  kind: "typescript",
120
160
  config: `${dependency}/tsconfig.build.json`,
121
161
  });
122
- for (const config of (await manifest(dependency)).fungi?.build
123
- ?.typescript ?? [])
162
+ for (const config of own.typescript ?? [])
124
163
  steps.push({ kind: "typescript", config: `${dependency}/${config}` });
125
164
  }
165
+ const appManifest = await manifest(appDirectory);
126
166
  for (const name of policy.packageStylesheets ?? []) {
127
167
  const owner = projects.get(name);
168
+ // A package installed from a registry ships its built stylesheet; only a
169
+ // workspace package's stylesheet compiles here.
170
+ if (owner === undefined && appManifest.dependencies?.[name] !== undefined)
171
+ continue;
128
172
  if (owner === undefined || !dependencies.includes(owner))
129
173
  throw new Error(`Stylesheet package ${name} is not a workspace dependency`);
130
- const declared = (await manifest(owner)).fungi?.build?.stylesheet;
174
+ const declared = policyOf(await manifest(owner)).stylesheet;
131
175
  if (!declared)
132
176
  throw new Error(`${name} declares no stylesheet`);
133
177
  steps.push({
@@ -2,3 +2,6 @@
2
2
  export * from "./build/effect.js";
3
3
  /** Team App candidate wire contract: submission schema and codec. */
4
4
  export * from "./build/submission-contract.js";
5
+ export { workspaceBuildPolicy } from "./build/build-policy.js";
6
+ /** A build's uploaded objects in an R2-shaped, content-addressed store. */
7
+ export * from "./build/content-objects.js";
@@ -2,3 +2,6 @@
2
2
  export * from "./build/effect.js";
3
3
  /** Team App candidate wire contract: submission schema and codec. */
4
4
  export * from "./build/submission-contract.js";
5
+ export { workspaceBuildPolicy } from "./build/build-policy.js";
6
+ /** A build's uploaded objects in an R2-shaped, content-addressed store. */
7
+ export * from "./build/content-objects.js";
@@ -0,0 +1,62 @@
1
+ import { type CandidateObject, type CandidateObjectMediaType, type TeamAppCandidateManifest } from "./build-contract.js";
2
+ /** The two facts a build job reports; a host's Team Git event is a superset
3
+ * that also carries pushes, so a host's own record callback is always
4
+ * assignable here. */
5
+ export type BuildGitEvent = Readonly<{
6
+ kind: "build-artifact";
7
+ jobId: string;
8
+ credentialId: string;
9
+ digest: string;
10
+ bytes: number;
11
+ }> | Readonly<{
12
+ kind: "build-status";
13
+ jobId: string;
14
+ credentialId: string;
15
+ status: "succeeded" | "failed";
16
+ detail: string | null;
17
+ }>;
18
+ /** A Git principal admitted to read exactly one live build job's one
19
+ * repository: the job-scoped read clone. It never pushes, and its reads are
20
+ * never recorded as Team Git push events. */
21
+ export type BuildGitPrincipal = Readonly<{
22
+ kind: "build";
23
+ jobId: string;
24
+ credentialId: string;
25
+ }>;
26
+ /** True when an admitted Git principal is a build job's job-scoped read
27
+ * credential rather than an Agent or a Workflow Git pass. A host's Git
28
+ * egress uses this to admit the job-scoped read clone while refusing any
29
+ * write and skipping push-event recording for it. Generic so a host's own
30
+ * wider principal union still narrows away the "build" member afterward,
31
+ * exactly as the literal `principal.kind === "build"` comparison it
32
+ * replaces did. */
33
+ export declare function isBuildGitPrincipal<P extends Readonly<{
34
+ kind: string;
35
+ }>>(principal: P): principal is Extract<P, Readonly<{
36
+ kind: "build";
37
+ }>>;
38
+ /**
39
+ * The build-report handler for `/jobs/:id/(candidate|artifact|status)` and
40
+ * `PUT /jobs/:id/objects/sha256-<hex>`. `admitBuild` is the host's own
41
+ * live-job lookup; `admitObject` admits one object upload for a live job's
42
+ * grant and answers its size bound; `storeObject` streams an admitted object
43
+ * into the host's content-addressed store, which verifies its digest;
44
+ * `submitCandidate` hands an admitted manifest to the host's Publisher
45
+ * custody; `record` delivers a reported fact to the host's own projection.
46
+ * `report` observes a body or JSON decode failure without changing its 400
47
+ * outcome; a host may wire its own structured logger or omit it.
48
+ */
49
+ export declare function createBuildEgress(options: Readonly<{
50
+ admitBuild: (jobId: string) => Promise<Readonly<{
51
+ credentialId: string;
52
+ }> | null>;
53
+ admitObject: (jobId: string, grant: string) => Promise<Readonly<{
54
+ maxObjectBytes: number;
55
+ }> | null>;
56
+ storeObject: (object: CandidateObject, body: ReadableStream<Uint8Array> | null, mediaType: CandidateObjectMediaType) => Promise<"stored" | "refused">;
57
+ submitCandidate: (jobId: string, manifest: TeamAppCandidateManifest, grant: string, credentialId: string) => Promise<Response>;
58
+ record: (event: BuildGitEvent) => Promise<void>;
59
+ report?: (operation: string, cause: unknown) => void;
60
+ }>): Readonly<{
61
+ fetch(request: Request, url: URL): Promise<Response>;
62
+ }>;
@@ -0,0 +1,148 @@
1
+ import * as Schema from "effect/Schema";
2
+ import { z } from "zod";
3
+ import { teamAppCandidateManifestSchema, TEAM_APP_CANDIDATE_MANIFEST_MAX_BYTES, candidateObjectMediaType, candidateObjectSchema, } from "./build-contract.js";
4
+ /**
5
+ * The Computer's own egress for its build job's `/jobs/:id/*` reports. A
6
+ * live build uploads each built object alone, streamed straight into the
7
+ * host's content-addressed store, then submits the small manifest naming
8
+ * them through Team submission custody, then reports bytes/status. Selection
9
+ * and promotion are never admitted here. The
10
+ * Computer presents no credential of its own: every decision is
11
+ * port-injected so a host (Hub today, Oyster's standalone host once it
12
+ * mounts a Computer build) supplies its own admission, submission and event
13
+ * recording without this module reaching into either host's state.
14
+ */
15
+ const statusSchema = z.lazy(() => z.strictObject({
16
+ status: z.enum(["succeeded", "failed"]),
17
+ detail: z.string().max(2_000).optional(),
18
+ }));
19
+ const artifactSchema = z.lazy(() => z.strictObject({
20
+ digest: z.string().regex(/^sha256:[0-9a-f]{64}$/),
21
+ bytes: z.number().int().nonnegative(),
22
+ }));
23
+ /** True when an admitted Git principal is a build job's job-scoped read
24
+ * credential rather than an Agent or a Workflow Git pass. A host's Git
25
+ * egress uses this to admit the job-scoped read clone while refusing any
26
+ * write and skipping push-event recording for it. Generic so a host's own
27
+ * wider principal union still narrows away the "build" member afterward,
28
+ * exactly as the literal `principal.kind === "build"` comparison it
29
+ * replaces did. */
30
+ export function isBuildGitPrincipal(principal) {
31
+ return principal.kind === "build";
32
+ }
33
+ /**
34
+ * The build-report handler for `/jobs/:id/(candidate|artifact|status)` and
35
+ * `PUT /jobs/:id/objects/sha256-<hex>`. `admitBuild` is the host's own
36
+ * live-job lookup; `admitObject` admits one object upload for a live job's
37
+ * grant and answers its size bound; `storeObject` streams an admitted object
38
+ * into the host's content-addressed store, which verifies its digest;
39
+ * `submitCandidate` hands an admitted manifest to the host's Publisher
40
+ * custody; `record` delivers a reported fact to the host's own projection.
41
+ * `report` observes a body or JSON decode failure without changing its 400
42
+ * outcome; a host may wire its own structured logger or omit it.
43
+ */
44
+ export function createBuildEgress(options) {
45
+ const report = options.report ??
46
+ ((operation, cause) => {
47
+ console.error(operation, cause);
48
+ });
49
+ const putObject = async (request, jobId, hex) => {
50
+ const admitted = await options.admitObject(jobId, request.headers.get("X-Team-App-Build") ?? "");
51
+ if (admitted === null)
52
+ return new Response(null, { status: 403 });
53
+ // The stated length bounds what the store takes before a byte moves.
54
+ const length = request.headers.get("Content-Length") ?? "";
55
+ if (!/^(0|[1-9][0-9]{0,14})$/u.test(length))
56
+ return new Response(null, { status: 411 });
57
+ const byteLength = Number(length);
58
+ if (byteLength > admitted.maxObjectBytes) {
59
+ await request.body?.cancel();
60
+ return new Response(null, { status: 413 });
61
+ }
62
+ const mediaType = candidateObjectMediaType(request.headers.get("Content-Type"));
63
+ if (mediaType === null) {
64
+ await request.body?.cancel();
65
+ return new Response(null, { status: 415 });
66
+ }
67
+ const stored = await options.storeObject(Schema.decodeUnknownSync(candidateObjectSchema)({
68
+ digest: `sha256:${hex}`,
69
+ byteLength,
70
+ }), request.body, mediaType);
71
+ return new Response(null, { status: stored === "stored" ? 204 : 400 });
72
+ };
73
+ return Object.freeze({
74
+ async fetch(request, url) {
75
+ if (url.search !== "" || url.username || url.password)
76
+ return new Response(null, { status: 403 });
77
+ const object = /^\/jobs\/([a-zA-Z0-9_-]{1,128})\/objects\/sha256-([0-9a-f]{64})$/u.exec(url.pathname);
78
+ if (object !== null) {
79
+ const [, jobId = "", hex = ""] = object;
80
+ return request.method === "PUT"
81
+ ? putObject(request, jobId, hex)
82
+ : new Response(null, { status: 403 });
83
+ }
84
+ const match = /^\/jobs\/([a-zA-Z0-9_-]{1,128})\/(status|artifact|candidate)$/u.exec(url.pathname);
85
+ const jobId = match?.[1];
86
+ const action = match?.[2];
87
+ if (jobId === undefined || request.method !== "POST")
88
+ return new Response(null, { status: 403 });
89
+ const job = await options.admitBuild(jobId);
90
+ if (job === null)
91
+ return new Response(null, { status: 403 });
92
+ if (action === "candidate") {
93
+ let manifest;
94
+ try {
95
+ if (request.body === null)
96
+ return new Response(null, { status: 400 });
97
+ let size = 0;
98
+ const bounded = request.body.pipeThrough(new TransformStream({
99
+ transform(chunk, controller) {
100
+ size += chunk.byteLength;
101
+ if (size > TEAM_APP_CANDIDATE_MANIFEST_MAX_BYTES)
102
+ throw new Error("candidate manifest oversize");
103
+ controller.enqueue(chunk);
104
+ },
105
+ }));
106
+ manifest = Schema.decodeUnknownSync(teamAppCandidateManifestSchema, {
107
+ onExcessProperty: "error",
108
+ })(await new Response(bounded).json());
109
+ }
110
+ catch {
111
+ return new Response(null, { status: 400 });
112
+ }
113
+ return options.submitCandidate(jobId, manifest, request.headers.get("X-Team-App-Build") ?? "", job.credentialId);
114
+ }
115
+ let json;
116
+ try {
117
+ json = await request.json();
118
+ }
119
+ catch (cause) {
120
+ report("packages/oyster/src/build-egress.ts:report", cause);
121
+ return new Response(null, { status: 400 });
122
+ }
123
+ if (action === "artifact") {
124
+ const artifact = artifactSchema.safeParse(json);
125
+ if (!artifact.success)
126
+ return new Response(null, { status: 400 });
127
+ await options.record({
128
+ kind: "build-artifact",
129
+ jobId,
130
+ credentialId: job.credentialId,
131
+ ...artifact.data,
132
+ });
133
+ return new Response(null, { status: 204 });
134
+ }
135
+ const status = statusSchema.safeParse(json);
136
+ if (!status.success)
137
+ return new Response(null, { status: 400 });
138
+ await options.record({
139
+ kind: "build-status",
140
+ jobId,
141
+ credentialId: job.credentialId,
142
+ status: status.data.status,
143
+ detail: status.data.detail ?? null,
144
+ });
145
+ return new Response(null, { status: 204 });
146
+ },
147
+ });
148
+ }
package/dist/build.d.ts CHANGED
@@ -9,3 +9,4 @@ export { frozenSnapshotInstallArguments } from "./build/snapshot-package-store.j
9
9
  export { repositoryEnvironment, artifactsGitEnvironment, hostedArtifactsGitRemote, } from "./build/git-environment.js";
10
10
  /** Captured-dependency build program with guarded filesystem access. */
11
11
  export declare const dependencyBuildProgram: string;
12
+ export { stageBuildOutput, writeArtifactRoot, type ImmutableArtifactObjects, type StagedArtifactObject, type StagedBuildOutput, } from "./build/artifact-writer.js";
package/dist/build.js CHANGED
@@ -11,3 +11,4 @@ export { frozenSnapshotInstallArguments } from "./build/snapshot-package-store.j
11
11
  export { repositoryEnvironment, artifactsGitEnvironment, hostedArtifactsGitRemote, } from "./build/git-environment.js";
12
12
  /** Captured-dependency build program with guarded filesystem access. */
13
13
  export const dependencyBuildProgram = bundledDependencyBuildProgram;
14
+ export { stageBuildOutput, writeArtifactRoot, } from "./build/artifact-writer.js";
@@ -154,20 +154,20 @@ export function frameBindingEntrypoint(methods, dispatch) {
154
154
  }
155
155
  };
156
156
  }
157
- const facetHttpReplySchema = z.strictObject({
157
+ const facetHttpReplySchema = z.lazy(() => z.strictObject({
158
158
  status: z.number().int(),
159
159
  statusText: z.string(),
160
160
  headers: z.array(z.tuple([z.string(), z.string()])),
161
161
  body: z.instanceof(ReadableStream).nullable(),
162
- });
162
+ }));
163
163
  // App methods return JSON; HTTP carries binary bytes and lifecycle hooks return void.
164
164
  // Binding operation results keep their separate, operation-specific decoders.
165
- const facetHttpInputSchema = z.strictObject({
165
+ const facetHttpInputSchema = z.lazy(() => z.strictObject({
166
166
  url: z.string(),
167
167
  method: z.string(),
168
168
  headers: z.array(z.tuple([z.string(), z.string()])),
169
169
  body: z.instanceof(Uint8Array).nullable(),
170
- });
170
+ }));
171
171
  const facetInvocationSchema = rpcHandleSchema({
172
172
  invoke: rpcMethod(z.union([z.json(), z.undefined()])).input(z.tuple([z.string(), z.array(z.unknown()), z.unknown()])),
173
173
  http: rpcMethod(facetHttpReplySchema).input(z.tuple([facetHttpInputSchema, z.unknown()])),