@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
@@ -0,0 +1,223 @@
1
+ import { spawn } from "node:child_process";
2
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { parseAppId, parseSourceRevision, } from "@fungi.computer/puffball";
6
+ import { authenticatedGitEnvironment, repositoryEnvironment, } from "../build/git-environment.js";
7
+ /** The fixed identity of every seed commit, so a retry of the same tree on
8
+ * the same parent is the same commit. */
9
+ const seedIdentity = {
10
+ GIT_AUTHOR_NAME: "Botanical App Source",
11
+ GIT_AUTHOR_EMAIL: "app-source@botanical.invalid",
12
+ GIT_AUTHOR_DATE: "@0 +0000",
13
+ GIT_COMMITTER_NAME: "Botanical App Source",
14
+ GIT_COMMITTER_EMAIL: "app-source@botanical.invalid",
15
+ GIT_COMMITTER_DATE: "@0 +0000",
16
+ };
17
+ /** Artifacts' native fork drops delta-encoded objects, so seed packs carry
18
+ * whole objects only. Every local object is loose (written here, or fetched
19
+ * with an unpack limit no fetch reaches), so no delta exists to reuse; a zero
20
+ * window computes none, `--no-thin` sends none against remote objects and no
21
+ * pack is reused verbatim. */
22
+ const wholeObjectPush = [
23
+ "-c",
24
+ "pack.window=0",
25
+ "-c",
26
+ "pack.depth=0",
27
+ "-c",
28
+ "pack.allowPackReuse=false",
29
+ ];
30
+ const looseFetch = ["-c", "fetch.unpackLimit=1000000000"];
31
+ const objectId = /^[0-9a-f]{40}$/u;
32
+ /** One whole publication (project, fetch, push, confirm) is bounded. */
33
+ const TEMPLATE_PUSH_TIMEOUT_MS = 10 * 60_000;
34
+ /** The Git subcommand of `args`, past any leading `-c name=value` pairs. */
35
+ function command(args) {
36
+ let at = 0;
37
+ while (args[at] === "-c")
38
+ at += 2;
39
+ return args[at] ?? "";
40
+ }
41
+ function runGit(args, environment, signal, input) {
42
+ return new Promise((resolve, reject) => {
43
+ const child = spawn("git", [...args], {
44
+ env: environment,
45
+ shell: false,
46
+ signal,
47
+ stdio: ["pipe", "pipe", "pipe"],
48
+ });
49
+ let stdout = "";
50
+ let stderr = "";
51
+ let spawnError;
52
+ child.stdout.setEncoding("utf8");
53
+ child.stderr.setEncoding("utf8");
54
+ child.stdout.on("data", (chunk) => {
55
+ stdout += chunk;
56
+ });
57
+ child.stderr.on("data", (chunk) => {
58
+ stderr += chunk;
59
+ });
60
+ child.once("error", (error) => {
61
+ spawnError = error;
62
+ });
63
+ child.once("close", (code) => {
64
+ if (code === 0 && spawnError === undefined) {
65
+ resolve(stdout);
66
+ return;
67
+ }
68
+ const detail = stderr.trim();
69
+ reject(spawnError ??
70
+ new Error(`git ${command(args)} failed${detail ? `: ${detail}` : ` (${String(code)})`}`));
71
+ });
72
+ child.stdin.end(input);
73
+ });
74
+ }
75
+ /** A template path names one regular file below the root: relative, no empty,
76
+ * `.`, `..` or `.git` part, and never both a file and a directory. */
77
+ function checkTemplatePaths(files) {
78
+ const paths = new Set();
79
+ for (const { path } of files) {
80
+ const parts = path.split("/");
81
+ if (path.length > 4096 ||
82
+ path.includes("\\") ||
83
+ path.includes("\0") ||
84
+ parts.some((part) => part === "" ||
85
+ part === "." ||
86
+ part === ".." ||
87
+ part.toLowerCase() === ".git"))
88
+ throw new Error("Template source path is invalid");
89
+ if (paths.has(path))
90
+ throw new Error("Duplicate template source path");
91
+ paths.add(path);
92
+ }
93
+ for (const path of paths) {
94
+ const parts = path.split("/");
95
+ for (let end = 1; end < parts.length; end += 1)
96
+ if (paths.has(parts.slice(0, end).join("/")))
97
+ throw new Error("Template source path is both a file and a directory");
98
+ }
99
+ }
100
+ function oid(output) {
101
+ const value = output.trim();
102
+ if (!objectId.test(value))
103
+ throw new Error("git returned no object id");
104
+ return value;
105
+ }
106
+ function remoteMain(output) {
107
+ for (const line of output.split("\n")) {
108
+ const [hash, ref] = line.split("\t");
109
+ if (ref === "refs/heads/main" && hash !== undefined && objectId.test(hash))
110
+ return hash;
111
+ }
112
+ return null;
113
+ }
114
+ /**
115
+ * Commit one projected App source tree onto its template repository's `main`
116
+ * with ordinary Git, and push it there over smart HTTP.
117
+ *
118
+ * The tree holds exactly the given bytes (no attributes or filters), each a
119
+ * regular non-executable file. The first seed is a parentless commit; a later
120
+ * different tree is an ordinary fast-forward on the current head; an exact
121
+ * retry pushes nothing and answers the current head. Nothing is forced. The
122
+ * `headers` authenticate every request and reach Git only through its config
123
+ * environment.
124
+ */
125
+ export async function pushTemplateSource(input) {
126
+ const appId = parseAppId(input.appId);
127
+ const sourceRevision = parseSourceRevision(input.sourceRevision);
128
+ const remote = new URL(input.remote);
129
+ if (remote.username || remote.password || remote.search || remote.hash)
130
+ throw new Error("Template remote must not carry credentials or a query");
131
+ if (input.files.length === 0)
132
+ throw new Error("Template source is empty");
133
+ checkTemplatePaths(input.files);
134
+ const deadline = AbortSignal.timeout(TEMPLATE_PUSH_TIMEOUT_MS);
135
+ const signal = input.signal === undefined
136
+ ? deadline
137
+ : AbortSignal.any([input.signal, deadline]);
138
+ const work = await mkdtemp(join(tmpdir(), "oyster-template-push-"));
139
+ try {
140
+ const gitDir = join(work, "repository.git");
141
+ const staging = join(work, "files");
142
+ const local = {
143
+ ...repositoryEnvironment(),
144
+ HOME: work,
145
+ GIT_CONFIG_NOSYSTEM: "1",
146
+ GIT_CONFIG_GLOBAL: "/dev/null",
147
+ GIT_TERMINAL_PROMPT: "0",
148
+ GIT_DIR: gitDir,
149
+ GIT_INDEX_FILE: join(work, "index"),
150
+ };
151
+ const network = {
152
+ ...authenticatedGitEnvironment(input.headers),
153
+ HOME: work,
154
+ GIT_CONFIG_NOSYSTEM: "1",
155
+ GIT_CONFIG_GLOBAL: "/dev/null",
156
+ GIT_TERMINAL_PROMPT: "0",
157
+ GIT_DIR: gitDir,
158
+ };
159
+ const git = (args, stdin) => runGit(args, local, signal, stdin);
160
+ const remoteGit = (args) => runGit(args, network, signal);
161
+ await runGit(["init", "--quiet", "--bare", "--initial-branch=main", gitDir], local, signal);
162
+ // Blobs: raw bytes, loose objects, no clean/smudge or eol filters.
163
+ await mkdir(staging);
164
+ const paths = [];
165
+ for (const [at, file] of input.files.entries()) {
166
+ const path = join(staging, String(at));
167
+ await writeFile(path, file.contents);
168
+ paths.push(path);
169
+ }
170
+ const blobs = (await git(["hash-object", "-w", "--no-filters", "--stdin-paths"], `${paths.join("\n")}\n`))
171
+ .trim()
172
+ .split("\n");
173
+ if (blobs.length !== input.files.length ||
174
+ !blobs.every((b) => objectId.test(b)))
175
+ throw new Error("git did not write every template blob");
176
+ const index = input.files
177
+ .map((file, at) => `100644 ${blobs[at] ?? ""}\t${file.path}\0`)
178
+ .join("");
179
+ await git(["update-index", "-z", "--add", "--index-info"], index);
180
+ const tree = oid(await git(["write-tree"]));
181
+ const head = remoteMain(await remoteGit(["ls-remote", input.remote, "refs/heads/main"]));
182
+ let parents = [];
183
+ if (head !== null) {
184
+ await remoteGit([
185
+ ...looseFetch,
186
+ "fetch",
187
+ "--quiet",
188
+ "--depth=1",
189
+ "--no-tags",
190
+ input.remote,
191
+ "refs/heads/main",
192
+ ]);
193
+ if (oid(await git(["rev-parse", `${head}^{tree}`])) === tree)
194
+ return {
195
+ seedRevision: parseSourceRevision(`git:${head}`),
196
+ pushed: false,
197
+ };
198
+ parents = ["-p", head];
199
+ }
200
+ const commit = oid(await runGit([
201
+ "commit-tree",
202
+ tree,
203
+ ...parents,
204
+ "-m",
205
+ `Seed ${appId} from ${sourceRevision}`,
206
+ ], { ...local, ...seedIdentity }, signal));
207
+ await remoteGit([
208
+ ...wholeObjectPush,
209
+ "push",
210
+ "--quiet",
211
+ "--no-thin",
212
+ input.remote,
213
+ `${commit}:refs/heads/main`,
214
+ ]);
215
+ // A successful push is not sufficient: main must now name this commit.
216
+ if (remoteMain(await remoteGit(["ls-remote", input.remote, "refs/heads/main"])) !== commit)
217
+ throw new Error("Template main moved during the push");
218
+ return { seedRevision: parseSourceRevision(`git:${commit}`), pushed: true };
219
+ }
220
+ finally {
221
+ await rm(work, { recursive: true, force: true });
222
+ }
223
+ }
@@ -1,2 +1,4 @@
1
1
  /** Derive the private editable source repository from its validated App identity. */
