@fungi.computer/agar 0.0.0-stage → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +766 -2
  3. package/dist/agent-message-meta.d.ts +74 -0
  4. package/dist/agent-message-meta.js +63 -0
  5. package/dist/agents.d.ts +170 -0
  6. package/dist/agents.js +218 -0
  7. package/dist/app-build-contract.d.ts +43 -0
  8. package/dist/app-build-contract.js +24 -0
  9. package/dist/apps.d.ts +66 -0
  10. package/dist/apps.js +47 -0
  11. package/dist/backend-declaration.d.ts +6 -0
  12. package/dist/backend-declaration.js +65 -0
  13. package/dist/backend-definition.d.ts +88 -0
  14. package/dist/backend-definition.js +158 -0
  15. package/dist/backend-refusal.d.ts +10 -0
  16. package/dist/backend-refusal.js +10 -0
  17. package/dist/backend.d.ts +30 -0
  18. package/dist/backend.js +28 -0
  19. package/dist/capabilities.d.ts +30 -0
  20. package/dist/capabilities.js +50 -0
  21. package/dist/channels.d.ts +142 -0
  22. package/dist/channels.js +97 -0
  23. package/dist/commands.d.ts +33 -0
  24. package/dist/commands.js +46 -0
  25. package/dist/computer-contracts.d.ts +73 -0
  26. package/dist/computer-contracts.js +57 -0
  27. package/dist/computer.d.ts +294 -0
  28. package/dist/computer.js +319 -0
  29. package/dist/connect.d.ts +11 -0
  30. package/dist/connect.js +211 -0
  31. package/dist/errors.d.ts +34 -0
  32. package/dist/errors.js +106 -0
  33. package/dist/failure.d.ts +2 -0
  34. package/dist/failure.js +9 -0
  35. package/dist/files.d.ts +25 -0
  36. package/dist/files.js +84 -0
  37. package/dist/game-servers.d.ts +530 -0
  38. package/dist/game-servers.js +244 -0
  39. package/dist/gills.d.ts +43 -0
  40. package/dist/gills.js +174 -0
  41. package/dist/git-commit.d.ts +143 -0
  42. package/dist/git-commit.js +251 -0
  43. package/dist/git-rpc.d.ts +310 -0
  44. package/dist/git-rpc.js +166 -0
  45. package/dist/git-steps.d.ts +579 -0
  46. package/dist/git-steps.js +804 -0
  47. package/dist/git-wire.d.ts +33 -0
  48. package/dist/git-wire.js +240 -0
  49. package/dist/git.d.ts +322 -0
  50. package/dist/git.js +98 -0
  51. package/dist/host.d.ts +97 -0
  52. package/dist/host.js +732 -0
  53. package/dist/inbox.d.ts +99 -0
  54. package/dist/inbox.js +184 -0
  55. package/dist/index.d.ts +31 -0
  56. package/dist/index.js +15 -0
  57. package/dist/inference.d.ts +17 -0
  58. package/dist/inference.js +115 -0
  59. package/dist/internal/app-error.d.ts +8 -0
  60. package/dist/internal/app-error.js +36 -0
  61. package/dist/internal/app-session.d.ts +8 -0
  62. package/dist/internal/app-session.js +551 -0
  63. package/dist/internal/back-inputs.d.ts +22 -0
  64. package/dist/internal/back-inputs.js +93 -0
  65. package/dist/internal/browser-bridge.d.ts +30 -0
  66. package/dist/internal/browser-bridge.js +203 -0
  67. package/dist/internal/capabilities.d.ts +13 -0
  68. package/dist/internal/capabilities.js +138 -0
  69. package/dist/internal/commands.d.ts +9 -0
  70. package/dist/internal/commands.js +257 -0
  71. package/dist/internal/disconnect-host.d.ts +7 -0
  72. package/dist/internal/disconnect-host.js +19 -0
  73. package/dist/internal/git-edit-tree.d.ts +6 -0
  74. package/dist/internal/git-edit-tree.js +46 -0
  75. package/dist/internal/memory-fs.d.ts +39 -0
  76. package/dist/internal/memory-fs.js +154 -0
  77. package/dist/internal/protocol.d.ts +116 -0
  78. package/dist/internal/protocol.js +6 -0
  79. package/dist/internal/query-feed.d.ts +23 -0
  80. package/dist/internal/query-feed.js +111 -0
  81. package/dist/internal/result.d.ts +22 -0
  82. package/dist/internal/result.js +61 -0
  83. package/dist/internal/rpc-contract.d.ts +24 -0
  84. package/dist/internal/rpc-contract.js +78 -0
  85. package/dist/internal/schemas.d.ts +307 -0
  86. package/dist/internal/schemas.js +524 -0
  87. package/dist/internal/store.d.ts +8 -0
  88. package/dist/internal/store.js +30 -0
  89. package/dist/internal/terminal-gate.d.ts +17 -0
  90. package/dist/internal/terminal-gate.js +94 -0
  91. package/dist/lifecycle.d.ts +44 -0
  92. package/dist/lifecycle.js +1 -0
  93. package/dist/marketplace.d.ts +159 -0
  94. package/dist/marketplace.js +102 -0
  95. package/dist/media-json.d.ts +181 -0
  96. package/dist/media-json.js +204 -0
  97. package/dist/media-library.d.ts +11 -0
  98. package/dist/media-library.js +15 -0
  99. package/dist/media-request-id.d.ts +7 -0
  100. package/dist/media-request-id.js +16 -0
  101. package/dist/media.d.ts +354 -0
  102. package/dist/media.js +654 -0
  103. package/dist/method-refusal.d.ts +64 -0
  104. package/dist/method-refusal.js +48 -0
  105. package/dist/pending.d.ts +10 -0
  106. package/dist/pending.js +10 -0
  107. package/dist/query.d.ts +76 -0
  108. package/dist/query.js +195 -0
  109. package/dist/react-mount.d.ts +23 -0
  110. package/dist/react-mount.js +108 -0
  111. package/dist/react.d.ts +41 -0
  112. package/dist/react.js +111 -0
  113. package/dist/read-copy.d.ts +103 -0
  114. package/dist/read-copy.js +195 -0
  115. package/dist/roles/files.d.ts +295 -0
  116. package/dist/roles/files.js +184 -0
  117. package/dist/root.d.ts +21 -0
  118. package/dist/root.js +1 -0
  119. package/dist/rpc.d.ts +1 -0
  120. package/dist/rpc.js +1 -0
  121. package/dist/status.d.ts +17 -0
  122. package/dist/status.js +50 -0
  123. package/dist/storage-wire.d.ts +167 -0
  124. package/dist/storage-wire.js +115 -0
  125. package/dist/storage.d.ts +72 -0
  126. package/dist/storage.js +143 -0
  127. package/dist/store.d.ts +11 -0
  128. package/dist/store.js +1 -0
  129. package/dist/test-kit/git.d.ts +31 -0
  130. package/dist/test-kit/git.js +174 -0
  131. package/dist/ui.d.ts +169 -0
  132. package/dist/ui.js +10 -0
  133. package/dist/window.d.ts +41 -0
  134. package/dist/window.js +15 -0
  135. package/dist/world.d.ts +16 -0
  136. package/dist/world.js +17 -0
  137. package/docs/api/README.md +28 -0
  138. package/docs/api/app-build-contract.md +113 -0
  139. package/docs/api/backend-declaration.md +62 -0
  140. package/docs/api/backend-definition.md +215 -0
  141. package/docs/api/backend.md +269 -0
  142. package/docs/api/errors.md +169 -0
  143. package/docs/api/files.md +91 -0
  144. package/docs/api/gills.md +164 -0
  145. package/docs/api/git-commit.md +279 -0
  146. package/docs/api/git-rpc.md +243 -0
  147. package/docs/api/git-steps.md +1010 -0
  148. package/docs/api/git-wire.md +197 -0
  149. package/docs/api/git.md +579 -0
  150. package/docs/api/index.md +2880 -0
  151. package/docs/api/media-json.md +507 -0
  152. package/docs/api/media-request-id.md +32 -0
  153. package/docs/api/modules.md +32 -0
  154. package/docs/api/query.md +253 -0
  155. package/docs/api/react.md +216 -0
  156. package/docs/api/read-copy.md +204 -0
  157. package/docs/api/rpc.md +128 -0
  158. package/docs/api/storage.md +620 -0
  159. package/package.json +197 -3
