amalgm 0.1.238 → 0.1.240

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 (86) hide show
  1. package/lib/shared-realtime-tunnel.js +22 -14
  2. package/package.json +1 -1
  3. package/runtime/scripts/amalgm-mcp/browser/cookie-jar.js +68 -20
  4. package/runtime/scripts/amalgm-mcp/project-context/store.js +87 -19
  5. package/runtime/scripts/amalgm-mcp/state/attachments.js +40 -12
  6. package/runtime/scripts/amalgm-mcp/state/db.js +216 -17
  7. package/runtime/scripts/amalgm-mcp/state/doc-disk.js +550 -0
  8. package/runtime/scripts/amalgm-mcp/state/docs.js +1101 -301
  9. package/runtime/scripts/amalgm-mcp/state/mutation-contracts.js +99 -0
  10. package/runtime/scripts/amalgm-mcp/state/mutations.js +507 -130
  11. package/runtime/scripts/amalgm-mcp/state/promotions.js +21 -1
  12. package/runtime/scripts/amalgm-mcp/state/replicas.js +134 -13
  13. package/runtime/scripts/amalgm-mcp/state/shared-rest.js +14 -8
  14. package/runtime/scripts/amalgm-mcp/tests/browser-cookie-cloud.test.js +193 -0
  15. package/runtime/scripts/amalgm-mcp/tests/code-project-stream.test.js +313 -94
  16. package/runtime/scripts/amalgm-mcp/tests/fixtures/code-project-stream-v2.json +1 -0
  17. package/runtime/scripts/amalgm-mcp/tests/project-context.test.js +32 -17
  18. package/runtime/scripts/amalgm-mcp/tests/shared-tunnel-rehydration.test.js +84 -0
  19. package/runtime/scripts/amalgm-mcp/tests/state-docs.test.js +376 -13
  20. package/runtime/scripts/amalgm-mcp/tests/state-mutations.test.js +309 -0
  21. package/runtime/scripts/amalgm-mcp/tests/state-shared-replica.test.js +758 -30
  22. package/runtime/scripts/amalgm-mcp/tests/workspace-cards-store.test.js +72 -3
  23. package/runtime/scripts/amalgm-mcp/tests/workspace-cards.test.js +33 -6
  24. package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint-reducer.test.js +27 -0
  25. package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint.test.js +334 -191
  26. package/runtime/scripts/amalgm-mcp/tests/workspace-durable.test.js +24 -0
  27. package/runtime/scripts/amalgm-mcp/tests/workspace-local-intent.test.js +1999 -0
  28. package/runtime/scripts/amalgm-mcp/tests/workspace-native-file-transaction.test.js +364 -0
  29. package/runtime/scripts/amalgm-mcp/tests/workspace-object-tunnel.test.js +81 -3
  30. package/runtime/scripts/amalgm-mcp/tests/workspace-objects.test.js +114 -8
  31. package/runtime/scripts/amalgm-mcp/tests/workspace-provenance.test.js +185 -0
  32. package/runtime/scripts/amalgm-mcp/tests/workspace-publication-invariants.test.js +174 -269
  33. package/runtime/scripts/amalgm-mcp/tests/workspace-reducer-liveness.test.js +26 -0
  34. package/runtime/scripts/amalgm-mcp/tests/workspace-reducer.test.js +2060 -407
  35. package/runtime/scripts/amalgm-mcp/tests/workspace-sync-stats.test.js +119 -0
  36. package/runtime/scripts/amalgm-mcp/tests/workspace-transition.test.js +1446 -0
  37. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-cloud.test.js +5319 -3
  38. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-core.test.js +191 -1
  39. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-store.test.js +3140 -17
  40. package/runtime/scripts/amalgm-mcp/tests/workspace-wall.test.js +135 -637
  41. package/runtime/scripts/amalgm-mcp/workspace/access-store.js +5 -1
  42. package/runtime/scripts/amalgm-mcp/workspace/card.js +7 -7
  43. package/runtime/scripts/amalgm-mcp/workspace/cards.js +36 -12
  44. package/runtime/scripts/amalgm-mcp/workspace/checkpoint.js +436 -173
  45. package/runtime/scripts/amalgm-mcp/workspace/content.js +32 -0
  46. package/runtime/scripts/amalgm-mcp/workspace/durable.js +45 -0
  47. package/runtime/scripts/amalgm-mcp/workspace/git.js +391 -37
  48. package/runtime/scripts/amalgm-mcp/workspace/intent.js +535 -0
  49. package/runtime/scripts/amalgm-mcp/workspace/merge.js +89 -6
  50. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/BUILD.md +31 -0
  51. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/darwin-arm64/amalgm-file-transaction +0 -0
  52. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/linux-x64/amalgm-file-transaction +0 -0
  53. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/build.mjs +72 -0
  54. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/client.js +165 -0
  55. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.mod +5 -0
  56. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.sum +2 -0
  57. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/main.go +2201 -0
  58. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/manifest.json +19 -0
  59. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_darwin.go +18 -0
  60. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_linux.go +18 -0
  61. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_darwin.go +33 -0
  62. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_linux.go +47 -0
  63. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/verify.mjs +43 -0
  64. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_darwin.go +114 -0
  65. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_linux.go +84 -0
  66. package/runtime/scripts/amalgm-mcp/workspace/objects.js +119 -54
  67. package/runtime/scripts/amalgm-mcp/workspace/project-stream.js +616 -168
  68. package/runtime/scripts/amalgm-mcp/workspace/provenance.js +240 -0
  69. package/runtime/scripts/amalgm-mcp/workspace/reducer.js +1979 -570
  70. package/runtime/scripts/amalgm-mcp/workspace/rest.js +49 -28
  71. package/runtime/scripts/amalgm-mcp/workspace/store.js +77 -4
  72. package/runtime/scripts/amalgm-mcp/workspace/sync-stats.js +240 -0
  73. package/runtime/scripts/amalgm-mcp/workspace/transition.js +1729 -192
  74. package/runtime/scripts/amalgm-mcp/workspace/tree/floor.js +100 -0
  75. package/runtime/scripts/amalgm-mcp/workspace/tree/inclusion.js +7 -1
  76. package/runtime/scripts/amalgm-mcp/workspace/tree/local-state.js +73 -61
  77. package/runtime/scripts/amalgm-mcp/workspace/tree/observation-holds.js +127 -0
  78. package/runtime/scripts/amalgm-mcp/workspace/tree/path-state.js +12 -0
  79. package/runtime/scripts/amalgm-mcp/workspace/tree/reducer.js +51 -6
  80. package/runtime/scripts/amalgm-mcp/workspace/tree/root-binding.js +125 -0
  81. package/runtime/scripts/amalgm-mcp/workspace/tree/snapshot.js +79 -8
  82. package/runtime/scripts/amalgm-mcp/workspace/tree/symlinks.js +76 -0
  83. package/runtime/scripts/amalgm-mcp/workspace/tree-cloud.js +3480 -165
  84. package/runtime/scripts/amalgm-mcp/workspace/tree-materialization.js +3766 -0
  85. package/runtime/scripts/amalgm-mcp/workspace/tree-store.js +899 -71
  86. package/runtime/scripts/amalgm-mcp/workspace/wall.js +352 -497
