diff --git a/agents/herb/work/queue-215/BUILD.md b/agents/herb/work/queue-215/BUILD.md index e52b6b0e5..7357ecec0 100644 --- a/agents/herb/work/queue-215/BUILD.md +++ b/agents/herb/work/queue-215/BUILD.md @@ -31,6 +31,18 @@ unsafe or mismatched state, including a missing file while the served ref exists. The version-1 shared contract below is approved. Row 222 also owns runtime.mjs for its sync and timer wiring, recorded in that row's manifest. +Additional rulings, sage-1860, after Filbert's CHANGES on 6a508804f: + +- B1 adds packages/cli/src/config.mjs and cli/tests/trackers-boot.test.mjs. + trackerFor skips library entries. A real bus boot with tracker.baseUrl and + a library entry succeeds and gets no tracker from that entry. +- B2 refuses a role publish grant naming a library project in the loader. +- B3 readers use the sync lock. mirror.mjs exports withLibraryLock, with a + 5000 ms wait bound and library-sync-busy on timeout. Sync locks only the + served-ref move and state publication, never the fetch. +- N3, git-socket doPush/recordPush accepting a library ID, is a later row. + Sage records it in DEFERRED.md when 215 lands. No fix here. + ## Shared contract with row 222 Agreed in herb-0048, megavolt-0081/0082 and sage-1852. This section is the @@ -81,6 +93,43 @@ Exports consumed by row 222: | ensureMirror(context) | Promise of the mirror path; race-safe idempotent bare init; no remote or fetch | | servedCommit(mirror) | Promise of full commit ID or null; refs/heads/served only | | readSyncState(mirror, {branch}) | Promise of checked version-1 state; compare with served ref | +| withLibraryLock(mirror, operation) | Await the same exclusive lock as readers, then await operation({readSyncState}); always release | + +The lock is a stable private regular file, mosaic-library-sync.lock inside +the mirror. It is never unlinked. A kernel flock is held by the caller's +file descriptor. Acquisition waits at most 5000 ms and refuses +library-sync-busy on timeout. Unsafe lock metadata refuses library-state-invalid. + +There is no fs.flock API. The exact protocol is: open the stable file with +O_NOFOLLOW, check regular/private/owned/single-link metadata, pass its fd as +child FD 3 to `flock --exclusive --wait 5 --conflict-exit-code 75 3` using +asynchronous spawn, await +that child's exit 0, then run the callback while the parent still holds its +original descriptor. Both descriptors share the open file description, so +the lock survives acquisition-child exit. Finally close the parent's fd. +Process death also closes it. No holder child stays alive in the callback, +no stale file is deleted, and unrelated operation children inherit only 0-2. +Acquisition never uses spawnSync: the bus must serve other calls while a +library reader waits (megavolt-0087). + +A scratch-only probe on node v26.8.1 at 2026-10-21T08:34:00Z confirmed this: +acquisition child exited 0; a second flock -n returned 1 while the parent fd +stayed open; after closeSync it returned 0. The probe removed its own root. +This was a design probe, not a package test, suite, Docker command or proof. + +readSyncState takes that lock around reading the served ref and state file. +The callback-scoped readSyncState({branch}) supplied by withLibraryLock reads +under the already-held lock and does not reacquire it. The helper awaits +asynchronous callbacks and releases in finally. A callback-scoped reader +used after its callback ends refuses; it cannot escape its lock lifetime. + +Row 222 fetches without this lock, then calls withLibraryLock across the +served-ref move and atomic state write. Its tests hold publication between +those steps and prove a concurrent reader waits, then sees the new consistent +pair, including first sync. Row 215 tests that publication shape and timeout. +A file read releases the lock after snapshotting a consistent served SHA, +then reads that immutable commit. Row 222's separate outer sync lock spans +fetch to prevent two sync operations interleaving (megavolt-0086). Host adapter exports for this row: @@ -91,13 +140,40 @@ Host adapter exports for this row: commit, working-tree fallback, fetch or write. C2-01's R1/R2/R7 exclusions apply. A never-synced mirror refuses library-commit for a file read. -Row 222 owns syncCommand, sync, writeSyncState, the lock, the timer and state -publication. It rebuilds problems with existing readLibrary({repo, commit, +Row 222 owns syncCommand, sync, writeSyncState, its outer sync lock, the timer +and state publication. Row 215 owns the shared publication/read lock per +sage-1860. Sync rebuilds problems with existing readLibrary({repo, commit, mapping}). It does not need a second rebuild function from this row. +## Root consumer audit and reviewer notes + +On a2c72ac95, ran exactly the sage-1860 audit: +`git grep -n 'entry\.root\|\.root)' -- 'packages/*/src/*.mjs'`. +It has 14 hits. runtime.mjs:95, business/cli.mjs:61,72,78,118, +instance-settings.mjs:142-143, resolve.mjs:44, cli/config.mjs:62-63 and +project/paths.mjs:42 all get the library guard in this row. business.mjs:233 +and :242 are code-project validation after the library dispatch, not an +unguarded consumer. queue.mjs:1144 compares queue genesis roots and never +iterates business.projects; it is safe and unchanged. A second search for +projectFilePath/business.projects/b.projects found no additional root reader. +ensureCheckouts destructures root rather than spelling .root; its guard and +the launcher's filter remain in T1. Bare projectFilePath/loadProject remain +strict and unchanged. + +Filbert N1 is handled at the existing owned paths.mjs boundary: projectRepo +defaults to record-project use and refuses a library entry with project-invalid. +Only mirrorPath's internal call opts into allowLibrary. Thus ordinary human +and tester project init cannot occupy an uninitialized library mirror path. +No record API or bus argument exposes that internal option. + +Filbert N2: ensureMirror failure refuses bus boot. No socket becomes ready. +The runtime closes resources it opened, following its existing failure path. +A T4 test uses an unsafe/pre-existing mirror and proves the refusal, rather +than serving a half-configured library adapter. + ## Verification record -Planning stage. No product code, test run, suite, image build, live operation +Planning stage. No product code, package test, suite, image build, live operation or push for row 215 has happened. Product implementation awaits Filbert's manifest approval. Sage owns suite and full-gate grants. Row 197's proof commit is separate and is not part of this row's changes. diff --git a/agents/herb/work/queue-215/MANIFEST.md b/agents/herb/work/queue-215/MANIFEST.md index 75f59afd0..bd9cd4610 100644 --- a/agents/herb/work/queue-215/MANIFEST.md +++ b/agents/herb/work/queue-215/MANIFEST.md @@ -16,7 +16,7 @@ the broker's sandbox import closure. Sync and HTTP routes belong to later rows. No dependencies. node_modules remains the plain canonical symlink. **Spec:** Pinned C2-04 brief; Gosalyn's approved design 1.1, 1.3, 2.1, -M4/M9 and 7.2; BUILD.md's sage-1848/1852 scope and shared-state contract. +M4/M9 and 7.2; BUILD.md's sage-1848/1852/1860 scope and shared-state contract. ## Constraints and review focus @@ -47,7 +47,8 @@ the result and human-only admission (T4). | packages/business/src/instance-settings.mjs | Skip library before projectFilePath (:142), nothing else | | packages/seat/src/checkout.mjs | Skip library entries in ensureCheckouts (:315-327) | | packages/cli/src/launcher.mjs | Pass only code entries to ensureCheckouts (:354) | -| packages/project/src/paths.mjs | Skip library entries in root overlap loop (:41-44); keep default projectRepo location | +| packages/cli/src/config.mjs | Skip library entries in trackerFor's root loop (:61-63), per sage-1860 | +| packages/project/src/paths.mjs | Skip library entries in overlap loop (:41-44); default record use refuses library, mirrorPath internally opts into allowLibrary | | packages/library/src/mirror.mjs | New bare init, checked state and served-commit file read module | | packages/library/src/index.mjs | Export mirror module interface and SYNC_STATE_FILE | | packages/bus/src/broker.mjs | New injected library adapter, human-only admission and two async verbs | @@ -61,7 +62,7 @@ scripts/stack-copy.sh already copies packages/library/src and needs no edit. Test paths to modify: business/tests/business.test.mjs, cli.test.mjs, resolve.test.mjs, instance-settings.test.mjs; seat/tests/checkout.test.mjs; -cli/tests/launcher.test.mjs; project/tests/project.test.mjs; +cli/tests/launcher.test.mjs, trackers-boot.test.mjs; project/tests/project.test.mjs; bus/tests/project-runtime.test.mjs. All paths are under packages/. New tests: packages/business/tests/library-projects-protected.test.mjs; @@ -78,13 +79,22 @@ row when implementation begins. Sage owns docs/SESSIONS.md. - [ ] Add failing business tests: accept a frozen library mapping without a root, retain mixed/only-library documents, and propagate C2-01's mapping - and playground refusal codes. Existing code validation remains unchanged. + and playground refusal codes. A role publish naming a library project + refuses at load, even with an otherwise valid fork identity and remote. + Existing code validation remains unchanged. - [ ] Add a separate regression for each consumer: CLI validate and its credential exclusion roots; CLI resolve --project books has no project layer; resolveInstance finds a later real project; instance settings still writes an unrelated setting; ensureCheckouts returns no library checkout; launcher creates no books workspace clone; projectRepo skips a library while still rejecting overlap with a real code root. Put books first. +- [ ] Add the real bus boot regression in cli/tests/trackers-boot.test.mjs: + tracker.baseUrl is set and books is a library entry; boot succeeds, books + produces no tracker, and a later real tracker project still works. +- [ ] projectRepo(context, {allowLibrary = false} = {}) refuses a library + entry by default with project-invalid. Only mirrorPath opts in. Test that + ordinary human and tester record initialization leave no directory at the + future mirror path; no public verb can supply allowLibrary. - [ ] Add runtime's protectedRoots consumer regression in T4's real runtime fixture. A valid credential outside code/data roots still loads with a library entry, and existing credential protections still refuse unsafe files. @@ -121,6 +131,17 @@ as the design's skip requires. It does not fabricate project vars or a root. state/ref, valid state, malformed JSON, unknown keys/types, branch mismatch, invalid times/IDs/problem shape, symlink/hardlink/non-private file and missing or mismatched served ref. Assert reads change no bytes. +- [ ] Add shared-lock tests: withLibraryLock(mirror, operation) waits at most + 5000 ms, refuses library-sync-busy on timeout and releases after a throw. + A reader arriving between the publisher's served move and state write + waits, then reads the consistent pair. Cover first sync and fast-forward + in independent processes, same-process independent acquisitions and + release after owner death. A callback-scoped reader expires on release. +- [ ] Implement the lock by async spawn of flock on the parent's inherited + fd 3, as pinned in BUILD.md. Await acquisition-child exit before calling + operation. Never spawnSync during acquisition, which could freeze the bus + for 5 s. A parallel ordinary bus read remains responsive while a library + read waits. Only the publication/read lock is shared; fetch is outside it. - [ ] Add served-read tests against local fixtures containing spaces, capitals, a .canvas and unmapped top-level note. Seed a served ref and state without fetching. A newer unrelated HEAD does not change the returned @@ -152,6 +173,9 @@ approved library-state-invalid code. State reads are bounded and private. - [ ] Inject a separate library adapter via runtime, initialize only library mirrors, add the two async request paths, and include them in server's awaited list. Do not import library or fs into broker. +- [ ] Unsafe or pre-existing non-mirror initialization refuses bus boot, + closes opened resources and never publishes a ready socket. Add that + runtime regression in library.test.mjs. - [ ] Wire main and tester Console callbacks beside project-file callbacks. No HTTP route is shipped. The new CLI test captures the startServer options with a test-only module loader and calls the supplied callbacks @@ -178,6 +202,8 @@ approved library-state-invalid code. State reads are bounded and private. ## Self-review Every brief acceptance item is in T1-T4. Added host support paths match -sage-1848. State filename, types and authority match sage-1852 and Megavolt's -agreement. Fetch, timers, locks for sync and HTTP stay with their later rows. +sage-1848/1860. BUILD.md records the complete 14-hit root audit. State shape +and authority match sage-1852; the shared read/publication lock follows +sage-1860 and Megavolt's agreement. Fetch, timers, outer sync serialization +and HTTP stay with their later rows. This is a plan only; no product edits or test execution precede approval.