Row 286 C2-03b: build plan and four scope questions (BUILD.md only)
This commit is contained in:
1 parent
45dca69f6e
commit
4faa3f6e9a
1 file changed
+97
@@ -0,0 +1,97 @@
|
||||
# Row 286: C2-03b push and per-seat credentials for the library mirror (#1791)
|
||||
|
||||
Owner Ludwig. Reviewer Dewey. Gate owner Sage (sage-2909).
|
||||
Brief: `docs/plans/2026-10-20_case2-rows.md`, "## C2-03b", at 94c33a43c. Design 7.2 and the ruling
|
||||
after r11 "agents never write story" in `agents/gosalyn/work/case2/DESIGN.md` at b68d79663.
|
||||
Base: `refactor` at 94c33a43c. The brief cites aca16404c, and lines have moved since:
|
||||
`GRANT_EVIDENCE` is `broker.mjs:253` now, not `:246`.
|
||||
|
||||
Status: plan only. No code and no node yet. Row 258 comes first in my order.
|
||||
|
||||
## What the code does today (94c33a43c)
|
||||
|
||||
- `packages/library/src/sync.mjs:32` `syncCommand(kind, url, {mirror, branch, user, slot})`. It refuses
|
||||
any kind but `fetch`. The env passes `MOSAIC_LIBRARY_HOST`, `_USER` and `_SLOT` to `bin/askpass`.
|
||||
- `bin/askpass` checks `-f` and `! -L` on the slot, then reads it. It doesn't check the uid or the mode.
|
||||
- `sync.mjs:51` `slotPresent` checks lstat: a regular file, our uid, no group or other bits.
|
||||
- The slot path is built in `packages/bus/src/runtime.mjs:211-217` (`syncOptions`) as
|
||||
`<configDir>/secrets/<business>/library-gitea`. The broker only calls `this.#library.sync(context,
|
||||
{accept})` (`broker.mjs:4630`), and the timer calls `libraryModule.sync` directly (`runtime.mjs:533`).
|
||||
- The sync state file has a strict key list in `packages/library/src/mirror.mjs:222`.
|
||||
- `chief-of-staff` is a definition (`STAFF_TO`, `broker.mjs:240`), not a fixed instance name.
|
||||
- A seat's Gitea user is in `gitea_grants.user` and `gitea_users`. Its token file is
|
||||
`<storeRoot>/<business>/<agent>/gitea` (`gitea-grant.mjs:379`). `giteaKeys.held(business, agent)`
|
||||
answers the held case.
|
||||
|
||||
## Plan
|
||||
|
||||
1. `syncCommand(kind, url, {mirror, branch, user, file, commit})`. `kind` is `fetch` or `push`. Same
|
||||
`SAFE` prefix, `-c` keys and env for both. A push is
|
||||
`push --porcelain --no-verify --end-of-options <url> <commit>:refs/heads/<branch>`, with no `+`.
|
||||
`commit` must be a full sha. The env var stays `MOSAIC_LIBRARY_SLOT` but now names whichever file
|
||||
answers: a seat file, the chief's file, slot `library-gitea` or slot `library-human`.
|
||||
2. `push(context, {commit, credential, retried, command})` in `sync.mjs`. git's porcelain line for
|
||||
the ref decides:
|
||||
- `!` with `[rejected]` and `(fetch first)` or `(non-fast-forward)` means the tip moved. Fetch once
|
||||
with the same credential and return `{pushed: false, raced: true, tip}`. With `retried: true`, the
|
||||
same rejection refuses `stale-base`. C2-02 owns the re-apply.
|
||||
- Any other failure refuses `library-push-failed`. git's stderr isn't kept.
|
||||
- A remote that fails C2-04's URL rules refuses `library-remote-invalid` before any git call.
|
||||
3. `bin/askpass` gets `readSecretFile`'s checks, the same way: no symlink, the bus's uid, mode 0600.
|
||||
In sh: `test -f && test ! -L && test -O`, plus a mode check through `stat -c %a`. Only the path
|
||||
reaches that argv. The user check stays an exact match on the one configured user. The check and the
|
||||
read happen at different times (TOCTOU). The bus owns the directories, so I take that as the
|
||||
accepted limit (Q1).
|
||||
4. Credential choice for the fetch, as a pure function `fetchCredential(store, business, keys)` in the
|
||||
bus. The broker exposes it, and `runtime.mjs` passes its answer to `sync` for the verb and the timer:
|
||||
- The chief is the one instance whose definition is `chief-of-staff`.
|
||||
- Evidence: `SELECT 1 FROM events WHERE business=? AND ${GRANT_EVIDENCE} AND
|
||||
json_extract(body,'$.method')='gitea' AND json_extract(body,'$.agent')=?`, of any action. It
|
||||
names no project and never reads `gitea_grants`.
|
||||
- No evidence: slot `library-gitea`, `{kind: 'slot'}`.
|
||||
- Evidence, a live `gitea_grants` row for the chief and not held: `{kind: 'chief', user, file}`,
|
||||
with the user from that row. The fallback test never reads the table. This path does, because it
|
||||
needs the user.
|
||||
- Evidence and anything else (revoked, fired or held): `library-fetch-no-grant`, before any file is
|
||||
opened.
|
||||
- No chief instance (fired): Q2.
|
||||
5. The sync state gets `credential: {kind, user} | null`. `library.status` shows it.
|
||||
`library-fetch-no-grant` shows as `lastError.code`. This changes `mirror.mjs`'s key check (Q3).
|
||||
6. Slot `library-human` is `<configDir>/secrets/<business>/library-human`. `humanCredential(...)`
|
||||
refuses `library-human-missing` before any git call when the slot is missing or fails the file checks
|
||||
(lstat: regular file, our uid, 0600). Her Gitea user: Q4.
|
||||
|
||||
## Scope questions (for Dewey and Sage before code)
|
||||
|
||||
- Q1. The askpass TOCTOU above. Is it accepted as is, or should the helper be a node script that does
|
||||
O_NOFOLLOW plus fstat? If node, it would read the token in process and print it to git's pipe.
|
||||
- Q2. Fired chief. The instance and its `gitea_grants` row are gone, so I can't name the agent for the
|
||||
evidence check. My recommendation fails safe: with no chief instance, the fetch refuses
|
||||
`library-fetch-no-grant` when any gitea grant event exists in the business, and uses the slot only
|
||||
when none exists.
|
||||
- Q3. Files owned names `sync.mjs`, `askpass`, `broker.mjs` and the tests. The choice also needs
|
||||
`packages/bus/src/runtime.mjs` (`syncOptions` and the timer's call) and
|
||||
`packages/library/src/mirror.mjs` (the state key list for `credential`). I'd add both to the
|
||||
manifest, with only those hunks changed.
|
||||
- Q4. Her Gitea user for `library-human`. The entry's `user` (`mapping.mjs:52`) is today the sync
|
||||
user, the bot's. The design names `editor {name, email}` (C2-11a) but no Gitea login. My
|
||||
recommendation: the entry's `user` becomes her Gitea login, used only with slot `library-human`. The
|
||||
fallback fetch keeps that same user too, because the bot slot was set up for it. Otherwise this row
|
||||
takes the user as a parameter and C2-02 or C2-12b names the field.
|
||||
|
||||
## Tests (brief list, mapped)
|
||||
|
||||
T1 push lands a fast-forward. T2 a moved tip reports the race. T3 a second race refuses `stale-base`.
|
||||
T4 a dead remote refuses `library-push-failed`. T5 askpass answers with the named seat's user and file,
|
||||
and refuses another seat's user, a symlink, 0644 and another uid (with a fallback note if the test can't
|
||||
chown). T6 the fetch uses the chief's file with a live grant, and the slot only with no chief event.
|
||||
T7 revoked (through `grants.revoke`), fired (through `instance.fire`) and held each refuse
|
||||
`library-fetch-no-grant` with a valid slot present, and each asserts the `gitea_grants` row is gone
|
||||
first. T8 a chief grant on another project ends the fallback. T9 T7's three cases with the slot at 0000.
|
||||
T10 a missing `library-human` refuses `library-human-missing` and runs no git. T11 a token search of
|
||||
argv, the mirror config, the run log and the test output. Mutant: a `+` added to the push refspec fails.
|
||||
|
||||
## Gate
|
||||
|
||||
Dewey approves the manifest. The library and bus suites, plus every `scripts/test-*.sh`, pass on Sage's
|
||||
gate. The mutant dies. A token search of the run's output finds nothing.
|
||||
Reference in new issue
Block a user