@@ -0,0 +1,2201 @@
1
+ // amalgm-file-transaction is the descriptor-relative mutation boundary for
2
+ // ordered workspace file applies. It never resolves a repository-relative
3
+ // parent after opening it: every create, rename, link, unlink, and directory
4
+ // sync is performed relative to O_NOFOLLOW directory descriptors.
5
+ package main
6
+
7
+ import (
8
+ "bytes"
9
+ "crypto/sha256"
10
+ "encoding/base64"
11
+ "encoding/hex"
12
+ "encoding/json"
13
+ "errors"
14
+ "fmt"
15
+ "io"
16
+ "os"
17
+ "path/filepath"
18
+ "strconv"
19
+ "strings"
20
+ "time"
21
+
22
+ "golang.org/x/sys/unix"
23
+ )
24
+
25
+ type identity struct {
26
+ Kind string `json:"kind"`
27
+ Exec bool `json:"exec,omitempty"`
28
+ Bytes int64 `json:"bytes,omitempty"`
29
+ SHA256 string `json:"sha256,omitempty"`
30
+ Dev string `json:"dev,omitempty"`
31
+ Ino string `json:"ino,omitempty"`
32
+ }
33
+
34
+ type target struct {
35
+ Kind string `json:"kind"`
36
+ Exec bool `json:"exec,omitempty"`
37
+ Bytes int64 `json:"bytes"`
38
+ SHA256 string `json:"sha256"`
39
+ Content *string `json:"content,omitempty"`
40
+ }
41
+
42
+ type treeEntryIdentity struct {
43
+ Kind string `json:"kind"`
44
+ Dev string `json:"dev"`
45
+ Ino string `json:"ino"`
46
+ }
47
+
48
+ type request struct {
49
+ Action string `json:"action"`
50
+ Backend string `json:"backend,omitempty"`
51
+ Root string `json:"root"`
52
+ RootDev string `json:"rootDev"`
53
+ RootIno string `json:"rootIno"`
54
+ Source string `json:"source,omitempty"`
55
+ SourceIdentity *treeEntryIdentity `json:"sourceIdentity,omitempty"`
56
+ SourceParentIdentity *treeEntryIdentity `json:"sourceParentIdentity,omitempty"`
57
+ TargetParentIdentity *treeEntryIdentity `json:"targetParentIdentity,omitempty"`
58
+ Path string `json:"path"`
59
+ ID string `json:"id"`
60
+ Operation string `json:"operation"`
61
+ Expected *identity `json:"expected"`
62
+ Target *target `json:"target"`
63
+ TestPauseAfterParentOpenMillis int `json:"testPauseAfterParentOpenMillis,omitempty"`
64
+ TestPauseAfterExtractionMillis int `json:"testPauseAfterExtractionMillis,omitempty"`
65
+ TestPauseBeforeInstallMillis int `json:"testPauseBeforeInstallMillis,omitempty"`
66
+ TestPauseAfterTreeMoveMillis int `json:"testPauseAfterTreeMoveMillis,omitempty"`
67
+ TestCrashAfterExtraction bool `json:"testCrashAfterExtraction,omitempty"`
68
+ TestCrashBeforeInstall bool `json:"testCrashBeforeInstall,omitempty"`
69
+ TestCrashAfterInstall bool `json:"testCrashAfterInstall,omitempty"`
70
+ TestCrashAfterTreeMove bool `json:"testCrashAfterTreeMove,omitempty"`
71
+ PID int `json:"pid,omitempty"`
72
+ }
73
+
74
+ type response struct {
75
+ Status string `json:"status"`
76
+ Reason string `json:"reason,omitempty"`
77
+ Identity string `json:"identity,omitempty"`
78
+ RootDev string `json:"rootDev,omitempty"`
79
+ RootIno string `json:"rootIno,omitempty"`
80
+ Path string `json:"path,omitempty"`
81
+ Value *identity `json:"value,omitempty"`
82
+ Content *string `json:"content,omitempty"`
83
+ }
84
+
85
+ type openedParent struct {
86
+ rootFD int
87
+ parentFD int
88
+ parentAbs string
89
+ base string
90
+ dev uint64
91
+ ino uint64
92
+ }
93
+
94
+ func main() {
95
+ var input request
96
+ decoder := json.NewDecoder(io.LimitReader(os.Stdin, 16*1024*1024))
97
+ decoder.DisallowUnknownFields()
98
+ if err := decoder.Decode(&input); err != nil {
99
+ fail(fmt.Errorf("invalid request: %w", err))
100
+ }
101
+ if err := validateRequest(input); err != nil {
102
+ fail(err)
103
+ }
104
+ var result response
105
+ var err error
106
+ if input.Backend == "process-identity" {
107
+ switch input.Action {
108
+ case "identity":
109
+ var identity string
110
+ var live bool
111
+ identity, live, err = processIdentity(input.PID)
112
+ if live {
113
+ result = response{Status: "identified", Identity: identity}
114
+ } else {
115
+ result = response{Status: "exited"}
116
+ }
117
+ default:
118
+ err = fmt.Errorf("unsupported action %q", input.Action)
119
+ }
120
+ } else if input.Backend == "git-lock" {
121
+ switch input.Action {
122
+ case "apply":
123
+ result, err = applyGitLock(input)
124
+ case "recover":
125
+ result, err = recoverGitLock(input)
126
+ case "receipt":
127
+ result, err = readGitLockReceipt(input)
128
+ case "release":
129
+ result, err = releaseGitLockReceipt(input)
130
+ default:
131
+ err = fmt.Errorf("unsupported action %q", input.Action)
132
+ }
133
+ } else if input.Backend == "git-guard" {
134
+ switch input.Action {
135
+ case "guard":
136
+ result, err = guardGitLock(input)
137
+ case "recover", "unguard":
138
+ result, err = releaseGitGuard(input)
139
+ default:
140
+ err = fmt.Errorf("unsupported action %q", input.Action)
141
+ }
142
+ } else if input.Backend == "git-worktree-guard" {
143
+ switch input.Action {
144
+ case "guard":
145
+ result, err = guardGitWorktreeNamespace(input)
146
+ case "recover", "unguard":
147
+ result, err = releaseGitWorktreeNamespace(input)
148
+ default:
149
+ err = fmt.Errorf("unsupported action %q", input.Action)
150
+ }
151
+ } else if input.Backend == "tree-rename" {
152
+ switch input.Action {
153
+ case "move":
154
+ result, err = moveTreeEntryNoReplace(input)
155
+ default:
156
+ err = fmt.Errorf("unsupported action %q", input.Action)
157
+ }
158
+ } else {
159
+ switch input.Action {
160
+ case "observe":
161
+ result, err = observe(input)
162
+ case "apply":
163
+ result, err = apply(input)
164
+ case "recover":
165
+ result, err = recoverTransaction(input)
166
+ case "receipt":
167
+ result, err = readReceipt(input)
168
+ case "release":
169
+ result, err = releaseReceipt(input)
170
+ default:
171
+ err = fmt.Errorf("unsupported action %q", input.Action)
172
+ }
173
+ }
174
+ if err != nil {
175
+ fail(err)
176
+ }
177
+ if err := json.NewEncoder(os.Stdout).Encode(result); err != nil {
178
+ fail(err)
179
+ }
180
+ }
181
+
182
+ func fail(err error) {
183
+ _ = json.NewEncoder(os.Stdout).Encode(map[string]string{
184
+ "status": "error",
185
+ "reason": err.Error(),
186
+ })
187
+ os.Exit(1)
188
+ }
189
+
190
+ func validateRequest(input request) error {
191
+ if input.Backend == "process-identity" {
192
+ if input.Action != "identity" {
193
+ return errors.New("process identity action is invalid")
194
+ }
195
+ if input.PID < 1 {
196
+ return errors.New("process identity PID is invalid")
197
+ }
198
+ return nil
199
+ }
200
+ if input.Backend != "" &&
201
+ input.Backend != "git-lock" &&
202
+ input.Backend != "git-guard" &&
203
+ input.Backend != "git-worktree-guard" &&
204
+ input.Backend != "tree-rename" {
205
+ return errors.New("backend is invalid")
206
+ }
207
+ if !filepath.IsAbs(input.Root) {
208
+ return errors.New("root must be absolute")
209
+ }
210
+ if _, err := strconv.ParseUint(input.RootDev, 10, 64); err != nil {
211
+ return errors.New("root device identity is invalid")
212
+ }
213
+ if _, err := strconv.ParseUint(input.RootIno, 10, 64); err != nil {
214
+ return errors.New("root inode identity is invalid")
215
+ }
216
+ if input.Path == "" || filepath.IsAbs(input.Path) || strings.Contains(input.Path, `\`) {
217
+ return errors.New("path must be a Unix repository-relative path")
218
+ }
219
+ for _, component := range strings.Split(input.Path, "/") {
220
+ if component == "" || component == "." || component == ".." {
221
+ return errors.New("path contains an unsafe component")
222
+ }
223
+ }
224
+ if input.Backend == "tree-rename" {
225
+ if input.Action != "move" {
226
+ return errors.New("tree rename action is invalid")
227
+ }
228
+ if input.Source == "" || filepath.IsAbs(input.Source) || strings.Contains(input.Source, `\`) {
229
+ return errors.New("tree rename source must be a Unix root-relative path")
230
+ }
231
+ for _, component := range strings.Split(input.Source, "/") {
232
+ if component == "" || component == "." || component == ".." {
233
+ return errors.New("tree rename source contains an unsafe component")
234
+ }
235
+ }
236
+ if input.Expected != nil || input.Target != nil {
237
+ return errors.New("tree rename has unexpected byte identities")
238
+ }
239
+ if input.SourceIdentity == nil {
240
+ return errors.New("tree rename source identity is missing")
241
+ }
242
+ if input.SourceParentIdentity == nil || input.TargetParentIdentity == nil {
243
+ return errors.New("tree rename parent identities are missing")
244
+ }
245
+ if input.SourceIdentity.Kind != "file" &&
246
+ input.SourceIdentity.Kind != "folder" &&
247
+ input.SourceIdentity.Kind != "symlink" &&
248
+ input.SourceIdentity.Kind != "other" {
249
+ return errors.New("tree rename source kind is invalid")
250
+ }
251
+ if _, err := strconv.ParseUint(input.SourceIdentity.Dev, 10, 64); err != nil {
252
+ return errors.New("tree rename source device identity is invalid")
253
+ }
254
+ if _, err := strconv.ParseUint(input.SourceIdentity.Ino, 10, 64); err != nil {
255
+ return errors.New("tree rename source inode identity is invalid")
256
+ }
257
+ for label, parent := range map[string]*treeEntryIdentity{
258
+ "source": input.SourceParentIdentity,
259
+ "target": input.TargetParentIdentity,
260
+ } {
261
+ if parent.Kind != "folder" {
262
+ return fmt.Errorf("tree rename %s parent must be a folder", label)
263
+ }
264
+ if _, err := strconv.ParseUint(parent.Dev, 10, 64); err != nil {
265
+ return fmt.Errorf("tree rename %s parent device identity is invalid", label)
266
+ }
267
+ if _, err := strconv.ParseUint(parent.Ino, 10, 64); err != nil {
268
+ return fmt.Errorf("tree rename %s parent inode identity is invalid", label)
269
+ }
270
+ }
271
+ }
272
+ for _, pause := range []int{
273
+ input.TestPauseAfterParentOpenMillis,
274
+ input.TestPauseAfterExtractionMillis,
275
+ input.TestPauseBeforeInstallMillis,
276
+ input.TestPauseAfterTreeMoveMillis,
277
+ } {
278
+ if pause < 0 || pause > 5000 {
279
+ return errors.New("test pause is outside its bounded range")
280
+ }
281
+ }
282
+ if input.Action == "observe" {
283
+ if input.Backend != "" {
284
+ return errors.New("observation does not support a backend")
285
+ }
286
+ if input.ID != "" || input.Operation != "" || input.Expected != nil || input.Target != nil {
287
+ return errors.New("observation has unexpected transaction fields")
288
+ }
289
+ if input.TestPauseAfterExtractionMillis != 0 ||
290
+ input.TestPauseBeforeInstallMillis != 0 ||
291
+ input.TestCrashAfterExtraction ||
292
+ input.TestCrashBeforeInstall ||
293
+ input.TestCrashAfterInstall {
294
+ return errors.New("observation has unexpected mutation test hooks")
295
+ }
296
+ return nil
297
+ }
298
+ if len(input.ID) != 64 {
299
+ return errors.New("transaction id must be a SHA-256 string")
300
+ }
301
+ if _, err := hex.DecodeString(input.ID); err != nil {
302
+ return errors.New("transaction id must be hexadecimal")
303
+ }
304
+ if input.Operation == "" || len(input.Operation) > 1024 {
305
+ return errors.New("operation identity is invalid")
306
+ }
307
+ if input.Backend == "git-lock" {
308
+ if input.Path != "index" && input.Path != "HEAD" {
309
+ return errors.New("git-lock path must be index or HEAD")
310
+ }
311
+ if input.Target == nil || input.Target.Kind != "file" || input.Target.Exec {
312
+ return errors.New("git-lock target must be a non-executable file")
313
+ }
314
+ if input.Expected != nil && (input.Expected.Kind != "file" || input.Expected.Exec) {
315
+ return errors.New("git-lock expected value must be a non-executable file")
316
+ }
317
+ }
318
+ if input.Backend == "git-guard" {
319
+ if input.Path != "HEAD" {
320
+ return errors.New("git-guard path must be HEAD")
321
+ }
322
+ if input.Target == nil || input.Target.Kind != "file" || input.Target.Exec {
323
+ return errors.New("git-guard target must be a non-executable file")
324
+ }
325
+ if input.Expected == nil || input.Expected.Kind != "file" || input.Expected.Exec {
326
+ return errors.New("git-guard expected value must be a non-executable file")
327
+ }
328
+ targetIdentity, err := requestedTargetIdentity(input.Target)
329
+ if err != nil {
330
+ return err
331
+ }
332
+ if !identitiesEqual(input.Expected, targetIdentity) {
333
+ return errors.New("git-guard target must equal its expected HEAD")
334
+ }
335
+ }
336
+ if input.Backend == "git-worktree-guard" {
337
+ if input.Path != "worktrees" {
338
+ return errors.New("git-worktree-guard path must be worktrees")
339
+ }
340
+ if input.Expected == nil || input.Expected.Kind != "directory" {
341
+ return errors.New("git-worktree-guard expected value must be a directory")
342
+ }
343
+ if input.Target == nil || input.Target.Kind != "file" || input.Target.Exec {
344
+ return errors.New("git-worktree-guard target must be a non-executable file")
345
+ }
346
+ }
347
+ if input.Expected != nil {
348
+ if input.Expected.Kind == "directory" && input.Backend != "git-worktree-guard" {
349
+ return errors.New("directory identity is only valid for a Git worktree guard")
350
+ }
351
+ if err := validateIdentity(*input.Expected); err != nil {
352
+ return fmt.Errorf("invalid expected identity: %w", err)
353
+ }
354
+ }
355
+ if input.Target != nil {
356
+ if input.Target.Kind != "file" && input.Target.Kind != "symlink" {
357
+ return errors.New("target kind must be file or symlink")
358
+ }
359
+ if err := validateIdentity(identity{
360
+ Kind: input.Target.Kind,
361
+ Exec: input.Target.Exec,
362
+ Bytes: input.Target.Bytes,
363
+ SHA256: input.Target.SHA256,
364
+ }); err != nil {
365
+ return fmt.Errorf("invalid target identity: %w", err)
366
+ }
367
+ if input.Action == "apply" && input.Backend != "git-lock" {
368
+ if input.Target.Content == nil {
369
+ return errors.New("apply target content is missing")
370
+ }
371
+ content, err := base64.StdEncoding.DecodeString(*input.Target.Content)
372
+ if err != nil {
373
+ return errors.New("target content is not base64")
374
+ }
375
+ sum := sha256.Sum256(content)
376
+ if int64(len(content)) != input.Target.Bytes ||
377
+ hex.EncodeToString(sum[:]) != input.Target.SHA256 {
378
+ return errors.New("target content does not match its identity")
379
+ }
380
+ }
381
+ }
382
+ return nil
383
+ }
384
+
385
+ // moveTreeEntryNoReplace is the one descriptor-relative rename boundary used
386
+ // by workspace-tree WALs. It never overwrites a destination that appeared
387
+ // after JavaScript observed absence. Source identity remains caller-owned WAL
388
+ // evidence and is reclassified after this atomic namespace operation.
389
+ func moveTreeEntryNoReplace(input request) (response, error) {
390
+ source, err := openParent(input.Root, input.Source, false)
391
+ if err != nil {
392
+ if errors.Is(err, unix.ENOENT) || errors.Is(err, unix.ENOTDIR) {
393
+ return response{Status: "changed", Reason: "source changed before exclusive move"}, nil
394
+ }
395
+ return response{}, err
396
+ }
397
+ defer source.close()
398
+ target, err := openParent(input.Root, input.Path, false)
399
+ if err != nil {
400
+ return response{}, err
401
+ }
402
+ defer target.close()
403
+ if err := verifyRootIdentity(source, input); err != nil {
404
+ return response{}, err
405
+ }
406
+ if err := verifyRootIdentity(target, input); err != nil {
407
+ return response{}, err
408
+ }
409
+ if !openedParentMatches(source, *input.SourceParentIdentity) {
410
+ return response{Status: "changed", Reason: "source parent changed before exclusive move"}, nil
411
+ }
412
+ if !openedParentMatches(target, *input.TargetParentIdentity) {
413
+ return response{Status: "changed", Reason: "target parent changed before exclusive move"}, nil
414
+ }
415
+ matches, err := matchesTreeEntryIdentity(
416
+ source.parentFD,
417
+ source.base,
418
+ *input.SourceIdentity,
419
+ )
420
+ if err != nil {
421
+ if errors.Is(err, unix.ENOENT) || errors.Is(err, unix.ENOTDIR) {
422
+ return response{Status: "changed", Reason: "source changed before exclusive move"}, nil
423
+ }
424
+ return response{}, err
425
+ }
426
+ if !matches {
427
+ return response{Status: "changed", Reason: "source changed before exclusive move"}, nil
428
+ }
429
+ if input.TestPauseBeforeInstallMillis > 0 {
430
+ time.Sleep(time.Duration(input.TestPauseBeforeInstallMillis) * time.Millisecond)
431
+ }
432
+ if err := moveNoReplace(source.parentFD, source.base, target.parentFD, target.base); err != nil {
433
+ if errors.Is(err, unix.EEXIST) || errors.Is(err, unix.ENOTEMPTY) {
434
+ return response{Status: "changed", Reason: "destination changed before exclusive move"}, nil
435
+ }
436
+ if errors.Is(err, unix.ENOENT) || errors.Is(err, unix.ENOTDIR) {
437
+ return response{Status: "changed", Reason: "source changed before exclusive move"}, nil
438
+ }
439
+ return response{}, fmt.Errorf("exclusive tree move: %w", err)
440
+ }
441
+ if input.TestPauseAfterTreeMoveMillis > 0 {
442
+ time.Sleep(time.Duration(input.TestPauseAfterTreeMoveMillis) * time.Millisecond)
443
+ }
444
+ if input.TestCrashAfterTreeMove {
445
+ os.Exit(89)
446
+ }
447
+ matches, err = matchesTreeEntryIdentity(
448
+ target.parentFD,
449
+ target.base,
450
+ *input.SourceIdentity,
451
+ )
452
+ if err != nil {
453
+ return response{}, fmt.Errorf("verify exclusive tree move source: %w", err)
454
+ }
455
+ if !matches {
456
+ undoErr := moveNoReplace(
457
+ target.parentFD,
458
+ target.base,
459
+ source.parentFD,
460
+ source.base,
461
+ )
462
+ if undoErr == nil {
463
+ if err := unix.Fsync(source.parentFD); err != nil {
464
+ return response{}, fmt.Errorf("sync undone tree move source parent: %w", err)
465
+ }
466
+ if target.parentFD != source.parentFD {
467
+ if err := unix.Fsync(target.parentFD); err != nil {
468
+ return response{}, fmt.Errorf("sync undone tree move target parent: %w", err)
469
+ }
470
+ }
471
+ return response{
472
+ Status: "changed",
473
+ Reason: "source changed during exclusive move and was restored",
474
+ }, nil
475
+ }
476
+ if errors.Is(undoErr, unix.EEXIST) || errors.Is(undoErr, unix.ENOTEMPTY) {
477
+ return response{
478
+ Status: "changed",
479
+ Reason: "source changed during exclusive move; both entries were retained",
480
+ }, nil
481
+ }
482
+ return response{}, fmt.Errorf("undo changed exclusive tree move: %w", undoErr)
483
+ }
484
+ if err := unix.Fsync(source.parentFD); err != nil {
485
+ return response{}, fmt.Errorf("sync tree move source parent: %w", err)
486
+ }
487
+ if target.parentFD != source.parentFD {
488
+ if err := unix.Fsync(target.parentFD); err != nil {
489
+ return response{}, fmt.Errorf("sync tree move target parent: %w", err)
490
+ }
491
+ }
492
+ return response{Status: "applied"}, nil
493
+ }
494
+
495
+ func openedParentMatches(opened *openedParent, expected treeEntryIdentity) bool {
496
+ return expected.Kind == "folder" &&
497
+ strconv.FormatUint(opened.dev, 10) == expected.Dev &&
498
+ strconv.FormatUint(opened.ino, 10) == expected.Ino
499
+ }
500
+
501
+ func matchesTreeEntryIdentity(
502
+ dirFD int,
503
+ name string,
504
+ expected treeEntryIdentity,
505
+ ) (bool, error) {
506
+ var stat unix.Stat_t
507
+ if err := unix.Fstatat(dirFD, name, &stat, unix.AT_SYMLINK_NOFOLLOW); err != nil {
508
+ return false, err
509
+ }
510
+ var kind string
511
+ switch uint32(stat.Mode) & unix.S_IFMT {
512
+ case unix.S_IFREG:
513
+ kind = "file"
514
+ case unix.S_IFDIR:
515
+ kind = "folder"
516
+ case unix.S_IFLNK:
517
+ kind = "symlink"
518
+ default:
519
+ kind = "other"
520
+ }
521
+ return kind == expected.Kind &&
522
+ strconv.FormatUint(uint64(stat.Dev), 10) == expected.Dev &&
523
+ strconv.FormatUint(uint64(stat.Ino), 10) == expected.Ino, nil
524
+ }
525
+
526
+ func validateIdentity(value identity) error {
527
+ if value.Kind == "directory" {
528
+ if value.Exec || value.Bytes != 0 || value.SHA256 != "" {
529
+ return errors.New("directory identity has unexpected byte fields")
530
+ }
531
+ if _, err := strconv.ParseUint(value.Dev, 10, 64); err != nil {
532
+ return errors.New("directory device identity is invalid")
533
+ }
534
+ if _, err := strconv.ParseUint(value.Ino, 10, 64); err != nil {
535
+ return errors.New("directory inode identity is invalid")
536
+ }
537
+ return nil
538
+ }
539
+ if value.Kind != "file" && value.Kind != "symlink" {
540
+ return errors.New("kind must be file, symlink, or directory")
541
+ }
542
+ if (value.Dev == "") != (value.Ino == "") {
543
+ return errors.New("byte identity must name both device and inode or neither")
544
+ }
545
+ if value.Dev != "" {
546
+ if _, err := strconv.ParseUint(value.Dev, 10, 64); err != nil {
547
+ return errors.New("byte device identity is invalid")
548
+ }
549
+ if _, err := strconv.ParseUint(value.Ino, 10, 64); err != nil {
550
+ return errors.New("byte inode identity is invalid")
551
+ }
552
+ }
553
+ if value.Bytes < 0 || len(value.SHA256) != 64 {
554
+ return errors.New("byte identity is incomplete")
555
+ }
556
+ if _, err := hex.DecodeString(value.SHA256); err != nil {
557
+ return errors.New("sha256 must be hexadecimal")
558
+ }
559
+ return nil
560
+ }
561
+
562
+ func scratchName(id string) string {
563
+ return ".amalgm-apply-" + id
564
+ }
565
+
566
+ const (
567
+ preparedMarker = "prepared"
568
+ deletedMarker = "deleted"
569
+ gitReceiptDir = "amalgm-metadata-apply"
570
+ )
571
+
572
+ func openAbsoluteDirectoryNoFollow(directory string) (int, error) {
573
+ clean := filepath.Clean(directory)
574
+ currentFD, err := unix.Open(
575
+ string(filepath.Separator),
576
+ unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC,
577
+ 0,
578
+ )
579
+ if err != nil {
580
+ return -1, err
581
+ }
582
+ for _, component := range strings.Split(strings.TrimPrefix(clean, string(filepath.Separator)), string(filepath.Separator)) {
583
+ if component == "" {
584
+ continue
585
+ }
586
+ nextFD, openErr := unix.Openat(
587
+ currentFD,
588
+ component,
589
+ unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC,
590
+ 0,
591
+ )
592
+ _ = unix.Close(currentFD)
593
+ if openErr != nil {
594
+ return -1, openErr
595
+ }
596
+ currentFD = nextFD
597
+ }
598
+ return currentFD, nil
599
+ }
600
+
601
+ func openParent(root, relative string, create bool) (*openedParent, error) {
602
+ rootFD, err := openAbsoluteDirectoryNoFollow(root)
603
+ if err != nil {
604
+ return nil, fmt.Errorf("open root: %w", err)
605
+ }
606
+ components := strings.Split(relative, "/")
607
+ currentFD := rootFD
608
+ currentAbs := root
609
+ for _, component := range components[:len(components)-1] {
610
+ nextFD, openErr := unix.Openat(
611
+ currentFD,
612
+ component,
613
+ unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC,
614
+ 0,
615
+ )
616
+ if openErr != nil && errors.Is(openErr, unix.ENOENT) && create {
617
+ if mkdirErr := unix.Mkdirat(currentFD, component, 0o755); mkdirErr != nil &&
618
+ !errors.Is(mkdirErr, unix.EEXIST) {
619
+ closeParentFDs(rootFD, currentFD)
620
+ return nil, fmt.Errorf("create parent %q: %w", component, mkdirErr)
621
+ }
622
+ if syncErr := unix.Fsync(currentFD); syncErr != nil {
623
+ closeParentFDs(rootFD, currentFD)
624
+ return nil, fmt.Errorf("sync parent after mkdir: %w", syncErr)
625
+ }
626
+ nextFD, openErr = unix.Openat(
627
+ currentFD,
628
+ component,
629
+ unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC,
630
+ 0,
631
+ )
632
+ }
633
+ if openErr != nil {
634
+ closeParentFDs(rootFD, currentFD)
635
+ return nil, fmt.Errorf("open parent %q without following links: %w", component, openErr)
636
+ }
637
+ if currentFD != rootFD {
638
+ _ = unix.Close(currentFD)
639
+ }
640
+ currentFD = nextFD
641
+ currentAbs = filepath.Join(currentAbs, component)
642
+ }
643
+ var stat unix.Stat_t
644
+ if err := unix.Fstat(currentFD, &stat); err != nil {
645
+ closeParentFDs(rootFD, currentFD)
646
+ return nil, fmt.Errorf("stat parent: %w", err)
647
+ }
648
+ return &openedParent{
649
+ rootFD: rootFD,
650
+ parentFD: currentFD,
651
+ parentAbs: currentAbs,
652
+ base: components[len(components)-1],
653
+ dev: uint64(stat.Dev),
654
+ ino: uint64(stat.Ino),
655
+ }, nil
656
+ }
657
+
658
+ func verifyRootIdentity(opened *openedParent, input request) error {
659
+ expectedDev, _ := strconv.ParseUint(input.RootDev, 10, 64)
660
+ expectedIno, _ := strconv.ParseUint(input.RootIno, 10, 64)
661
+ var stat unix.Stat_t
662
+ if err := unix.Fstat(opened.rootFD, &stat); err != nil {
663
+ return fmt.Errorf("stat opened root: %w", err)
664
+ }
665
+ if uint64(stat.Dev) != expectedDev || uint64(stat.Ino) != expectedIno {
666
+ return errors.New("workspace root changed before file transaction")
667
+ }
668
+ return nil
669
+ }
670
+
671
+ func closeParentFDs(rootFD, parentFD int) {
672
+ if parentFD >= 0 && parentFD != rootFD {
673
+ _ = unix.Close(parentFD)
674
+ }
675
+ if rootFD >= 0 {
676
+ _ = unix.Close(rootFD)
677
+ }
678
+ }
679
+
680
+ func (opened *openedParent) close() {
681
+ closeParentFDs(opened.rootFD, opened.parentFD)
682
+ opened.rootFD = -1
683
+ opened.parentFD = -1
684
+ }
685
+
686
+ const maxObservedBytes = 16 * 1024 * 1024
687
+
688
+ func readObservedFile(fd int) ([]byte, error) {
689
+ if _, err := unix.Seek(fd, 0, io.SeekStart); err != nil {
690
+ return nil, fmt.Errorf("seek observed file: %w", err)
691
+ }
692
+ content := make([]byte, 0, 64*1024)
693
+ buffer := make([]byte, 64*1024)
694
+ for {
695
+ count, err := unix.Read(fd, buffer)
696
+ if count > 0 {
697
+ if len(content)+count > maxObservedBytes {
698
+ return nil, fmt.Errorf("observed file exceeds %d bytes", maxObservedBytes)
699
+ }
700
+ content = append(content, buffer[:count]...)
701
+ }
702
+ if err != nil {
703
+ if errors.Is(err, unix.EINTR) {
704
+ continue
705
+ }
706
+ return nil, fmt.Errorf("read observed file: %w", err)
707
+ }
708
+ if count == 0 {
709
+ return content, nil
710
+ }
711
+ }
712
+ }
713
+
714
+ func sameObservedStat(left, right unix.Stat_t) bool {
715
+ return left.Dev == right.Dev &&
716
+ left.Ino == right.Ino &&
717
+ left.Mode == right.Mode &&
718
+ left.Size == right.Size &&
719
+ statChangedAt(left) == statChangedAt(right)
720
+ }
721
+
722
+ // observe is the descriptor-relative read boundary. The parent directory and
723
+ // child file stay pinned from identity through bytes, so a rename followed by
724
+ // a replacement at the old spelling can never combine two physical objects
725
+ // into one observation.
726
+ func observe(input request) (response, error) {
727
+ opened, err := openParent(input.Root, input.Path, false)
728
+ if err != nil {
729
+ return response{}, err
730
+ }
731
+ defer opened.close()
732
+ if err := verifyRootIdentity(opened, input); err != nil {
733
+ return response{}, err
734
+ }
735
+ if input.TestPauseAfterParentOpenMillis > 0 {
736
+ time.Sleep(time.Duration(input.TestPauseAfterParentOpenMillis) * time.Millisecond)
737
+ }
738
+
739
+ fd, err := unix.Openat(
740
+ opened.parentFD,
741
+ opened.base,
742
+ unix.O_RDONLY|unix.O_NOFOLLOW|unix.O_CLOEXEC,
743
+ 0,
744
+ )
745
+ if errors.Is(err, unix.ENOENT) {
746
+ return response{
747
+ Status: "observed",
748
+ RootDev: input.RootDev,
749
+ RootIno: input.RootIno,
750
+ Path: input.Path,
751
+ }, nil
752
+ }
753
+ if err != nil {
754
+ return response{}, fmt.Errorf("open observed file: %w", err)
755
+ }
756
+ defer unix.Close(fd)
757
+
758
+ var before unix.Stat_t
759
+ if err := unix.Fstat(fd, &before); err != nil {
760
+ return response{}, fmt.Errorf("stat observed file: %w", err)
761
+ }
762
+ if uint32(before.Mode)&unix.S_IFMT != unix.S_IFREG {
763
+ return response{}, errors.New("observed path is not a regular file")
764
+ }
765
+ first, err := readObservedFile(fd)
766
+ if err != nil {
767
+ return response{}, err
768
+ }
769
+ var middle unix.Stat_t
770
+ if err := unix.Fstat(fd, &middle); err != nil {
771
+ return response{}, fmt.Errorf("restat observed file: %w", err)
772
+ }
773
+ second, err := readObservedFile(fd)
774
+ if err != nil {
775
+ return response{}, err
776
+ }
777
+ var after unix.Stat_t
778
+ if err := unix.Fstat(fd, &after); err != nil {
779
+ return response{}, fmt.Errorf("final stat observed file: %w", err)
780
+ }
781
+ if !sameObservedStat(before, middle) ||
782
+ !sameObservedStat(middle, after) ||
783
+ int64(len(second)) != after.Size ||
784
+ !bytes.Equal(first, second) {
785
+ return response{Status: "changed", Reason: "file changed during observation"}, nil
786
+ }
787
+
788
+ sum := sha256.Sum256(second)
789
+ content := base64.StdEncoding.EncodeToString(second)
790
+ value := &identity{
791
+ Kind: "file",
792
+ Exec: uint32(after.Mode)&0o100 != 0,
793
+ Bytes: int64(len(second)),
794
+ SHA256: hex.EncodeToString(sum[:]),
795
+ Dev: strconv.FormatUint(uint64(after.Dev), 10),
796
+ Ino: strconv.FormatUint(uint64(after.Ino), 10),
797
+ }
798
+ return response{
799
+ Status: "observed",
800
+ RootDev: input.RootDev,
801
+ RootIno: input.RootIno,
802
+ Path: input.Path,
803
+ Value: value,
804
+ Content: &content,
805
+ }, nil
806
+ }
807
+
808
+ func reopenParent(input request, dev, ino uint64) (bool, error) {
809
+ reopened, err := openParent(input.Root, input.Path, false)
810
+ if err != nil {
811
+ return false, nil
812
+ }
813
+ defer reopened.close()
814
+ if err := verifyRootIdentity(reopened, input); err != nil {
815
+ return false, nil
816
+ }
817
+ return reopened.dev == dev && reopened.ino == ino, nil
818
+ }
819
+
820
+ func apply(input request) (response, error) {
821
+ opened, err := openParent(input.Root, input.Path, true)
822
+ if err != nil {
823
+ return response{}, err
824
+ }
825
+ defer opened.close()
826
+ if err := verifyRootIdentity(opened, input); err != nil {
827
+ return response{}, err
828
+ }
829
+ if input.TestPauseAfterParentOpenMillis > 0 {
830
+ time.Sleep(time.Duration(input.TestPauseAfterParentOpenMillis) * time.Millisecond)
831
+ }
832
+ scratch := scratchName(input.ID)
833
+ if err := unix.Mkdirat(opened.parentFD, scratch, 0o700); err != nil {
834
+ return response{}, fmt.Errorf("create transaction scratch: %w", err)
835
+ }
836
+ if err := unix.Fsync(opened.parentFD); err != nil {
837
+ return response{}, fmt.Errorf("sync scratch parent: %w", err)
838
+ }
839
+ scratchFD, err := unix.Openat(
840
+ opened.parentFD,
841
+ scratch,
842
+ unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC,
843
+ 0,
844
+ )
845
+ if err != nil {
846
+ return response{}, fmt.Errorf("open transaction scratch: %w", err)
847
+ }
848
+ defer unix.Close(scratchFD)
849
+
850
+ if input.Target != nil {
851
+ if err := prepareTarget(scratchFD, *input.Target); err != nil {
852
+ return response{}, err
853
+ }
854
+ if err := writeMarker(scratchFD, preparedMarker); err != nil {
855
+ return response{}, fmt.Errorf("record prepared target: %w", err)
856
+ }
857
+ }
858
+
859
+ if input.Expected == nil {
860
+ if held, err := identityAt(opened.parentFD, opened.base); err != nil {
861
+ return response{}, err
862
+ } else if held != nil {
863
+ if cleanupErr := cleanupScratch(opened.parentFD, scratchFD, scratch); cleanupErr != nil {
864
+ return response{}, cleanupErr
865
+ }
866
+ return response{Status: "changed", Reason: "destination appeared before create"}, nil
867
+ }
868
+ } else {
869
+ if err := unix.Renameat(opened.parentFD, opened.base, scratchFD, "source"); err != nil {
870
+ return response{}, fmt.Errorf("extract expected source: %w", err)
871
+ }
872
+ if err := syncBoth(opened.parentFD, scratchFD); err != nil {
873
+ return response{}, err
874
+ }
875
+ if input.TestCrashAfterExtraction {
876
+ os.Exit(86)
877
+ }
878
+ if input.TestPauseAfterExtractionMillis > 0 {
879
+ time.Sleep(time.Duration(input.TestPauseAfterExtractionMillis) * time.Millisecond)
880
+ }
881
+ anchored, err := reopenParent(input, opened.dev, opened.ino)
882
+ if err != nil {
883
+ return response{}, err
884
+ }
885
+ if !anchored {
886
+ return rollbackExtracted(
887
+ opened,
888
+ scratchFD,
889
+ scratch,
890
+ input,
891
+ "repository parent changed during apply",
892
+ )
893
+ }
894
+ writable, err := hasWritableOpen(
895
+ scratchFD,
896
+ "source",
897
+ filepath.Join(opened.parentAbs, scratch, "source"),
898
+ )
899
+ if err != nil {
900
+ return response{}, fmt.Errorf("inspect extracted source writers: %w", err)
901
+ }
902
+ if writable {
903
+ return rollbackExtracted(
904
+ opened,
905
+ scratchFD,
906
+ scratch,
907
+ input,
908
+ "pre-existing writer still owns the source inode",
909
+ )
910
+ }
911
+ held, err := identityAt(scratchFD, "source")
912
+ if err != nil {
913
+ return response{}, err
914
+ }
915
+ if !identitiesEqual(held, input.Expected) {
916
+ return rollbackExtracted(
917
+ opened,
918
+ scratchFD,
919
+ scratch,
920
+ input,
921
+ "source changed before ordered apply",
922
+ )
923
+ }
924
+ }
925
+
926
+ anchored, err := reopenParent(input, opened.dev, opened.ino)
927
+ if err != nil {
928
+ return response{}, err
929
+ }
930
+ if !anchored {
931
+ if input.Expected != nil {
932
+ return rollbackExtracted(
933
+ opened,
934
+ scratchFD,
935
+ scratch,
936
+ input,
937
+ "repository parent changed during apply",
938
+ )
939
+ }
940
+ if err := cleanupScratch(opened.parentFD, scratchFD, scratch); err != nil {
941
+ return response{}, err
942
+ }
943
+ return response{Status: "changed", Reason: "repository parent changed during create"}, nil
944
+ }
945
+ if input.TestPauseBeforeInstallMillis > 0 {
946
+ time.Sleep(time.Duration(input.TestPauseBeforeInstallMillis) * time.Millisecond)
947
+ }
948
+ if input.TestCrashBeforeInstall {
949
+ os.Exit(87)
950
+ }
951
+
952
+ if input.Target != nil {
953
+ if err := moveNoReplace(scratchFD, "target", opened.parentFD, opened.base); err != nil {
954
+ if errors.Is(err, unix.EEXIST) {
955
+ if input.Expected != nil {
956
+ return rollbackExtracted(
957
+ opened,
958
+ scratchFD,
959
+ scratch,
960
+ input,
961
+ "destination changed before exclusive install",
962
+ )
963
+ }
964
+ if cleanupErr := cleanupScratch(opened.parentFD, scratchFD, scratch); cleanupErr != nil {
965
+ return response{}, cleanupErr
966
+ }
967
+ return response{Status: "changed", Reason: "destination changed before exclusive install"}, nil
968
+ }
969
+ return response{}, fmt.Errorf("install target: %w", err)
970
+ }
971
+ if err := syncBoth(opened.parentFD, scratchFD); err != nil {
972
+ return response{}, fmt.Errorf("sync installed target: %w", err)
973
+ }
974
+ if input.TestCrashAfterInstall {
975
+ os.Exit(88)
976
+ }
977
+ }
978
+
979
+ anchored, err = reopenParent(input, opened.dev, opened.ino)
980
+ if err != nil {
981
+ return response{}, err
982
+ }
983
+ if !anchored {
984
+ if input.Expected != nil {
985
+ return restoreSourceOverTarget(
986
+ opened,
987
+ scratchFD,
988
+ scratch,
989
+ input,
990
+ "repository parent changed during target install",
991
+ )
992
+ }
993
+ if input.Target != nil {
994
+ _ = unix.Unlinkat(opened.parentFD, opened.base, 0)
995
+ _ = unix.Fsync(opened.parentFD)
996
+ }
997
+ if err := cleanupScratch(opened.parentFD, scratchFD, scratch); err != nil {
998
+ return response{}, err
999
+ }
1000
+ return response{Status: "changed", Reason: "repository parent changed during target install"}, nil
1001
+ }
1002
+
1003
+ if input.Target == nil {
1004
+ if err := writeMarker(scratchFD, deletedMarker); err != nil {
1005
+ return response{}, fmt.Errorf("record ordered deletion: %w", err)
1006
+ }
1007
+ }
1008
+ // The scratch directory is the receipt. It survives the successful
1009
+ // transaction and is removed only after the caller durably advances its
1010
+ // verified cursor.
1011
+ return response{Status: "applied"}, nil
1012
+ }
1013
+
1014
+ func gitPreparedPath(id string) string {
1015
+ return gitReceiptDir + "/" + id + ".target"
1016
+ }
1017
+
1018
+ func gitClaimedName(id string) string {
1019
+ return id + ".claimed"
1020
+ }
1021
+
1022
+ func gitLockName(input request) string {
1023
+ return input.Path + ".lock"
1024
+ }
1025
+
1026
+ func openGitReceipt(input request) (*openedParent, error) {
1027
+ receipt, err := openParent(input.Root, gitPreparedPath(input.ID), false)
1028
+ if err != nil {
1029
+ return nil, err
1030
+ }
1031
+ if err := verifyRootIdentity(receipt, input); err != nil {
1032
+ receipt.close()
1033
+ return nil, err
1034
+ }
1035
+ return receipt, nil
1036
+ }
1037
+
1038
+ func sameInode(
1039
+ leftFD int,
1040
+ leftName string,
1041
+ rightFD int,
1042
+ rightName string,
1043
+ ) (same bool, rightExists bool, err error) {
1044
+ var left unix.Stat_t
1045
+ if err := unix.Fstatat(leftFD, leftName, &left, unix.AT_SYMLINK_NOFOLLOW); err != nil {
1046
+ return false, false, fmt.Errorf("stat receipt %q: %w", leftName, err)
1047
+ }
1048
+ var right unix.Stat_t
1049
+ if err := unix.Fstatat(rightFD, rightName, &right, unix.AT_SYMLINK_NOFOLLOW); err != nil {
1050
+ if errors.Is(err, unix.ENOENT) {
1051
+ return false, false, nil
1052
+ }
1053
+ return false, false, fmt.Errorf("stat lock %q: %w", rightName, err)
1054
+ }
1055
+ return left.Dev == right.Dev && left.Ino == right.Ino, true, nil
1056
+ }
1057
+
1058
+ func syncEntry(dirFD int, name string) error {
1059
+ fd, err := unix.Openat(
1060
+ dirFD,
1061
+ name,
1062
+ unix.O_RDONLY|unix.O_NOFOLLOW|unix.O_CLOEXEC,
1063
+ 0,
1064
+ )
1065
+ if err != nil {
1066
+ return err
1067
+ }
1068
+ if err := unix.Fsync(fd); err != nil {
1069
+ _ = unix.Close(fd)
1070
+ return err
1071
+ }
1072
+ return unix.Close(fd)
1073
+ }
1074
+
1075
+ func cleanupGitReceipt(receipt *openedParent, input request) error {
1076
+ for _, name := range []string{receipt.base, gitClaimedName(input.ID)} {
1077
+ if err := removeAt(receipt.parentFD, name, 0); err != nil {
1078
+ return fmt.Errorf("remove Git metadata receipt %q: %w", name, err)
1079
+ }
1080
+ }
1081
+ if err := unix.Fsync(receipt.parentFD); err != nil {
1082
+ return fmt.Errorf("sync Git metadata receipt removal: %w", err)
1083
+ }
1084
+ return nil
1085
+ }
1086
+
1087
+ func gitLockCrossed(
1088
+ destination *openedParent,
1089
+ receipt *openedParent,
1090
+ input request,
1091
+ ) (bool, error) {
1092
+ claimed, err := existsAt(receipt.parentFD, gitClaimedName(input.ID))
1093
+ if err != nil {
1094
+ return false, err
1095
+ }
1096
+ if !claimed {
1097
+ return false, nil
1098
+ }
1099
+ same, _, err := sameInode(
1100
+ receipt.parentFD,
1101
+ gitClaimedName(input.ID),
1102
+ destination.parentFD,
1103
+ gitLockName(input),
1104
+ )
1105
+ if err != nil {
1106
+ return false, err
1107
+ }
1108
+ // The claimed hard link survives the canonical lock's rename. A missing
1109
+ // lock, or a later lock with another inode, therefore proves this
1110
+ // operation crossed without consulting the destination's current bytes.
1111
+ return !same, nil
1112
+ }
1113
+
1114
+ func removeOwnedGitLock(
1115
+ destination *openedParent,
1116
+ receipt *openedParent,
1117
+ input request,
1118
+ ) error {
1119
+ lock := gitLockName(input)
1120
+ for _, receiptName := range []string{gitClaimedName(input.ID), receipt.base} {
1121
+ exists, err := existsAt(receipt.parentFD, receiptName)
1122
+ if err != nil {
1123
+ return err
1124
+ }
1125
+ if !exists {
1126
+ continue
1127
+ }
1128
+ same, _, err := sameInode(
1129
+ receipt.parentFD,
1130
+ receiptName,
1131
+ destination.parentFD,
1132
+ lock,
1133
+ )
1134
+ if err != nil {
1135
+ return err
1136
+ }
1137
+ if same {
1138
+ if err := removeAt(destination.parentFD, lock, 0); err != nil {
1139
+ return fmt.Errorf("remove retained Git lock: %w", err)
1140
+ }
1141
+ return unix.Fsync(destination.parentFD)
1142
+ }
1143
+ }
1144
+ return nil
1145
+ }
1146
+
1147
+ // applyGitLock uses the same lock name Git itself uses. The target is prepared
1148
+ // durably in the per-worktree Git directory before this process starts. An
1149
+ // exclusive hard link claims index.lock/HEAD.lock without copying large index
1150
+ // bytes through JSON. Renaming that lock over the destination is the one
1151
+ // crossing step; the retained hard link is its operation-owned receipt.
1152
+ func applyGitLock(input request) (response, error) {
1153
+ destination, err := openParent(input.Root, input.Path, false)
1154
+ if err != nil {
1155
+ return response{}, err
1156
+ }
1157
+ defer destination.close()
1158
+ if err := verifyRootIdentity(destination, input); err != nil {
1159
+ return response{}, err
1160
+ }
1161
+ receipt, err := openGitReceipt(input)
1162
+ if err != nil {
1163
+ return response{}, fmt.Errorf("open prepared Git metadata: %w", err)
1164
+ }
1165
+ defer receipt.close()
1166
+
1167
+ prepared, err := identityAt(receipt.parentFD, receipt.base)
1168
+ if err != nil {
1169
+ return response{}, err
1170
+ }
1171
+ targetIdentity, err := requestedTargetIdentity(input.Target)
1172
+ if err != nil {
1173
+ return response{}, err
1174
+ }
1175
+ if !identitiesEqual(prepared, targetIdentity) {
1176
+ return response{}, errors.New("prepared Git metadata does not match target identity")
1177
+ }
1178
+
1179
+ lock := gitLockName(input)
1180
+ if err := unix.Linkat(
1181
+ receipt.parentFD,
1182
+ receipt.base,
1183
+ destination.parentFD,
1184
+ lock,
1185
+ 0,
1186
+ ); err != nil {
1187
+ if errors.Is(err, unix.EEXIST) {
1188
+ return response{Status: "pending", Reason: "Git metadata lock is already held"}, nil
1189
+ }
1190
+ return response{}, fmt.Errorf("claim Git metadata lock: %w", err)
1191
+ }
1192
+ if err := syncEntry(destination.parentFD, lock); err != nil {
1193
+ return response{}, fmt.Errorf("sync Git metadata lock: %w", err)
1194
+ }
1195
+ if err := unix.Fsync(destination.parentFD); err != nil {
1196
+ return response{}, fmt.Errorf("sync claimed Git metadata lock: %w", err)
1197
+ }
1198
+
1199
+ current, err := identityAt(destination.parentFD, destination.base)
1200
+ if err != nil {
1201
+ return response{}, err
1202
+ }
1203
+ if !identitiesEqual(current, input.Expected) {
1204
+ if err := removeOwnedGitLock(destination, receipt, input); err != nil {
1205
+ return response{}, err
1206
+ }
1207
+ if err := cleanupGitReceipt(receipt, input); err != nil {
1208
+ return response{}, err
1209
+ }
1210
+ return response{Status: "changed", Reason: "Git metadata changed before ordered apply"}, nil
1211
+ }
1212
+
1213
+ if err := unix.Linkat(
1214
+ destination.parentFD,
1215
+ lock,
1216
+ receipt.parentFD,
1217
+ gitClaimedName(input.ID),
1218
+ 0,
1219
+ ); err != nil {
1220
+ return response{}, fmt.Errorf("record claimed Git metadata lock: %w", err)
1221
+ }
1222
+ if err := unix.Fsync(receipt.parentFD); err != nil {
1223
+ return response{}, fmt.Errorf("sync claimed Git metadata receipt: %w", err)
1224
+ }
1225
+ if input.TestCrashBeforeInstall {
1226
+ os.Exit(87)
1227
+ }
1228
+ if err := unix.Renameat(
1229
+ destination.parentFD,
1230
+ lock,
1231
+ destination.parentFD,
1232
+ destination.base,
1233
+ ); err != nil {
1234
+ return response{}, fmt.Errorf("install Git metadata target: %w", err)
1235
+ }
1236
+ if err := unix.Fsync(destination.parentFD); err != nil {
1237
+ return response{}, fmt.Errorf("sync installed Git metadata: %w", err)
1238
+ }
1239
+ if input.TestCrashAfterInstall {
1240
+ os.Exit(88)
1241
+ }
1242
+ return response{Status: "applied"}, nil
1243
+ }
1244
+
1245
+ // guardGitLock retains Git's canonical HEAD.lock while a branch-ref
1246
+ // transaction runs. Exact HEAD bytes are verified only after the lock is
1247
+ // owned, so no checkout or symbolic-ref change can enter between that proof
1248
+ // and the guarded ref movement.
1249
+ func guardGitLock(input request) (response, error) {
1250
+ destination, err := openParent(input.Root, input.Path, false)
1251
+ if err != nil {
1252
+ return response{}, err
1253
+ }
1254
+ defer destination.close()
1255
+ if err := verifyRootIdentity(destination, input); err != nil {
1256
+ return response{}, err
1257
+ }
1258
+ receipt, err := openGitReceipt(input)
1259
+ if err != nil {
1260
+ return response{}, fmt.Errorf("open prepared Git guard: %w", err)
1261
+ }
1262
+ defer receipt.close()
1263
+
1264
+ prepared, err := identityAt(receipt.parentFD, receipt.base)
1265
+ if err != nil {
1266
+ return response{}, err
1267
+ }
1268
+ targetIdentity, err := requestedTargetIdentity(input.Target)
1269
+ if err != nil {
1270
+ return response{}, err
1271
+ }
1272
+ if !identitiesEqual(prepared, targetIdentity) {
1273
+ return response{}, errors.New("prepared Git guard does not match expected HEAD")
1274
+ }
1275
+
1276
+ lock := gitLockName(input)
1277
+ if err := unix.Linkat(
1278
+ receipt.parentFD,
1279
+ receipt.base,
1280
+ destination.parentFD,
1281
+ lock,
1282
+ 0,
1283
+ ); err != nil {
1284
+ if errors.Is(err, unix.EEXIST) {
1285
+ return response{Status: "pending", Reason: "Git HEAD lock is already held"}, nil
1286
+ }
1287
+ return response{}, fmt.Errorf("claim Git HEAD guard: %w", err)
1288
+ }
1289
+ if err := syncEntry(destination.parentFD, lock); err != nil {
1290
+ return response{}, fmt.Errorf("sync Git HEAD guard: %w", err)
1291
+ }
1292
+ if err := unix.Fsync(destination.parentFD); err != nil {
1293
+ return response{}, fmt.Errorf("sync claimed Git HEAD guard: %w", err)
1294
+ }
1295
+ if err := unix.Linkat(
1296
+ destination.parentFD,
1297
+ lock,
1298
+ receipt.parentFD,
1299
+ gitClaimedName(input.ID),
1300
+ 0,
1301
+ ); err != nil {
1302
+ return response{}, fmt.Errorf("record claimed Git HEAD guard: %w", err)
1303
+ }
1304
+ if err := unix.Fsync(receipt.parentFD); err != nil {
1305
+ return response{}, fmt.Errorf("sync claimed Git HEAD guard receipt: %w", err)
1306
+ }
1307
+
1308
+ current, err := identityAt(destination.parentFD, destination.base)
1309
+ if err != nil {
1310
+ return response{}, err
1311
+ }
1312
+ if !identitiesEqual(current, input.Expected) {
1313
+ if err := removeOwnedGitLock(destination, receipt, input); err != nil {
1314
+ return response{}, err
1315
+ }
1316
+ if err := cleanupGitReceipt(receipt, input); err != nil {
1317
+ return response{}, err
1318
+ }
1319
+ return response{Status: "changed", Reason: "HEAD changed before branch guard"}, nil
1320
+ }
1321
+ return response{Status: "guarded"}, nil
1322
+ }
1323
+
1324
+ func releaseGitGuard(input request) (response, error) {
1325
+ destination, err := openParent(input.Root, input.Path, false)
1326
+ if err != nil {
1327
+ return response{Status: "released"}, nil
1328
+ }
1329
+ defer destination.close()
1330
+ if err := verifyRootIdentity(destination, input); err != nil {
1331
+ return response{}, err
1332
+ }
1333
+ receipt, err := openGitReceipt(input)
1334
+ if err != nil {
1335
+ return response{Status: "released"}, nil
1336
+ }
1337
+ defer receipt.close()
1338
+ if err := removeOwnedGitLock(destination, receipt, input); err != nil {
1339
+ return response{}, err
1340
+ }
1341
+ if err := cleanupGitReceipt(receipt, input); err != nil {
1342
+ return response{}, err
1343
+ }
1344
+ return response{Status: "released"}, nil
1345
+ }
1346
+
1347
+ // guardGitWorktreeNamespace atomically exchanges the common `worktrees`
1348
+ // directory with an operation-owned regular file. Stock Git therefore
1349
+ // cannot register a new linked worktree until the deletion transaction
1350
+ // finishes, while the complete original directory remains recoverable at
1351
+ // the prepared receipt path.
1352
+ func guardGitWorktreeNamespace(input request) (response, error) {
1353
+ destination, err := openParent(input.Root, input.Path, false)
1354
+ if err != nil {
1355
+ return response{}, err
1356
+ }
1357
+ defer destination.close()
1358
+ if err := verifyRootIdentity(destination, input); err != nil {
1359
+ return response{}, err
1360
+ }
1361
+ receipt, err := openGitReceipt(input)
1362
+ if err != nil {
1363
+ return response{}, fmt.Errorf("open prepared worktree namespace guard: %w", err)
1364
+ }
1365
+ defer receipt.close()
1366
+
1367
+ current, err := identityAt(destination.parentFD, destination.base)
1368
+ if err != nil {
1369
+ return response{}, err
1370
+ }
1371
+ if !identitiesEqual(current, input.Expected) {
1372
+ return response{Status: "changed", Reason: "Git worktree namespace changed before guard"}, nil
1373
+ }
1374
+ prepared, err := identityAt(receipt.parentFD, receipt.base)
1375
+ if err != nil {
1376
+ return response{}, err
1377
+ }
1378
+ targetIdentity, err := requestedTargetIdentity(input.Target)
1379
+ if err != nil {
1380
+ return response{}, err
1381
+ }
1382
+ if !identitiesEqual(prepared, targetIdentity) {
1383
+ return response{}, errors.New("prepared worktree namespace marker does not match target")
1384
+ }
1385
+
1386
+ if err := unix.Linkat(
1387
+ receipt.parentFD,
1388
+ receipt.base,
1389
+ receipt.parentFD,
1390
+ gitClaimedName(input.ID),
1391
+ 0,
1392
+ ); err != nil {
1393
+ if !errors.Is(err, unix.EEXIST) {
1394
+ return response{}, fmt.Errorf("record claimed worktree namespace guard: %w", err)
1395
+ }
1396
+ same, _, sameErr := sameInode(
1397
+ receipt.parentFD,
1398
+ receipt.base,
1399
+ receipt.parentFD,
1400
+ gitClaimedName(input.ID),
1401
+ )
1402
+ if sameErr != nil {
1403
+ return response{}, sameErr
1404
+ }
1405
+ if !same {
1406
+ return response{}, errors.New("worktree namespace guard claim belongs to another inode")
1407
+ }
1408
+ }
1409
+ if err := unix.Fsync(receipt.parentFD); err != nil {
1410
+ return response{}, fmt.Errorf("sync claimed worktree namespace guard: %w", err)
1411
+ }
1412
+ if err := exchangeEntries(
1413
+ destination.parentFD,
1414
+ destination.base,
1415
+ receipt.parentFD,
1416
+ receipt.base,
1417
+ ); err != nil {
1418
+ return response{}, fmt.Errorf("guard Git worktree namespace: %w", err)
1419
+ }
1420
+ if err := unix.Fsync(destination.parentFD); err != nil {
1421
+ return response{}, fmt.Errorf("sync guarded Git worktree namespace: %w", err)
1422
+ }
1423
+ if err := unix.Fsync(receipt.parentFD); err != nil {
1424
+ return response{}, fmt.Errorf("sync retained Git worktree namespace: %w", err)
1425
+ }
1426
+ same, _, err := sameInode(
1427
+ receipt.parentFD,
1428
+ gitClaimedName(input.ID),
1429
+ destination.parentFD,
1430
+ destination.base,
1431
+ )
1432
+ if err != nil {
1433
+ return response{}, err
1434
+ }
1435
+ if !same {
1436
+ return response{}, errors.New("Git worktree namespace guard did not retain its marker")
1437
+ }
1438
+ return response{Status: "guarded"}, nil
1439
+ }
1440
+
1441
+ func releaseGitWorktreeNamespace(input request) (response, error) {
1442
+ destination, err := openParent(input.Root, input.Path, false)
1443
+ if err != nil {
1444
+ return response{}, fmt.Errorf("open guarded worktree namespace: %w", err)
1445
+ }
1446
+ defer destination.close()
1447
+ if err := verifyRootIdentity(destination, input); err != nil {
1448
+ return response{}, err
1449
+ }
1450
+ receipt, err := openGitReceipt(input)
1451
+ if err != nil {
1452
+ return response{Status: "released"}, nil
1453
+ }
1454
+ defer receipt.close()
1455
+ claimed, err := existsAt(receipt.parentFD, gitClaimedName(input.ID))
1456
+ if err != nil {
1457
+ return response{}, err
1458
+ }
1459
+ prepared, err := existsAt(receipt.parentFD, receipt.base)
1460
+ if err != nil {
1461
+ return response{}, err
1462
+ }
1463
+ if !claimed && !prepared {
1464
+ return response{Status: "released"}, nil
1465
+ }
1466
+ if !claimed && prepared {
1467
+ if err := cleanupGitReceipt(receipt, input); err != nil {
1468
+ return response{}, err
1469
+ }
1470
+ return response{Status: "released"}, nil
1471
+ }
1472
+ if !prepared {
1473
+ current, err := identityAt(destination.parentFD, destination.base)
1474
+ if err != nil {
1475
+ return response{}, err
1476
+ }
1477
+ if !identitiesEqual(current, input.Expected) {
1478
+ return response{}, errors.New("worktree namespace guard receipt is incomplete")
1479
+ }
1480
+ if err := cleanupGitReceipt(receipt, input); err != nil {
1481
+ return response{}, err
1482
+ }
1483
+ return response{Status: "released"}, nil
1484
+ }
1485
+ guarded, _, err := sameInode(
1486
+ receipt.parentFD,
1487
+ gitClaimedName(input.ID),
1488
+ destination.parentFD,
1489
+ destination.base,
1490
+ )
1491
+ if err != nil {
1492
+ return response{}, err
1493
+ }
1494
+ if guarded {
1495
+ retained, err := identityAt(receipt.parentFD, receipt.base)
1496
+ if err != nil {
1497
+ return response{}, err
1498
+ }
1499
+ if retained == nil || retained.Kind != "directory" {
1500
+ return response{}, errors.New("retained Git worktree namespace is not a directory")
1501
+ }
1502
+ if err := exchangeEntries(
1503
+ destination.parentFD,
1504
+ destination.base,
1505
+ receipt.parentFD,
1506
+ receipt.base,
1507
+ ); err != nil {
1508
+ return response{}, fmt.Errorf("restore Git worktree namespace: %w", err)
1509
+ }
1510
+ if err := unix.Fsync(destination.parentFD); err != nil {
1511
+ return response{}, fmt.Errorf("sync restored Git worktree namespace: %w", err)
1512
+ }
1513
+ if err := unix.Fsync(receipt.parentFD); err != nil {
1514
+ return response{}, fmt.Errorf("sync released worktree namespace marker: %w", err)
1515
+ }
1516
+ } else {
1517
+ current, err := identityAt(destination.parentFD, destination.base)
1518
+ if err != nil {
1519
+ return response{}, err
1520
+ }
1521
+ retainedMarker, _, err := sameInode(
1522
+ receipt.parentFD,
1523
+ gitClaimedName(input.ID),
1524
+ receipt.parentFD,
1525
+ receipt.base,
1526
+ )
1527
+ if err != nil {
1528
+ return response{}, err
1529
+ }
1530
+ if current == nil || current.Kind != "directory" || !retainedMarker {
1531
+ return response{}, errors.New("Git worktree namespace changed outside its guard")
1532
+ }
1533
+ }
1534
+ if err := cleanupGitReceipt(receipt, input); err != nil {
1535
+ return response{}, err
1536
+ }
1537
+ return response{Status: "released"}, nil
1538
+ }
1539
+
1540
+ func recoverGitLock(input request) (response, error) {
1541
+ destination, err := openParent(input.Root, input.Path, false)
1542
+ if err != nil {
1543
+ return response{Status: "pending", Reason: "Git metadata path is not safely reachable"}, nil
1544
+ }
1545
+ defer destination.close()
1546
+ if err := verifyRootIdentity(destination, input); err != nil {
1547
+ return response{Status: "pending", Reason: err.Error()}, nil
1548
+ }
1549
+ receipt, err := openGitReceipt(input)
1550
+ if err != nil {
1551
+ return response{Status: "changed", Reason: "operation has no Git metadata receipt"}, nil
1552
+ }
1553
+ defer receipt.close()
1554
+ crossed, err := gitLockCrossed(destination, receipt, input)
1555
+ if err != nil {
1556
+ return response{}, err
1557
+ }
1558
+ if crossed {
1559
+ return response{Status: "applied"}, nil
1560
+ }
1561
+ if err := removeOwnedGitLock(destination, receipt, input); err != nil {
1562
+ return response{}, err
1563
+ }
1564
+ if err := cleanupGitReceipt(receipt, input); err != nil {
1565
+ return response{}, err
1566
+ }
1567
+ return response{Status: "changed", Reason: "Git metadata operation stopped before crossing"}, nil
1568
+ }
1569
+
1570
+ func readGitLockReceipt(input request) (response, error) {
1571
+ destination, err := openParent(input.Root, input.Path, false)
1572
+ if err != nil {
1573
+ return response{Status: "not-crossed"}, nil
1574
+ }
1575
+ defer destination.close()
1576
+ if err := verifyRootIdentity(destination, input); err != nil {
1577
+ return response{Status: "not-crossed"}, nil
1578
+ }
1579
+ receipt, err := openGitReceipt(input)
1580
+ if err != nil {
1581
+ return response{Status: "not-crossed"}, nil
1582
+ }
1583
+ defer receipt.close()
1584
+ crossed, err := gitLockCrossed(destination, receipt, input)
1585
+ if err != nil {
1586
+ return response{}, err
1587
+ }
1588
+ if crossed {
1589
+ return response{Status: "crossed"}, nil
1590
+ }
1591
+ return response{Status: "not-crossed"}, nil
1592
+ }
1593
+
1594
+ func releaseGitLockReceipt(input request) (response, error) {
1595
+ destination, err := openParent(input.Root, input.Path, false)
1596
+ if err != nil {
1597
+ return response{Status: "released"}, nil
1598
+ }
1599
+ defer destination.close()
1600
+ if err := verifyRootIdentity(destination, input); err != nil {
1601
+ return response{}, err
1602
+ }
1603
+ receipt, err := openGitReceipt(input)
1604
+ if err != nil {
1605
+ return response{Status: "released"}, nil
1606
+ }
1607
+ defer receipt.close()
1608
+ prepared, err := existsAt(receipt.parentFD, receipt.base)
1609
+ if err != nil {
1610
+ return response{}, err
1611
+ }
1612
+ claimed, err := existsAt(receipt.parentFD, gitClaimedName(input.ID))
1613
+ if err != nil {
1614
+ return response{}, err
1615
+ }
1616
+ // Native cleanup is durable before the JavaScript control record is
1617
+ // removed. A crash in that final gap leaves neither artifact; release is
1618
+ // therefore already complete and must remain idempotent.
1619
+ if !prepared && !claimed {
1620
+ return response{Status: "released"}, nil
1621
+ }
1622
+ crossed, err := gitLockCrossed(destination, receipt, input)
1623
+ if err != nil {
1624
+ return response{}, err
1625
+ }
1626
+ if !crossed {
1627
+ return response{Status: "pending", Reason: "operation has no Git metadata crossing receipt"}, nil
1628
+ }
1629
+ if err := cleanupGitReceipt(receipt, input); err != nil {
1630
+ return response{}, err
1631
+ }
1632
+ return response{Status: "released"}, nil
1633
+ }
1634
+
1635
+ func rollbackExtracted(
1636
+ opened *openedParent,
1637
+ scratchFD int,
1638
+ scratch string,
1639
+ input request,
1640
+ reason string,
1641
+ ) (response, error) {
1642
+ if err := linkEntry(scratchFD, "source", opened.parentFD, opened.base); err != nil {
1643
+ if errors.Is(err, unix.EEXIST) {
1644
+ return response{Status: "pending", Reason: reason + "; a newer destination also exists"}, nil
1645
+ }
1646
+ return response{}, fmt.Errorf("restore extracted source: %w", err)
1647
+ }
1648
+ if err := unix.Fsync(opened.parentFD); err != nil {
1649
+ return response{}, fmt.Errorf("sync restored source: %w", err)
1650
+ }
1651
+ if err := cleanupScratch(opened.parentFD, scratchFD, scratch); err != nil {
1652
+ return response{}, err
1653
+ }
1654
+ return response{Status: "changed", Reason: reason}, nil
1655
+ }
1656
+
1657
+ func restoreSourceOverTarget(
1658
+ opened *openedParent,
1659
+ scratchFD int,
1660
+ scratch string,
1661
+ input request,
1662
+ reason string,
1663
+ ) (response, error) {
1664
+ current, err := identityAt(opened.parentFD, opened.base)
1665
+ if err != nil {
1666
+ return response{}, err
1667
+ }
1668
+ targetIdentity, err := requestedTargetIdentity(input.Target)
1669
+ if err != nil {
1670
+ return response{}, err
1671
+ }
1672
+ if current != nil {
1673
+ if !identitiesEqual(current, targetIdentity) {
1674
+ return response{Status: "pending", Reason: reason + "; a newer destination also exists"}, nil
1675
+ }
1676
+ if err := unix.Renameat(opened.parentFD, opened.base, scratchFD, "current"); err != nil {
1677
+ return response{}, fmt.Errorf("retain installed target during rollback: %w", err)
1678
+ }
1679
+ }
1680
+ if err := linkEntry(scratchFD, "source", opened.parentFD, opened.base); err != nil {
1681
+ if current != nil {
1682
+ _ = unix.Renameat(scratchFD, "current", opened.parentFD, opened.base)
1683
+ }
1684
+ return response{}, fmt.Errorf("restore source over target: %w", err)
1685
+ }
1686
+ if err := unix.Fsync(opened.parentFD); err != nil {
1687
+ return response{}, fmt.Errorf("sync source rollback: %w", err)
1688
+ }
1689
+ if err := cleanupScratch(opened.parentFD, scratchFD, scratch); err != nil {
1690
+ return response{}, err
1691
+ }
1692
+ return response{Status: "changed", Reason: reason}, nil
1693
+ }
1694
+
1695
+ func recoverTransaction(input request) (response, error) {
1696
+ opened, err := openParent(input.Root, input.Path, false)
1697
+ if err != nil {
1698
+ return response{Status: "pending", Reason: "recorded parent is not safely reachable"}, nil
1699
+ }
1700
+ defer opened.close()
1701
+ if err := verifyRootIdentity(opened, input); err != nil {
1702
+ return response{Status: "pending", Reason: err.Error()}, nil
1703
+ }
1704
+ scratch := scratchName(input.ID)
1705
+ scratchFD, err := unix.Openat(
1706
+ opened.parentFD,
1707
+ scratch,
1708
+ unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC,
1709
+ 0,
1710
+ )
1711
+ if errors.Is(err, unix.ENOENT) {
1712
+ return classifyWithoutScratch(opened, input)
1713
+ }
1714
+ if err != nil {
1715
+ return response{}, fmt.Errorf("open retained transaction scratch: %w", err)
1716
+ }
1717
+ defer unix.Close(scratchFD)
1718
+
1719
+ crossed, err := transactionCrossed(scratchFD, input)
1720
+ if err != nil {
1721
+ return response{}, err
1722
+ }
1723
+ if crossed {
1724
+ return response{Status: "applied"}, nil
1725
+ }
1726
+
1727
+ source, err := identityAt(scratchFD, "source")
1728
+ if err != nil {
1729
+ return response{}, err
1730
+ }
1731
+ current, err := identityAt(opened.parentFD, opened.base)
1732
+ if err != nil {
1733
+ return response{}, err
1734
+ }
1735
+ targetIdentity, err := requestedTargetIdentity(input.Target)
1736
+ if err != nil {
1737
+ return response{}, err
1738
+ }
1739
+
1740
+ if source != nil {
1741
+ writable, err := hasWritableOpen(
1742
+ scratchFD,
1743
+ "source",
1744
+ filepath.Join(opened.parentAbs, scratch, "source"),
1745
+ )
1746
+ if err != nil {
1747
+ return response{}, err
1748
+ }
1749
+ sourceIsExpected := identitiesEqual(source, input.Expected)
1750
+ if writable || !sourceIsExpected {
1751
+ if current == nil {
1752
+ return rollbackExtracted(
1753
+ opened,
1754
+ scratchFD,
1755
+ scratch,
1756
+ input,
1757
+ "retained source is local or still writable",
1758
+ )
1759
+ }
1760
+ if identitiesEqual(current, targetIdentity) {
1761
+ return restoreSourceOverTarget(
1762
+ opened,
1763
+ scratchFD,
1764
+ scratch,
1765
+ input,
1766
+ "retained source is local or still writable",
1767
+ )
1768
+ }
1769
+ if !writable {
1770
+ sourceTime, sourceTimeErr := entryChangedAt(scratchFD, "source")
1771
+ if sourceTimeErr != nil {
1772
+ return response{}, sourceTimeErr
1773
+ }
1774
+ currentTime, currentTimeErr := entryChangedAt(opened.parentFD, opened.base)
1775
+ if currentTimeErr != nil {
1776
+ return response{}, currentTimeErr
1777
+ }
1778
+ if sourceTime > currentTime {
1779
+ return replaceCurrentWithSource(
1780
+ opened,
1781
+ scratchFD,
1782
+ scratch,
1783
+ "retained source is the later local write",
1784
+ )
1785
+ }
1786
+ if err := cleanupScratch(opened.parentFD, scratchFD, scratch); err != nil {
1787
+ return response{}, err
1788
+ }
1789
+ return response{
1790
+ Status: "changed",
1791
+ Reason: "visible destination is the later local write",
1792
+ }, nil
1793
+ }
1794
+ return response{
1795
+ Status: "pending",
1796
+ Reason: "retained source and visible destination are both local",
1797
+ }, nil
1798
+ }
1799
+ if identitiesEqual(current, targetIdentity) || (input.Target == nil && current == nil) {
1800
+ if err := cleanupScratch(opened.parentFD, scratchFD, scratch); err != nil {
1801
+ return response{}, err
1802
+ }
1803
+ return response{Status: "applied"}, nil
1804
+ }
1805
+ if current == nil {
1806
+ return rollbackExtracted(
1807
+ opened,
1808
+ scratchFD,
1809
+ scratch,
1810
+ input,
1811
+ "apply stopped before target installation",
1812
+ )
1813
+ }
1814
+ if err := cleanupScratch(opened.parentFD, scratchFD, scratch); err != nil {
1815
+ return response{}, err
1816
+ }
1817
+ return response{Status: "changed", Reason: "newer visible destination won interrupted apply"}, nil
1818
+ }
1819
+
1820
+ if identitiesEqual(current, targetIdentity) || (input.Target == nil && current == nil) {
1821
+ if err := cleanupScratch(opened.parentFD, scratchFD, scratch); err != nil {
1822
+ return response{}, err
1823
+ }
1824
+ return response{Status: "applied"}, nil
1825
+ }
1826
+ if err := cleanupScratch(opened.parentFD, scratchFD, scratch); err != nil {
1827
+ return response{}, err
1828
+ }
1829
+ return response{Status: "changed", Reason: "visible destination superseded interrupted apply"}, nil
1830
+ }
1831
+
1832
+ func readReceipt(input request) (response, error) {
1833
+ opened, err := openParent(input.Root, input.Path, false)
1834
+ if err != nil {
1835
+ return response{Status: "not-crossed"}, nil
1836
+ }
1837
+ defer opened.close()
1838
+ if err := verifyRootIdentity(opened, input); err != nil {
1839
+ return response{Status: "not-crossed"}, nil
1840
+ }
1841
+ scratchFD, err := unix.Openat(
1842
+ opened.parentFD,
1843
+ scratchName(input.ID),
1844
+ unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC,
1845
+ 0,
1846
+ )
1847
+ if errors.Is(err, unix.ENOENT) {
1848
+ return response{Status: "not-crossed"}, nil
1849
+ }
1850
+ if err != nil {
1851
+ return response{}, fmt.Errorf("open receipt artifact: %w", err)
1852
+ }
1853
+ defer unix.Close(scratchFD)
1854
+ crossed, err := transactionCrossed(scratchFD, input)
1855
+ if err != nil {
1856
+ return response{}, err
1857
+ }
1858
+ if crossed {
1859
+ return response{Status: "crossed"}, nil
1860
+ }
1861
+ return response{Status: "not-crossed"}, nil
1862
+ }
1863
+
1864
+ func releaseReceipt(input request) (response, error) {
1865
+ opened, err := openParent(input.Root, input.Path, false)
1866
+ if err != nil {
1867
+ // A missing locator does not prove the operation-owned receipt is
1868
+ // absent. The physical parent may merely have moved and will be rebound
1869
+ // by the durable document plan. Keep that plan until the exact parent
1870
+ // can be reopened and the receipt is inspected.
1871
+ return response{
1872
+ Status: "pending",
1873
+ Reason: fmt.Sprintf("recorded receipt parent is unavailable: %v", err),
1874
+ }, nil
1875
+ }
1876
+ defer opened.close()
1877
+ if err := verifyRootIdentity(opened, input); err != nil {
1878
+ return response{}, err
1879
+ }
1880
+ scratch := scratchName(input.ID)
1881
+ scratchFD, err := unix.Openat(
1882
+ opened.parentFD,
1883
+ scratch,
1884
+ unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC,
1885
+ 0,
1886
+ )
1887
+ if errors.Is(err, unix.ENOENT) {
1888
+ return response{Status: "released"}, nil
1889
+ }
1890
+ if err != nil {
1891
+ return response{}, fmt.Errorf("open receipt artifact: %w", err)
1892
+ }
1893
+ defer unix.Close(scratchFD)
1894
+ crossed, err := transactionCrossed(scratchFD, input)
1895
+ if err != nil {
1896
+ return response{}, err
1897
+ }
1898
+ if !crossed {
1899
+ return response{Status: "pending", Reason: "operation has no crossing receipt"}, nil
1900
+ }
1901
+ if err := cleanupScratch(opened.parentFD, scratchFD, scratch); err != nil {
1902
+ return response{}, err
1903
+ }
1904
+ return response{Status: "released"}, nil
1905
+ }
1906
+
1907
+ func transactionCrossed(scratchFD int, input request) (bool, error) {
1908
+ if input.Target == nil {
1909
+ deleted, err := existsAt(scratchFD, deletedMarker)
1910
+ if err != nil {
1911
+ return false, err
1912
+ }
1913
+ source, err := existsAt(scratchFD, "source")
1914
+ if err != nil {
1915
+ return false, err
1916
+ }
1917
+ return deleted && source, nil
1918
+ }
1919
+ prepared, err := existsAt(scratchFD, preparedMarker)
1920
+ if err != nil {
1921
+ return false, err
1922
+ }
1923
+ target, err := existsAt(scratchFD, "target")
1924
+ if err != nil {
1925
+ return false, err
1926
+ }
1927
+ return prepared && !target, nil
1928
+ }
1929
+
1930
+ func replaceCurrentWithSource(
1931
+ opened *openedParent,
1932
+ scratchFD int,
1933
+ scratch string,
1934
+ reason string,
1935
+ ) (response, error) {
1936
+ if err := unix.Renameat(opened.parentFD, opened.base, scratchFD, "current"); err != nil {
1937
+ return response{}, fmt.Errorf("retain older visible destination: %w", err)
1938
+ }
1939
+ if err := linkEntry(scratchFD, "source", opened.parentFD, opened.base); err != nil {
1940
+ _ = unix.Renameat(scratchFD, "current", opened.parentFD, opened.base)
1941
+ return response{}, fmt.Errorf("restore later retained source: %w", err)
1942
+ }
1943
+ if err := unix.Fsync(opened.parentFD); err != nil {
1944
+ return response{}, fmt.Errorf("sync later retained source: %w", err)
1945
+ }
1946
+ if err := cleanupScratch(opened.parentFD, scratchFD, scratch); err != nil {
1947
+ return response{}, err
1948
+ }
1949
+ return response{Status: "changed", Reason: reason}, nil
1950
+ }
1951
+
1952
+ func classifyWithoutScratch(opened *openedParent, input request) (response, error) {
1953
+ return response{
1954
+ Status: "changed",
1955
+ Reason: "transaction has no operation-owned crossing receipt",
1956
+ }, nil
1957
+ }
1958
+
1959
+ func requestedTargetIdentity(value *target) (*identity, error) {
1960
+ if value == nil {
1961
+ return nil, nil
1962
+ }
1963
+ return &identity{
1964
+ Kind: value.Kind,
1965
+ Exec: value.Kind == "file" && value.Exec,
1966
+ Bytes: value.Bytes,
1967
+ SHA256: value.SHA256,
1968
+ }, nil
1969
+ }
1970
+
1971
+ func prepareTarget(scratchFD int, value target) error {
1972
+ if value.Content == nil {
1973
+ return errors.New("apply target content is missing")
1974
+ }
1975
+ content, err := base64.StdEncoding.DecodeString(*value.Content)
1976
+ if err != nil {
1977
+ return err
1978
+ }
1979
+ if value.Kind == "symlink" {
1980
+ if bytes.IndexByte(content, 0) >= 0 {
1981
+ return errors.New("symlink target contains NUL")
1982
+ }
1983
+ if err := unix.Symlinkat(string(content), scratchFD, "target"); err != nil {
1984
+ return fmt.Errorf("prepare target symlink: %w", err)
1985
+ }
1986
+ return unix.Fsync(scratchFD)
1987
+ }
1988
+ mode := uint32(0o644)
1989
+ if value.Exec {
1990
+ mode = 0o755
1991
+ }
1992
+ fd, err := unix.Openat(
1993
+ scratchFD,
1994
+ "target",
1995
+ unix.O_WRONLY|unix.O_CREAT|unix.O_EXCL|unix.O_NOFOLLOW|unix.O_CLOEXEC,
1996
+ mode,
1997
+ )
1998
+ if err != nil {
1999
+ return fmt.Errorf("prepare target file: %w", err)
2000
+ }
2001
+ file := os.NewFile(uintptr(fd), "target")
2002
+ if _, err := file.Write(content); err != nil {
2003
+ _ = file.Close()
2004
+ return fmt.Errorf("write target: %w", err)
2005
+ }
2006
+ if err := unix.Fchmod(fd, mode); err != nil {
2007
+ _ = file.Close()
2008
+ return fmt.Errorf("chmod target: %w", err)
2009
+ }
2010
+ if err := unix.Fsync(fd); err != nil {
2011
+ _ = file.Close()
2012
+ return fmt.Errorf("sync target: %w", err)
2013
+ }
2014
+ if err := file.Close(); err != nil {
2015
+ return fmt.Errorf("close target: %w", err)
2016
+ }
2017
+ return unix.Fsync(scratchFD)
2018
+ }
2019
+
2020
+ func writeMarker(scratchFD int, name string) error {
2021
+ fd, err := unix.Openat(
2022
+ scratchFD,
2023
+ name,
2024
+ unix.O_WRONLY|unix.O_CREAT|unix.O_EXCL|unix.O_NOFOLLOW|unix.O_CLOEXEC,
2025
+ 0o600,
2026
+ )
2027
+ if err != nil {
2028
+ return err
2029
+ }
2030
+ if err := unix.Fsync(fd); err != nil {
2031
+ _ = unix.Close(fd)
2032
+ return err
2033
+ }
2034
+ if err := unix.Close(fd); err != nil {
2035
+ return err
2036
+ }
2037
+ return unix.Fsync(scratchFD)
2038
+ }
2039
+
2040
+ func existsAt(dirFD int, name string) (bool, error) {
2041
+ var stat unix.Stat_t
2042
+ if err := unix.Fstatat(dirFD, name, &stat, unix.AT_SYMLINK_NOFOLLOW); err != nil {
2043
+ if errors.Is(err, unix.ENOENT) {
2044
+ return false, nil
2045
+ }
2046
+ return false, err
2047
+ }
2048
+ return true, nil
2049
+ }
2050
+
2051
+ func identityAt(dirFD int, name string) (*identity, error) {
2052
+ var stat unix.Stat_t
2053
+ if err := unix.Fstatat(dirFD, name, &stat, unix.AT_SYMLINK_NOFOLLOW); err != nil {
2054
+ if errors.Is(err, unix.ENOENT) {
2055
+ return nil, nil
2056
+ }
2057
+ return nil, fmt.Errorf("stat %q: %w", name, err)
2058
+ }
2059
+ switch uint32(stat.Mode) & unix.S_IFMT {
2060
+ case unix.S_IFDIR:
2061
+ return &identity{
2062
+ Kind: "directory",
2063
+ Dev: strconv.FormatUint(uint64(stat.Dev), 10),
2064
+ Ino: strconv.FormatUint(uint64(stat.Ino), 10),
2065
+ }, nil
2066
+ case unix.S_IFLNK:
2067
+ content, err := readlinkAt(dirFD, name)
2068
+ if err != nil {
2069
+ return nil, err
2070
+ }
2071
+ sum := sha256.Sum256(content)
2072
+ return &identity{
2073
+ Kind: "symlink",
2074
+ Bytes: int64(len(content)),
2075
+ SHA256: hex.EncodeToString(sum[:]),
2076
+ Dev: strconv.FormatUint(uint64(stat.Dev), 10),
2077
+ Ino: strconv.FormatUint(uint64(stat.Ino), 10),
2078
+ }, nil
2079
+ case unix.S_IFREG:
2080
+ fd, err := unix.Openat(dirFD, name, unix.O_RDONLY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
2081
+ if err != nil {
2082
+ return nil, fmt.Errorf("open %q for identity: %w", name, err)
2083
+ }
2084
+ file := os.NewFile(uintptr(fd), name)
2085
+ hash := sha256.New()
2086
+ bytesRead, readErr := io.Copy(hash, file)
2087
+ closeErr := file.Close()
2088
+ if readErr != nil {
2089
+ return nil, fmt.Errorf("hash %q: %w", name, readErr)
2090
+ }
2091
+ if closeErr != nil {
2092
+ return nil, fmt.Errorf("close %q: %w", name, closeErr)
2093
+ }
2094
+ return &identity{
2095
+ Kind: "file",
2096
+ Exec: uint32(stat.Mode)&0o100 != 0,
2097
+ Bytes: bytesRead,
2098
+ SHA256: hex.EncodeToString(hash.Sum(nil)),
2099
+ Dev: strconv.FormatUint(uint64(stat.Dev), 10),
2100
+ Ino: strconv.FormatUint(uint64(stat.Ino), 10),
2101
+ }, nil
2102
+ default:
2103
+ return nil, fmt.Errorf("%q is not a regular file or symlink", name)
2104
+ }
2105
+ }
2106
+
2107
+ func entryChangedAt(dirFD int, name string) (int64, error) {
2108
+ var stat unix.Stat_t
2109
+ if err := unix.Fstatat(dirFD, name, &stat, unix.AT_SYMLINK_NOFOLLOW); err != nil {
2110
+ return 0, fmt.Errorf("stat %q change time: %w", name, err)
2111
+ }
2112
+ return statChangedAt(stat), nil
2113
+ }
2114
+
2115
+ func statChangedAt(stat unix.Stat_t) int64 {
2116
+ mtime := stat.Mtim.Sec*1_000_000_000 + stat.Mtim.Nsec
2117
+ ctime := stat.Ctim.Sec*1_000_000_000 + stat.Ctim.Nsec
2118
+ if ctime > mtime {
2119
+ return ctime
2120
+ }
2121
+ return mtime
2122
+ }
2123
+
2124
+ func readlinkAt(dirFD int, name string) ([]byte, error) {
2125
+ size := 256
2126
+ for size <= 1024*1024 {
2127
+ buffer := make([]byte, size)
2128
+ count, err := unix.Readlinkat(dirFD, name, buffer)
2129
+ if err != nil {
2130
+ return nil, fmt.Errorf("read symlink %q: %w", name, err)
2131
+ }
2132
+ if count < len(buffer) {
2133
+ return buffer[:count], nil
2134
+ }
2135
+ size *= 2
2136
+ }
2137
+ return nil, errors.New("symlink target exceeds one MiB")
2138
+ }
2139
+
2140
+ func identitiesEqual(left, right *identity) bool {
2141
+ if left == nil || right == nil {
2142
+ return left == nil && right == nil
2143
+ }
2144
+ return left.Kind == right.Kind &&
2145
+ left.Exec == right.Exec &&
2146
+ left.Bytes == right.Bytes &&
2147
+ left.SHA256 == right.SHA256 &&
2148
+ (left.Dev == "" || right.Dev == "" || left.Dev == right.Dev) &&
2149
+ (left.Ino == "" || right.Ino == "" || left.Ino == right.Ino)
2150
+ }
2151
+
2152
+ func linkEntry(sourceFD int, source string, destinationFD int, destination string) error {
2153
+ return unix.Linkat(sourceFD, source, destinationFD, destination, 0)
2154
+ }
2155
+
2156
+ func removeAt(dirFD int, name string, flags int) error {
2157
+ err := unix.Unlinkat(dirFD, name, flags)
2158
+ if errors.Is(err, unix.ENOENT) {
2159
+ return nil
2160
+ }
2161
+ return err
2162
+ }
2163
+
2164
+ func cleanupScratch(parentFD, scratchFD int, scratch string) error {
2165
+ for _, name := range []string{
2166
+ "source",
2167
+ "target",
2168
+ "current",
2169
+ preparedMarker,
2170
+ deletedMarker,
2171
+ } {
2172
+ if err := removeAt(scratchFD, name, 0); err != nil {
2173
+ return fmt.Errorf("remove retained %s: %w", name, err)
2174
+ }
2175
+ }
2176
+ if err := unix.Fsync(scratchFD); err != nil {
2177
+ return fmt.Errorf("sync emptied scratch: %w", err)
2178
+ }
2179
+ if err := removeAt(parentFD, scratch, unix.AT_REMOVEDIR); err != nil {
2180
+ return fmt.Errorf("remove transaction scratch: %w", err)
2181
+ }
2182
+ if err := unix.Fsync(parentFD); err != nil {
2183
+ return fmt.Errorf("sync scratch removal: %w", err)
2184
+ }
2185
+ return nil
2186
+ }
2187
+
2188
+ func syncBoth(first, second int) error {
2189
+ if err := unix.Fsync(first); err != nil {
2190
+ return fmt.Errorf("sync destination parent: %w", err)
2191
+ }
2192
+ if err := unix.Fsync(second); err != nil {
2193
+ return fmt.Errorf("sync transaction scratch: %w", err)
2194
+ }
2195
+ return nil
2196
+ }
2197
+
2198
+ func parseFD(value string) (int, bool) {
2199
+ fd, err := strconv.Atoi(value)
2200
+ return fd, err == nil
2201
+ }