2
2
  export declare function templateRepositoryForApp(appId: string): string;
3
+ /** The App whose template `repository` is, or null when it names none. */
4
+ export declare function appForTemplateRepository(repository: string): string | null;
@@ -1,4 +1,7 @@
1
1
  import { parseAppId } from "@fungi.computer/puffball";
2
+ import { appIdSchema } from "@fungi.computer/puffball/effect";
3
+ import * as Option from "effect/Option";
4
+ import * as Schema from "effect/Schema";
2
5
  const TEMPLATE_REPOSITORY_PREFIX = "template-";
3
6
  /** Derive the private editable source repository from its validated App identity. */
4
7
  export function templateRepositoryForApp(appId) {
@@ -6,3 +9,13 @@ export function templateRepositoryForApp(appId) {
6
9
  // AppId's maximum 63-character suffix makes this repository at most 72 chars.
7
10
  return `${TEMPLATE_REPOSITORY_PREFIX}${parsed.slice("app_".length)}`;
8
11
  }
12
+ /** The App whose template `repository` is, or null when it names none. */
13
+ export function appForTemplateRepository(repository) {
14
+ if (!repository.startsWith(TEMPLATE_REPOSITORY_PREFIX))
15
+ return null;
16
+ const appId = Schema.decodeUnknownOption(appIdSchema)(`app_${repository.slice(TEMPLATE_REPOSITORY_PREFIX.length)}`);
17
+ return Option.isSome(appId) &&
18
+ templateRepositoryForApp(appId.value) === repository
19
+ ? appId.value
20
+ : null;
21
+ }
@@ -1,12 +1,17 @@
1
1
  import * as Schema from "effect/Schema";
2
2
  import type { Artifacts } from "@fungi.computer/burrow";
3
3
  import type { PublisherTemplateSeedReceipts } from "./seed-receipt.js";
4
- /** A bounded operator source upload; decoding happens only at the Publisher boundary. */
5
- export declare const TEMPLATE_WRITE_MAX_REQUEST_BYTES: number;
6
- export declare const templateWriteInputSchema: Schema.Struct<{
4
+ /** A seed claim is a few fields and one pinned recipe; the source itself
5
+ * arrived by `git push`. */
6
+ export declare const TEMPLATE_SEED_CLAIM_MAX_BYTES: number;
7
+ /** The operator's claim after pushing a projected source commit to the App's
8
+ * template: that `seedRevision` is that commit, projected from
9
+ * `sourceRevision`, and builds with `recipe`. */
10
+ export declare const templateSeedClaimSchema: Schema.Struct<{
7
11
  readonly appId: Schema.refine<import("@fungi.computer/puffball").AppId, Schema.String>;
8
12
  readonly sourceRevision: Schema.refine<import("@fungi.computer/puffball").SourceRevision, Schema.String>;
9
13
  readonly sourceTreeDigest: Schema.refine<import("@fungi.computer/puffball").Sha256Digest, Schema.String>;
14
+ readonly seedRevision: Schema.refine<import("@fungi.computer/puffball").SourceRevision, Schema.String>;
10
15
  readonly recipe: Schema.Struct<{
11
16
  readonly kind: Schema.optional<Schema.Literals<readonly ["bundled-html", "source-html"]>>;
12
17
  readonly bundle: Schema.Literal<true>;
@@ -33,24 +38,18 @@ export declare const templateWriteInputSchema: Schema.Struct<{
33
38
  }>]>;
34
39
  readonly target: Schema.Tuple<readonly [Schema.Literal<"es2022">]>;
35
40
  }>;
36
- readonly files: Schema.$Array<Schema.Struct<{
37
- readonly path: Schema.String;
38
- readonly contents: Schema.Uint8ArrayFromBase64;
39
- }>>;
40
41
  }>;
41
- export type TemplateWriteInput = Schema.Schema.Type<typeof templateWriteInputSchema>;
42
- /** The Publisher's own `exchangeGit` admission root (`source-git.ts`) stays the
43
- * sole caller of that physical transport; this port only ever sees a Request
44
- * in, a Response out, already bound to the one template repository. */
45
- export type TemplateSourceTransport = Readonly<{
46
- ensureRepository: Artifacts["ensureRepository"];
47
- exchangeTemplateGit(repository: string, request: Request): Promise<Response>;
48
- }>;
49
- /** The only operator template-write result contains provenance, never custody. */
50
- export declare function seedPublisherTemplate(receipts: PublisherTemplateSeedReceipts, transport: TemplateSourceTransport, rawAppId: string, request: Request): Promise<{
51
- readonly kind: "invalid" | "too-large";
52
- readonly receipt?: undefined;
53
- } | {
42
+ export type TemplateSeedClaim = Schema.Schema.Type<typeof templateSeedClaimSchema>;
43
+ /** Decode one claim body at its I/O boundary, strictly; null when it is not
44
+ * exactly a seed claim. */
45
+ export declare function parseTemplateSeedClaim(body: string): TemplateSeedClaim | null;
46
+ /**
47
+ * Record the seed receipt for a template the operator already pushed. The
48
+ * template's current `main` must be exactly the claimed seed commit, whose
49
+ * hash pins its tree and history; the recipe digest is computed here; an
50
+ * existing receipt for that commit must carry the same provenance.
51
+ */
52
+ export declare function recordTemplateSeed(receipts: PublisherTemplateSeedReceipts, artifacts: Pick<Artifacts, "readDefaultBranchHead">, rawAppId: string, claim: TemplateSeedClaim): Promise<{
54
53
  readonly kind: "conflict";
55
54
  readonly receipt?: undefined;
56
55
  } | {
@@ -0,0 +1,80 @@
1
+ import * as Option from "effect/Option";
2
+ import * as Schema from "effect/Schema";
3
+ import { parseAppId } from "@fungi.computer/puffball";
4
+ import { appIdSchema, sha256DigestSchema, sourceRevisionSchema, } from "@fungi.computer/puffball/effect";
5
+ import { capturedPinnedRecipeSchema, digestPinnedRecipe, } from "../build-contract.js";
6
+ import { templateRepositoryForApp } from "./template-repository.js";
7
+ /** A seed claim is a few fields and one pinned recipe; the source itself
8
+ * arrived by `git push`. */
9
+ export const TEMPLATE_SEED_CLAIM_MAX_BYTES = 64 * 1024;
10
+ /** The operator's claim after pushing a projected source commit to the App's
11
+ * template: that `seedRevision` is that commit, projected from
12
+ * `sourceRevision`, and builds with `recipe`. */
13
+ export const templateSeedClaimSchema = Schema.Struct({
14
+ appId: appIdSchema,
15
+ sourceRevision: sourceRevisionSchema,
16
+ sourceTreeDigest: sha256DigestSchema,
17
+ seedRevision: sourceRevisionSchema,
18
+ recipe: capturedPinnedRecipeSchema,
19
+ });
20
+ /** Decode one claim body at its I/O boundary, strictly; null when it is not
21
+ * exactly a seed claim. */
22
+ export function parseTemplateSeedClaim(body) {
23
+ const decoded = Schema.decodeUnknownOption(Schema.fromJsonString(templateSeedClaimSchema), { onExcessProperty: "error" })(body);
24
+ return Option.isSome(decoded) ? decoded.value : null;
25
+ }
26
+ function logEvent(operation, cause) {
27
+ console.error(JSON.stringify({
28
+ source: "app-source",
29
+ operation,
30
+ failureKind: cause instanceof Error ? "Error" : "NonErrorThrown",
31
+ }));
32
+ }
33
+ /**
34
+ * Record the seed receipt for a template the operator already pushed. The
35
+ * template's current `main` must be exactly the claimed seed commit, whose
36
+ * hash pins its tree and history; the recipe digest is computed here; an
37
+ * existing receipt for that commit must carry the same provenance.
38
+ */
39
+ export async function recordTemplateSeed(receipts, artifacts, rawAppId, claim) {
40
+ try {
41
+ const appId = parseAppId(rawAppId);
42
+ if (claim.appId !== appId)
43
+ return { kind: "conflict" };
44
+ const repository = templateRepositoryForApp(appId);
45
+ const head = await artifacts.readDefaultBranchHead(repository);
46
+ if (head === null || `git:${head}` !== claim.seedRevision)
47
+ return { kind: "conflict" };
48
+ const receipt = {
49
+ appId,
50
+ sourceRevision: claim.sourceRevision,
51
+ sourceTreeDigest: claim.sourceTreeDigest,
52
+ seedRevision: claim.seedRevision,
53
+ recipe: claim.recipe,
54
+ buildRecipeDigest: digestPinnedRecipe(claim.recipe),
55
+ };
56
+ const existing = await receipts.read(appId, claim.seedRevision);
57
+ if (existing !== null &&
58
+ (existing.sourceRevision !== receipt.sourceRevision ||
59
+ existing.sourceTreeDigest !== receipt.sourceTreeDigest ||
60
+ existing.buildRecipeDigest !== receipt.buildRecipeDigest))
61
+ return { kind: "conflict" };
62
+ // The write-once, digest-checked receipt owner records it.
63
+ await receipts.record(receipt);
64
+ return {
65
+ kind: existing === null ? "recorded" : "existing",
66
+ receipt: {
67
+ appId,
68
+ repository,
69
+ sourceRevision: receipt.sourceRevision,
70
+ sourceTreeDigest: receipt.sourceTreeDigest,
71
+ seedRevision: receipt.seedRevision,
72
+ buildRecipeDigest: receipt.buildRecipeDigest,
73
+ },
74
+ };
75
+ }
76
+ catch (cause) {
77
+ logEvent("operator.template.record", cause);
78
+ return { kind: "unavailable" };
79
+ }
80
+ }
@@ -0,0 +1,4 @@
1
+ import type { TenantScope } from "../tenant.js";
2
+ import type { StandaloneAgentsEnv } from "./agents.js";
3
+ /** Host-only lookup from the already selected frame tenant; never derive it from an App or caller. */
4
+ export declare function agentDirectoryForScope(env: Pick<StandaloneAgentsEnv, "AGENT_DIRECTORY">, scope: Pick<TenantScope, "durableName">): DurableObjectStub<import("./agents.js").AgentDirectory>;
@@ -0,0 +1,4 @@
1
+ /** Host-only lookup from the already selected frame tenant; never derive it from an App or caller. */
2
+ export function agentDirectoryForScope(env, scope) {
3
+ return env.AGENT_DIRECTORY.getByName(scope.durableName("agents"));
4
+ }
@@ -0,0 +1,227 @@
1
+ import { type StandaloneTenancyEnv } from "./tenancy.js";
2
+ import { DurableObject } from "cloudflare:workers";
3
+ import type { StandaloneComputerEnv } from "./computer.js";
4
+ import { ShiitakeDO } from "@fungi.computer/shiitake/cloudflare";
5
+ import type { CloudflareShiitakeOptions } from "@fungi.computer/shiitake/cloudflare";
6
+ import { type AiBinding } from "@earendil-works/pi-ai/api/cloudflare-ai-binding";
7
+ import * as Schema from "effect/Schema";
8
+ import { type FrameBindingProps } from "../cloudflare.js";
9
+ import type { Avatar } from "@fungi.computer/sprite/avatar";
10
+ import type { InvocationFrame } from "../invocation.js";
11
+ import type { OysterJson } from "../host.js";
12
+ import { type FleetMessage } from "@fungi.computer/shiitake/fleet";
13
+ import { type AppToolPolicy } from "../apps/index.js";
14
+ import { type OysterPrincipal, type OysterHost } from "../host.js";
15
+ import { type TenantScope, type TenantId } from "../tenant.js";
16
+ import type { ReadonlyTenancy } from "../tenancy/index.js";
17
+ import type { HostActor } from "@fungi.computer/puffball/app";
18
+ import type { OysterWorkerEnv } from "../workers.js";
19
+ import { type AgentMcpServer, type AgentMcpServerInput, type AgentMcpServerRefusal } from "../agents.js";
20
+ import type { AgentPolicyActor, OysterAgents, OysterSessionRow } from "../agents.js";
21
+ /** The model door an agent runs on; Shiitake's own contract. */
22
+ export type StandaloneModelSource = CloudflareShiitakeOptions["model"];
23
+ /** One directory entry: an agent's id, display name and own avatar. */
24
+ export type AgentEntry = Readonly<{
25
+ agentId: string;
26
+ name: string;
27
+ avatar: Avatar;
28
+ }>;
29
+ /** Bindings the agent host reads. */
30
+ export type StandaloneAgentsEnv = Readonly<{
31
+ /** Configured standalone-owner identity subject; never a stored member ID. */
32
+ OYSTER_OWNER_NAME?: string;
33
+ /** Gateway custody remains in Veil; only the host receives these bindings. */
34
+ AI_GATEWAY_AUTH_TOKEN?: string;
35
+ AI_GATEWAY_ACCOUNT_ID?: string;
36
+ AI_GATEWAY_ID?: string;
37
+ /** Workers AI, the default model door. */
38
+ AI?: AiBinding;
39
+ /** The model: `@fungi/luna` via AI Gateway, or a Workers AI id (default {@link defaultAgentModel}). */
40
+ OYSTER_AGENT_MODEL?: string;
41
+ /** The AI Gateway the binding routes through (default `default`). */
42
+ OYSTER_AI_GATEWAY?: string;
43
+ /** Maintained current-member authority; absent means no nondefault-tenant policy admission. */
44
+ TENANCY?: Pick<ReadonlyTenancy, "memberById">;
45
+ TENANTS?: StandaloneTenancyEnv["TENANTS"];
46
+ KEYS?: StandaloneTenancyEnv["KEYS"];
47
+ OYSTER_ORIGIN: string;
48
+ OYSTER_OWNER_KEY?: string;
49
+ AGENTS: DurableObjectNamespace<StandaloneAgentBase>;
50
+ /** Host-only directory custody. Guest entrypoints expose operations, not this binding; Codemode receives only Mycelium modules. */
51
+ AGENT_DIRECTORY: DurableObjectNamespace<AgentDirectory>;
52
+ }>;
53
+ /** An agent host's whole env: its own bindings and the Oyster's. */
54
+ export type StandaloneAgentEnv = StandaloneAgentsEnv & OysterWorkerEnv & StandaloneComputerEnv;
55
+ declare const policyDirectorySchema: Schema.Struct<{
56
+ readonly directoryName: Schema.NonEmptyString;
57
+ readonly tenantId: Schema.brand<Schema.String, "OysterTenantId">;
58
+ }>;
59
+ /** A row of this agent's own inbox, before the directory names the agent. */
60
+ export type AgentInboxRows = Readonly<{
61
+ rows: readonly OysterSessionRow[];
62
+ revision: number;
63
+ }>;
64
+ /** One agent: Shiitake's Cloudflare host over this Durable Object; its root supplies `hostFor`. */
65
+ export declare abstract class StandaloneAgentBase<Env extends StandaloneAgentEnv = StandaloneAgentEnv> extends ShiitakeDO<Env> {
66
+ #private;
67
+ /** The deployable root's host for this Agent's pinned tenant. */
68
+ protected abstract hostFor(scope: TenantScope): OysterHost;
69
+ /** The root host for this Agent's pinned policy directory; unpinned Agents have no App tools. */
70
+ private appHost;
71
+ /** Choose this agent's model door; override to plug in another. */
72
+ protected modelSource(): StandaloneModelSource;
73
+ /** Host-only initialization; Agent/App transports never receive this directory binding. */
74
+ usePolicyDirectory(input: {
75
+ directoryName: string;
76
+ tenantId: TenantId;
77
+ }): Promise<void>;
78
+ /** Host-only policy pin read, used before an owner can approve tools. */
79
+ policyDirectory(): Promise<typeof policyDirectorySchema.Type | null>;
80
+ /** Listeners from live codemode acquisitions, told when the server list changes. */
81
+ private readonly mcpInvalidations;
82
+ private storedMcpServers;
83
+ private writeMcpServers;
84
+ /** Host-only: this Agent's MCP servers, without their bearer tokens. */
85
+ mcpServers(): Promise<readonly AgentMcpServer[]>;
86
+ /** Host-only: connect one remote MCP server, admitted by the owner route. */
87
+ addMcpServer(input: AgentMcpServerInput): Promise<AgentMcpServer | AgentMcpServerRefusal>;
88
+ /** Host-only: disconnect one MCP server. */
89
+ removeMcpServer(serverId: string): Promise<boolean>;
90
+ protected shiitakeOptions(): CloudflareShiitakeOptions;
91
+ /** Send an installation-admitted message through fleet Session operations. */
92
+ sendFromApp(installationId: string, sessionId: string, message: FleetMessage): Promise<{
93
+ sessionId: string;
94
+ status: "accepted";
95
+ commandId: string;
96
+ }>;
97
+ /** Latest inbox clock from the same Shiitake owner, without reading Session rows. */
98
+ revision(): Promise<number>;
99
+ /** Every live Session of this agent, as inbox rows. */
100
+ inboxRows(): Promise<AgentInboxRows>;
101
+ }
102
+ /** Names this Oyster's agents and remembers which Sessions the member opened. */
103
+ export declare class AgentDirectory extends DurableObject<StandaloneAgentEnv> {
104
+ private readonly directory;
105
+ private readonly avatars;
106
+ private composedTenancy;
107
+ private grant;
108
+ constructor(ctx: DurableObjectState, env: StandaloneAgentEnv);
109
+ list(): readonly AgentEntry[];
110
+ has(agentId: string): boolean;
111
+ /** Resolve the existing recorded creator for host-side owner admission. */
112
+ readCreator(agentId: string): string | null;
113
+ /** Owner-only rename; refuses a name already held by another Agent. */
114
+ rename(agentId: string, name: string, principal: OysterPrincipal): AgentEntry | Readonly<{
115
+ error: "not_found" | "name_taken" | "not_granted";
116
+ }>;
117
+ /** Owner-only avatar change; the Agent wears it in every App and list. */
118
+ setAvatar(agentId: string, avatar: Avatar, principal: OysterPrincipal): AgentEntry | Readonly<{
119
+ error: "not_found" | "not_granted";
120
+ }>;
121
+ create(name: string, requestId: string, principal: OysterPrincipal, actor: AgentPolicyActor, avatar?: Avatar): AgentEntry & {
122
+ policyRevision: string | null;
123
+ };
124
+ private writePolicy;
125
+ private storedPolicy;
126
+ private readPolicy;
127
+ /** Current revision for the direct owner's compare-and-swap request. */
128
+ policyRevision(agentId: string): string | null;
129
+ /** Whether this Agent's App-tool policy still awaits the owner's approval. */
130
+ policyStatus(agentId: string): Readonly<{
131
+ status: "pending" | "approved";
132
+ revision: string;
133
+ }> | null;
134
+ private currentMember;
135
+ /** Approval stays revoked after any membership authority revision. */
136
+ appToolPolicy(actor: Extract<HostActor, {
137
+ kind: "agent";
138
+ }>, tenantId?: TenantId): Promise<AppToolPolicy | null>;
139
+ /** Only direct human approval records an authenticated canonical grantor. */
140
+ approveAppToolPolicy(agentId: string, principal: OysterPrincipal, level: AppToolPolicy["level"], expectedRevision: string | null, actor: AgentPolicyActor, tenantId: TenantId): Promise<Awaited<ReturnType<OysterAgents["approveAppToolPolicy"]>>>;
141
+ visit(agentId: string, sessionId: string): void;
142
+ visits(): readonly Readonly<{
143
+ agentId: string;
144
+ sessionId: string;
145
+ visitedAt: number;
146
+ }>[];
147
+ /** Grows on every directory change (a new agent, a visit). */
148
+ revision(): number;
149
+ private bump;
150
+ }
151
+ /** The `agents` adapter over a Worker's agent bindings. */
152
+ export declare function standaloneAgents(env: StandaloneAgentsEnv, scope?: TenantScope): OysterAgents;
153
+ /** Shared packet codecs; the Instance alone admits the current frame's operations. */
154
+ export declare const agentFrameMethods: {
155
+ INBOX: {
156
+ page: {
157
+ decodeArgs: (raw: readonly unknown[]) => [({
158
+ after?: string | null | undefined;
159
+ } | undefined)?];
160
+ decodeResult: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => {
161
+ readonly items: readonly ({
162
+ readonly agentId: string;
163
+ readonly title: string;
164
+ readonly state: "done" | "running" | "needs_you" | "errored";
165
+ readonly createdAt: number;
166
+ readonly sessionId: string;
167
+ readonly lastActivityAt: number;
168
+ readonly triggerId?: string | undefined;
169
+ readonly settledAt?: number | undefined;
170
+ readonly visitedAt?: number | undefined;
171
+ } | {
172
+ readonly agentId: string;
173
+ readonly sessionId: string;
174
+ readonly state: "unavailable";
175
+ } | {
176
+ readonly key: {
177
+ readonly runId: string;
178
+ readonly nodeId: string;
179
+ readonly iteration: string;
180
+ };
181
+ readonly kind: "workflow-review";
182
+ readonly installationId: string;
183
+ readonly state: "pending" | "delivered" | "answered" | "timed_out" | "closed";
184
+ readonly workflowId: string | null;
185
+ readonly workflowTitle: string;
186
+ readonly reviewTitle: string;
187
+ readonly reviewers: "runner" | "owners";
188
+ readonly show: readonly Schema.Json[];
189
+ readonly openedAt: number;
190
+ readonly expiresAt: number;
191
+ readonly choices?: readonly string[] | undefined;
192
+ })[];
193
+ readonly next: null;
194
+ readonly cursor: number;
195
+ };
196
+ requiredOperations: () => string[];
197
+ };
198
+ visit: {
199
+ decodeArgs: (raw: readonly unknown[]) => [{
200
+ agentId: string;
201
+ sessionId: string;
202
+ }];
203
+ decodeResult: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => {
204
+ readonly visited: true;
205
+ };
206
+ requiredOperations: () => string[];
207
+ };
208
+ };
209
+ AGENTS: {
210
+ send: {
211
+ decodeArgs: (raw: readonly unknown[]) => [{
212
+ agentId: string;
213
+ sessionId: string;
214
+ text: string;
215
+ delivery: "followUp";
216
+ }];
217
+ decodeResult: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => Readonly<{
218
+ status: "accepted";
219
+ commandId: string;
220
+ }>;
221
+ requiredOperations: () => string[];
222
+ };
223
+ };
224
+ };
225
+ /** Provider effects are admitted by the originating Instance's live frame. */
226
+ export declare function dispatchAgentBinding(env: StandaloneAgentsEnv, scope: TenantScope, frame: InvocationFrame, props: FrameBindingProps, method: string, args: readonly unknown[]): Promise<OysterJson>;
227
+ export {};