@@ -0,0 +1,804 @@
1
+ /** Resumable, credential-free Git writes. Each step performs at most one exchange. */
2
+ import { Buffer } from "buffer";
3
+ import * as git from "isomorphic-git";
4
+ import * as z from "zod";
5
+ import { gitRepositoryNameSchema, gitSha1Schema, } from "@fungi.computer/puffball/git-representation";
6
+ import { acceptedRefs, advertisedRefs, advertisedCapabilities, pktLine, sideBandData, } from "./git-wire.js";
7
+ import { memoryFs } from "./internal/memory-fs.js";
8
+ import { editTree } from "./internal/git-edit-tree.js";
9
+ const DIR = "/repository";
10
+ const ZERO = "0".repeat(40);
11
+ const ROW_BYTES = 1024 * 1024;
12
+ const COMMIT_BYTES = 8 * ROW_BYTES;
13
+ const MERGE_BYTES = 16 * ROW_BYTES;
14
+ const requestKeySchema = z.string().min(1).max(256);
15
+ const materialOwnerSchema = z.tuple([
16
+ z.enum(["commit", "merge"]),
17
+ requestKeySchema,
18
+ ]);
19
+ function checkedOwnerPrefix(prefix) {
20
+ try {
21
+ if (!prefix.startsWith("git:") || !prefix.endsWith(":"))
22
+ throw new RangeError("Invalid Git material owner");
23
+ const owner = materialOwnerSchema.parse(JSON.parse(prefix.slice(4, -1)));
24
+ if (prefix !== `git:${JSON.stringify(owner)}:`)
25
+ throw new RangeError("Invalid Git material owner");
26
+ return prefix;
27
+ }
28
+ catch {
29
+ throw new RangeError("Invalid Git material owner");
30
+ }
31
+ }
32
+ const encoder = new TextEncoder();
33
+ if (!("Buffer" in globalThis))
34
+ Reflect.set(globalThis, "Buffer", Buffer);
35
+ /** Permanent continuation failure: background owners settle their lane instead of retrying it. */
36
+ export class GitStepMaterialError extends Error {
37
+ /** Invalid material, unavailable atomic support, or an exhausted unchanged-head push refusal. */
38
+ code;
39
+ /** Construct the canonical permanent continuation error.
40
+ * @param code - Permanent continuation failure reason.
41
+ */
42
+ constructor(code) {
43
+ super({
44
+ unavailable: "Git material unavailable",
45
+ conflict: "Git material key conflict",
46
+ digest_mismatch: "Git material digest mismatch",
47
+ atomic_unavailable: "Atomic Git push unavailable",
48
+ push_refused: "Git push refused",
49
+ }[code]);
50
+ this.name = "GitStepMaterialError";
51
+ this.code = code;
52
+ }
53
+ }
54
+ const materialRow = z.strictObject({
55
+ seq: z.number().int().nonnegative(),
56
+ bytes: z.instanceof(ArrayBuffer).transform((bytes) => new Uint8Array(bytes)),
57
+ });
58
+ /** One-MiB material rows; exact replays are idempotent, conflicting material refuses. */
59
+ export function sqliteGitStepMaterial(sql) {
60
+ sql.exec("CREATE TABLE IF NOT EXISTS git_step_material (key TEXT NOT NULL, seq INTEGER NOT NULL, bytes BLOB NOT NULL CHECK(length(bytes)<=1048576), PRIMARY KEY(key,seq))");
61
+ return {
62
+ put(key, seq, bytes) {
63
+ return Promise.resolve().then(() => {
64
+ if (bytes.length > ROW_BYTES ||
65
+ !Number.isSafeInteger(seq) ||
66
+ seq < 0 ||
67
+ seq >= MERGE_BYTES / ROW_BYTES)
68
+ throw new RangeError("Invalid Git material row");
69
+ sql.exec("INSERT INTO git_step_material(key,seq,bytes) VALUES(?,?,?) ON CONFLICT(key,seq) DO NOTHING", key, seq, new Uint8Array(bytes).buffer);
70
+ const row = sql
71
+ .exec("SELECT seq,bytes FROM git_step_material WHERE key=? AND seq=?", key, seq)
72
+ .toArray()[0];
73
+ const stored = materialRow.parse(row);
74
+ if (!Buffer.from(stored.bytes).equals(Buffer.from(bytes)))
75
+ throw new GitStepMaterialError("conflict");
76
+ });
77
+ },
78
+ read(key) {
79
+ return Promise.resolve().then(() => {
80
+ const rows = sql
81
+ .exec("SELECT seq,bytes FROM git_step_material WHERE key=? ORDER BY seq LIMIT 17", key)
82
+ .toArray()
83
+ .map((row) => materialRow.parse(row));
84
+ if (rows.length === 0 ||
85
+ rows.length > MERGE_BYTES / ROW_BYTES ||
86
+ rows.some((row, index) => row.seq !== index))
87
+ throw new GitStepMaterialError("unavailable");
88
+ return new Uint8Array(Buffer.concat(rows.map((row) => row.bytes)));
89
+ });
90
+ },
91
+ drop(prefix) {
92
+ return Promise.resolve().then(() => {
93
+ checkedOwnerPrefix(prefix);
94
+ sql.exec("DELETE FROM git_step_material WHERE substr(key,1,length(?))=?", prefix, prefix);
95
+ });
96
+ },
97
+ };
98
+ }
99
+ /** Volatile foreground driver material; background callers use the facet owner. */
100
+ export function memoryGitStepMaterial() {
101
+ const rows = new Map();
102
+ return {
103
+ put(key, seq, bytes) {
104
+ return Promise.resolve().then(() => {
105
+ if (bytes.length > ROW_BYTES ||
106
+ !Number.isSafeInteger(seq) ||
107
+ seq < 0 ||
108
+ seq >= MERGE_BYTES / ROW_BYTES)
109
+ throw new RangeError("Invalid Git material row");
110
+ let values = rows.get(key);
111
+ if (values === undefined) {
112
+ values = new Map();
113
+ rows.set(key, values);
114
+ }
115
+ const previous = values.get(seq);
116
+ if (previous !== undefined &&
117
+ !Buffer.from(previous).equals(Buffer.from(bytes)))
118
+ throw new GitStepMaterialError("conflict");
119
+ values.set(seq, new Uint8Array(bytes));
120
+ });
121
+ },
122
+ read(key) {
123
+ return Promise.resolve().then(() => {
124
+ const values = rows.get(key);
125
+ if (values === undefined || values.size === 0)
126
+ throw new GitStepMaterialError("unavailable");
127
+ const parts = [...values].sort(([a], [b]) => a - b);
128
+ if (parts.some(([seq], index) => seq !== index))
129
+ throw new GitStepMaterialError("unavailable");
130
+ return new Uint8Array(Buffer.concat(parts.map(([, bytes]) => bytes)));
131
+ });
132
+ },
133
+ drop(prefix) {
134
+ return Promise.resolve().then(() => {
135
+ checkedOwnerPrefix(prefix);
136
+ for (const key of rows.keys())
137
+ if (key.startsWith(prefix))
138
+ rows.delete(key);
139
+ });
140
+ },
141
+ };
142
+ }
143
+ function ownerPrefix(step) {
144
+ return `git:${JSON.stringify([step.operation.kind, step.key])}:`;
145
+ }
146
+ function changesKey(step) {
147
+ return `${ownerPrefix(step)}changes:${step.operation.changesDigest}`;
148
+ }
149
+ function sourceKey(step) {
150
+ return `${ownerPrefix(step)}source:${step.sourceDigest}`;
151
+ }
152
+ function packKey(step) {
153
+ return `${ownerPrefix(step)}pack:${step.built}:${step.packDigest}`;
154
+ }
155
+ async function restore(material, key, digest, limit) {
156
+ const bytes = new Uint8Array(await material.read(key));
157
+ if (bytes.length > limit)
158
+ throw new RangeError("Git material too large");
159
+ if ((await materialDigest(bytes)) !== digest)
160
+ throw new GitStepMaterialError("digest_mismatch");
161
+ return bytes;
162
+ }
163
+ async function materialDigest(bytes) {
164
+ return Buffer.from(await globalThis.crypto.subtle.digest("SHA-256", new Uint8Array(bytes).buffer)).toString("hex");
165
+ }
166
+ async function retain(material, key, bytes) {
167
+ for (let offset = 0, seq = 0; offset < bytes.length || seq === 0; offset += ROW_BYTES, seq += 1) {
168
+ await material.put(key, seq, bytes.subarray(offset, offset + ROW_BYTES));
169
+ }
170
+ }
171
+ const identitySchema = z.strictObject({ name: z.string(), email: z.string() });
172
+ const commitRequest = z
173
+ .strictObject({
174
+ repository: gitRepositoryNameSchema,
175
+ branch: z.string().min(1),
176
+ expectedHead: gitSha1Schema.nullable(),
177
+ startFrom: z.string().min(1).optional(),
178
+ message: z.string(),
179
+ author: identitySchema,
180
+ })
181
+ .refine((request) => request.startFrom === undefined || request.expectedHead === null, { message: "startFrom requires an empty branch", path: ["startFrom"] });
182
+ const mergeRequest = z.strictObject({
183
+ target: gitRepositoryNameSchema,
184
+ source: gitRepositoryNameSchema,
185
+ branch: z.string().min(1),
186
+ sourceBranch: z.string().min(1),
187
+ expectedSource: gitSha1Schema,
188
+ expectedTarget: gitSha1Schema.optional(),
189
+ behind: z.number().int().nonnegative().optional(),
190
+ message: z.string(),
191
+ author: identitySchema,
192
+ });
193
+ const operation = z.discriminatedUnion("kind", [
194
+ z.strictObject({
195
+ kind: z.literal("commit"),
196
+ request: commitRequest,
197
+ changesDigest: z.hash("sha256"),
198
+ tags: z.array(z.strictObject({
199
+ ref: z.string().startsWith("refs/tags/"),
200
+ expected: gitSha1Schema.nullable(),
201
+ })),
202
+ }),
203
+ z.strictObject({ kind: z.literal("merge"), request: mergeRequest }),
204
+ ]);
205
+ const common = {
206
+ key: requestKeySchema,
207
+ timestamp: z.number().int().nonnegative(),
208
+ operation,
209
+ };
210
+ const built = {
211
+ ...common,
212
+ old: gitSha1Schema.nullable(),
213
+ built: gitSha1Schema,
214
+ packDigest: z.hash("sha256"),
215
+ attempt: z.number().int().min(0).max(3),
216
+ pushBefore: gitSha1Schema.nullable().optional(),
217
+ atomic: z.literal(true).optional(),
218
+ };
219
+ /** Strict, JSON-only continuation grammar; material and authority are not stored in it. */
220
+ export const gitStepSchema = z.discriminatedUnion("phase", [
221
+ z.strictObject({
222
+ ...common,
223
+ phase: z.literal("fetch-base"),
224
+ base: gitSha1Schema.nullable(),
225
+ }),
226
+ z.strictObject({ ...common, phase: z.literal("too-large") }),
227
+ z.strictObject({ ...common, phase: z.literal("inspect-base") }),
228
+ z.strictObject({ ...common, phase: z.literal("inspect-source") }),
229
+ z.strictObject({
230
+ ...common,
231
+ phase: z.literal("fetch-source"),
232
+ target: gitSha1Schema.optional(),
233
+ }),
234
+ z.strictObject({
235
+ ...common,
236
+ phase: z.literal("inspect-target"),
237
+ sourceDigest: z.hash("sha256"),
238
+ }),
239
+ z.strictObject({
240
+ ...common,
241
+ phase: z.literal("fetch-target"),
242
+ sourceDigest: z.hash("sha256"),
243
+ target: gitSha1Schema,
244
+ }),
245
+ z.strictObject({ ...built, phase: z.literal("inspect-merge-push") }),
246
+ z.strictObject({ ...built, phase: z.literal("inspect-atomic") }),
247
+ z.strictObject({ ...built, phase: z.literal("push") }),
248
+ z.strictObject({ ...built, phase: z.literal("confirm") }),
249
+ z.strictObject({
250
+ ...built,
251
+ phase: z.literal("confirm-ancestry"),
252
+ head: gitSha1Schema,
253
+ }),
254
+ ]);
255
+ const changesSchema = z.array(z.strictObject({
256
+ path: z.string().min(1).max(4096),
257
+ content: z.base64().nullable(),
258
+ mode: z.enum(["100644", "100755"]).optional(),
259
+ }));
260
+ function checkedPath(path) {
261
+ if (path.length === 0 ||
262
+ path.length > 4096 ||
263
+ path.split("/").some((part) => part === "" || part === "." || part === ".."))
264
+ throw new RangeError("Invalid repository path");
265
+ return path;
266
+ }
267
+ /** Prepare immutable request material before atomically enqueueing the first checkpoint. */
268
+ export async function begin(input, material) {
269
+ const key = input.requestKey;
270
+ const timestamp = input.timestamp ?? Math.floor(Date.now() / 1000);
271
+ if (input.kind === "merge")
272
+ return gitStepSchema.parse({
273
+ key,
274
+ timestamp,
275
+ operation: { kind: "merge", request: input.request },
276
+ phase: "inspect-source",
277
+ });
278
+ const { changes, ...request } = input.request;
279
+ const bytes = encoder.encode(JSON.stringify(changesSchema.parse(changes.map((change) => ({
280
+ ...change,
281
+ path: checkedPath(change.path),
282
+ content: change.content === null
283
+ ? null
284
+ : Buffer.from(change.content).toString("base64"),
285
+ })))));
286
+ const changesDigest = await materialDigest(bytes);
287
+ const checkpoint = gitStepSchema.parse({
288
+ key,
289
+ timestamp,
290
+ operation: {
291
+ kind: "commit",
292
+ request,
293
+ changesDigest,
294
+ tags: input.tags ?? [],
295
+ },
296
+ phase: "inspect-base",
297
+ });
298
+ if (bytes.length > COMMIT_BYTES)
299
+ return gitStepSchema.parse({
300
+ key,
301
+ timestamp,
302
+ operation: checkpoint.operation,
303
+ phase: "too-large",
304
+ });
305
+ await retain(material, changesKey({ ...checkpoint, operation: commitOnly(checkpoint) }), bytes);
306
+ return checkpoint;
307
+ }
308
+ async function freshFs() {
309
+ const fs = memoryFs();
310
+ await fs.promises.mkdir(DIR);
311
+ await git.init({ fs, dir: DIR });
312
+ return fs;
313
+ }
314
+ async function index(fs, pack, name) {
315
+ const filepath = `.git/objects/pack/${name}.pack`;
316
+ await fs.promises.writeFile(`${DIR}/${filepath}`, pack);
317
+ return (await git.indexPack({ fs, dir: DIR, filepath })).oids;
318
+ }
319
+ async function looseObjects(fs) {
320
+ const result = [];
321
+ for (const prefix of await fs.promises.readdir(`${DIR}/.git/objects`)) {
322
+ if (!/^[0-9a-f]{2}$/u.test(prefix))
323
+ continue;
324
+ for (const suffix of await fs.promises.readdir(`${DIR}/.git/objects/${prefix}`))
325
+ if (/^[0-9a-f]{38}$/u.test(suffix))
326
+ result.push(prefix + suffix);
327
+ }
328
+ return result;
329
+ }
330
+ async function fetchPack(store, repo, wants, shallow) {
331
+ const body = wants
332
+ .map((oid, index) => pktLine(`want ${oid}${index === 0 ? " side-band-64k ofs-delta no-progress" : ""}\n`))
333
+ .join("") +
334
+ (shallow ? pktLine("deepen 1\n") : "") +
335
+ "0000" +
336
+ pktLine("done\n");
337
+ return store.exchange(repo, {
338
+ service: "upload-pack",
339
+ phase: "exchange",
340
+ headers: { "content-type": "application/x-git-upload-pack-request" },
341
+ body: encoder.encode(body),
342
+ });
343
+ }
344
+ function packResponse(response) {
345
+ if (response.status !== 200)
346
+ throw new Error("Git fetch unavailable");
347
+ const pack = sideBandData(response.body);
348
+ if (pack.length === 0)
349
+ throw new Error("Git fetch returned no pack");
350
+ return pack;
351
+ }
352
+ async function refHeads(store, repo) {
353
+ const response = await store.exchange(repo, {
354
+ service: "upload-pack",
355
+ phase: "advertise",
356
+ body: null,
357
+ headers: {},
358
+ });
359
+ if (response.status !== 200)
360
+ throw new Error("Git advertisement unavailable");
361
+ return advertisedRefs(response.body);
362
+ }
363
+ function commitOnly(step) {
364
+ if (step.operation.kind !== "commit")
365
+ throw new Error("Invalid commit checkpoint");
366
+ return step.operation;
367
+ }
368
+ function mergeOnly(step) {
369
+ if (step.operation.kind !== "merge")
370
+ throw new Error("Invalid merge checkpoint");
371
+ return step.operation.request;
372
+ }
373
+ async function preparePush(step, fs, old, oid, oids, ports) {
374
+ const { packfile } = await git.packObjects({
375
+ fs,
376
+ dir: DIR,
377
+ oids: [...new Set(oids)],
378
+ });
379
+ if (packfile === undefined)
380
+ throw new Error("Git pack unavailable");
381
+ if (packfile.length > COMMIT_BYTES)
382
+ return { done: { kind: "too_large" } };
383
+ const packDigest = await materialDigest(packfile);
384
+ const materialKey = packKey({ ...step, built: oid, packDigest });
385
+ await retain(ports.material, materialKey, packfile);
386
+ return {
387
+ next: gitStepSchema.parse({
388
+ key: step.key,
389
+ timestamp: step.timestamp,
390
+ operation: step.operation,
391
+ phase: step.operation.kind === "merge"
392
+ ? "inspect-merge-push"
393
+ : step.operation.tags.length > 0
394
+ ? "inspect-atomic"
395
+ : "push",
396
+ old,
397
+ built: oid,
398
+ packDigest,
399
+ attempt: 0,
400
+ }),
401
+ };
402
+ }
403
+ async function buildCommit(step, fs, base, ports) {
404
+ const op = commitOnly(step);
405
+ const changes = changesSchema
406
+ .parse(JSON.parse(new TextDecoder().decode(await restore(ports.material, changesKey({ ...step, operation: op }), op.changesDigest, COMMIT_BYTES))))
407
+ .map((change) => {
408
+ const restored = {
409
+ path: change.path,
410
+ content: change.content === null
411
+ ? null
412
+ : new Uint8Array(Buffer.from(change.content, "base64")),
413
+ parts: checkedPath(change.path).split("/"),
414
+ };
415
+ if (change.mode === undefined)
416
+ return restored;
417
+ return { ...restored, mode: change.mode };
418
+ });
419
+ const baseTree = base === null
420
+ ? null
421
+ : (await git.readCommit({ fs, dir: DIR, oid: base })).commit.tree;
422
+ const tree = await editTree(fs, baseTree, changes);
423
+ if (base !== null && tree === baseTree) {
424
+ if (op.tags.length === 0)
425
+ return { done: { kind: "unchanged", commit: base } };
426
+ return preparePush(step, fs, op.request.expectedHead, base, await looseObjects(fs), ports);
427
+ }
428
+ const identity = {
429
+ ...op.request.author,
430
+ timestamp: step.timestamp,
431
+ timezoneOffset: 0,
432
+ };
433
+ const oid = await git.commit({
434
+ fs,
435
+ dir: DIR,
436
+ message: op.request.message,
437
+ author: identity,
438
+ committer: identity,
439
+ tree: tree ?? (await git.writeTree({ fs, dir: DIR, tree: [] })),
440
+ parent: base === null ? [] : [base],
441
+ ref: `refs/heads/${op.request.branch}`,
442
+ });
443
+ return preparePush(step, fs, op.request.expectedHead, oid, await looseObjects(fs), ports);
444
+ }
445
+ async function buildMerge(step, fs, target, sourceOids, targetOids, ports) {
446
+ const request = mergeOnly(step);
447
+ const theirs = request.expectedSource;
448
+ if (target === theirs ||
449
+ (await git.isDescendent({
450
+ fs,
451
+ dir: DIR,
452
+ oid: target,
453
+ ancestor: theirs,
454
+ depth: -1,
455
+ })))
456
+ return { done: { kind: "up-to-date", head: target } };
457
+ await git.writeRef({
458
+ fs,
459
+ dir: DIR,
460
+ ref: `refs/heads/${request.branch}`,
461
+ value: target,
462
+ force: true,
463
+ });
464
+ await git.writeRef({
465
+ fs,
466
+ dir: DIR,
467
+ ref: "refs/heads/step-source",
468
+ value: theirs,
469
+ force: true,
470
+ });
471
+ const identity = {
472
+ ...request.author,
473
+ timestamp: step.timestamp,
474
+ timezoneOffset: 0,
475
+ };
476
+ let oid;
477
+ try {
478
+ oid = (await git.merge({
479
+ fs,
480
+ dir: DIR,
481
+ ours: request.branch,
482
+ theirs: "step-source",
483
+ fastForward: false,
484
+ abortOnConflict: true,
485
+ message: request.message,
486
+ author: identity,
487
+ committer: identity,
488
+ })).oid;
489
+ }
490
+ catch (cause) {
491
+ if (cause instanceof git.Errors.MergeConflictError)
492
+ return {
493
+ done: { kind: "conflict", head: target, paths: cause.data.filepaths },
494
+ };
495
+ throw cause;
496
+ }
497
+ if (oid === undefined)
498
+ return { done: { kind: "up-to-date", head: target } };
499
+ const excluded = new Set(targetOids);
500
+ return preparePush(step, fs, target, oid, [
501
+ ...sourceOids.filter((id) => !excluded.has(id)),
502
+ ...(await looseObjects(fs)),
503
+ ], ports);
504
+ }
505
+ function outcome(step) {
506
+ return step.operation.kind === "commit"
507
+ ? { kind: "committed", commit: step.built }
508
+ : { kind: "merged", before: step.old ?? ZERO, after: step.built };
509
+ }
510
+ function frameClosed(signal) {
511
+ return signal.aborted;
512
+ }
513
+ /** Execute one fresh-admitted frame; the caller persists next and its successor job atomically. */
514
+ export async function runStep(raw, ports, signal) {
515
+ const step = gitStepSchema.parse(raw);
516
+ if (frameClosed(signal))
517
+ return { yield: true };
518
+ switch (step.phase) {
519
+ case "too-large":
520
+ return { done: { kind: "too_large" } };
521
+ case "inspect-base": {
522
+ const op = commitOnly(step);
523
+ const heads = await refHeads(ports.git, op.request.repository);
524
+ const current = heads.get(`refs/heads/${op.request.branch}`) ?? null;
525
+ if (current !== op.request.expectedHead)
526
+ return { done: { kind: "stale", head: current } };
527
+ const base = current !== null || op.request.startFrom === undefined
528
+ ? current
529
+ : (heads.get(`refs/heads/${op.request.startFrom}`) ?? null);
530
+ if (current === null &&
531
+ op.request.startFrom !== undefined &&
532
+ base === null)
533
+ throw new Error("Start branch unavailable");
534
+ if (base === null)
535
+ return buildCommit(step, await freshFs(), null, ports);
536
+ return {
537
+ next: gitStepSchema.parse({ ...step, phase: "fetch-base", base }),
538
+ };
539
+ }
540
+ case "fetch-base": {
541
+ const op = commitOnly(step);
542
+ const fs = await freshFs();
543
+ if (step.base !== null) {
544
+ if (frameClosed(signal))
545
+ return { yield: true };
546
+ const response = await fetchPack(ports.git, op.request.repository, [step.base], true);
547
+ const pack = packResponse(response);
548
+ if (pack.length > COMMIT_BYTES)
549
+ return { done: { kind: "too_large" } };
550
+ await index(fs, pack, "base");
551
+ }
552
+ return buildCommit(step, fs, step.base, ports);
553
+ }
554
+ case "inspect-source": {
555
+ const request = mergeOnly(step);
556
+ const heads = await refHeads(ports.git, request.source);
557
+ const head = heads.get(`refs/heads/${request.sourceBranch}`);
558
+ if (head === undefined)
559
+ throw new Error("Source branch unavailable");
560
+ if (head !== request.expectedSource)
561
+ return { done: { kind: "source-moved", head } };
562
+ const target = request.source === request.target
563
+ ? heads.get(`refs/heads/${request.branch}`)
564
+ : request.expectedTarget;
565
+ const next = {
566
+ key: step.key,
567
+ timestamp: step.timestamp,
568
+ operation: step.operation,
569
+ phase: "fetch-source",
570
+ };
571
+ if (target !== undefined)
572
+ next.target = target;
573
+ return { next: gitStepSchema.parse(next) };
574
+ }
575
+ case "fetch-source": {
576
+ const request = mergeOnly(step);
577
+ const wants = [
578
+ request.expectedSource,
579
+ ...(request.source === request.target && step.target !== undefined
580
+ ? [step.target]
581
+ : []),
582
+ ];
583
+ const pack = packResponse(await fetchPack(ports.git, request.source, [...new Set(wants)], false));
584
+ if (pack.length > MERGE_BYTES)
585
+ return { done: { kind: "too_large" } };
586
+ const fs = await freshFs();
587
+ const oids = await index(fs, pack, "source");
588
+ if (request.source === request.target && step.target !== undefined)
589
+ return buildMerge(step, fs, step.target, [], oids, ports);
590
+ const sourceDigest = await materialDigest(pack);
591
+ await retain(ports.material, sourceKey({ ...step, sourceDigest }), pack);
592
+ return {
593
+ next: gitStepSchema.parse({
594
+ key: step.key,
595
+ timestamp: step.timestamp,
596
+ operation: step.operation,
597
+ phase: "inspect-target",
598
+ sourceDigest,
599
+ }),
600
+ };
601
+ }
602
+ case "inspect-target": {
603
+ const request = mergeOnly(step);
604
+ const source = await restore(ports.material, sourceKey(step), step.sourceDigest, MERGE_BYTES);
605
+ const fs = await freshFs();
606
+ const sourceOids = await index(fs, source, "source");
607
+ const copied = request.behind === 0 ? request.expectedTarget : undefined;
608
+ const contains = copied !== undefined &&
609
+ copied !== request.expectedSource &&
610
+ (await git.isDescendent({
611
+ fs,
612
+ dir: DIR,
613
+ oid: request.expectedSource,
614
+ ancestor: copied,
615
+ depth: -1,
616
+ }));
617
+ if (frameClosed(signal))
618
+ return { yield: true };
619
+ const heads = await refHeads(ports.git, request.target);
620
+ const target = heads.get(`refs/heads/${request.branch}`);
621
+ if (target === undefined)
622
+ throw new Error("Target branch unavailable");
623
+ if (contains && target !== copied)
624
+ return { done: { kind: "stale", head: target } };
625
+ if (contains)
626
+ return buildMerge(step, fs, target, sourceOids, [], ports);
627
+ return {
628
+ next: gitStepSchema.parse({ ...step, phase: "fetch-target", target }),
629
+ };
630
+ }
631
+ case "fetch-target": {
632
+ const request = mergeOnly(step);
633
+ const source = await restore(ports.material, sourceKey(step), step.sourceDigest, MERGE_BYTES);
634
+ const fs = await freshFs();
635
+ const sourceOids = await index(fs, source, "source");
636
+ if (frameClosed(signal))
637
+ return { yield: true };
638
+ const target = packResponse(await fetchPack(ports.git, request.target, [step.target], false));
639
+ if (source.length + target.length > MERGE_BYTES)
640
+ return { done: { kind: "too_large" } };
641
+ const targetOids = await index(fs, target, "target");
642
+ return buildMerge(step, fs, step.target, sourceOids, targetOids, ports);
643
+ }
644
+ case "inspect-merge-push": {
645
+ const request = mergeOnly(step);
646
+ const heads = await refHeads(ports.git, request.source);
647
+ const source = heads.get(`refs/heads/${request.sourceBranch}`);
648
+ if (source === undefined)
649
+ throw new Error("Source branch unavailable");
650
+ if (source !== request.expectedSource)
651
+ return { done: { kind: "source-moved", head: source } };
652
+ if (request.source === request.target) {
653
+ const target = heads.get(`refs/heads/${request.branch}`) ?? null;
654
+ if (target !== step.old)
655
+ return { done: { kind: "stale", head: target } };
656
+ }
657
+ return { next: gitStepSchema.parse({ ...step, phase: "push" }) };
658
+ }
659
+ case "inspect-atomic": {
660
+ const op = commitOnly(step);
661
+ const response = await ports.git.exchange(op.request.repository, {
662
+ service: "receive-pack",
663
+ phase: "advertise",
664
+ body: null,
665
+ headers: {},
666
+ });
667
+ if (response.status !== 200)
668
+ throw new Error("Git advertisement unavailable");
669
+ if (!advertisedCapabilities(response.body).has("atomic"))
670
+ throw new GitStepMaterialError("atomic_unavailable");
671
+ return {
672
+ next: gitStepSchema.parse({ ...step, phase: "push", atomic: true }),
673
+ };
674
+ }
675
+ case "push": {
676
+ const repo = step.operation.kind === "commit"
677
+ ? step.operation.request.repository
678
+ : step.operation.request.target;
679
+ const ref = `refs/heads/${step.operation.request.branch}`;
680
+ const refs = [
681
+ {
682
+ ref,
683
+ old: step.pushBefore === undefined ? step.old : step.pushBefore,
684
+ },
685
+ ...(step.operation.kind === "commit"
686
+ ? step.operation.tags.map((tag) => ({
687
+ ref: tag.ref,
688
+ old: tag.expected,
689
+ }))
690
+ : []),
691
+ ];
692
+ if (refs.length > 1 && step.atomic !== true)
693
+ throw new GitStepMaterialError("atomic_unavailable");
694
+ const pack = await restore(ports.material, packKey(step), step.packDigest, COMMIT_BYTES);
695
+ const commands = refs
696
+ .map((update, index) => pktLine(`${update.old ?? ZERO} ${step.built} ${update.ref}${index === 0 ? `\0report-status side-band-64k${refs.length > 1 ? " atomic" : ""}` : ""}\n`))
697
+ .join("") + "0000";
698
+ if (frameClosed(signal))
699
+ return { yield: true };
700
+ try {
701
+ const response = await ports.git.exchange(repo, {
702
+ service: "receive-pack",
703
+ phase: "exchange",
704
+ headers: { "content-type": "application/x-git-receive-pack-request" },
705
+ body: new Uint8Array(Buffer.concat([encoder.encode(commands), pack])),
706
+ });
707
+ const accepted = acceptedRefs(response.body, true);
708
+ if (response.status === 200 &&
709
+ refs.every(({ ref }) => accepted.has(ref)))
710
+ return {
711
+ next: gitStepSchema.parse({
712
+ ...step,
713
+ phase: "confirm",
714
+ attempt: Math.min(step.attempt + 1, 3),
715
+ }),
716
+ };
717
+ }
718
+ catch {
719
+ /* An uncertain POST is resolved by the next independently admitted ref read. */
720
+ }
721
+ return {
722
+ next: gitStepSchema.parse({
723
+ ...step,
724
+ phase: "confirm",
725
+ attempt: Math.min(step.attempt + 1, 3),
726
+ }),
727
+ };
728
+ }
729
+ case "confirm": {
730
+ const repo = step.operation.kind === "commit"
731
+ ? step.operation.request.repository
732
+ : step.operation.request.target;
733
+ const heads = await refHeads(ports.git, repo);
734
+ const ref = `refs/heads/${step.operation.request.branch}`;
735
+ const head = heads.get(ref) ?? null;
736
+ const tags = step.operation.kind === "commit" ? step.operation.tags : [];
737
+ const tagsBuilt = tags.every(({ ref }) => heads.get(ref) === step.built);
738
+ if (tagsBuilt &&
739
+ (head === step.built ||
740
+ (tags.length > 0 && head !== step.old && step.atomic === true)))
741
+ return { done: outcome(step) };
742
+ if (tags.length === 0 &&
743
+ head !== null &&
744
+ head !== step.old &&
745
+ head !== step.built)
746
+ return {
747
+ next: gitStepSchema.parse({
748
+ ...step,
749
+ phase: "confirm-ancestry",
750
+ head,
751
+ }),
752
+ };
753
+ if ((head === step.old || head === step.built) &&
754
+ tags.every(({ ref, expected }) => (heads.get(ref) ?? null) === expected ||
755
+ heads.get(ref) === step.built) &&
756
+ step.attempt < 3)
757
+ return {
758
+ next: gitStepSchema.parse({
759
+ ...step,
760
+ phase: step.operation.kind === "merge" ? "inspect-merge-push" : "push",
761
+ pushBefore: head,
762
+ operation: step.operation.kind === "commit"
763
+ ? {
764
+ ...step.operation,
765
+ tags: tags.map((tag) => ({
766
+ ...tag,
767
+ expected: heads.get(tag.ref) ?? null,
768
+ })),
769
+ }
770
+ : step.operation,
771
+ }),
772
+ };
773
+ if (head === step.old &&
774
+ tags.every(({ ref, expected }) => (heads.get(ref) ?? null) === expected ||
775
+ heads.get(ref) === step.built))
776
+ throw new GitStepMaterialError("push_refused");
777
+ return { done: { kind: "stale", head } };
778
+ }
779
+ case "confirm-ancestry": {
780
+ const repo = step.operation.kind === "commit"
781
+ ? step.operation.request.repository
782
+ : step.operation.request.target;
783
+ const pack = packResponse(await fetchPack(ports.git, repo, [step.head], false));
784
+ if (pack.length > MERGE_BYTES)
785
+ throw new RangeError("Git confirmation material too large");
786
+ const fs = await freshFs();
787
+ await index(fs, pack, "confirmation");
788
+ const landed = await git.isDescendent({
789
+ fs,
790
+ dir: DIR,
791
+ oid: step.head,
792
+ ancestor: step.built,
793
+ depth: -1,
794
+ });
795
+ return {
796
+ done: landed ? outcome(step) : { kind: "stale", head: step.head },
797
+ };
798
+ }
799
+ }
800
+ }
801
+ /** Drop material only after the owner has durably recorded the terminal outcome. */
802
+ export async function dropMaterial(step, material) {
803
+ await material.drop(ownerPrefix(step));
804
+ }