Compare commits

...
57 changed files with 5286 additions and 70 deletions

No files matched your search

+6
View File
@@ -5532,3 +5532,9 @@ claim. Receipts and limitations in agents/lena/work/queue-296/BUILD.md and run-r
- 2026-10-24 (rockerduck, row286 / #1791 r6, sage-4112 / #1825): corrected only two fixture constants to unique19-character values after the landing secrets check refused r5. No waiver/guard/assertion/product change. Added-package-lines secrets grep empty; touched6/6+24/24, library151/151, bus981/981 on fresh shortTMP timeout-k10; both exact new values absent from all four outputs. Mutants not rerun as directed. Plain evidence in agents/rockerduck/work/queue-286/BUILD.md and gate-r6; new manifest/push and round3 follow.
2026-10-24T14:37:02Z | Sage | Row 286 (#1791, C2-03b, Rockerduck on Codex, Boyd and Dewey) LANDED 5d7801dc1. Before: refactor at d55944edb. After: r6 candidate 75f6d56b1's 19 commits cherry-picked onto d55944edb. The one conflict (64f1ff950, BUILD-LOG.md and SESSIONS.md) is append-only and kept both sides, with 0 lines removed against either parent. Apart from the logs, the tree equals the candidate except on the 6 paths of my doc commit d55944edb. Reviews: round 2 (r5 ec8cb10c0) Boyd c31584 rev 3928, Dewey c31585 rev 3929; round 3 (r6) Boyd c31587 rev 3942, Dewey c31588 rev 3943. Sage posted the requests (c31583, c31586) because Rockerduck has no token. Gate GREEN on r5's tree e9c73ab96 at 14:14:11Z, then the push stopped at push-check STOP secrets. The guard came in today with #1825. Two added fixture constants (SLOT_TOKEN in library-credential.test.mjs, SEAT_TOKEN in sync.test.mjs) were 31 characters and matched its token-assignment pattern. sage-4112: no waiver (mos-1570) and no guard edit; r6 shortened only those two values to 19 characters, with reviewers confirming the delta. Gate GREEN again on 5d7801dc1 at 14:36:42Z with its own npm ci: 17 package suites, 4251 tests, 0 fail, 1 skip, which is the allowed FED R7 entry whose runner passed 1/1. All 12 scripts/test-*.sh rc 0. Neither old nor new fixture values appear in any of the 29 run outputs. Mutants: Rockerduck killed P1, P2 and B285-M1 (gitea-grant X-G1). For Dewey's N1 I also ran B285-M1 against library-credential.test.mjs; T7 and T9 failed (4/6) and passed after the restore. sage-4100 (chief grant seeded as fixture until C2-21) is recorded in the 285 entry. Overlap sweep found no in-review row on the 9 code paths. push-check FF_OK, NO_BINARY, SECRETS_CLEAN, NO_ATTRIBUTION. #1791 closed with c31591. No P19 task names row 286, so there is no Vikunja task to close. Brief template gains a sage-4112 line: test credentials stay under 24 characters. The live stack is not re-pinned.
- 2026-10-24 — steelbeak — #1792 / row 287 C2-21 before: library grants still fork; creative and chief grants refuse. Recovered Megavolt's binding design on 18334d713 and documented current-code delta in agents/steelbeak/work/queue-287/BUILD.md. No tests or implementation yet; Sage holds test clearance, scope delta reported in steelbeak-0001.
- 2026-10-24 — steelbeak — #1792 / row 287 C2-21 after: library grants use admin collaborator PUT, creative write/chief read levels, definition scopes, exact publish-wide reach and kind/level binding; business fence and chief install identity/publish updated; T7/T9 seeds replaced by real minting. Scratch Gitea F-A/F-B pass, F-C owner control negative recorded, F-D normal/force control positive. Verified bus 998, business 283, CLI 989 plus dedicated FED 1 (one expected CLI skip), Docker 2, install 27, all 12 shell gates, M1-M11 killed and restored; token audit hits0 and owned-process/container cleanup clean. Test fixture corrections approved under mos-1590/sage-4139; non-executable land-work launcher rc126 resolved by sage-4141 bash invocation. Evidence and manifest at agents/steelbeak/work/queue-287/; handing off to Boyd, then Dewey, with no live change or landing.
- 2026-10-24 — steelbeak — #1792 / row 287 first-review correction, boyd-0034: frozen candidate c2338ed58 approved, no blocking code findings. Product/test diff-check is clean; the full committed diff has trailing whitespace in preserved raw RED/mutant logs, correcting the earlier unqualified claim. Added verified old-test numstats in BUILD.md as a separate doc-only follow-up under lead137. Queue round1 opened at rev3963; automatic request failed at rev3964 for missing Steelbeak credential, reported to Sage without workaround. No product edits, tests or slot use.
+427
View File
@@ -0,0 +1,427 @@
# Row 287 design notes: C2-21, the grant executor for a library project (#1792)
Builder Megavolt, reviewer Dewey, gate owner Sage (sage-2910, Q226, mos-1310). Branch megavolt/row287, a
doc-only draft off 94c33a43c, written during Gizmoduck's slot with local git only. The build starts on the
candidates of row 285 (C2-08b, Scrooge) and row 226 (C2-13b, Gosalyn) and rebases onto their landings
(Q177). Nothing here has been run.
The brief is "C2-21 the grant executor for a library project" in docs/plans/2026-10-20_case2-rows.md at
94c33a43c. Its gate: Dewey approves the manifest; the bus suite, the Gitea Docker test and the install test
are green on Sage's gate, along with every `scripts/test-*.sh`; a search of the test run's output for the
fixture admin and seat tokens finds nothing.
## Sources (line cites at 94c33a43c)
- `packages/bus/src/gitea-grant.mjs`: the admin api (`adminApi`, no collaborator call yet), the seat api
(`forkGet`, `fork`, `repos`), `identitySha256`, `REPO_PAGE`/`REPO_PAGES`.
- `packages/bus/src/broker.mjs`: `#giteaRole` :2132, `#giteaTargets` :2149, `#giteaGrantBinding` and its
block :2167-2196, `#giteaRecheck` :2200, `#giteaSteps` step 4 :2425-2441 and step 5 :2442-2473, the
`grant.applied` body and notice :2480-2488, the host refusals `host-creative-role` :1106 and :1254.
- `packages/business/src/business.mjs` :266-288: a `kind: "library"` project goes through `checkMapping`, and
:286 refuses any role whose `publish` names a library project.
- `packages/library/src/mapping.mjs`: a library entry carries `remote`, `branch` and an optional `user`.
- `packages/bus/tests/gitea-grant.test.mjs` :297-334 pins the `grant.applied` body (`forks: [...]`) and the
block; `fake-gitea-api.mjs` keeps collaborators as a list of logins with no permission.
- `packages/bus/tests/gitea-grant-docker.test.mjs`: the scratch gitea/gitea:1.27.1 container; it uses
`os.tmpdir()`.
## 1. Rulings, blockers and questions
- B1, the publish rule. `business.mjs:286` refuses any `publish` entry that names a library project
("library repositories cannot be published by agents"). The brief's chief needs `publish: ["books"]`, and a
creative seat needs the library project in `publish` for `#giteaRole` and `#giteaTargets` to pass. No test
under packages/*/tests or scripts pins the :286 message (positive control: "publish names" hits
agent-identity.test.mjs).
- Ruling sage-2916 (18:42Z 10-22), files-owned extension: row 287 owns `packages/business/src/business.mjs`
:283-291 and the `packages/business/tests/business.test.mjs` cases for it. Row 285 leaves business.mjs
alone. Conditions:
- The refusal is rewritten, not deleted. A library project in `publish` loads only for a creative
instance (`CREATIVE_DEFINITIONS`) or the chief of staff, each with a gitea credential. Any other
instance still refuses, with a new message that names the rule. Tests cover both sides. The grant path
serves an allowed one with collaborator reach, never a fork. (To confirm on the base: how business.mjs
names the chief, by definition `chief-of-staff` or by `launch.by`.)
- Before in-review, grep 'packages/*/tests/*' and `scripts/test-*.sh` again for the old message text,
with a positive control.
- PRD 1.15 REQ-LIB-6 is the authority for this. If Mos changes 1.15 (sage-2914, `library.write`
routing), check this text against what lands.
- Q1 to Q4 below: Sage accepted them as stated assumptions (sage-2916). Dewey checks them in the round.
- Q1, the hash. `identitySha256` covers identity, publish and remotes, not a project's kind or the role's
definition name, and the level comes from the definition name. Proposal: add a `library` key
(`{project: level}`) to the hash input only when a publish project is a library, so every non-library
grant's hash stays byte-identical and no open grant turns `decision-mismatch` on upgrade.
- Q2, the event. `grant.applied` and its notice carry `forks`, and the test at :322 pins that body.
Proposal: `forks` stays (empty when every target is a library), and a `repositories:
[{project, repository, level}]` key appears only when a library target exists. The notice names them the
same way.
- Q3, mixed raises. A creative raise must name only library projects (mos-1320 item 2). The chief may
name a library project next to forked ones in one raise; step 4 forks the others and adds the collaborator
on the library one. Proposal: allow that, and test it. Under sage-2916, any other non-creative instance
can't hold a library project in `publish` at all, so the business file refuses it before any raise.
- Q4, the token search for the Docker test. The fake API's tokens are fixtures and can be grepped. The
Docker test's tokens are minted by the container at run time, so the gate grep can't know them in advance.
Proposal: the Docker test captures its own diagnostics and asserts that no token it minted appears in
them, and the gate greps the run's output for every fake-API fixture token.
### Early look, Dewey (dewey-0862, 18:53Z 10-22, on 5bcfef0ad, read only), folded
These replace the matching proposals in sections 2 and 3 where they differ.
- Q2 accepted. `forks` stays and is `[]` when every target is a library. `repositories:
[{project, repository, level}]` appears only when a library target exists. `repository` is the remote
URL, the same form as `forks`; the `full_name` that step 5 matched is asserted in a test and doesn't go
in the body. The only src reader of `forks` is the notice (:2483). The notice drops its forks clause when
the list is empty and adds "repositories: <url> (<level>)". A non-library notice keeps today's text.
- The read line: "The token can read every path of this repository. It can't push to it, because the user
is a read collaborator." The seat scope is still `write:repository` (:2189).
- B-a: `\nForks:` (:2187) prints only when a fork target exists.
- B-b: "Step 5 checks that this is only its forks." (:2193) is unchanged with no library target. With one, it
reads "Step 5 checks that this is only its forks and the repositories above at their level."
- B-c: `others` (:2181, :2194) splits. An untargeted library project in publish gets its own line, "It may
also reach these repositories as a collaborator: <url> (<level>)", because a revoke leaves the
collaborator entry and step 5 accepts it publish-wide. Forked others keep today's line. L8 covers B-a to
B-c.
- Q1 holds. Test: a project flipped to `kind: "library"` between the raise and the recheck gives
`decision-mismatch` (L10).
- Q3 holds. Library projects stay out of step 5's fork upstream set (:2446), so a fork of her repository
refuses `gitea-reach-wider`; L4 covers it.
- Q4 holds if the assertion covers what the test prints, TAP stdout and stderr, not only an in-test
buffer. The Docker test prints one line, "minted N, hits 0", for the gate to grep.
- Step 5 compares exactly the keys `admin`, `push` and `pull`. BUILD.md records F-B's actual `permissions`
object, so an extra Gitea key can't pass or fail by accident.
- The chief is named by definition `chief-of-staff`, not `launch.by`, matching broker `STAFF_TO` (:240) and
row 286 (dewey-0860). A fired chief keeps its roles entry (instance-settings.mjs:355), so B-2 also loads a
fired chief's entry.
### The definitions' gitea credential (dewey-0865)
At 94c33a43c, chief-of-staff.json and all six creative definitions have `credentials: []`, so `#giteaRole`
(:2137-2143) would refuse them all `gitea-not-declared`. Row 287 doesn't add the credential. Row 285 (C2-08b)
owns those role files, and its brief (case2-rows.md :1823-1824) gives each creative role and the chief
`[{"service": "gitea"}]`, which Mos authorized in mos-1309 item 5. That is why 287 builds after 285 lands. Two
notes for 285, not for this row:
- The grant path reads only `service === 'gitea'`. The seat token's scopes come from `SEAT_SCOPES` in code,
not from the definition. coder.json adds `scopes`, and 285's validator decides whether the bare form passes.
- Five bus tests read roles/chief-of-staff.json directly (chief-of-staff, fire-revokes-grants,
instances-verb, names, routes-verb), and 285's gate names the business suite, not the bus suite.
- Settled (sage-2950, 19:06Z 10-22): 285's gate runs the bus suite whole (sage-2946), and Scrooge has the
role.mjs no-scopes check. Dewey withdrew dewey-0865 (dewey-0867). Mos withdrew the mos-1332 item 3 publish
fence (mos-1333), so business.mjs stays out of 285's files and sage-2916 stands as written.
- Correction (sage-2958, Scrooge): role.mjs refuses `{"service": "gitea"}` with no scopes
(checkGiteaScopes :100-111, util.mjs:42); only a host entry may omit them (:134). So 285's entries name
their scopes, equal to SEAT_SCOPES (sage-2915). The chief-of-staff.json readers are seven, not five: add
host-run and reload. 287's tests that build definitions give gitea entries `scopes: ["read:user",
"write:repository"]`.
## 2. The change (proposed)
- Order in the executor: step 2 (user) and step 3 (mint) as today. Step 4, for each target: a library
project gets `PUT /api/v1/repos/{owner}/{repo}/collaborators/{user}` with `{permission}` through the admin
api, op `collaborator-add`, the `token` header, logged like every call. Expect 204, or anything else fails
with `gitea-failed`. A non-library project forks as today.
- The level: `write` when `CREATIVE_DEFINITIONS.includes(role.definition)`, `read` otherwise. The raise has
no field for it; `giteaGrantCheck` still refuses any key other than method, agent, rotateBy and target.
- Step 5: the upstream set stays for forks. A repository is accepted when it is the user's fork of a
non-library publish upstream (today's rule), or when its `full_name` is a library publish project's
`owner/repo` and its `permissions` are exactly `{admin: false, push: level === 'write', pull: true}`. Any
other repository, a wider permission or `admin: true` refuses `gitea-reach-wider` with the repository
named.
- `#giteaRole`: the `CREATIVE_DEFINITIONS` refusal becomes "unless every project the raise names is a
library project". `#giteaRole` gets the raise's project list from its two callers (the binding at
:2168 and the recheck at :2201), so it can decide. The comment cites mos-1320 item 2 and keeps
mos-1163 for every other project and every host grant. The host refusals at :1106 and :1254 and their
tests stay as they are.
- `#giteaTargets` returns `{project, upstream, library: true, repository, level}` for a library project and
today's `{project, upstream, fork}` otherwise. `forkProblem` still runs for both: a library remote on
another host refuses `remote-host-mismatch`, and one the seat user owns refuses `remote-not-fork`.
- The block: a library target prints `Repository: <remote>, level <write|read>.` in place of its fork line,
plus the brief's line "The token can write every path of this repository. Only the bus's path check
limits it to supporting folders." for a `write` level. For a `read` level the line says the token can read
every path. (The brief gives only the write line; Dewey can rule the read wording.)
- Revoke deletes the token as today; the collaborator entry stays, as forks do. A fire sets
`prohibit_login` as today. Neither path changes.
- `scripts/case2-install.sh` (row 226's file): the chief of staff's instance gets `identity` and
`publish: ["books"]`. If `#grantableAgent` refuses the chief as its own target, the row stops and goes to
Sage. Read of :2037-2042 says it passes when the chief is `launch.by`; a test proves it.
## 3. Tests (proposed)
Docker (`gitea-grant-docker.test.mjs`), each fact first, before any code is trusted:
- F-A: the admin token with `ADMIN_SCOPES` adds a collaborator to a repository the admin account doesn't
own. F-B: `/user/repos` with the seat token lists that repository with the granted `permissions`. Either
failing stops the row; the output goes to Mos.
- F-C (recorded, not gating): a protected file pattern on her branch refuses a seat user's push to a story
path while her own push passes. F-D (recorded): branch protection refuses a seat user's force-push while a
normal push passes. Each logs its result as a diagnostic and doesn't fail the file; BUILD.md and a message
to Mos carry them.
Fake API (`gitea-grant.test.mjs`, `fake-gitea-api.mjs`): collaborators carry a permission, `repoJson`
returns `permissions`, and the PUT route requires a site admin token with `write:repository`.
- L1 a creative definition gets `write`; L2 the chief gets `read`, including a mixed raise of the library
project and a forked one (Q3).
- L4 a repository with `push: true` for a read grant, or `admin: true`, or a fork of her library repository,
refuses `gitea-reach-wider` naming it.
- L5 revoke deletes the token and leaves the collaborator.
- L6 a non-library project still forks, and its `grant.applied` body is byte-identical to today's.
- L7 a creative raise naming a non-library project, or a library and a non-library one, refuses
`gitea-not-declared`.
- L8 the block holds the repository, the level and the brief's line, plus B-a to B-c (dewey-0862); the notice
has no "forks: ." on a library-only grant.
- L10 a project flipped to `kind: "library"` between the raise and the recheck gives `decision-mismatch`.
- L9 the chief raises its own grant through `credential.mint`.
- Install test: the business file gives the chief `identity` and `publish`.
Business (`business.test.mjs`, sage-2916): B-1 a creative instance with identity, a gitea credential and a
library project in `publish` loads; B-2 so does the chief; B-3 any other instance with a library project in
`publish` refuses with the new message; B-4 a creative instance or the chief without a gitea credential
still refuses under the existing `publish needs identity and a gitea definition` rule. Mutant: M5 drop the
creative-or-chief condition (any instance loads), killed by B-3.
Mutants: M1 drop the library-only condition from `#giteaRole` (the brief's), killed by L7. Others to list
once the code exists: the level forced to `write` (L2), step 5's exact-permission check loosened to
`push || pull` (L4), the fork path taken for a library project (L1, a fork call counted).
## 4. Gate plan (draft)
On Sage's CLEAR, with TMPDIR from `mktemp -d /mnt/storage/scratch/tmp/gXXXXXX`, stopping at the first red:
the touched bus test files whole, then the mutants, the bus suite, the Docker test (checking for `^skip`
first, since a missing image skips it), the install test, every `scripts/test-*.sh` under row 281's limits,
then the token grep over the saved output. Timeouts and limits go in this file before the run.
## Runs
Filled in after the runs.
## Steelbeak takeover (2026-10-24, base 18334d713)
Sage-4124 dispatches Steelbeak as builder, Boyd first reviewer and Dewey landing reviewer. The
inherited text above is retained as design history; this section records current execution.
No test has run. Root npm ci uses this worktree only. Short TMPDIR is
`/mnt/storage/scratch/tmp/1ysAmV`; every scripts/test-*.sh invocation will use `timeout -k 10`.
Canonical queue next returned nothing while assignment was pending. Start op steelbeak-287-start
refused the sage-4122 assign lock; retry no sooner than two minutes after that refusal.
Delta at 18334d713: broker grantableAgent 2667-2672 (launch.by self eligible), giteaRole 2760-2777
(new chief refusal), targets 2781-2789, binding/block 2799-2830, recheck 2832-2842, step4
3070-3085, step5 3087-3120, applied/notice 3124-3132. gitea-grant hash 75-80 and API 250-264
unchanged. Business publish fence is 291-299. Install library helper is 109-127. Row286
library-credential.test.mjs seeds 153-179, consumed at 219, 225 and 286, must become real minting.
Chief scope delta raised in steelbeak-0001: row285 declares read scopes and sage-2921 fences the
existing write-scoped executor; inherited dewey-0862 retained SEAT_SCOPES and Q3 allowed mixed
chief grants. Await explicit resolution of that boundary before implementing it. Other accepted
rulings (conditional library hash, optional repositories event key, exact permission keys, business
publish fence, preserved host creative refusals) remain applicable.
Tests planned: Docker F-A/F-B gating first; F-C/F-D recorded. Fake L1/L2/L4-L10 and business
B1-B4 from inherited plan; install chief identity/publish and row286 real-mint replacement.
Mutants M1 library-only guard, M2 forced write, M3 loose permissions, M4 library fork,
M5 missing creative-or-chief publish fence. No suite runs before Sage CLEAR.
### Rulings
Sage-4125 (2026-10-24 14:49:27Z), replacing Q3 and dewey-0862's chief SEAT_SCOPES line:
1. Chief grants name only library projects; mixed and fork-only chief raises refuse gitea-not-declared.
2. Library mint scopes come from the definition's gitea credential: chief read:repository/read:user,
creative its declared scopes. Fork grants retain SEAT_SCOPES.
3. Role comment retains sage-2921's reason and cites mos-1320 item 2 and sage-4125.
4. L2 pins chief read collaborator and read token scopes; L7b pins chief nonlibrary/mixed refusal;
M6 substitutes SEAT_SCOPES for library minting, M7 drops chief library-only condition.
5. Stop/report if grantableAgent or business validator still refuses chief after that condition.
Root npm ci passed (128 packages), webui npm ci passed (2), both worktree-local with ignore-scripts,
no-audit and no-fund. Start retry met a different queue lock (louie-290-in-review); no bypass.
Prepared tests only (not executed): Docker F-A/F-B against both creative write and chief read users;
F-B prints the actual permissions object. F-C/F-D use a private scratch repo and HTTP Git, logging
only exit codes. A parent captures the fact child's entire TAP stdout/stderr and checks actual minted
tokens before emitting diagnostics. New executor/business/install cases are drafted, including
same-definition-hash level drift. Production files and fake API remain unchanged until Docker facts
and expected RED evidence are recorded under CLEAR.
Row286 T7/T9 fixture draft now raises chief/books, resolves the human yes and awaits the executor;
asserts repositories read level and actual token read scopes. The mapping remote is the fake's
acme/books; revoke/fire assertions are unchanged. This intentional replacement is expected RED on
the pre-C2-21 executor, separately from regressions in unchanged tests. No execution yet.
### Implementation sequence and acceptance
After CLEAR: run only the C2-21 Docker facts first, under a 360-second outer timeout, and stop/report
if F-A or F-B fails or skips. Then run new C2-21 tests and the intentionally replaced T7/T9 cases
against unchanged production to establish expected RED. Save the evidence and report the first RED.
No production fix until that evidence exists. Old tests retain assertions; the old chief refusal
case still targets a non-library project and should remain green.
Implementation boundaries: definition-based scopes only for library minting, collaborator PUT through
admin token, exact admin/push/pull check publish-wide with library projects excluded from fork parents,
conditional library hash input preserving legacy hashes, optional repositories event field preserving
legacy event shape, scope-accurate block/notice. Business library publish remains fenced to creative
or chief and existing identity/credential validation. Install adds only chief identity/publish for the
library profile. Host creative refusals remain untouched.
Builder verification after expected RED and fix: touched test files whole (600 seconds each), bus,
business and cli whole (1800 seconds each); mutants M1-M7 with targeted test filters (180 seconds
each), restoring source after each; Docker whole (600 seconds); all scripts/test-*.sh (1200 seconds
each, timeout -k 10); token search across saved output. Tests do not run concurrently. Stop on an
unexpected old-test RED or second RED and report to Sage before any remediation. The gate's cli
skip-list rules and Sage's slot ownership remain authoritative. No live host change, push to refactor,
merge or deployment is authorized. Own-branch push is authorized after applicable suites are green.
Canonical start succeeded at 14:56Z: `ok steelbeak-287-start rev 3955 row 287 briefed→in-progress`.
Prepared all independent pre-CLEAR work; no tests, production edits, commits or pushes. Reported
ready in steelbeak-0004. The first execution is the Docker facts, never a fake or package suite.
## CLEAR run (sage-4130, 2026-10-24 15:04Z)
Worktree-local root npm ci passed (128 packages) and webui npm ci passed (2), ignore-scripts,
no-audit, no-fund. New short TMPDIR `/mnt/storage/scratch/tmp/HvDvQ8`.
First Docker facts invocation returned a vacuous green: inherited NODE_TEST_CONTEXT made Node skip
the nested runner, minted 0. INVALID evidence, no fact pass claimed; reported steelbeak-0006.
Corrected only the new harness child environment (delete NODE_TEST_CONTEXT) and added a requirement
that four actual tokens were minted. Second facts invocation is running. Production unchanged.
### Docker facts (gitea/gitea:1.27.1, corrected harness, rc 0)
Evidence: evidence/docker-facts-r2.log, 1 pass, 0 fail, 0 skip; all four tokens minted and no minted
value in complete captured TAP stdout/stderr. Container g287-* removed by t.after cleanup.
- F-A PASS: exact admin scopes write:admin/write:repository/write:user; collaborator PUT on writer/books
(admin does not own it) returned 204 for creative write and chief read.
- F-B PASS: seat /user/repos lists writer/books. Actual creative permissions
`{"admin":false,"push":true,"pull":true}`; chief permissions
`{"admin":false,"push":false,"pull":true}`. Chief token has read:repository/read:user scopes.
- F-C NOT ESTABLISHED: Story/** protection rejected both seat push (rc 1) and owner push (rc 1).
The required owner control did not pass; do not claim a useful seat-only protected-file fence.
- F-D ESTABLISHED: on protected branch normal seat supporting-file push rc 0, force-push rc 1.
Proceeding to expected RED against untouched production, then sage-4125 implementation.
### RED and correction record
New-test first RED: evidence/red-new.log, 17 tests, 4 pass (refusal controls), 13 fail at missing
behavior. T7/T9 replacement expected RED: evidence/red-replacement.log, 2 fail gitea-not-declared.
After implementation, touched-r1.log: 122 tests, 115 pass, 7 fail, 0 skip, all failures new cases.
Stopped/reported steelbeak-0008 before changes. Mos-1590 via sage-4138 approved two fixture fixes:
seat-only /user/repos overrides (not admin prechecks) and real declared coder scopes for kind drift.
Dedicated-admin refusal already covered by X-G2 "admin set refuses each failed check and writes
nothing, then writes the slot once" at gitea-grant.test.mjs:276 (:293 asserts grant-admin-not-dedicated).
Touched-r2.log: 123 tests, 122 pass, 1 fail, 0 skip. New L10's extra zero-network assertion was false:
executor correctly performs four read-only admin checks before binding recheck. Stopped/reported
steelbeak-0009. Sage-4139 approves exact GET admin checks, no /admin/users, /tokens or /collaborators,
unchanged decision-mismatch, and absent grant file/record. Add M10, an early write ahead of recheck.
The added no-declared-scopes case ran green before its mutant, an ordering error recorded honestly;
its proof will be M8 removing the scopes check, followed by restored green. No prior RED-first proof
is claimed for that added case. Add M9 dedication-check removal against existing X-G2.
F-C follow-up (sage-4134): a protected Story/** rule refused owner and seat; the first row depending
on owner pushes to a protected branch must resolve this. Record only; no 287 behavior changes.
Install choice: books is the chief's read project when present; another library project's existing
install support is retained by using the first library project. Default identity is business-id-chief
on that project's origin, with a mosaic.invalid email; an explicit input identity is preserved.
Touched-r3.log: 123 tests, 123 pass, 0 fail/skip after sage-4139. No unchanged assertion removed.
Mutants M1-M11: all KILLED by assertion failures (rc 1, positive fail count, zero skips), no timeout,
syntax or module-load kill. Every edited file restored byte-for-byte after each mutant; runner is
mutants.py, evidence/mutants-run.log and evidence/M*.log hold the outcomes. M8's restored proof is
the forthcoming full bus run; the added case's initial green remains recorded above.
| Mutant | Change | Catch |
| --- | --- | --- |
| M1 | drop creative library-only guard | L7 creative nonlibrary/mixed |
| M2 | force library level write | L2 chief read |
| M3 | accept pull regardless of admin/push | L4 widened read |
| M4 | fork instead of collaborator | L1 creative write |
| M5 | remove library publish fence | business only creative/chief |
| M6 | library mint SEAT_SCOPES | L2 chief declared read scopes |
| M7 | drop chief library-only guard | L7b chief nonlibrary/mixed |
| M8 | remove declared-scopes check | no-declared-scopes refusal |
| M9 | remove admin dedication check | existing X-G2 |
| M10 | move step2 user-create write before binding recheck | L10 exact GETs, no writes/file/record |
| M11 | omit library kind/level from identity hash | L10 drift |
### Full suites in progress
Restored full bus: evidence/bus-suite.log, rc 0, 998/998 pass, 0 fail/skip. This includes restored M8
proof and both Docker tests (fact child diagnostics also carry their own nested summary).
Full business: evidence/business-suite.log, rc 0, 283/283 pass, 0 fail/skip.
CLI is running sequentially after business at --test-concurrency=2. Every package invocation uses
short TMPDIR and an outer timeout -k 10; no live provider opt-in is passed.
Docker executor container prefix is now g287-executor-* (only the test's scratch name changed),
so the separately required full Docker rerun and final sweep can target only this row's g287-*.
Full CLI: evidence/cli-suite.log, rc 0, 990 tests, 989 pass, 0 fail, 1 skip, duration 851 seconds.
The existing skip is packages/cli/tests/federation-acceptance.test.mjs, "FED R7 scratch two-instance
acceptance", runner scripts/test-federation-acceptance.sh. That runner must pass in this same gate
before the CLI skip is accepted (BRIEF-TEMPLATE mos-1583); no new skipped test was introduced.
Dedicated full Docker and install-file reruns are now running; shell gates follow sequentially.
Dedicated Docker whole: evidence/docker-whole.log, rc 0, 2/2 pass, 0 fail/skip; actual F-A/F-B
permissions and F-C negative/F-D positive reproduced. Complete captured fact TAP: minted 4, hits 0.
Dedicated install whole: evidence/install-whole.log, rc 0, 27/27 pass, 0 fail/skip.
Docker shell wrapper adds an EXIT/INT/TERM cleanup for g287-* only, alongside each test's t.after.
Every scripts/test-*.sh is now running sequentially at timeout -k 10 1200, NO_COLOR=1 and
MOSAIC_TEST_CONCURRENCY=2, with MOSAIC_LIVE_PROVIDER and NODE_TEST_CONTEXT unset. Each script's own
scratch-project cleanup remains active. Stop at the first nonzero result; shell-summary.txt records rc.
Shell launcher error: direct timeout invocation of scripts/test-land-work.sh returned rc 126,
Permission denied. File is tracked 100644; no assertion ran. Not a test RED. Reported steelbeak-0013;
sage-4141 approved bash invocation for this and remaining scripts, matching the landing gate.
No chmod or source change. Preserve initial shell-summary.txt and add shell-resume-summary.txt.
## Builder verification complete (sage-4130)
All twelve shell scripts passed (shell-summary.txt plus shell-resume-summary.txt), with the recorded
rc126 launcher error followed by the approved bash run. Counts: auth 15, conductor 17, config 55,
discord 82, extension-package 18, FED 1, foundation 44, land-work 86, push-check 38 checks plus its
six killed internal mutants, queue 28 shell checks and 156 node passes, release 15, task 92; zero
failed checks. Queue's canonical-only verify/render checks are intentionally skipped by its linked
worktree runner. Task's live-provider cases remain off as required by row244 and this assignment.
Summary: bus 998/998; business 283/283; CLI 989 passed, zero failed and the one existing FED skip
covered by dedicated runner 1/1 with zero skips and cleanup receipt; Docker whole 2/2; install 27/27;
all 12 shell scripts rc0; M1-M11 killed without timeout/syntax kills and byte-restored.
Known fixture search: 3 values across 37 logs, hits0. Fact runner checked four actual minted values
against complete TAP stdout/stderr, hits0. Fake cases check every issued value against stored data.
Final cleanup: no owned test node/chromium processes and no g287 Docker containers; test-task and
release network removal checks passed; FED reports no groups/ports/sockets. Other containers and
preexisting harness/MCP services were untouched. git diff --check passes.
Scope result: a creative library seat gets collaborator write; chief library read uses its declared
read token scopes; creative/chief non-library or mixed raises refuse. Business publish remains fenced
to creative/chief; legacy forks/event fields and host creative refusals retain coverage. T7/T9 now mint
real chief grants before their unchanged revoke/fire assertions. F-C owner-protection follow-up is
recorded above; F-D force-push observation is positive. No live Gitea or host change.
Review handoff: Boyd first, Dewey landing review, Sage gate. Base 18334d713; branch steelbeak/row287.
The review manifest freezes all changed code, tests, logs and these notes (excluding only the
manifest itself). Commit/push are authorized by sage-4130; review/landing remain separate stages.
## First-review doc-only follow-up (Boyd boyd-0034)
Boyd approved the frozen product candidate c2338ed588b9d54e35ebf628f514cfd8d7321b52 and manifest
3683b40a32b68163c3ab0430ee0d6a4603c97882bf62e982d33a40058507c2b6 (56 entries), static review,
no tests or slot use. Dewey received the Q210 landing-review handoff.
Correction to the earlier diff-check claims: plain `git diff --check` checked the then-current
unstaged changes only. The complete base 18334d713 → frozen candidate diff-check returns rc2 because
raw RED/mutant evidence logs retain Node assertion formatting with trailing whitespace. The complete
product/test diff-check returns rc0. Raw evidence is preserved; do not claim the entire committed diff
is whitespace-clean. This correction changes documentation only and does not rerun or weaken a test.
Old-test numstats, verified with `git diff --numstat 18334d713 c2338ed58` (added/deleted lines):
| Existing test file | Added | Deleted |
| --- | ---: | ---: |
| packages/bus/tests/fake-gitea-api.mjs | 13 | 3 |
| packages/bus/tests/gitea-grant-docker.test.mjs | 127 | 2 |
| packages/bus/tests/gitea-grant.test.mjs | 155 | 0 |
| packages/bus/tests/library-credential.test.mjs | 24 | 35 |
| packages/business/tests/business.test.mjs | 25 | 0 |
| packages/cli/tests/case2-install.test.mjs | 13 | 0 |
Canonical queue move steelbeak-287-r1-review succeeded at rev3963, round1 on #1792. Its automatic
review request failed at rev3964 because MOSAIC_GITEA_CREDENTIAL_FILE is unset; this seat has no
Gitea credential. No substitution or secret access attempted. Sage received the precise boundary
in steelbeak-0016 and handles the normal lead request/comment route. The test slot was returned.
Per lead decision137, this is a separate doc-only follow-up to the approved candidate. The frozen
manifest and product/test bytes remain unchanged; the follow-up is not a new product review round.
@@ -0,0 +1,40 @@
✖ C2-21 L7/L7b: research refuses non-library targets lib (56.821785ms)
✖ C2-21 L7/L7b: research refuses non-library targets app,lib (37.82327ms)
ℹ tests 2
ℹ suites 0
ℹ pass 0
ℹ fail 2
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 174.924413
✖ failing tests:
test at packages/bus/tests/gitea-grant.test.mjs:1370:5
✖ C2-21 L7/L7b: research refuses non-library targets lib (56.821785ms)
AssertionError [ERR_ASSERTION]: Missing expected rejection.
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/gitea-grant.test.mjs:1372:7)
at async Test.run (node:internal/test_runner/test:1409:7)
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: undefined,
operator: 'rejects',
diff: 'simple'
}
test at packages/bus/tests/gitea-grant.test.mjs:1370:5
✖ C2-21 L7/L7b: research refuses non-library targets app,lib (37.82327ms)
AssertionError [ERR_ASSERTION]: Missing expected rejection.
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/gitea-grant.test.mjs:1372:7)
at async Test.run (node:internal/test_runner/test:1409:7)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: undefined,
operator: 'rejects',
diff: 'simple'
}
@@ -0,0 +1,35 @@
✖ C2-21 L10: project kind or library level changing after raise refuses decision-mismatch (94.748852ms)
ℹ tests 1
ℹ suites 0
ℹ pass 0
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 176.166886
✖ failing tests:
test at packages/bus/tests/gitea-grant.test.mjs:1416:1
✖ C2-21 L10: project kind or library level changing after raise refuses decision-mismatch (94.748852ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal:
+ actual - expected
[
'GET /api/v1/version',
'GET /api/v1/user',
'GET /api/v1/token',
'GET /api/v1/user/repos',
+ 'POST /api/v1/admin/users'
]
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/gitea-grant.test.mjs:1434:12)
at async Test.run (node:internal/test_runner/test:1409:7)
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: [ 'GET /api/v1/version', 'GET /api/v1/user', 'GET /api/v1/token', 'GET /api/v1/user/repos', 'POST /api/v1/admin/users' ],
expected: [ 'GET /api/v1/version', 'GET /api/v1/user', 'GET /api/v1/token', 'GET /api/v1/user/repos' ],
operator: 'deepStrictEqual',
diff: 'simple'
}
@@ -0,0 +1,30 @@
✖ C2-21 L10: project kind or library level changing after raise refuses decision-mismatch (114.899902ms)
ℹ tests 1
ℹ suites 0
ℹ pass 0
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 190.404699
✖ failing tests:
test at packages/bus/tests/gitea-grant.test.mjs:1416:1
✖ C2-21 L10: project kind or library level changing after raise refuses decision-mismatch (114.899902ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
+ actual - expected
+ undefined
- 'decision-mismatch'
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/gitea-grant.test.mjs:1433:12)
at async Test.run (node:internal/test_runner/test:1409:7)
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: undefined,
expected: 'decision-mismatch',
operator: 'strictEqual',
diff: 'simple'
}
@@ -0,0 +1,29 @@
✖ C2-21 L1/L2/L9: chief-of-staff gets read on her repository with declared scopes through raise and yes (56.777186ms)
ℹ tests 1
ℹ suites 0
ℹ pass 0
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 137.201615
✖ failing tests:
test at packages/bus/tests/gitea-grant.test.mjs:1325:3
✖ C2-21 L1/L2/L9: chief-of-staff gets read on her repository with declared scopes through raise and yes (56.777186ms)
AssertionError [ERR_ASSERTION]: The expression evaluated to a falsy value:
assert.ok(block.includes(`Repository: ${f.gitea.url}/acme/app, level ${level}.`))
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/gitea-grant.test.mjs:1338:12)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.run (node:internal/test_runner/test:1409:7)
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: false,
expected: true,
operator: '==',
diff: 'simple'
}
@@ -0,0 +1,30 @@
✖ C2-21 L4: read widened to write refuses gitea-reach-wider and deletes the minted token (119.045451ms)
ℹ tests 1
ℹ suites 0
ℹ pass 0
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 197.170231
✖ failing tests:
test at packages/bus/tests/gitea-grant.test.mjs:1386:3
✖ C2-21 L4: read widened to write refuses gitea-reach-wider and deletes the minted token (119.045451ms)
AssertionError [ERR_ASSERTION]: {"action":"grant.applied","method":"gitea","grantDecision":"18acf1bb-f896-4b14-9c47-a5cff85a898d","agent":"chief","user":"demo-chief","userId":7,"tokenId":8,"forks":[],"repositories":[{"project":"app","repository":"https://127.0.0.1:33421/acme/app","level":"read"}]}
+ actual - expected
+ undefined
- 'gitea-reach-wider'
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/gitea-grant.test.mjs:1394:12)
at async Test.run (node:internal/test_runner/test:1409:7)
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: undefined,
expected: 'gitea-reach-wider',
operator: 'strictEqual',
diff: 'simple'
}
@@ -0,0 +1,31 @@
✖ C2-21 L1/L2/L9: research gets write on her repository with declared scopes through raise and yes (135.204477ms)
ℹ tests 1
ℹ suites 0
ℹ pass 0
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 215.545162
✖ failing tests:
test at packages/bus/tests/gitea-grant.test.mjs:1325:3
✖ C2-21 L1/L2/L9: research gets write on her repository with declared scopes through raise and yes (135.204477ms)
AssertionError [ERR_ASSERTION]: {"action":"grant.failed","method":"gitea","grantDecision":"0c2e03db-8b1f-4b25-b460-02d551551433","agent":"coder","code":"gitea-reach-wider","repository":"demo-coder/app","user":"demo-coder"}
+ actual - expected
+ 'grant.failed'
- 'grant.applied'
^
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/gitea-grant.test.mjs:1348:12)
at async Test.run (node:internal/test_runner/test:1409:7)
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 'grant.failed',
expected: 'grant.applied',
operator: 'strictEqual',
diff: 'simple'
}
@@ -0,0 +1,25 @@
✖ C2-21: only creative definitions and the chief with gitea may publish a library project (17.773466ms)
ℹ tests 1
ℹ suites 0
ℹ pass 0
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 71.348342
✖ failing tests:
test at packages/business/tests/business.test.mjs:365:1
✖ C2-21: only creative definitions and the chief with gitea may publish a library project (17.773466ms)
AssertionError [ERR_ASSERTION]: Missing expected exception.
at refuses (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/business/tests/business.test.mjs:53:10)
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/business/tests/business.test.mjs:380:5)
at async Test.run (node:internal/test_runner/test:1409:7)
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: undefined,
operator: 'throws',
diff: 'simple'
}
@@ -0,0 +1,33 @@
✖ C2-21 L1/L2/L9: chief-of-staff gets read on her repository with declared scopes through raise and yes (117.793367ms)
ℹ tests 1
ℹ suites 0
ℹ pass 0
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 201.446169
✖ failing tests:
test at packages/bus/tests/gitea-grant.test.mjs:1325:3
✖ C2-21 L1/L2/L9: chief-of-staff gets read on her repository with declared scopes through raise and yes (117.793367ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal:
+ actual - expected
[
- 'read:repository',
'read:user',
+ 'write:repository'
]
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/gitea-grant.test.mjs:1353:12)
at async Test.run (node:internal/test_runner/test:1409:7)
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: [ 'read:user', 'write:repository' ],
expected: [ 'read:repository', 'read:user' ],
operator: 'deepStrictEqual',
diff: 'simple'
}
@@ -0,0 +1,40 @@
✖ C2-21 L7/L7b: chief-of-staff refuses non-library targets lib (52.058911ms)
✖ C2-21 L7/L7b: chief-of-staff refuses non-library targets app,lib (38.384854ms)
ℹ tests 2
ℹ suites 0
ℹ pass 0
ℹ fail 2
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 169.051619
✖ failing tests:
test at packages/bus/tests/gitea-grant.test.mjs:1370:5
✖ C2-21 L7/L7b: chief-of-staff refuses non-library targets lib (52.058911ms)
AssertionError [ERR_ASSERTION]: Missing expected rejection.
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/gitea-grant.test.mjs:1372:7)
at async Test.run (node:internal/test_runner/test:1409:7)
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: undefined,
operator: 'rejects',
diff: 'simple'
}
test at packages/bus/tests/gitea-grant.test.mjs:1370:5
✖ C2-21 L7/L7b: chief-of-staff refuses non-library targets app,lib (38.384854ms)
AssertionError [ERR_ASSERTION]: Missing expected rejection.
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/gitea-grant.test.mjs:1372:7)
at async Test.run (node:internal/test_runner/test:1409:7)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: undefined,
operator: 'rejects',
diff: 'simple'
}
@@ -0,0 +1,35 @@
✖ C2-21: a library grant with no declared scopes refuses gitea-not-declared at raise (53.012865ms)
ℹ tests 1
ℹ suites 0
ℹ pass 0
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 133.866141
✖ failing tests:
test at packages/bus/tests/gitea-grant.test.mjs:1455:1
✖ C2-21: a library grant with no declared scopes refuses gitea-not-declared at raise (53.012865ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
+ actual - expected
+ 'storage-refused'
- 'gitea-not-declared'
at Object.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/gitea-grant.test.mjs:40:63)
at expectedException (node:assert:547:17)
at expectsError (node:assert:672:21)
at strict.rejects (node:assert:738:3)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/gitea-grant.test.mjs:1460:3)
at async Test.run (node:internal/test_runner/test:1409:7)
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: 'storage-refused',
expected: 'gitea-not-declared',
operator: 'strictEqual',
diff: 'simple'
}
@@ -0,0 +1,25 @@
✖ X-G2: admin set refuses each failed check and writes nothing, then writes the slot once (146.452903ms)
ℹ tests 1
ℹ suites 0
ℹ pass 0
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 226.323297
✖ failing tests:
test at packages/bus/tests/gitea-grant.test.mjs:275:1
✖ X-G2: admin set refuses each failed check and writes nothing, then writes the slot once (146.452903ms)
AssertionError [ERR_ASSERTION]: Missing expected rejection.
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/gitea-grant.test.mjs:293:3)
at async Test.run (node:internal/test_runner/test:1409:7)
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: undefined,
operator: 'rejects',
diff: 'simple'
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,291 @@
✔ a grant's level is collaborator or viewer (1.2135ms)
✔ any other level refuses with the key path named (0.705817ms)
✔ covers tells a viewer grant from a collaborator grant (0.16459ms)
✔ covers refuses a call with no level, so a viewer grant can't slip past a collaborator check (0.123619ms)
✔ a viewer grant still needs its business, project, instance and expiry to cover (0.12482ms)
✔ a valid document passes and comes back frozen (2.021061ms)
✔ checkAccess does not freeze or alias the caller's document (0.237983ms)
✔ LEVELS is the single allowed level set, collaborator and viewer (row 193a) (0.098348ms)
✔ accessFilePath is access.json in the config directory (0.319126ms)
✔ top level: object, unknown keys, accessVersion, revision, arrays (0.858299ms)
✔ users: unknown keys and the id rule (0.249483ms)
✔ users: human, tester and any business's human are refused (0.24794ms)
✔ users: home is a given peer or local (0.203275ms)
✔ users: subjects, provider, sub, uniqueness and owner subjects (0.531512ms)
✔ grants: unknown keys, id, user and business (0.431077ms)
✔ grants: projects is ["*"] or distinct lowercase UUIDs (0.609265ms)
✔ grants: instances is ["*"] or distinct declared instances of that business (0.320727ms)
✔ grants: any level other than collaborator or viewer refuses, naming grants[i].level (0.367521ms)
✔ grants: expires is optional ISO time, and a past expiry is not refused (0.747486ms)
✔ the context is required and checked (0.336112ms)
✔ loadAccess reads a valid file and checks it (0.501525ms)
✔ loadAccess refuses a missing file, a symlink and a directory with exit 4 (0.324663ms)
✔ loadAccess refuses another owner and group or other write (0.341248ms)
✔ loadAccess refuses bad JSON and a bad document with exit 2 (0.328302ms)
✔ covers: * and listed ids match (0.194537ms)
✔ covers: an unlisted project, instance or business doesn't match (0.116729ms)
✔ covers: an expired grant doesn't match, at and after the instant (0.118575ms)
✔ covers: level is the least level asked for (row 193a) (0.108766ms)
✔ covers: a missing or unknown level throws (row 193a, N1) (0.235393ms)
✔ covers: a grant with an unknown level covers nothing (row 193a) (0.095664ms)
✔ covers: now must be a valid Date (0.111546ms)
✔ today's files load unchanged: identity, publish and remote are optional (4.289784ms)
✔ a granted agent loads with its identity, grant and the project's upstream (1.918421ms)
✔ N10: publish needs identity and a gitea definition, and the error names the grant (2.193949ms)
✔ row 285: a chief with its gitea reference, identity and publish loads (2.646658ms)
✔ publish names declared projects that have a remote, each once (2.695792ms)
✔ identity fields are checked (2.941449ms)
✔ D9c: the upstream must be its own normalized form, exactly /OWNER/REPO (7.397104ms)
✔ D9c: one .git is stripped, and the fork is built from the stripped name (0.736416ms)
✔ D9c: gitea.url names the instance only (3.765187ms)
✔ the upstream's host and port must be the agent's Gitea's (1.274431ms)
✔ remote-not-fork: an upstream the agent's own user owns, compared without case (1.27873ms)
✔ store references: own path only (design D1, D3a) (0.253823ms)
✔ the role file loads and resolves through loadBusiness (5.248984ms)
✔ the shipped file is exactly the approved list (0.568801ms)
✔ the assistant's authority is message.send and record.write within role (1.643851ms)
✔ the assistant talks to the user and the chief of staff only, and no other shipped role is limited (0.468874ms)
✔ a business grant through limits does not widen the role (2.103126ms)
✔ a launch block whose by is the assistant is refused (1.227137ms)
✔ the instance takes no credential references and no tracker (1.712585ms)
✔ an arbiter whose role definition declares messaging lists is refused, for each kind (3.414519ms)
✔ an arbiter without messaging lists is accepted beside the limited assistant (1.570128ms)
✔ the contract passes work to the chief of staff and directs no one (0.485429ms)
✔ REQ-HARN-2: the assistant, companion and research hold the web grant, and no other role file does (0.988738ms)
✔ C2-04: library mappings load without roots, remain frozen, and retain mapping refusals (11.206347ms)
✔ C2-04: an otherwise valid publish grant cannot name a library repo (1.732724ms)
✔ config directory and file path follow MOSAIC_CONFIG (0.227073ms)
✔ the fixture business validates and comes back frozen (0.994283ms)
✔ two instances may share a definition (1.013991ms)
✔ top-level refusals (2.939224ms)
✔ arbiters and projects (4.586351ms)
✔ role instances (2.30027ms)
✔ Vikunja bots (5.245177ms)
✔ a role without Vikunja takes no tracker block (2.857595ms)
✔ credential references match the definition's services (2.272475ms)
✔ launch (8.361716ms)
✔ REQ-THREAD-2 REQ-LAUNCH-1 launch.threadIdleSeconds: absent is the default, a positive integer is used, anything else refuses (4.793795ms)
✔ loadBusiness: file checks (2.040605ms)
✔ loadBusiness: not a regular file (41.081919ms)
✔ loading writes nothing (1.077813ms)
✔ names that are Object.prototype properties don't count as declared (3.161448ms)
✔ the shipped example refuses as written and validates once filled in (0.811309ms)
✔ persona vars: agent layer, creative definition only, both or neither (8.817701ms)
✔ row 285: a chief or creative instance without credentials.gitea refuses at load (6.447728ms)
✔ row 285: a creative instance with identity and publish loads (0.774254ms)
✔ row 194 keep, noHire and retired validate, and a file without them loads as before (1.57886ms)
✔ row 194 each bad keep, noHire or retired entry refuses (5.865272ms)
✔ C2-21: only creative definitions and the chief with gitea may publish a library project (10.140247ms)
✔ row 323: committed Case 1 document loads unchanged through the business loader (4.882073ms)
✔ row 323: real business validate accepts the Case 1 fixture without tracker credentials (65.86572ms)
✔ row 323: real factory install accepts the document's scratch input and writes it unchanged (161.468205ms)
✔ row 323: the Case 1 business boots without a tracker or @sync credential (50.794373ms)
✔ row 323: factory install refuses bus.tester false before writing (34.17225ms)
✔ row 323: literal library key refuses through final business validation (178.477589ms)
✔ row 323: a library project refuses through the factory profile plan (33.530043ms)
✔ row 325: the Case 1 project's sole key matches the repository project id (4.587873ms)
✔ row 325: real business validate reports the copied repository project valid (66.272939ms)
✔ row 325: the old mosaic-stack project key refuses against those same project bytes (66.102296ms)
✔ row 285 amendment 1: no library loads with empty chief credentials (3.385712ms)
✔ row 285 amendment 1: a library without chief gitea refuses by name (2.200618ms)
✔ row 285 amendment 1: no library with chief gitea refuses the extra reference (0.708673ms)
✔ the role file loads and resolves through loadBusiness (8.538323ms)
✔ the shipped file is exactly the approved list (0.339503ms)
✔ the resolved record matches the approved list, so a widened file fails (2.406238ms)
✔ the chief of staff holds role.launch, message.send, role.end, organization.change, record.write, instance.name, role.hire and role.fire; every other action is gated (2.024787ms)
✔ the pm does not hold role.launch (2.017621ms)
✔ a business grant of a gated action through limits does not widen the role (1.216462ms)
✔ a limit that drops role.launch leaves the chief of staff with no launch block (1.161866ms)
✔ a launch block whose by is the pm instance is refused (1.3663ms)
✔ the contract names no duty to talk to the human and the pm contract no launch (0.368325ms)
✔ the instance takes no credential references and no tracker (1.990093ms)
✔ C2-04: business validate and resolve skip a library before code projects and credential checks (200.876248ms)
✔ usage errors exit 4 (277.707279ms)
✔ validate: a good business exits 0 and prints instance digests (70.09797ms)
✔ validate: project files (308.414394ms)
✔ validate: missing files and a broken system config (237.80596ms)
✔ validate: credential reference problems exit 2 and name each one (62.694272ms)
✔ validate: a not-issued store agent dated today passes, and an issued one with a past expiry fails (137.769151ms)
✔ validate: a token file inside the repository is refused (67.194357ms)
✔ validate: role definitions come from MOSAIC_ROLES_DIR (189.791235ms)
✔ resolve: prints one instance's record (258.920062ms)
✔ resolve: refusals (383.486673ms)
✔ parse: exactly one of file or env, plus the service's date (1.863836ms)
✔ check: a good file has no problems (0.63731ms)
✔ check never opens the file: a write-only token passes (0.259065ms)
✔ check: file problems (0.654057ms)
✔ check: token files can't live in the repository or dataRoot, even through a linked directory (0.666643ms)
✔ check: dates and environment references (0.302686ms)
✔ each definition loads, with a contract file, and is exactly the approved list (2.422404ms)
✔ an organization file that declares them passes, and each resolves to least privilege (4.82324ms)
✔ the chief of staff launches each one; no other role does (2.43161ms)
✔ a C-suite role can't be made the launcher: launch.by must hold role.launch (1.392659ms)
✔ limits can narrow a C-suite role but never widen it (1.344546ms)
✔ any unknown key, tool, action or version in a role file is refused (0.395675ms)
✔ row 298: checkGrantScope takes the D2 shape and refuses anything else (1.345091ms)
✔ row 298: a missing scope is empty and a bad one falls back to empty, which covers nothing (0.239287ms)
✔ row 298: a Gitea grant is in scope only when its agent and every project are listed (0.120581ms)
✔ row 298 (sage-3594 Q2): host commands match whole argv tokens, and enrollment is never in scope (0.172913ms)
✔ row 298: a bad grantScope in the file still loads, and the read shows the fallback (9.548945ms)
✔ row 298: the writer checks the scope, logs the change and removes the key for {} (29.2548ms)
✔ row 298 (sage-3594 Q1): the general writers can't write grantScope (6.509756ms)
✔ REQ-WAIT-1: GATED_ONLY is exactly the hard waits; the new actions are in the vocabulary (0.953961ms)
✔ REQ-ROLE-2: END_PROTECTED includes the assistant's thread definition (0.140081ms)
✔ REQ-COS-3: role.hire and role.fire are routed, not hard waits (0.086622ms)
✔ REQ-WAIT-3: library.write is routed under REQ-LIB-6 (1.16), not a hard wait (0.079125ms)
✔ REQ-WAIT-3: every action is routed or a hard wait, never both, and the routed list names only actions (0.142718ms)
✔ REQ-WAIT-3: instance.name is routed under REQ-MSG-2 (1.5), not a hard wait (row 141, sage-0589) (0.070496ms)
✔ REQ-WAIT-2: a role listing a hard wait at either class is refused; the actions that left can be granted (2.750593ms)
✔ S1: the business file refuses hosts at the top level and in a role, and host.<id> references (12.095424ms)
✔ T1: address, user and host key checks, which the enrollment raise uses (4.457254ms)
✔ T26: no wildcard and no shell, runner or interpreter as a prefix (1.428718ms)
✔ D6: argv shape and the literal prefix match (0.265339ms)
✔ row 285 P: creative equal-service changes preserve every existing credential reference (23.443808ms)
✔ row 285 P: creative declared-service mismatch refuses even when host has no reference (4.777219ms)
✔ row 285 P: an equal-service non-creative target keeps the base credential refusal (4.051484ms)
✔ row 285 P: business validation precedes the creative credential fence (4.080403ms)
✔ row 285 P: listing derives creative from authority and accepts only existing gitea minting (2.940761ms)
✔ C2-04: instance settings skip a library while checking a later code project (24.897209ms)
✔ row 165 read lists each instance and the definitions add accepts (2.526881ms)
✔ row 165 add: two instances join, validate exits 0, the log names the human, and the writer asks for no restart (row 194, D3) (82.478108ms)
✔ row 165 change: definition, vars and launch membership log before and after; none asks for a restart, the bus reloads (row 194, D3) (100.666633ms)
✔ row 165 refuses each validation failure and leaves the file's bytes alone (35.076218ms)
✔ T19 add of a credentialed definition writes store references dated today, names its bot, and validate exits 0 (85.693778ms)
✔ T19 add of a definition that declares host writes references for its token services only (12.728252ms)
✔ row 165 refuses a definition change to or from one that declares credentials (5.117931ms)
✔ row 165 a vars or launch change on an instance with its binding succeeds and keeps the binding (75.996979ms)
✔ row 165 refuses an END_PROTECTED definition and every change to a protected instance (8.113739ms)
✔ row 165 refuses while a project file doesn't resolve, as validate would (16.000833ms)
✔ row 165 refuses a stale revision and leaves the file's bytes alone (11.161353ms)
✔ row 65 an instance write refuses harness-off, thinking-unsupported, an unknown model and a partial fallback (30.579803ms)
✔ row 213 a creative instance is the human's: add, vars and definition changes, persona digest only (85.63908ms)
✔ row 256 T1-T3, T5 the human deletes a creative instance, no other actor does, and the assistant stays (99.46135ms)
✔ row 256 T4 the human's delete of the only listed instance, a creative one, is the last-listed refusal (3.143347ms)
✔ REQ-LIMIT-1 schema: every scope unset or zero, adjustable above four, bad values and unknown models refuse (11.58859ms)
✔ REQ-LIMIT-1 counts each exact scope at/over limit, unlimited and old tuple after model reassignment (0.653762ms)
✔ REQ-ROLE-3 writer: atomic validated revision, immutable previous bytes and authenticated actor/old/new receipt (17.075687ms)
✔ REQ-ROLE-3 writer: pre-rename fault leaves old file, post-rename recovery records commit, conflicting recovery refuses (13.840934ms)
✔ REQ-LIMIT-1 / decision145 migration prints only, preserves values, rejects ambiguity, and is idempotent (3.72795ms)
✔ REQ-LIMIT-1 shared settings validates the complete proposed selection and logs exact actor (19.857864ms)
✔ REQ-LIMIT-1 provider is table data: another provider routes separately and aliases share exact counts (1.748236ms)
✔ REQ-ROLE-3 refuses live writer locks, symlink targets and uncertain recovery without changing bytes (9.132518ms)
✔ Q169 assistants: vars and definition change and log the actor; tracker null leaves no tracker (16.727332ms)
✔ Q169 assistants: refuses other instances, other definitions, holder and credentials, empty entries, a tracker the definition has no credential for and a stale revision (18.030778ms)
✔ Q169 assistants: a definition with a vikunja credential takes a tracker change and refuses its removal or a used bot (11.719638ms)
✔ row 65 routes lists every instance with harness, model, thinking and fallback, the switched-on harnesses, the model keys and each harness's levels (2.212163ms)
✔ row 65 setRoutes saves a harness, model, thinking and whole fallback, reads each back from the file, logs the actor, and a null fallback clears all three (14.340575ms)
✔ row 65 setRoutes refuses a partial fallback, a switched-off harness, a level the harness doesn't apply, an unloaded module, an unknown model or instance, a malformed route, no change and a stale revision, with the file unchanged (19.4324ms)
✔ row 65 a file with harness vars and no plugins.harness loads and its views read; a model change still saves, and a route change refuses harness-off (9.559348ms)
✔ B7 kept instance: the models keys it names and the instances key are the human's (25.258332ms)
✔ B7 kept instance: its route is the human's (8.773053ms)
✔ M10 creative instance: models keys it names, the instances key and its route are the human's (31.644759ms)
✔ C2-04 M9: changed playground and hybrid entries leave business and changes.jsonl byte-identical (20.984808ms)
✔ C2-04 M9: code projects, additions, deletions and array changes are protected too (15.600225ms)
✔ C2-04 M9: canonical key order is allowed and a non-project setter still records its change (12.997939ms)
✔ C2-04 M9: a serialization hook cannot change projects after the canonical comparison (9.845749ms)
✔ M7/M8: library seats come from library businesses, creative seats from their creative definitions (6.566752ms)
✔ M8: a business with no library project puts no seat in either set (1.612265ms)
✔ M7/M8: a business file that is not valid JSON throws; no empty set (1.658908ms)
✔ M7/M8: an absent businesses directory gives empty sets (0.32243ms)
✔ M7/M8: a businesses path that is not a directory throws; no empty set (0.725625ms)
✔ M7/M8: a businesses symlink to a missing target throws; no empty set (0.382107ms)
✔ M7/M8: a config directory under a regular file throws (lstat fails with ENOTDIR); no empty set (0.297168ms)
✔ M7/M8: a business file that can't be read throws; no empty set (0.755072ms)
✔ T6 delete refuses in the design's order and leaves the file's bytes alone (28.611933ms)
✔ T6 delete refuses the last listed instance (3.226085ms)
✔ T6 a clean delete removes the entry and its own references, adds retired, and the name can't come back (83.48393ms)
✔ T7 veto: keep, unkeep, nohire and hire each write once, a repeat is no change, and the log names the human (112.086534ms)
✔ T7 veto refuses an unknown or END_PROTECTED keep, a bad request, a stale revision and a business without launch (17.584597ms)
✔ D2.2 hire adds and lists a new instance, logs the chief's context, and sets only model and limits (78.115614ms)
✔ D2.2 hire refuses noHire, a kept name and a listed name, and re-lists a fired one only as it was (22.285503ms)
✔ D2.2 fire delists and keeps the entry, a repeat is done with no write, and each refusal holds (79.084361ms)
✔ row 256 checkDelete passes the human's creative delete and refuses another actor's; fire still refuses (5.186556ms)
✔ row 239 T1 a fire on a revision that goes stale before the writer's lock refuses there and revokes nothing (4.342942ms)
✔ row 239 T2 a write after the revoke refused before publication carries revoked, and the file keeps the edit (7.275516ms)
✔ row 239 T3 any error after the revoke carries revoked, for a fire and a delete; a revoke that throws carries nothing (24.024395ms)
✔ Q247: omitting tracker loads as null and preserves independent role bots (4.88928ms)
✔ Q247: real business validate passes without a sync credential reference or file (67.498247ms)
✔ Q247: present tracker retains its values and validation without mutating the input (2.36343ms)
✔ Q247: present null, undefined and malformed trackers still refuse (3.735298ms)
✔ Q247: absent business tracker does not waive a role's Vikunja bot requirement (0.785091ms)
✔ REQ-PLUG-1 schema: plugins is optional, names known kinds and checked module names (10.412775ms)
✔ REQ-PLUG-1 REQ-ROLE-3 the setter writes plugins through the organization writer and logs old and new names (21.608828ms)
✔ vikunja:19/309 a patch changes only the kinds it names; null removes one kind, explicitly and in the trail (22.123619ms)
✔ vikunja:19/309 an unknown kind (set or removed), a bad module, an empty patch and null on the wrong shape are refused, nothing written (1.61091ms)
✔ REQ-PLUG-1 REQ-ROLE-3 the writer checks the bytes it writes: a Date in plugins is refused there too (3.81691ms)
✔ REQ-PLUG-1 REQ-ROLE-3 a refused setter write throws a code, changes nothing and logs nothing (1.453096ms)
✔ vikunja:19/305 with an exists check, the setter refuses a module no tree file answers to, before anything is written (6.975244ms)
✔ optional project repo is preserved and must be a normalized absolute path (8.248464ms)
✔ loaded business project repo cannot be nested in any declared code checkout (1.924299ms)
✔ path and load (1.814342ms)
✔ refusals (0.952563ms)
✔ record.write is within-role for the eleven approved v2 agents, never gated-only (2.278902ms)
✔ C2-06 M11 human replacement works; omitted settings return to defaults (27.784744ms)
✔ C2-06 M11 agents and tester cannot change settings, including by changing human (24.473091ms)
✔ C2-06 M11 serialization cannot replace settings after the first fence (8.808556ms)
✔ C2-06 M11 an unrelated actor cannot appoint itself human and change reply limits (7.973319ms)
✔ C2-06 invalid setter values preserve the business and changes.jsonl (8.939774ms)
✔ C2-06 validator accepts hand-edited settings; raw reader fails closed on invalid files (10.916335ms)
✔ C2-04: a library first in projects does not hide a later project layer (6.843821ms)
✔ systemVars flattens the validated config (0.836289ms)
✔ precedence: system, business, project, project role, agent (1.614161ms)
✔ limits narrow the definition and never widen it (2.069759ms)
✔ role.launch stays within-role only for the instance the launch block names (4.437162ms)
✔ limits.authority without role.launch leaves the launcher with no launch block (1.556099ms)
✔ limits.authority narrows cross-role actions too (0.872466ms)
✔ classify (0.882709ms)
✔ the record carries what the broker and launcher need (0.73785ms)
✔ digest: key order doesn't matter, any value change does (5.81602ms)
✔ refusals (1.779158ms)
✔ the four shipped version 2 roles load (2.378087ms)
✔ shipped role scopes match addendum B section 2 and the SR runbook (0.811462ms)
✔ shipped authority follows the note's table (4.746427ms)
✔ every role file in roles/ loads through the validator, and a v2 contract exists (1.129712ms)
✔ version 1 files keep loading with no authority (0.820814ms)
✔ the conductor policy isn't a role (0.145369ms)
✔ a missing role file is exit 4, a symbolic link too (0.319514ms)
✔ version 2 refusals (0.929286ms)
✔ authority: closed vocabulary, no gated-only action, no overlap (1.548788ms)
✔ messaging: optional, role definitions and human, never the tester (1.049825ms)
✔ row 64 skills: optional, distinct skill directory names, none on version 1 (1.019457ms)
✔ credentials: Gitea scopes (0.605626ms)
✔ credentials: Vikunja scopes are a group-to-verbs map from the grantable list (0.6735ms)
✔ credentials: services (0.330683ms)
✔ contract: a non-empty regular Markdown file beside the role file (0.469895ms)
✔ the six creative roles load: read tools, message.send and library.write, gitea, messaging with the human only (0.353918ms)
✔ M1: a creative role file wider than read and answer is refused (2.717807ms)
✔ hireable: HIRE_ALLOWED is exactly the six plain work actions, and every one is routed (0.08884ms)
✔ hireable: a list naming a definition that holds any refused action refuses at load, one case per action (6.677373ms)
✔ hireable: a mixed definition refuses, a clean list loads, and the named file's own hireable isn't read (0.544957ms)
✔ hireable: a missing, version 1, duplicate or badly named entry refuses, and the key is optional (1.724693ms)
✔ the pm and the cto check a saved spec against settled decisions and work in flight, and answer with a BLOCK or an accept (4.171158ms)
✔ the chief of staff relays a BLOCK or an accept to the assistant as a reply to the message that carried it (0.402275ms)
✔ the assistant summarizes a spec review in its own words and doesn't repeat the chief of staff's text (0.368389ms)
✔ REQ-LIMIT-3 schema: each subject kind, limit 0 and no ceiling are accepted; bad limits, times, keys and undeclared instances refuse (12.866323ms)
✔ REQ-LIMIT-3 the writer stamps since: a new limit gets now, a changed one keeps its time, a different time is refused (38.335671ms)
✔ REQ-LIMIT-3 only tokens.* at schema depth holds token limits: agent limits named `limit` save as sent, and a token limit named `limit` is stamped and reset (39.769908ms)
✔ REQ-LIMIT-3 a reset moves since to now, is logged with its actor, and refuses a path that is not a limit (21.162763ms)
✔ REQ-LIMIT-2 schema: launch.account names a credential set; a daily quota needs a reset and an IANA zone and names the account (22.640957ms)
✔ REQ-LIMIT-2 the writer stamps a daily quota's since, keeps its reset and zone, and a reset moves since (22.016802ms)
✔ every key names known layers and a merge rule (0.630788ms)
✔ unknown keys and wrong layers refuse (0.532995ms)
✔ types (1.62428ms)
✔ merge: defaults, then the most specific layer wins (0.217143ms)
✔ merge: limits only narrow, and provenance lists each source (0.286677ms)
✔ merge doesn't change its inputs (0.115663ms)
✔ web.searxng takes https anywhere and http on loopback, so Jason's 127.0.0.1:8888 passes (0.148911ms)
✔ both definitions load, with a contract file, and are exactly the approved lists (1.885538ms)
✔ security-reviewer has no write or shell tool and classifies only the verdict as its own (5.091977ms)
✔ merge-gate's only grant is git.merge.protected cross-role, and it has no write tool (1.67274ms)
✔ neither role can be an arbiter: its messaging lists refuse the business file (row 133) (3.925963ms)
✔ every role contract carries the proactive line in its Authority section (0.562759ms)
✔ pm and chief-of-staff say where a raise goes under REQ-DEC-2 (0.479526ms)
✔ the merge landing takes two runs: merge-gate, cto and chief-of-staff each say their part (0.372532ms)
ℹ tests 283
ℹ suites 0
ℹ pass 283
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 3476.671216
@@ -0,0 +1,7 @@
owned test node/chromium processes found: 0
owned test processes remaining: 0
owned g287 Docker containers removed in final sweep: 0
owned g287 Docker containers remaining: 0
FED runner: no child groups, ports or sockets remain.
Task/release scripts: owned compose network removal checks pass.
Preexisting Codex/MCP runtime services excluded from the test-process sweep; other containers untouched.
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,27 @@
✔ C2-21 F-A/F-B: admin collaborator grant on another owner and seat permission reach; F-C/F-D protection observations (11763.04751ms)
ℹ ✔ C2-21 F-A/F-B: admin collaborator grant on another owner and seat permission reach; F-C/F-D protection observations (11616.122395ms)
ℹ F-A creative write: PUT status 204
ℹ F-B creative: permissions {"admin":false,"push":true,"pull":true}
ℹ F-A chief read: PUT status 204
ℹ F-B chief: permissions {"admin":false,"push":false,"pull":true}
ℹ F-C protected Story/**: seat rc 1, owner rc 1; established false
ℹ F-D protected branch: normal rc 0, force rc 1; established true
ℹ minted 4, hits 0
ℹ tests 1
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 11727.640162
ℹ minted 4, hits 0 (complete TAP stdout/stderr)
ℹ tests 1
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 11858.472832
@@ -0,0 +1,13 @@
✔ C2-21 F-A/F-B: admin collaborator grant on another owner and seat permission reach; F-C/F-D protection observations (40.800678ms)
ℹ (node:3897534) Warning: node:test run() is being called recursively within a test file. skipping running files.
(Use `node --trace-warnings ...` to show where the warning was created)
ℹ minted 0, hits 0 (complete TAP stdout/stderr)
ℹ tests 1
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 148.828093
@@ -0,0 +1,28 @@
✔ C2-21 F-A/F-B: admin collaborator grant on another owner and seat permission reach; F-C/F-D protection observations (12203.540198ms)
ℹ ✔ C2-21 F-A/F-B: admin collaborator grant on another owner and seat permission reach; F-C/F-D protection observations (12086.277368ms)
ℹ F-A creative write: PUT status 204
ℹ F-B creative: permissions {"admin":false,"push":true,"pull":true}
ℹ F-A chief read: PUT status 204
ℹ F-B chief: permissions {"admin":false,"push":false,"pull":true}
ℹ F-C protected Story/**: seat rc 1, owner rc 1; established false
ℹ F-D protected branch: normal rc 0, force rc 1; established true
ℹ minted 4, hits 0
ℹ tests 1
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 12174.33915
ℹ minted 4, hits 0 (complete TAP stdout/stderr)
✔ X-G3: on scratch 1.27.1, a grant makes the user, mints, forks, checks reach, writes the file, and revoke deletes the token (12131.714969ms)
ℹ tests 2
ℹ suites 0
ℹ pass 2
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 24419.085201
@@ -0,0 +1,35 @@
✔ the library install writes the business file, personas and slot dir, makes the copy and starts nothing (235.765785ms)
✔ a library re-run passes and leaves every file as it was (392.367807ms)
✔ a re-run refuses business-file-differs and leaves the file unchanged (273.242685ms)
✔ a re-run refuses persona-differs when an installed persona isn't the staged one, and writes nothing (272.374516ms)
✔ the factory install writes no library mapping, persona or slot dir (177.367057ms)
✔ the factory install refuses a library project and bus.tester off, before anything is written (73.809786ms)
✔ the library install refuses bus.tester on and a persona var in the input (75.800921ms)
✔ the install refuses root, a business id mismatch, another --home and a config dir with group bits (54.413413ms)
✔ the library install needs --personas (5.198673ms)
✔ a validate refusal removes the business file this run wrote (239.945383ms)
✔ N4: a validate refusal on a re-run leaves the matching business file in place, byte for byte (386.30537ms)
✔ a clean library host passes the precheck with no mirror yet (405.96297ms)
✔ the library precheck refuses a persona file at 0644 (394.702581ms)
✔ the library precheck refuses a persona whose digest isn't persona.sha256 (390.230566ms)
✔ the library precheck refuses a persona dir with group bits (384.651188ms)
✔ the library precheck refuses a business file at 0644 (394.310416ms)
✔ the library precheck refuses a changes.jsonl at 0644 (558.803755ms)
✔ the library precheck refuses a Discord binding (390.812738ms)
✔ --library-status refuses library-mirror-missing before the bus has made the mirror (303.064191ms)
✔ --library-status refuses library-never-synced on a fresh mirror (323.253782ms)
✔ --library-status names a recorded lastError before the null lastFetch it leaves (N3) (338.938239ms)
✔ --library-status prints each R-problem's path as an escaped JSON string, then the count, and passes (383.891631ms)
✔ --library-status takes no --profile (5.425522ms)
✔ Vikunja 614: install preserves the explicit notifier decision bytes and mode and creates none when absent (469.515908ms)
✔ row 322 F8: library personas install from the copy with no clone node_modules (139.58888ms)
✔ row 322 F9: importer refusal keeps the error line rather than the Node banner (88.736058ms)
✔ C2-21: library install supplies chief identity and books publish for its real read grant (234.472998ms)
ℹ tests 27
ℹ suites 0
ℹ pass 27
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 7494.342107
@@ -0,0 +1,11 @@
M1: rc=1 pass=0 fail=2 skip=0 KILLED
M2: rc=1 pass=0 fail=1 skip=0 KILLED
M3: rc=1 pass=0 fail=1 skip=0 KILLED
M4: rc=1 pass=0 fail=1 skip=0 KILLED
M5: rc=1 pass=0 fail=1 skip=0 KILLED
M6: rc=1 pass=0 fail=1 skip=0 KILLED
M7: rc=1 pass=0 fail=2 skip=0 KILLED
M8: rc=1 pass=0 fail=1 skip=0 KILLED
M9: rc=1 pass=0 fail=1 skip=0 KILLED
M10: rc=1 pass=0 fail=1 skip=0 KILLED
M11: rc=1 pass=0 fail=1 skip=0 KILLED
@@ -0,0 +1,11 @@
M1 KILLED rc=1
M2 KILLED rc=1
M3 KILLED rc=1
M4 KILLED rc=1
M5 KILLED rc=1
M6 KILLED rc=1
M7 KILLED rc=1
M8 KILLED rc=1
M9 KILLED rc=1
M10 KILLED rc=1
M11 KILLED rc=1
@@ -0,0 +1,243 @@
✖ C2-21 L1/L2/L9: research gets write on her repository with declared scopes through raise and yes (61.932385ms)
✖ C2-21 L1/L2/L9: chief-of-staff gets read on her repository with declared scopes through raise and yes (45.50288ms)
✖ C2-21 L1/L2/L9: coder gets read on her repository with declared scopes through raise and yes (46.877421ms)
✔ C2-21 L7/L7b: research refuses non-library targets lib (41.832555ms)
✔ C2-21 L7/L7b: research refuses non-library targets app,lib (37.221112ms)
✔ C2-21 L7/L7b: chief-of-staff refuses non-library targets lib (36.572846ms)
✔ C2-21 L7/L7b: chief-of-staff refuses non-library targets app,lib (37.961766ms)
✖ C2-21 L4: read widened to write refuses gitea-reach-wider and deletes the minted token (38.226051ms)
✖ C2-21 L4: admin privilege refuses gitea-reach-wider and deletes the minted token (39.45444ms)
✖ C2-21 L4: missing pull refuses gitea-reach-wider and deletes the minted token (39.007984ms)
✖ C2-21 L4: missing permission object refuses gitea-reach-wider and deletes the minted token (39.888865ms)
✖ C2-21 L4: library fork refuses gitea-reach-wider and deletes the minted token (40.565327ms)
✖ C2-21 L8: untargeted library collaborator reach is disclosed and accepted publish-wide (41.382167ms)
✖ C2-21 L10: project kind or library level changing after raise refuses decision-mismatch (123.506544ms)
✖ C2-21 L4: creative write requires push true but ignores unrelated permission keys (38.813518ms)
✖ C2-21: only creative definitions and the chief with gitea may publish a library project (7.989289ms)
✖ C2-21: library install supplies chief identity and books publish for its real read grant (264.267351ms)
ℹ tests 17
ℹ suites 0
ℹ pass 4
ℹ fail 13
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 805.736302
✖ failing tests:
test at packages/bus/tests/gitea-grant.test.mjs:1325:3
✖ C2-21 L1/L2/L9: research gets write on her repository with declared scopes through raise and yes (61.932385ms)
Error: gitea-not-declared
at fail (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:219:9)
at #giteaRole (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:2768:89)
at #giteaGrantBinding (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:2800:52)
at file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:2700:114
at Store.transaction (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/store.mjs:289:22)
at #raiseGrant (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:2699:28)
at Broker.request (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:4807:88)
at call (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/gitea-grant.test.mjs:109:49)
at Object.raise (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/gitea-grant.test.mjs:111:5)
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/gitea-grant.test.mjs:1336:23) {
code: 'gitea-not-declared'
}
test at packages/bus/tests/gitea-grant.test.mjs:1325:3
✖ C2-21 L1/L2/L9: chief-of-staff gets read on her repository with declared scopes through raise and yes (45.50288ms)
Error: gitea-not-declared
at fail (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:219:9)
at #giteaRole (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:2768:89)
at #giteaGrantBinding (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:2800:52)
at file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:2700:114
at Store.transaction (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/store.mjs:289:22)
at #raiseGrant (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:2699:28)
at Broker.request (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:4807:88)
at call (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/gitea-grant.test.mjs:109:49)
at Object.raise (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/gitea-grant.test.mjs:111:5)
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/gitea-grant.test.mjs:1336:23) {
code: 'gitea-not-declared'
}
test at packages/bus/tests/gitea-grant.test.mjs:1325:3
✖ C2-21 L1/L2/L9: coder gets read on her repository with declared scopes through raise and yes (46.877421ms)
AssertionError [ERR_ASSERTION]: The expression evaluated to a falsy value:
assert.ok(block.includes(`Repository: ${f.gitea.url}/acme/app, level ${level}.`))
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/gitea-grant.test.mjs:1338:12)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.run (node:internal/test_runner/test:1409:7)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: false,
expected: true,
operator: '==',
diff: 'simple'
}
test at packages/bus/tests/gitea-grant.test.mjs:1386:3
✖ C2-21 L4: read widened to write refuses gitea-reach-wider and deletes the minted token (38.226051ms)
Error: gitea-not-declared
at fail (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:219:9)
at #giteaRole (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:2768:89)
at #giteaGrantBinding (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:2800:52)
at file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:2700:114
at Store.transaction (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/store.mjs:289:22)
at #raiseGrant (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:2699:28)
at Broker.request (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:4807:88)
at call (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/gitea-grant.test.mjs:109:49)
at raise (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/gitea-grant.test.mjs:111:5)
at Object.approve (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/gitea-grant.test.mjs:121:21) {
code: 'gitea-not-declared'
}
test at packages/bus/tests/gitea-grant.test.mjs:1386:3
✖ C2-21 L4: admin privilege refuses gitea-reach-wider and deletes the minted token (39.45444ms)
Error: gitea-not-declared
at fail (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:219:9)
at #giteaRole (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:2768:89)
at #giteaGrantBinding (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:2800:52)
at file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:2700:114
at Store.transaction (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/store.mjs:289:22)
at #raiseGrant (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:2699:28)
at Broker.request (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:4807:88)
at call (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/gitea-grant.test.mjs:109:49)
at raise (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/gitea-grant.test.mjs:111:5)
at Object.approve (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/gitea-grant.test.mjs:121:21) {
code: 'gitea-not-declared'
}
test at packages/bus/tests/gitea-grant.test.mjs:1386:3
✖ C2-21 L4: missing pull refuses gitea-reach-wider and deletes the minted token (39.007984ms)
Error: gitea-not-declared
at fail (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:219:9)
at #giteaRole (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:2768:89)
at #giteaGrantBinding (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:2800:52)
at file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:2700:114
at Store.transaction (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/store.mjs:289:22)
at #raiseGrant (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:2699:28)
at Broker.request (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:4807:88)
at call (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/gitea-grant.test.mjs:109:49)
at raise (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/gitea-grant.test.mjs:111:5)
at Object.approve (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/gitea-grant.test.mjs:121:21) {
code: 'gitea-not-declared'
}
test at packages/bus/tests/gitea-grant.test.mjs:1386:3
✖ C2-21 L4: missing permission object refuses gitea-reach-wider and deletes the minted token (39.888865ms)
Error: gitea-not-declared
at fail (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:219:9)
at #giteaRole (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:2768:89)
at #giteaGrantBinding (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:2800:52)
at file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:2700:114
at Store.transaction (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/store.mjs:289:22)
at #raiseGrant (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:2699:28)
at Broker.request (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:4807:88)
at call (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/gitea-grant.test.mjs:109:49)
at raise (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/gitea-grant.test.mjs:111:5)
at Object.approve (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/gitea-grant.test.mjs:121:21) {
code: 'gitea-not-declared'
}
test at packages/bus/tests/gitea-grant.test.mjs:1386:3
✖ C2-21 L4: library fork refuses gitea-reach-wider and deletes the minted token (40.565327ms)
Error: gitea-not-declared
at fail (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:219:9)
at #giteaRole (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:2768:89)
at #giteaGrantBinding (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:2800:52)
at file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:2700:114
at Store.transaction (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/store.mjs:289:22)
at #raiseGrant (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:2699:28)
at Broker.request (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:4807:88)
at call (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/gitea-grant.test.mjs:109:49)
at raise (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/gitea-grant.test.mjs:111:5)
at Object.approve (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/gitea-grant.test.mjs:121:21) {
code: 'gitea-not-declared'
}
test at packages/bus/tests/gitea-grant.test.mjs:1402:1
✖ C2-21 L8: untargeted library collaborator reach is disclosed and accepted publish-wide (41.382167ms)
Error: gitea-not-declared
at fail (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:219:9)
at #giteaRole (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:2768:89)
at #giteaGrantBinding (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:2800:52)
at file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:2700:114
at Store.transaction (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/store.mjs:289:22)
at #raiseGrant (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:2699:28)
at Broker.request (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:4807:88)
at call (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/gitea-grant.test.mjs:109:49)
at Object.raise (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/gitea-grant.test.mjs:111:5)
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/gitea-grant.test.mjs:1405:21) {
code: 'gitea-not-declared'
}
test at packages/bus/tests/gitea-grant.test.mjs:1416:1
✖ C2-21 L10: project kind or library level changing after raise refuses decision-mismatch (123.506544ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
+ actual - expected
+ undefined
- 'decision-mismatch'
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/gitea-grant.test.mjs:1430:12)
at async Test.run (node:internal/test_runner/test:1409:7)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: undefined,
expected: 'decision-mismatch',
operator: 'strictEqual',
diff: 'simple'
}
test at packages/bus/tests/gitea-grant.test.mjs:1435:1
✖ C2-21 L4: creative write requires push true but ignores unrelated permission keys (38.813518ms)
Error: gitea-not-declared
at fail (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:219:9)
at #giteaRole (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:2768:89)
at #giteaGrantBinding (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:2800:52)
at file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:2700:114
at Store.transaction (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/store.mjs:289:22)
at #raiseGrant (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:2699:28)
at Broker.request (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:4807:88)
at call (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/gitea-grant.test.mjs:109:49)
at raise (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/gitea-grant.test.mjs:111:5)
at Object.approve (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/gitea-grant.test.mjs:121:21) {
code: 'gitea-not-declared'
}
test at packages/business/tests/business.test.mjs:365:1
✖ C2-21: only creative definitions and the chief with gitea may publish a library project (7.989289ms)
Error [BusinessError]: /mnt/storage/scratch/tmp/HvDvQ8/mosaic-business-qXfqBQ/businesses/acme.json roles.bean.publish names library project books; library repositories cannot be published by agents
at refuse (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/business/src/errors.mjs:12:9)
at validateBusinessDocument (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/business/src/business.mjs:295:49)
at check (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/business/tests/business.test.mjs:66:10)
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/business/tests/business.test.mjs:375:22)
at async Test.run (node:internal/test_runner/test:1409:7)
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
exitCode: 2
}
test at packages/cli/tests/case2-install.test.mjs:481:1
✖ C2-21: library install supplies chief identity and books publish for its real read grant (264.267351ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal:
+ actual - expected
+ undefined
- [
- 'books'
- ]
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/cli/tests/case2-install.test.mjs:486:10)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.start (node:internal/test_runner/test:1262:17)
at startSubtestAfterBootstrap (node:internal/test_runner/harness:387:17) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: undefined,
expected: [ 'books' ],
operator: 'deepStrictEqual',
diff: 'simple'
}
@@ -0,0 +1,44 @@
✖ C2-03b T7: a chief revoked through grants.revoke or fired through instance.fire refuses, and the fallback stays ended (55.83288ms)
✖ C2-03b T9 runtime: the chief's store file and user, then a real revoke that runs no git with the slot at 0000 (52.459744ms)
ℹ tests 2
ℹ suites 0
ℹ pass 0
ℹ fail 2
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 240.265949
✖ failing tests:
test at packages/bus/tests/library-credential.test.mjs:205:1
✖ C2-03b T7: a chief revoked through grants.revoke or fired through instance.fire refuses, and the fallback stays ended (55.83288ms)
Error: gitea-not-declared
at fail (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:219:9)
at #giteaRole (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:2768:89)
at #giteaGrantBinding (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:2800:52)
at file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:2700:114
at Store.transaction (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/store.mjs:289:22)
at #raiseGrant (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:2699:28)
at Broker.request (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:4807:88)
at Object.mintChiefReadGrant (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/library-credential.test.mjs:154:35)
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/library-credential.test.mjs:208:17)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5) {
code: 'gitea-not-declared'
}
test at packages/bus/tests/library-credential.test.mjs:265:1
✖ C2-03b T9 runtime: the chief's store file and user, then a real revoke that runs no git with the slot at 0000 (52.459744ms)
Error: gitea-not-declared
at fail (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:219:9)
at #giteaRole (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:2768:89)
at #giteaGrantBinding (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:2800:52)
at file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:2700:114
at Store.transaction (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/store.mjs:289:22)
at #raiseGrant (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:2699:28)
at Broker.request (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/src/broker.mjs:4807:88)
at Object.mintChiefReadGrant (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/library-credential.test.mjs:154:35)
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/library-credential.test.mjs:275:17)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5) {
code: 'gitea-not-declared'
}
@@ -0,0 +1,5 @@
scripts/test-land-work.sh rc=0
scripts/test-push-check.sh rc=0
scripts/test-queue.sh rc=0
scripts/test-release.sh rc=0
scripts/test-task.sh rc=0
@@ -0,0 +1,8 @@
scripts/test-auth.sh rc=0
scripts/test-conductor.sh rc=0
scripts/test-config.sh rc=0
scripts/test-discord.sh rc=0
scripts/test-extension-package.sh rc=0
scripts/test-federation-acceptance.sh rc=0
scripts/test-foundation.sh rc=0
scripts/test-land-work.sh rc=126
@@ -0,0 +1,17 @@
OK status with missing harness credential exits 3 and still lists accounts
OK status reports harness credential (read-only) + mosaic accounts
OK api key material never reaches output
OK oauth token material never reaches output
OK unparseable credential file exits 2
OK symlinked credential file exits 4
OK env-side credential names reported
OK env var values never reach output
OK accounts without an accounts dir reports none and creates nothing
OK accounts lists files and marks the active one
OK loose account perms flagged in listing
OK agent --auth with missing account file refuses (exit 4)
OK agent --auth with non-0600 account file refuses
OK agent --auth with invalid account name refuses
OK auth.sh without valid config refuses
selftest: 15 passed, 0 failed
@@ -0,0 +1,23 @@
On branch steelbeak/row287
Your branch is up to date with 'origin/steelbeak/row287'.
nothing to commit, working tree clean
OK dry-run: allowed change, exit 0, nothing committed (exit 0)
OK dry-run committed nothing
OK apply: allowed change exits 0 (exit 0)
OK apply: attribution in commit subject
OK apply: target tree clean after commit
OK disallowed path refused (exit 1)
OK disallowed path: target untouched
OK syntax gate refused broken .mjs (exit 1)
OK syntax gate: target untouched
OK suite failure refused (exit 1)
OK suite failure: target reverted to clean
OK disabled policy refused (exit 2)
OK disabled policy: target untouched
OK failed run refused (exit 1)
OK failed run: target untouched
OK missing run exits 4 (exit 4)
OK invalid policy exits 2 (exit 2)
selftest: 17 passed, 0 failed
@@ -0,0 +1,57 @@
OK absent adapter defaults to pi
OK adapter mock validates (exit 0)
OK unsupported adapter exits 2 (exit 2)
OK env exports adapter
OK bootstrap creates default when absent (exit 0)
OK bootstrap wrote config file
OK bootstrap is idempotent on existing config (exit 0)
OK bootstrap did not rewrite existing config
OK validate missing config exits 3 (exit 3)
OK malformed JSON exits 2 (exit 2)
OK unsupported configVersion exits 2 (exit 2)
OK unknown top-level key exits 2 (exit 2)
OK unknown execution key exits 2 (exit 2)
OK unsupported backend exits 2 (exit 2)
OK unsupported environment exits 2 (exit 2)
OK relative dataRoot exits 2 (exit 2)
OK non-canonical dataRoot exits 2 (exit 2)
OK filesystem root dataRoot exits 2 (exit 2)
OK home directory dataRoot exits 2 (exit 2)
OK dataRoot containing config dir exits 2 (exit 2)
OK control character in provider exits 2 (exit 2)
OK symlinked config file exits 2 (exit 2)
OK env exports resolve correctly
OK valid console.sso loads and is in the output
OK console.sso HS256 alg exits 2 naming the path (exit 2)
OK console.sso alg none exits 2 naming the path (exit 2)
OK console.sso missing slot field exits 2 naming the path (exit 2)
OK console.sso slot name outside the pattern exits 2 naming the path (exit 2)
OK unknown key under console exits 2 naming the path (exit 2)
OK valid federation key loads and is in the output
OK federation unknown sub-key exits 2 naming the key (exit 2)
OK federation peer unknown sub-key exits 2 naming the key (exit 2)
OK federation peer origin over http exits 2 naming the key (exit 2)
OK federation peer origin with a path exits 2 naming the key (exit 2)
OK federation peer origin with userinfo exits 2 naming the key (exit 2)
OK federation peer id equal to self exits 2 naming the key (exit 2)
OK federation peer label empty exits 2 naming the key (exit 2)
OK federation not an object exits 2 naming the key (exit 2)
OK federation self pattern exits 2 naming the key (exit 2)
OK federation peers not an array exits 2 naming the key (exit 2)
OK federation peer not an object exits 2 naming the key (exit 2)
OK federation peer id pattern exits 2 naming the key (exit 2)
OK federation peer label over 80 characters exits 2 naming the key (exit 2)
OK federation duplicate peer id exits 2 naming the key (exit 2)
OK federation peer origin with a trailing slash exits 2 naming the key (exit 2)
OK federation self missing exits 2 naming the key (exit 2)
OK federation peer id missing exits 2 naming the key (exit 2)
OK federation peer label missing exits 2 naming the key (exit 2)
OK config without federation loads and has no federation output
OK config without console loads with unchanged output
OK compactionReadBytes 8388608 loads and is in the output as written
OK compactionReadBytes "x" loads and is in the output as written
OK compactionStreams 2048 loads and is in the output as written
OK compactionStreams "x" loads and is in the output as written
OK failed validation modified nothing
selftest: 55 passed, 0 failed
@@ -0,0 +1,86 @@
toolchain: node v26.8.1
OK syntax: packages/discord/src/approvals.mjs
OK syntax: packages/discord/src/authorize.mjs
OK syntax: packages/discord/src/binding.mjs
OK syntax: packages/discord/src/cli.mjs
OK syntax: packages/discord/src/connector.mjs
OK syntax: packages/discord/src/context.mjs
OK syntax: packages/discord/src/engine-pi.mjs
OK syntax: packages/discord/src/errors.mjs
OK syntax: packages/discord/src/gateway.mjs
OK syntax: packages/discord/src/git.mjs
OK syntax: packages/discord/src/journal.mjs
OK syntax: packages/discord/src/notify.mjs
OK syntax: packages/discord/src/processing.mjs
OK syntax: packages/discord/src/rest.mjs
OK syntax: packages/discord/src/setspark.mjs
OK syntax: packages/discord/src/tools.mjs
OK syntax: packages/discord/src/web.mjs
OK syntax: packages/discord/bin/git-credential.mjs
OK syntax: packages/discord/extension/tools.mjs
OK syntax: packages/discord/tests/approvals.test.mjs
OK syntax: packages/discord/tests/authorize.test.mjs
OK syntax: packages/discord/tests/binding.test.mjs
OK syntax: packages/discord/tests/connector.test.mjs
OK syntax: packages/discord/tests/context.test.mjs
OK syntax: packages/discord/tests/copy-service.test.mjs
OK syntax: packages/discord/tests/engine.test.mjs
OK syntax: packages/discord/tests/fake-pi.mjs
OK syntax: packages/discord/tests/gateway.test.mjs
OK syntax: packages/discord/tests/git.test.mjs
OK syntax: packages/discord/tests/helpers.mjs
OK syntax: packages/discord/tests/journal.test.mjs
OK syntax: packages/discord/tests/library-seat.test.mjs
OK syntax: packages/discord/tests/notify.test.mjs
OK syntax: packages/discord/tests/offline-check.test.mjs
OK syntax: packages/discord/tests/processing-stop.test.mjs
OK syntax: packages/discord/tests/processing.test.mjs
OK syntax: packages/discord/tests/recover.test.mjs
OK syntax: packages/discord/tests/rest.test.mjs
OK syntax: packages/discord/tests/setspark.test.mjs
OK syntax: packages/discord/tests/tools.test.mjs
OK syntax: packages/discord/tests/web.test.mjs
OK syntax: packages/discord/fixtures/claim-worker.mjs
OK syntax: packages/discord/fixtures/legacy-owner-worker.mjs
OK syntax: scripts/discord.sh
OK syntax: scripts/discord-service.sh
OK packages/discord declares no dependencies
OK no bot-token-shaped string in packages/discord
OK fixture binding uses placeholder ids only
OK fixture binding validates
OK real pi with the extension exposes exactly list_dir, read_file, search and no built-in tool
OK real pi with a writable root exposes exactly the three reads plus write_file and edit_file, and writes nothing at start
OK real pi with a web key exposes the three reads plus web_fetch and web_search, and no write tool without a writable root
OK real pi with a git root exposes the reads, writes and the four git verbs, commits nothing at start, and never shows the token
OK real pi with protocol vault adds reserve_id to the git verbs
OK real pi with a setspark key exposes the reads and the eight record verbs, no counters, and never shows the key
OK real pi refuses a git key on a read-only root (fail closed)
OK real pi with the pilot flags (--no-tools) exposes no tool at all
OK real pi exits non-zero without MOSAIC_DISCORD_TOOLS: no session, no tools (fail closed)
OK a failing nested test fails the run under a parent runner's NODE_TEST_CONTEXT
OK node --test packages/discord/tests/ (ℹ pass 200)
OK scripts/discord.sh --help exits 0
OK scripts/discord.sh check without a binding exits 4
OK scripts/discord.sh recover without a binding exits 4
OK scripts/discord.sh reload without a binding exits 4
OK scripts/discord-service.sh without a command exits 4
OK service unit renders with the repository path, a supervised run as the main process, exit 3 never retried, and reload as SIGHUP
OK service install writes the rendered unit (0644) and leaves no temp file
OK service install a second time reports unchanged
OK systemd-analyze verify accepts the rendered unit
OK service uninstall removes the unit file
OK service install with an unknown flag exits 4
OK service install with USER unset finishes and names the account for lingering
OK pinned render runs copy CLI with copy cwd/repo and no checkout executable
OK pinned install writes the rendered unit in a temporary unit directory
OK pinned install refuses short prerequisite and writes no unit
OK pinned install refuses missing prerequisite and writes no unit
OK pinned install refuses cli prerequisite and writes no unit
OK pinned install refuses pi prerequisite and writes no unit
OK status names the copy from fake systemctl effective cwd
OK checkout install explicitly recommends --copy
OK wrapper check --offline passes local fixture and makes no network call
OK wrapper run --offline refuses usage
discord suite: 82 passed, 0 failed
@@ -0,0 +1,21 @@
OK initial ordinary-file install
OK installed tree matches canonical source
OK installed tree has no symlinks
OK check detects installation drift
OK sync refuses to overwrite installation drift
OK check detects an extra destination file
OK check detects an extra destination directory
OK check rejects a destination symlink
OK sync accepts a canonical source update
OK updated installation matches canonical source
scripts/test-extension-package.sh: line 14: 383938 Killed "$@" > /dev/null 2>&1
OK forced interruption kills the replacing process
OK next invocation recovers old consistent installation
OK interrupted replacement rolled back
OK sync succeeds after interruption recovery
OK unlocked stale lock file does not block
OK active lock refuses a concurrent sync
OK source symlink fails closed
OK nested second entrypoint fails closed
extension package selftest: 18 passed, 0 failed
@@ -0,0 +1,10 @@
✔ FED R7 scratch two-instance acceptance (3271.695253ms)
ℹ tests 1
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 3348.152983
R7 cleanup: no child groups, ports or sockets remain; no units created
@@ -0,0 +1,53 @@
toolchain: node v26.8.1, python 3.12.8, jsonschema 4.26.0
OK syntax: scripts/foundation-inspect.mjs
OK syntax: scripts/foundation/strict-json.mjs
OK syntax: scripts/foundation/canonical.mjs
OK syntax: scripts/foundation/resolve.mjs
OK syntax: scripts/foundation/validate-record.mjs
OK syntax: scripts/foundation/fixtures/build-fixtures.mjs
OK syntax: scripts/foundation/canonical.test.mjs
OK syntax: scripts/foundation/cli.test.mjs
OK syntax: scripts/foundation/fixtures.test.mjs
OK syntax: scripts/foundation/resolve.test.mjs
OK syntax: scripts/foundation/strict-json.test.mjs
OK syntax: scripts/foundation/verify-schema.py (ast only; no bytecode written)
OK fixture generator runs
OK checked-in fixtures/bundles equal a fresh generation
OK checked-in fixtures/raw equal a fresh generation
OK checked-in fixtures/index.json equal a fresh generation
OK checked-in demo bundles equal a fresh generation
OK a failing nested test fails the run under a parent runner's NODE_TEST_CONTEXT
OK node --test scripts/foundation/ (ℹ pass 80)
OK differential schema oracle: PASS: differential schema oracle (finite corpus; compatibility evidence, not equivalence proof)
platform witness: strftime('%Y') for year 999 -> '999' (pinned checker refuses years 0001..0999)
node v26.8.1; corpus 1568 records (38 pinned fixtures, 478 unique bundle records, 1052 typeCase/mutation/lexical cases)
schema column: agree-valid 540, agree-invalid 991, DISAGREEMENTS 0; strict-only (parser-bound) cases: 27; unsupported-kind records not schema-assessed by the inspector: 10
profile column (schema-valid records only): profile-valid 510, profile-invalid 30
profile refusals asserted: 30 schema-agreed-valid records refused only by the strict typed-string profile (rule profile-pattern-mismatch), 12 declared by name; 73 named probes verified against declared schema/profile columns
OK oracle: zero schema-column disagreements with the pinned checker
OK oracle: strict-only profile refusals are counted and asserted
OK demo: permitted read preview exits 0 (exit 0)
OK demo: permitted file.change preview exits 0 (exit 0)
OK demo: assignment.change proposal is unresolved (exit 3) (exit 3)
OK demo: revoked registration is refused (exit 3) (exit 3)
OK demo: message is not authority (exit 3) (exit 3)
OK usage: no arguments exits 2 (exit 2)
OK io: missing file exits 4 (exit 4)
OK io: directory exits 4 (exit 4)
OK io: symlink exits 4 (O_NOFOLLOW) (exit 4)
OK bound: oversize fixture exits 2 (exit 2)
OK profile: one final LF in a typed selection id is refused before admission (exit 2) (exit 2)
OK profile: two final LFs fail the schema pattern itself (exit 2) (exit 2)
OK profile: escaped newlines in free-form text stay allowed (exit 0) (exit 0)
OK profile refusal is invalid-request/profile-pattern-mismatch with selection and operation withheld, value not echoed
OK text output starts with the disclaimer
OK json output is valid JSON with result allowed and exactly the charter §7 fields
OK json golden matches byte-for-byte
OK sandboxed bundle run (env -i, PATH=/nonexistent) produced the unresolved proposal
OK sandbox inventory (path/type/size/mode/uid/gid/inode/mtime/sha256) unchanged by runs
OK canary never printed (bundle run and credential-file run)
OK a non-bundle JSON file is refused at the shape gate, not read into output
OK no field of the non-bundle file is echoed
selftest: 44 passed, 0 failed
@@ -0,0 +1,88 @@
OK args: exit 2, target-branch
OK args: exit 2, target-branch
OK args: exit 4
OK args: exit 4
OK args: exit 4
OK --base next refuses before the config is read (got 2)
OK a missing system config exits 3 (got 3)
OK nobus: exit 3
OK schema-table: exit 2, bus-schema
OK schema-meta: exit 2, bus-schema
OK openBus opens the database read-only
OK other-business: exit 2, decision-not-found
OK no-raise: exit 2, decision-not-found
OK not-a-merge: exit 2, not-a-merge
OK open: exit 2, not-resolved
OK withdrawn: exit 2, not-resolved
OK by-agent: exit 2, not-human
OK via-webui: exit 2, not-human
OK no-answer: exit 2, not-human
OK answer-role: exit 2, not-human
OK answer-run: exit 2, not-human
OK answer-choice: exit 2, not-human
OK two-answers: exit 2, not-human
OK arbiter-cross: exit 2, not-human
OK arbiter-cited: exit 2, not-human
OK chose-no: exit 2, not-merge-option
OK two-refs: exit 2, work-ref
OK bad-ref: exit 2, work-ref
OK task-ref-disagrees: exit 2, work-ref
OK raise-task-disagrees: exit 2, work-ref
OK target-main: exit 2, target-branch
OK superseded-question: exit 2, superseded
OK superseded-task: exit 2, superseded
OK superseded-by: exit 2, superseded
OK no-verdict: exit 2, no-verdict
OK changes-later: exit 2, not-approved
OK short-commit: exit 2, not-approved
OK wrong-task-body: exit 2, not-approved
OK args: exit 2, decision-not-found
OK earlier-merge: exit 0
OK ref-moved: exit 2, ref-moved
OK root-top: exit 2, root-file
OK root-newdir: exit 2, root-file
OK root-src: exit 2, root-file
OK unrelated: exit 2, base-conflict
OK empty: exit 2, base-conflict
OK an empty reviewed diff says so
OK dirty-changed: exit 2, checkout-dirty
OK dirty-staged: exit 2, checkout-dirty
OK dirty-untracked: exit 2, checkout-dirty
OK conflict: exit 2, base-conflict
OK a refusal after the worktree is added leaves no worktree or scratch
OK cleanup looks for the worktree's Docker network
OK dirty-during: exit 2, checkout-dirty
OK a reviewed path dirtied during the gate moves no ref
OK drift: exit 2, patch-drift
OK patch-drift leaves no worktree or scratch
OK gate-pkg: exit 2, gate-failed
OK gate-failed names the failing step's log, and it exists
OK gate-failed leaves no worktree or scratch
OK gate-script: exit 2, gate-failed
OK a failing scripts/test-*.sh fails the gate
OK a failed gate moves no ref
OK nomods: exit 4
OK a target without node_modules leaves no worktree or scratch
OK moved: exit 2, base-moved
OK base-moved keeps the base where it moved to
OK dry: exit 0
OK --dry-run writes no ref
OK --dry-run prints the commit with --subject and --attribution
OK --dry-run runs the gate
OK --dry-run leaves no worktree or scratch
OK land: exit 0
OK the landing is one commit on the old base
OK its diff is the reviewed diff by patch-id
OK its tree is the dry run's
OK the message has the option's subject, the citations and the attribution last
OK it prints the line for the PM
OK the checked-out base has the reviewed paths and keeps the unrelated edit
OK only refs/heads/refactor exists in the target
OK the work ref is unchanged
OK the bus database is unchanged
OK the gate ran each package suite but queue, and each scripts/test-*.sh
OK the landing leaves no worktree or scratch
OK the target's node_modules is still there, with its contents
OK the worktree's Docker network is removed
---
land-work selftests: 86 passed, 0 failed
@@ -0,0 +1 @@
timeout: failed to run command ‘scripts/test-land-work.sh’: Permission denied
@@ -0,0 +1,7 @@
PASS push-check 38 checks
KILLED skip-hash
KILLED accept-changes
KILLED earlier-round
KILLED working-tree
KILLED no-row
KILLED skip-nul-scan
@@ -0,0 +1,36 @@
toolchain: node v26.8.1, git version 2.55.0
OK syntax: packages/queue/src/cli.mjs
OK syntax: packages/queue/src/errors.mjs
OK syntax: packages/queue/src/io.mjs
OK syntax: packages/queue/src/lock.mjs
OK syntax: packages/queue/src/queue.mjs
OK syntax: packages/queue/src/review.mjs
OK syntax: packages/queue/src/store.mjs
OK syntax: packages/queue/tests/commit.test.mjs
OK syntax: packages/queue/tests/data.test.mjs
OK syntax: packages/queue/tests/dispatch.test.mjs
OK syntax: packages/queue/tests/gate-owner.test.mjs
OK syntax: packages/queue/tests/helpers.mjs
OK syntax: packages/queue/tests/lock.test.mjs
OK syntax: packages/queue/tests/migration.test.mjs
OK syntax: packages/queue/tests/review.test.mjs
OK syntax: packages/queue/tests/store.test.mjs
OK syntax: packages/queue/tests/write.test.mjs
OK syntax: packages/queue/tests/fixtures/fake-gitea.mjs
OK syntax: packages/queue/tests/fixtures/kill-at.mjs
OK syntax: packages/queue/tests/fixtures/lock-child.mjs
OK syntax: packages/queue/tests/fixtures/mosaic-pre-a2.sh
OK syntax: scripts/queue-commit.sh
OK syntax: scripts/git-hooks/pre-commit
OK syntax: scripts/mosaic
OK queue-commit.sh, the guard and scripts/mosaic are executable
OK packages/queue declares no dependencies
ℹ tests 156
ℹ pass 156
ℹ fail 0
OK node --test packages/queue/tests/
OK scripts/mosaic queue help
skip queue verify and render --check: this checkout (/home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287) is not the queue's canonical root (/mnt/storage/src/mosaic-stack)
queue suite: 28 passed, 0 failed
@@ -0,0 +1,17 @@
OK valid RELEASE resolves (exit 0)
OK invalid RELEASE exits 1 (exit 1)
OK missing RELEASE exits 1 (exit 1)
OK valid RELEASE leaves image tag consistent with version
OK status safe on empty state (exit 0)
OK status created no pointer
OK fault-injected activation refuses (exit 1)
OK refused activation wrote no pointer
OK refusal logged exactly once with valid fields
OK healthy activation succeeds (exit 0)
OK pointer written with valid fields
OK repeat activation succeeds (log grows) (exit 0)
OK log is append-only across activations
OK rollback without previous refuses (exit 1)
OK compose network made under the run's project and removed after the run
selftest: 15 passed, 0 failed
@@ -0,0 +1,97 @@
OK valid task validates (exit 0)
OK unknown task key exits 2 (exit 2)
OK unsupported taskVersion exits 2 (exit 2)
OK invalid task id exits 2 (exit 2)
OK empty prompt exits 2 (exit 2)
OK NUL in expectExact exits 2 (exit 2)
OK out-of-range timeout exits 2 (exit 2)
OK missing mission file exits 4 (exit 4)
OK task with valid mission validates (exit 0)
OK invalid mission exits 2 (exit 2)
OK validate missing task exits 4 (exit 4)
OK validation does not modify the task file
OK prune dry-run exits 0 (exit 0)
OK dry-run deleted nothing
OK prune --keep=2 --yes removes oldest (exit 0)
OK kept exactly 2 newest runs
OK newest run kept, oldest pruned
OK append-only receipt written (3 entries)
OK sessions/workspaces untouched by prune
OK prune with invalid keep exits 4 (exit 4)
OK mock adapter: gate passes on matching mock response (exit 0)
OK mock adapter: expect-mismatch recorded (exit 1)
OK mismatch reason recorded
OK unknown adapter fails closed (exit 1)
OK mission task runs via mock adapter (exit 0)
OK mission section injected into generated prompt
OK retry of mission run succeeds (exit 0)
OK retriedFrom lineage recorded
OK mission section present after retry (relative path resolved)
OK retry of missing run exits 4 (exit 4)
OK skill install bundled ms-tools (exit 0)
OK installed to skills-available
OK double install refuses (exit 1)
OK activate enables skill (exit 0)
OK enabled dir populated
OK uninstall while enabled refuses (exit 1)
OK deactivate moves back to available (exit 0)
OK uninstall removes from available (exit 0)
OK seat with enabled skill launches
OK skill path delivered to adapter
OK shipped researcher contract resolves (ceiling + network)
OK resolve-role refuses a non-role document (conductor policy) (exit 2)
OK resolve-role refuses name/filename mismatch (exit 2)
OK resolve-role refuses unknown network declaration (exit 2)
OK resolve-role refuses duplicate role tool (exit 2)
OK resolve-role refuses unsupported tool (exit 2)
OK resolve-role refuses missing contract file (exit 4)
OK version 1 role prints no contract line
OK shipped version 2 roles resolve with their contracts (pm, cto, coder, reviewer)
OK resolve-role accepts a minimal version 2 role (exit 0)
OK resolve-role refuses an action outside the vocabulary (exit 2)
OK resolve-role refuses a gated-only action in a role file (exit 2)
OK resolve-role refuses a Vikunja verb no role may hold (exit 2)
OK resolve-role refuses a version 2 role whose contract is missing (exit 2)
OK resolve-role refuses a contract that is a symbolic link (exit 2)
OK seat launches under role ceiling
OK role ceiling narrows seat tools (read,write,bash -> read,bash)
OK narrowing recorded loudly
OK missing role contract refuses launch (exit 2)
OK missing-contract refusal names the role
OK empty ceiling intersection -> tool-free seat
OK tool-free outcome recorded loudly
OK overridden agents dir without seat definition refuses (exit 4)
OK seat-resolution refusal names the seat
OK policy: mission only -> mission tools
OK policy: task only -> task tools
OK policy: both -> intersection (task narrowed)
OK policy: empty intersection -> tool-free
OK invalid mission capabilities rejected (exit 2)
mosaic-task: unsupported mission tool: "sudo" (supported: read, write, edit, bash, grep, find, ls)
OK task run with user layer present (exit 0)
OK user context dispatched into generated prompt
OK workspace+tools task runs via mock (exit 0)
OK workspace path + tools delivered to adapter
OK persistent workspace created on host
OK plain task still runs (no workspace/tools) (exit 0)
OK workspace var present but empty when absent
OK tools empty when absent
OK unknown tool exits 2 (exit 2)
OK workspace traversal exits 2 (exit 2)
OK hello task succeeds with exact marker
OK result.json written in run dir
OK result.json contents are correct
OK wrong expectExact fails with exit 1 (reason: expect-mismatch)
OK each run gets a distinct run dir (no clobber)
OK list shows both runs
skip live provider cases (row 244: no provider call in the gate)
OK onboard without name exits 4 (non-interactive) (exit 4)
OK onboard --name renders profile (exit 0)
OK profile written
OK canon structure: required filled, optional placeholdered
OK canon sections present
skip live provider cases (row 244: no provider call in the gate)
OK no agent identity on headless run
OK compose network made under the run's project and removed after the run
selftest: 92 passed, 0 failed
@@ -0,0 +1,5 @@
known fixture values searched: 3
logs searched: 37
fixture hits: 0
Docker fact runner separately checked all four actual minted values against complete TAP stdout/stderr: hits 0.
Fake API cases assertClean/noLeak cover every issued token against events, messages, decisions and request logs.
@@ -0,0 +1,265 @@
✔ versionAtLeast compares numbers and refuses a prerelease of 1.27.0 (1.139073ms)
✔ X-G1: a raise that fails a check refuses and stores nothing (75.530438ms)
✔ X-G1: a creative role, a role without identity and a missing or due slot refuse at raise (193.46698ms)
✔ X-G1: the chief of staff refuses at raise, whatever its definition, identity and publish declare (39.523907ms)
✔ X-G2: admin set refuses each failed check and writes nothing, then writes the slot once (153.731441ms)
✔ X-G3 to X-G8: an approve makes the user, mints the token, forks, checks reach and writes the file, record and grant.applied (106.729946ms)
✔ X-G4: a user this stack did not make, an admin, a disabled user and a remade user each refuse (363.191747ms)
✔ X-G6 and X-G8: the user and mint steps are written before their calls, and each result after its call (100.705375ms)
✔ X-G4: a fork by the seat user of another repository than the project refuses fork-foreign (86.135966ms)
✔ X-G5: a failure after the mint deletes the token, unloads nothing it did not load and leaves no file (164.209877ms)
✔ X-G5: a token delete that fails is left at Gitea, shown by status and taken by sweep (125.633285ms)
✔ X-G5: a mint that fails deletes by name, and a refusal at step 0 leaves the decision unspent (151.600735ms)
✔ X-G5: an agent held for an unknown token is refused at step 0 with no Gitea call (65.645239ms)
✔ X-G5 and X-G9 (step 7): a rotation whose old-token delete fails stays applied with the old id left (174.510424ms)
✔ X-G10: in a rotation the store file holds the new token before the old one is deleted (148.773407ms)
✔ X-G6: a rotation cut before the rename settles back at once, and the old token stays live (171.547646ms)
✔ X-G6: a rotation whose step 8 write fails is settle-pending, and a restart settles it forward (179.655701ms)
✔ X-G6: a restart settles an open rotation back when the file still holds the old token (135.974464ms)
✔ X-G6: a restart settles an open rotation forward when the file holds the new token (135.770314ms)
✔ X-G6: a 401 on the file token loses the grant: both tokens go, the record goes and memory unloads (138.899782ms)
✔ X-G6: a 403, 404, 500, a hang and a dropped socket each leave the rotation settle-pending with nothing deleted (1029.94154ms)
✔ X-G6: a file token no grant minted is held; the next restart says so again; a revoke clears it and takes both ids (162.896203ms)
✔ N9: a revoke of an open rotation, then a restart: no settle, no Gitea call and no message (138.973458ms)
✔ N9: a 404 on a delete by id counts as done (133.050723ms)
✔ X-G6: a restart undoes each step a killed child left, and spends an approved unspent decision (414.134804ms)
✔ X-G6: a restart with no admin slot leaves the user an orphan and the token left (55.861549ms)
✔ X-G7: a change to the definition, identity, publish remotes or admin slot after the raise ends decision-mismatch (125.765675ms)
✔ X-G7 and X-G9: an admin rotate voids open decisions, and rotate --finish deletes what was left (114.25118ms)
✔ X-G9: a rotate whose name is taken refuses and writes nothing; remove takes the slot away (66.692412ms)
✔ X-G9: a slot past its rotate-by date refuses a run at step 0, leaving the decision unspent (49.649111ms)
✔ X-G11: run refuses every caller but the human, an unapproved, a spent and a busy decision (108.282506ms)
✔ kill switch (plan :16): while halted the chief still reads grants.status, and an agent grants.revoke refuses (106.579322ms)
✔ X-G11: a revoke while the executor runs aborts it and the grant ends revoked (107.282814ms)
✔ X-G11: a revoke unloads memory and removes the file before its Gitea delete (131.713832ms)
✔ X-G11: a revoke during a rotation step 7 ends it revoked, and step 8 writes no record back (203.032995ms)
✔ X-G11: a revoke while a sweep settles a rotation, forward or back, ends it revoked with no record (329.237353ms)
✔ X-G11: a revoke during a settle whoami that finds a foreign token ends it revoked, not held (194.559652ms)
✔ X-G11: a revoke during a settle whoami that gets a 401 ends it revoked, not lost (184.236777ms)
✔ X-G12: a token that reaches wider than its forks fails with the repository named (389.923565ms)
✔ X-G12: reach is paged by X-Total-Count and a count that does not add up refuses (266.358571ms)
✔ X-G13: an agent cannot record a grant event, and the trigger refuses one from any writer but the grant writer (43.235444ms)
✔ X-G14: credentials delete fires the user: tokens go, login is prohibited, and grant.revoke.closed records it (281.776428ms)
✔ X-G14: a fire whose token delete or login PATCH fails lands it in serviceLeft (133.556646ms)
✔ X-G14: a fire whose user is gone or remade skips the fire steps and says why (264.851425ms)
✔ X-G14: a sweep retries a fire only for the user it fired: a remade or gone user is skipped and named (279.235184ms)
✔ X-G14: a revoke Gitea cannot reach stays open, and a restart closes it (133.659853ms)
✔ row 281 T8: a tester grants.revoke refuses tester-dev-only once bus.tester is off; the human revoke keeps its records and starts no job (113.137966ms)
✔ X-G15: look-alike commit steps outside the grant writer are ignored at start, by revoke and by status (112.990765ms)
✔ X-G15: a look-alike spend under another kind with the grant writer pair is not an open grant (46.225594ms)
✔ status lists the slot, users, grants and nothing secret, and every grants verb needs service gitea (113.742539ms)
✔ C2-21 L1/L2/L9: research gets write on her repository with declared scopes through raise and yes (129.484221ms)
✔ C2-21 L1/L2/L9: chief-of-staff gets read on her repository with declared scopes through raise and yes (106.138514ms)
✔ C2-21 L1/L2/L9: coder gets read on her repository with declared scopes through raise and yes (104.316693ms)
✔ C2-21 L7/L7b: research refuses non-library targets lib (42.155496ms)
✔ C2-21 L7/L7b: research refuses non-library targets app,lib (40.652006ms)
✔ C2-21 L7/L7b: chief-of-staff refuses non-library targets lib (40.430096ms)
✔ C2-21 L7/L7b: chief-of-staff refuses non-library targets app,lib (40.868584ms)
✖ C2-21 L4: read widened to write refuses gitea-reach-wider and deletes the minted token (64.643789ms)
✖ C2-21 L4: admin privilege refuses gitea-reach-wider and deletes the minted token (63.595938ms)
✖ C2-21 L4: missing pull refuses gitea-reach-wider and deletes the minted token (64.302062ms)
✖ C2-21 L4: missing permission object refuses gitea-reach-wider and deletes the minted token (65.102053ms)
✖ C2-21 L4: library fork refuses gitea-reach-wider and deletes the minted token (64.826649ms)
✔ C2-21 L8: untargeted library collaborator reach is disclosed and accepted publish-wide (87.339871ms)
✖ C2-21 L10: project kind or library level changing after raise refuses decision-mismatch (64.895333ms)
✖ C2-21 L4: creative write requires push true but ignores unrelated permission keys (64.965654ms)
✔ C2-03b T6: the slot before any chief evidence, then the listed chief's live grant (32.72782ms)
✔ C2-03b T7: a chief revoked through grants.revoke or fired through instance.fire refuses, and the fallback stays ended (296.498344ms)
✔ C2-03b T7 choice checks: delisted, a fire under way and held each refuse; a rehire lists the chief again (36.638485ms)
✔ C2-03b T8: a chief grant on another project ends the fallback, and two listed chiefs refuse library-fetch-no-grant (32.149673ms)
✔ C2-03b departure (sage-2928): with no chief entry, any Gitea grant evidence refuses (18.858172ms)
✔ C2-03b T9 runtime: the chief's store file and user, then a real revoke that runs no git with the slot at 0000 (782.448934ms)
✔ C2-04: library mappings load without roots, remain frozen, and retain mapping refusals (11.031163ms)
✔ C2-04: an otherwise valid publish grant cannot name a library repo (1.672501ms)
✔ config directory and file path follow MOSAIC_CONFIG (0.332343ms)
✔ the fixture business validates and comes back frozen (0.995298ms)
✔ two instances may share a definition (0.963652ms)
✔ top-level refusals (2.767585ms)
✔ arbiters and projects (3.905801ms)
✔ role instances (2.3953ms)
✔ Vikunja bots (4.888461ms)
✔ a role without Vikunja takes no tracker block (8.053822ms)
✔ credential references match the definition's services (3.719872ms)
✔ launch (11.051038ms)
✔ REQ-THREAD-2 REQ-LAUNCH-1 launch.threadIdleSeconds: absent is the default, a positive integer is used, anything else refuses (6.346401ms)
✔ loadBusiness: file checks (2.217137ms)
✔ loadBusiness: not a regular file (44.303522ms)
✔ loading writes nothing (1.158385ms)
✔ names that are Object.prototype properties don't count as declared (3.580542ms)
✔ the shipped example refuses as written and validates once filled in (1.167705ms)
✔ persona vars: agent layer, creative definition only, both or neither (9.835969ms)
✔ row 285: a chief or creative instance without credentials.gitea refuses at load (7.574546ms)
✔ row 285: a creative instance with identity and publish loads (0.773536ms)
✔ row 194 keep, noHire and retired validate, and a file without them loads as before (2.123104ms)
✔ row 194 each bad keep, noHire or retired entry refuses (7.372634ms)
✔ C2-21: only creative definitions and the chief with gitea may publish a library project (12.000435ms)
✔ the library install writes the business file, personas and slot dir, makes the copy and starts nothing (260.836427ms)
✔ a library re-run passes and leaves every file as it was (404.249515ms)
✔ a re-run refuses business-file-differs and leaves the file unchanged (279.629668ms)
✔ a re-run refuses persona-differs when an installed persona isn't the staged one, and writes nothing (284.561951ms)
✔ the factory install writes no library mapping, persona or slot dir (178.261551ms)
✔ the factory install refuses a library project and bus.tester off, before anything is written (71.855815ms)
✔ the library install refuses bus.tester on and a persona var in the input (80.555838ms)
✔ the install refuses root, a business id mismatch, another --home and a config dir with group bits (54.023333ms)
✔ the library install needs --personas (5.849596ms)
✔ a validate refusal removes the business file this run wrote (233.892356ms)
✔ N4: a validate refusal on a re-run leaves the matching business file in place, byte for byte (404.647484ms)
✔ a clean library host passes the precheck with no mirror yet (426.936455ms)
✔ the library precheck refuses a persona file at 0644 (398.6846ms)
✔ the library precheck refuses a persona whose digest isn't persona.sha256 (404.193929ms)
✔ the library precheck refuses a persona dir with group bits (411.810016ms)
✔ the library precheck refuses a business file at 0644 (404.979409ms)
✔ the library precheck refuses a changes.jsonl at 0644 (572.602427ms)
✔ the library precheck refuses a Discord binding (408.215092ms)
✔ --library-status refuses library-mirror-missing before the bus has made the mirror (324.192926ms)
✔ --library-status refuses library-never-synced on a fresh mirror (337.170027ms)
✔ --library-status names a recorded lastError before the null lastFetch it leaves (N3) (354.03021ms)
✔ --library-status prints each R-problem's path as an escaped JSON string, then the count, and passes (397.284837ms)
✔ --library-status takes no --profile (5.679559ms)
✔ Vikunja 614: install preserves the explicit notifier decision bytes and mode and creates none when absent (477.032132ms)
✔ row 322 F8: library personas install from the copy with no clone node_modules (138.947889ms)
✔ row 322 F9: importer refusal keeps the error line rather than the Node banner (94.497291ms)
✔ C2-21: library install supplies chief identity and books publish for its real read grant (241.629315ms)
ℹ tests 122
ℹ suites 0
ℹ pass 115
ℹ fail 7
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 9691.559494
✖ failing tests:
test at packages/bus/tests/gitea-grant.test.mjs:1386:3
✖ C2-21 L4: read widened to write refuses gitea-reach-wider and deletes the minted token (64.643789ms)
AssertionError [ERR_ASSERTION]: {"action":"grant.failed","method":"gitea","grantDecision":"e07beff5-0dd2-474a-987b-7f742de95dee","agent":"chief","code":"grant-admin-not-dedicated"}
+ actual - expected
+ 'grant-admin-not-dedicated'
- 'gitea-reach-wider'
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/gitea-grant.test.mjs:1394:12)
at async Test.run (node:internal/test_runner/test:1409:7)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 'grant-admin-not-dedicated',
expected: 'gitea-reach-wider',
operator: 'strictEqual',
diff: 'simple'
}
test at packages/bus/tests/gitea-grant.test.mjs:1386:3
✖ C2-21 L4: admin privilege refuses gitea-reach-wider and deletes the minted token (63.595938ms)
AssertionError [ERR_ASSERTION]: {"action":"grant.failed","method":"gitea","grantDecision":"83dfd9a1-0794-4fb2-8fa5-a21aecaaa40a","agent":"chief","code":"grant-admin-not-dedicated"}
+ actual - expected
+ 'grant-admin-not-dedicated'
- 'gitea-reach-wider'
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/gitea-grant.test.mjs:1394:12)
at async Test.run (node:internal/test_runner/test:1409:7)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 'grant-admin-not-dedicated',
expected: 'gitea-reach-wider',
operator: 'strictEqual',
diff: 'simple'
}
test at packages/bus/tests/gitea-grant.test.mjs:1386:3
✖ C2-21 L4: missing pull refuses gitea-reach-wider and deletes the minted token (64.302062ms)
AssertionError [ERR_ASSERTION]: {"action":"grant.failed","method":"gitea","grantDecision":"88073d5b-3e4f-4a9a-b241-5b110d3be2c0","agent":"chief","code":"grant-admin-not-dedicated"}
+ actual - expected
+ 'grant-admin-not-dedicated'
- 'gitea-reach-wider'
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/gitea-grant.test.mjs:1394:12)
at async Test.run (node:internal/test_runner/test:1409:7)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 'grant-admin-not-dedicated',
expected: 'gitea-reach-wider',
operator: 'strictEqual',
diff: 'simple'
}
test at packages/bus/tests/gitea-grant.test.mjs:1386:3
✖ C2-21 L4: missing permission object refuses gitea-reach-wider and deletes the minted token (65.102053ms)
AssertionError [ERR_ASSERTION]: {"action":"grant.failed","method":"gitea","grantDecision":"d2e4e251-e9f3-4ede-aa40-284f014243ab","agent":"chief","code":"grant-admin-not-dedicated"}
+ actual - expected
+ 'grant-admin-not-dedicated'
- 'gitea-reach-wider'
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/gitea-grant.test.mjs:1394:12)
at async Test.run (node:internal/test_runner/test:1409:7)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 'grant-admin-not-dedicated',
expected: 'gitea-reach-wider',
operator: 'strictEqual',
diff: 'simple'
}
test at packages/bus/tests/gitea-grant.test.mjs:1386:3
✖ C2-21 L4: library fork refuses gitea-reach-wider and deletes the minted token (64.826649ms)
AssertionError [ERR_ASSERTION]: {"action":"grant.failed","method":"gitea","grantDecision":"676ebbc8-8397-4b63-90f4-3485fc277029","agent":"chief","code":"grant-admin-not-dedicated"}
+ actual - expected
+ 'grant-admin-not-dedicated'
- 'gitea-reach-wider'
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/gitea-grant.test.mjs:1394:12)
at async Test.run (node:internal/test_runner/test:1409:7)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 'grant-admin-not-dedicated',
expected: 'gitea-reach-wider',
operator: 'strictEqual',
diff: 'simple'
}
test at packages/bus/tests/gitea-grant.test.mjs:1416:1
✖ C2-21 L10: project kind or library level changing after raise refuses decision-mismatch (64.895333ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
+ actual - expected
+ 'gitea-not-declared'
- 'decision-mismatch'
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/gitea-grant.test.mjs:1430:12)
at async Test.run (node:internal/test_runner/test:1409:7)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: 'gitea-not-declared',
expected: 'decision-mismatch',
operator: 'strictEqual',
diff: 'simple'
}
test at packages/bus/tests/gitea-grant.test.mjs:1435:1
✖ C2-21 L4: creative write requires push true but ignores unrelated permission keys (64.965654ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
+ actual - expected
+ 'grant-admin-not-dedicated'
- 'gitea-reach-wider'
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/gitea-grant.test.mjs:1444:23)
at async Test.run (node:internal/test_runner/test:1409:7)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: 'grant-admin-not-dedicated',
expected: 'gitea-reach-wider',
operator: 'strictEqual',
diff: 'simple'
}
@@ -0,0 +1,150 @@
✔ versionAtLeast compares numbers and refuses a prerelease of 1.27.0 (1.633441ms)
✔ X-G1: a raise that fails a check refuses and stores nothing (77.604936ms)
✔ X-G1: a creative role, a role without identity and a missing or due slot refuse at raise (240.549157ms)
✔ X-G1: the chief of staff refuses at raise, whatever its definition, identity and publish declare (45.498213ms)
✔ X-G2: admin set refuses each failed check and writes nothing, then writes the slot once (183.346403ms)
✔ X-G3 to X-G8: an approve makes the user, mints the token, forks, checks reach and writes the file, record and grant.applied (112.792295ms)
✔ X-G4: a user this stack did not make, an admin, a disabled user and a remade user each refuse (377.194723ms)
✔ X-G6 and X-G8: the user and mint steps are written before their calls, and each result after its call (104.97076ms)
✔ X-G4: a fork by the seat user of another repository than the project refuses fork-foreign (110.564422ms)
✔ X-G5: a failure after the mint deletes the token, unloads nothing it did not load and leaves no file (196.260428ms)
✔ X-G5: a token delete that fails is left at Gitea, shown by status and taken by sweep (133.845626ms)
✔ X-G5: a mint that fails deletes by name, and a refusal at step 0 leaves the decision unspent (185.353007ms)
✔ X-G5: an agent held for an unknown token is refused at step 0 with no Gitea call (65.526316ms)
✔ X-G5 and X-G9 (step 7): a rotation whose old-token delete fails stays applied with the old id left (192.404202ms)
✔ X-G10: in a rotation the store file holds the new token before the old one is deleted (174.549257ms)
✔ X-G6: a rotation cut before the rename settles back at once, and the old token stays live (174.724588ms)
✔ X-G6: a rotation whose step 8 write fails is settle-pending, and a restart settles it forward (207.75696ms)
✔ X-G6: a restart settles an open rotation back when the file still holds the old token (152.596063ms)
✔ X-G6: a restart settles an open rotation forward when the file holds the new token (139.158765ms)
✔ X-G6: a 401 on the file token loses the grant: both tokens go, the record goes and memory unloads (144.64518ms)
✔ X-G6: a 403, 404, 500, a hang and a dropped socket each leave the rotation settle-pending with nothing deleted (1100.789745ms)
✔ X-G6: a file token no grant minted is held; the next restart says so again; a revoke clears it and takes both ids (181.734467ms)
✔ N9: a revoke of an open rotation, then a restart: no settle, no Gitea call and no message (146.803459ms)
✔ N9: a 404 on a delete by id counts as done (143.732923ms)
✔ X-G6: a restart undoes each step a killed child left, and spends an approved unspent decision (469.337565ms)
✔ X-G6: a restart with no admin slot leaves the user an orphan and the token left (63.327553ms)
✔ X-G7: a change to the definition, identity, publish remotes or admin slot after the raise ends decision-mismatch (143.161323ms)
✔ X-G7 and X-G9: an admin rotate voids open decisions, and rotate --finish deletes what was left (130.729706ms)
✔ X-G9: a rotate whose name is taken refuses and writes nothing; remove takes the slot away (77.884977ms)
✔ X-G9: a slot past its rotate-by date refuses a run at step 0, leaving the decision unspent (56.193013ms)
✔ X-G11: run refuses every caller but the human, an unapproved, a spent and a busy decision (125.223336ms)
✔ kill switch (plan :16): while halted the chief still reads grants.status, and an agent grants.revoke refuses (118.148549ms)
✔ X-G11: a revoke while the executor runs aborts it and the grant ends revoked (132.457409ms)
✔ X-G11: a revoke unloads memory and removes the file before its Gitea delete (134.269691ms)
✔ X-G11: a revoke during a rotation step 7 ends it revoked, and step 8 writes no record back (205.166166ms)
✔ X-G11: a revoke while a sweep settles a rotation, forward or back, ends it revoked with no record (340.037735ms)
✔ X-G11: a revoke during a settle whoami that finds a foreign token ends it revoked, not held (196.238899ms)
✔ X-G11: a revoke during a settle whoami that gets a 401 ends it revoked, not lost (189.34344ms)
✔ X-G12: a token that reaches wider than its forks fails with the repository named (401.574274ms)
✔ X-G12: reach is paged by X-Total-Count and a count that does not add up refuses (267.846613ms)
✔ X-G13: an agent cannot record a grant event, and the trigger refuses one from any writer but the grant writer (44.259928ms)
✔ X-G14: credentials delete fires the user: tokens go, login is prohibited, and grant.revoke.closed records it (284.587133ms)
✔ X-G14: a fire whose token delete or login PATCH fails lands it in serviceLeft (151.958016ms)
✔ X-G14: a fire whose user is gone or remade skips the fire steps and says why (334.678591ms)
✔ X-G14: a sweep retries a fire only for the user it fired: a remade or gone user is skipped and named (366.243469ms)
✔ X-G14: a revoke Gitea cannot reach stays open, and a restart closes it (134.383532ms)
✔ row 281 T8: a tester grants.revoke refuses tester-dev-only once bus.tester is off; the human revoke keeps its records and starts no job (111.177819ms)
✔ X-G15: look-alike commit steps outside the grant writer are ignored at start, by revoke and by status (132.34568ms)
✔ X-G15: a look-alike spend under another kind with the grant writer pair is not an open grant (44.455018ms)
✔ status lists the slot, users, grants and nothing secret, and every grants verb needs service gitea (94.729541ms)
✔ C2-21 L1/L2/L9: research gets write on her repository with declared scopes through raise and yes (132.848006ms)
✔ C2-21 L1/L2/L9: chief-of-staff gets read on her repository with declared scopes through raise and yes (108.726865ms)
✔ C2-21 L1/L2/L9: coder gets read on her repository with declared scopes through raise and yes (105.566834ms)
✔ C2-21 L7/L7b: research refuses non-library targets lib (43.04793ms)
✔ C2-21 L7/L7b: research refuses non-library targets app,lib (43.226528ms)
✔ C2-21 L7/L7b: chief-of-staff refuses non-library targets lib (40.740414ms)
✔ C2-21 L7/L7b: chief-of-staff refuses non-library targets app,lib (41.612697ms)
✔ C2-21 L4: read widened to write refuses gitea-reach-wider and deletes the minted token (106.28642ms)
✔ C2-21 L4: admin privilege refuses gitea-reach-wider and deletes the minted token (112.291928ms)
✔ C2-21 L4: missing pull refuses gitea-reach-wider and deletes the minted token (108.82183ms)
✔ C2-21 L4: missing permission object refuses gitea-reach-wider and deletes the minted token (107.25133ms)
✔ C2-21 L4: library fork refuses gitea-reach-wider and deletes the minted token (111.661727ms)
✔ C2-21 L8: untargeted library collaborator reach is disclosed and accepted publish-wide (110.617198ms)
✖ C2-21 L10: project kind or library level changing after raise refuses decision-mismatch (71.782699ms)
✔ C2-21 L4: creative write requires push true but ignores unrelated permission keys (216.51561ms)
✔ C2-21: a library grant with no declared scopes refuses gitea-not-declared at raise (45.980982ms)
✔ C2-03b T6: the slot before any chief evidence, then the listed chief's live grant (33.812383ms)
✔ C2-03b T7: a chief revoked through grants.revoke or fired through instance.fire refuses, and the fallback stays ended (388.895625ms)
✔ C2-03b T7 choice checks: delisted, a fire under way and held each refuse; a rehire lists the chief again (38.05536ms)
✔ C2-03b T8: a chief grant on another project ends the fallback, and two listed chiefs refuse library-fetch-no-grant (35.538194ms)
✔ C2-03b departure (sage-2928): with no chief entry, any Gitea grant evidence refuses (16.650711ms)
✔ C2-03b T9 runtime: the chief's store file and user, then a real revoke that runs no git with the slot at 0000 (899.871176ms)
✔ C2-04: library mappings load without roots, remain frozen, and retain mapping refusals (12.126655ms)
✔ C2-04: an otherwise valid publish grant cannot name a library repo (1.81521ms)
✔ config directory and file path follow MOSAIC_CONFIG (0.250758ms)
✔ the fixture business validates and comes back frozen (1.103053ms)
✔ two instances may share a definition (1.017458ms)
✔ top-level refusals (2.951099ms)
✔ arbiters and projects (5.522959ms)
✔ role instances (2.771534ms)
✔ Vikunja bots (6.347953ms)
✔ a role without Vikunja takes no tracker block (2.577895ms)
✔ credential references match the definition's services (2.989169ms)
✔ launch (9.573192ms)
✔ REQ-THREAD-2 REQ-LAUNCH-1 launch.threadIdleSeconds: absent is the default, a positive integer is used, anything else refuses (5.256373ms)
✔ loadBusiness: file checks (2.239389ms)
✔ loadBusiness: not a regular file (46.809559ms)
✔ loading writes nothing (1.257951ms)
✔ names that are Object.prototype properties don't count as declared (3.811661ms)
✔ the shipped example refuses as written and validates once filled in (0.970387ms)
✔ persona vars: agent layer, creative definition only, both or neither (11.317037ms)
✔ row 285: a chief or creative instance without credentials.gitea refuses at load (8.600878ms)
✔ row 285: a creative instance with identity and publish loads (0.949694ms)
✔ row 194 keep, noHire and retired validate, and a file without them loads as before (2.393095ms)
✔ row 194 each bad keep, noHire or retired entry refuses (7.173244ms)
✔ C2-21: only creative definitions and the chief with gitea may publish a library project (13.234306ms)
✔ the library install writes the business file, personas and slot dir, makes the copy and starts nothing (319.004449ms)
✔ a library re-run passes and leaves every file as it was (470.879755ms)
✔ a re-run refuses business-file-differs and leaves the file unchanged (312.005718ms)
✔ a re-run refuses persona-differs when an installed persona isn't the staged one, and writes nothing (335.01291ms)
✔ the factory install writes no library mapping, persona or slot dir (201.940121ms)
✔ the factory install refuses a library project and bus.tester off, before anything is written (83.824548ms)
✔ the library install refuses bus.tester on and a persona var in the input (76.59824ms)
✔ the install refuses root, a business id mismatch, another --home and a config dir with group bits (56.483195ms)
✔ the library install needs --personas (5.954284ms)
✔ a validate refusal removes the business file this run wrote (256.190647ms)
✔ N4: a validate refusal on a re-run leaves the matching business file in place, byte for byte (437.854592ms)
✔ a clean library host passes the precheck with no mirror yet (441.383612ms)
✔ the library precheck refuses a persona file at 0644 (461.331192ms)
✔ the library precheck refuses a persona whose digest isn't persona.sha256 (405.1089ms)
✔ the library precheck refuses a persona dir with group bits (464.242283ms)
✔ the library precheck refuses a business file at 0644 (457.841445ms)
✔ the library precheck refuses a changes.jsonl at 0644 (635.778232ms)
✔ the library precheck refuses a Discord binding (462.22964ms)
✔ --library-status refuses library-mirror-missing before the bus has made the mirror (331.544735ms)
✔ --library-status refuses library-never-synced on a fresh mirror (347.586128ms)
✔ --library-status names a recorded lastError before the null lastFetch it leaves (N3) (356.091238ms)
✔ --library-status prints each R-problem's path as an escaped JSON string, then the count, and passes (401.900674ms)
✔ --library-status takes no --profile (5.777431ms)
✔ Vikunja 614: install preserves the explicit notifier decision bytes and mode and creates none when absent (482.873578ms)
✔ row 322 F8: library personas install from the copy with no clone node_modules (146.255507ms)
✔ row 322 F9: importer refusal keeps the error line rather than the Node banner (89.465774ms)
✔ C2-21: library install supplies chief identity and books publish for its real read grant (294.210478ms)
ℹ tests 123
ℹ suites 0
ℹ pass 122
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 10963.095817
✖ failing tests:
test at packages/bus/tests/gitea-grant.test.mjs:1416:1
✖ C2-21 L10: project kind or library level changing after raise refuses decision-mismatch (71.782699ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
4 !== 0
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/steelbeak-row287/packages/bus/tests/gitea-grant.test.mjs:1434:12)
at async Test.run (node:internal/test_runner/test:1409:7)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: 4,
expected: 0,
operator: 'strictEqual',
diff: 'simple'
}
@@ -0,0 +1,131 @@
✔ versionAtLeast compares numbers and refuses a prerelease of 1.27.0 (1.280112ms)
✔ X-G1: a raise that fails a check refuses and stores nothing (71.349417ms)
✔ X-G1: a creative role, a role without identity and a missing or due slot refuse at raise (218.719606ms)
✔ X-G1: the chief of staff refuses at raise, whatever its definition, identity and publish declare (45.959539ms)
✔ X-G2: admin set refuses each failed check and writes nothing, then writes the slot once (166.537485ms)
✔ X-G3 to X-G8: an approve makes the user, mints the token, forks, checks reach and writes the file, record and grant.applied (108.835383ms)
✔ X-G4: a user this stack did not make, an admin, a disabled user and a remade user each refuse (368.565402ms)
✔ X-G6 and X-G8: the user and mint steps are written before their calls, and each result after its call (109.068207ms)
✔ X-G4: a fork by the seat user of another repository than the project refuses fork-foreign (103.652877ms)
✔ X-G5: a failure after the mint deletes the token, unloads nothing it did not load and leaves no file (132.5244ms)
✔ X-G5: a token delete that fails is left at Gitea, shown by status and taken by sweep (125.127842ms)
✔ X-G5: a mint that fails deletes by name, and a refusal at step 0 leaves the decision unspent (147.844515ms)
✔ X-G5: an agent held for an unknown token is refused at step 0 with no Gitea call (64.597545ms)
✔ X-G5 and X-G9 (step 7): a rotation whose old-token delete fails stays applied with the old id left (190.16061ms)
✔ X-G10: in a rotation the store file holds the new token before the old one is deleted (168.111001ms)
✔ X-G6: a rotation cut before the rename settles back at once, and the old token stays live (172.49915ms)
✔ X-G6: a rotation whose step 8 write fails is settle-pending, and a restart settles it forward (189.011204ms)
✔ X-G6: a restart settles an open rotation back when the file still holds the old token (141.149461ms)
✔ X-G6: a restart settles an open rotation forward when the file holds the new token (143.41922ms)
✔ X-G6: a 401 on the file token loses the grant: both tokens go, the record goes and memory unloads (142.157997ms)
✔ X-G6: a 403, 404, 500, a hang and a dropped socket each leave the rotation settle-pending with nothing deleted (1052.205948ms)
✔ X-G6: a file token no grant minted is held; the next restart says so again; a revoke clears it and takes both ids (173.950313ms)
✔ N9: a revoke of an open rotation, then a restart: no settle, no Gitea call and no message (139.936508ms)
✔ N9: a 404 on a delete by id counts as done (135.846367ms)
✔ X-G6: a restart undoes each step a killed child left, and spends an approved unspent decision (438.925152ms)
✔ X-G6: a restart with no admin slot leaves the user an orphan and the token left (58.025191ms)
✔ X-G7: a change to the definition, identity, publish remotes or admin slot after the raise ends decision-mismatch (128.865648ms)
✔ X-G7 and X-G9: an admin rotate voids open decisions, and rotate --finish deletes what was left (122.928741ms)
✔ X-G9: a rotate whose name is taken refuses and writes nothing; remove takes the slot away (67.477228ms)
✔ X-G9: a slot past its rotate-by date refuses a run at step 0, leaving the decision unspent (51.433851ms)
✔ X-G11: run refuses every caller but the human, an unapproved, a spent and a busy decision (106.410351ms)
✔ kill switch (plan :16): while halted the chief still reads grants.status, and an agent grants.revoke refuses (109.111514ms)
✔ X-G11: a revoke while the executor runs aborts it and the grant ends revoked (128.36085ms)
✔ X-G11: a revoke unloads memory and removes the file before its Gitea delete (134.277671ms)
✔ X-G11: a revoke during a rotation step 7 ends it revoked, and step 8 writes no record back (184.595189ms)
✔ X-G11: a revoke while a sweep settles a rotation, forward or back, ends it revoked with no record (343.152025ms)
✔ X-G11: a revoke during a settle whoami that finds a foreign token ends it revoked, not held (199.845732ms)
✔ X-G11: a revoke during a settle whoami that gets a 401 ends it revoked, not lost (185.12583ms)
✔ X-G12: a token that reaches wider than its forks fails with the repository named (399.290325ms)
✔ X-G12: reach is paged by X-Total-Count and a count that does not add up refuses (314.011296ms)
✔ X-G13: an agent cannot record a grant event, and the trigger refuses one from any writer but the grant writer (49.068987ms)
✔ X-G14: credentials delete fires the user: tokens go, login is prohibited, and grant.revoke.closed records it (296.702448ms)
✔ X-G14: a fire whose token delete or login PATCH fails lands it in serviceLeft (158.52798ms)
✔ X-G14: a fire whose user is gone or remade skips the fire steps and says why (302.003039ms)
✔ X-G14: a sweep retries a fire only for the user it fired: a remade or gone user is skipped and named (351.593821ms)
✔ X-G14: a revoke Gitea cannot reach stays open, and a restart closes it (134.257555ms)
✔ row 281 T8: a tester grants.revoke refuses tester-dev-only once bus.tester is off; the human revoke keeps its records and starts no job (109.730792ms)
✔ X-G15: look-alike commit steps outside the grant writer are ignored at start, by revoke and by status (131.637028ms)
✔ X-G15: a look-alike spend under another kind with the grant writer pair is not an open grant (44.820972ms)
✔ status lists the slot, users, grants and nothing secret, and every grants verb needs service gitea (114.502261ms)
✔ C2-21 L1/L2/L9: research gets write on her repository with declared scopes through raise and yes (127.306297ms)
✔ C2-21 L1/L2/L9: chief-of-staff gets read on her repository with declared scopes through raise and yes (109.289905ms)
✔ C2-21 L1/L2/L9: coder gets read on her repository with declared scopes through raise and yes (106.048934ms)
✔ C2-21 L7/L7b: research refuses non-library targets lib (42.311032ms)
✔ C2-21 L7/L7b: research refuses non-library targets app,lib (41.695354ms)
✔ C2-21 L7/L7b: chief-of-staff refuses non-library targets lib (41.862836ms)
✔ C2-21 L7/L7b: chief-of-staff refuses non-library targets app,lib (40.716511ms)
✔ C2-21 L4: read widened to write refuses gitea-reach-wider and deletes the minted token (87.149976ms)
✔ C2-21 L4: admin privilege refuses gitea-reach-wider and deletes the minted token (106.353171ms)
✔ C2-21 L4: missing pull refuses gitea-reach-wider and deletes the minted token (108.373202ms)
✔ C2-21 L4: missing permission object refuses gitea-reach-wider and deletes the minted token (109.736478ms)
✔ C2-21 L4: library fork refuses gitea-reach-wider and deletes the minted token (107.72263ms)
✔ C2-21 L8: untargeted library collaborator reach is disclosed and accepted publish-wide (113.114472ms)
✔ C2-21 L10: project kind or library level changing after raise refuses decision-mismatch (144.543711ms)
✔ C2-21 L4: creative write requires push true but ignores unrelated permission keys (214.033085ms)
✔ C2-21: a library grant with no declared scopes refuses gitea-not-declared at raise (47.400615ms)
✔ C2-03b T6: the slot before any chief evidence, then the listed chief's live grant (31.577774ms)
✔ C2-03b T7: a chief revoked through grants.revoke or fired through instance.fire refuses, and the fallback stays ended (333.151597ms)
✔ C2-03b T7 choice checks: delisted, a fire under way and held each refuse; a rehire lists the chief again (36.808837ms)
✔ C2-03b T8: a chief grant on another project ends the fallback, and two listed chiefs refuse library-fetch-no-grant (35.425842ms)
✔ C2-03b departure (sage-2928): with no chief entry, any Gitea grant evidence refuses (18.152711ms)
✔ C2-03b T9 runtime: the chief's store file and user, then a real revoke that runs no git with the slot at 0000 (731.269457ms)
✔ C2-04: library mappings load without roots, remain frozen, and retain mapping refusals (12.697745ms)
✔ C2-04: an otherwise valid publish grant cannot name a library repo (1.895805ms)
✔ config directory and file path follow MOSAIC_CONFIG (0.218002ms)
✔ the fixture business validates and comes back frozen (1.113423ms)
✔ two instances may share a definition (1.085918ms)
✔ top-level refusals (3.201752ms)
✔ arbiters and projects (4.428142ms)
✔ role instances (2.586059ms)
✔ Vikunja bots (6.556892ms)
✔ a role without Vikunja takes no tracker block (3.259111ms)
✔ credential references match the definition's services (2.945148ms)
✔ launch (9.192439ms)
✔ REQ-THREAD-2 REQ-LAUNCH-1 launch.threadIdleSeconds: absent is the default, a positive integer is used, anything else refuses (5.144938ms)
✔ loadBusiness: file checks (1.735446ms)
✔ loadBusiness: not a regular file (43.733004ms)
✔ loading writes nothing (1.135021ms)
✔ names that are Object.prototype properties don't count as declared (3.860107ms)
✔ the shipped example refuses as written and validates once filled in (0.993456ms)
✔ persona vars: agent layer, creative definition only, both or neither (11.635685ms)
✔ row 285: a chief or creative instance without credentials.gitea refuses at load (7.937253ms)
✔ row 285: a creative instance with identity and publish loads (0.868629ms)
✔ row 194 keep, noHire and retired validate, and a file without them loads as before (1.843993ms)
✔ row 194 each bad keep, noHire or retired entry refuses (6.103965ms)
✔ C2-21: only creative definitions and the chief with gitea may publish a library project (11.142387ms)
✔ the library install writes the business file, personas and slot dir, makes the copy and starts nothing (266.122171ms)
✔ a library re-run passes and leaves every file as it was (419.422061ms)
✔ a re-run refuses business-file-differs and leaves the file unchanged (294.559417ms)
✔ a re-run refuses persona-differs when an installed persona isn't the staged one, and writes nothing (294.731302ms)
✔ the factory install writes no library mapping, persona or slot dir (189.209583ms)
✔ the factory install refuses a library project and bus.tester off, before anything is written (73.526324ms)
✔ the library install refuses bus.tester on and a persona var in the input (75.290564ms)
✔ the install refuses root, a business id mismatch, another --home and a config dir with group bits (53.415023ms)
✔ the library install needs --personas (5.673132ms)
✔ a validate refusal removes the business file this run wrote (237.469747ms)
✔ N4: a validate refusal on a re-run leaves the matching business file in place, byte for byte (430.013288ms)
✔ a clean library host passes the precheck with no mirror yet (417.064064ms)
✔ the library precheck refuses a persona file at 0644 (430.044192ms)
✔ the library precheck refuses a persona whose digest isn't persona.sha256 (395.659433ms)
✔ the library precheck refuses a persona dir with group bits (420.475466ms)
✔ the library precheck refuses a business file at 0644 (418.657123ms)
✔ the library precheck refuses a changes.jsonl at 0644 (612.437015ms)
✔ the library precheck refuses a Discord binding (405.124749ms)
✔ --library-status refuses library-mirror-missing before the bus has made the mirror (329.538375ms)
✔ --library-status refuses library-never-synced on a fresh mirror (341.974764ms)
✔ --library-status names a recorded lastError before the null lastFetch it leaves (N3) (357.309304ms)
✔ --library-status prints each R-problem's path as an escaped JSON string, then the count, and passes (394.067863ms)
✔ --library-status takes no --profile (5.871472ms)
✔ Vikunja 614: install preserves the explicit notifier decision bytes and mode and creates none when absent (536.196884ms)
✔ row 322 F8: library personas install from the copy with no clone node_modules (164.278338ms)
✔ row 322 F9: importer refusal keeps the error line rather than the Node banner (95.063494ms)
✔ C2-21: library install supplies chief identity and books publish for its real read grant (261.537958ms)
ℹ tests 123
ℹ suites 0
ℹ pass 123
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 10632.428445
@@ -0,0 +1,39 @@
#!/usr/bin/env python3
"""Row287 semantic mutants. Each restoration is byte-checked; timeout/error is never a kill."""
from pathlib import Path
import hashlib, os, re, subprocess, sys
root=Path(__file__).resolve().parents[4]
os.chdir(root)
broker='packages/bus/src/broker.mjs'; grant='packages/bus/src/gitea-grant.mjs'; business='packages/business/src/business.mjs'
bus_test='packages/bus/tests/gitea-grant.test.mjs'; biz_test='packages/business/tests/business.test.mjs'
evidence=Path('agents/steelbeak/work/queue-287/evidence')
mutants=[
('M1',broker,"(CREATIVE_DEFINITIONS.includes(role.definition) || role.definition === STAFF_TO) && !libraryOnly", "role.definition === STAFF_TO && !libraryOnly",'C2-21 L7/L7b: research',bus_test),
('M2',broker,"level: CREATIVE_DEFINITIONS.includes(role.definition) ? 'write' : 'read'", "level: 'write'",'C2-21 L1/L2/L9: chief-of-staff',bus_test),
('M3',broker,"repo.permissions?.admin === false && repo.permissions?.push === (level === 'write') && repo.permissions?.pull === true", "repo.permissions?.pull === true",'C2-21 L4: read widened',bus_test),
('M4',broker," if (t.library) {", " if (false && t.library) {",'C2-21 L1/L2/L9: research',bus_test),
('M5',business,"projects[project].kind === 'library' && !CREATIVE_DEFINITIONS.includes(role.definition) && role.definition !== 'chief-of-staff'", "false",'C2-21: only creative',biz_test),
('M6',broker,"api.tokenCreate(user, tokenName, scopes)", "api.tokenCreate(user, tokenName)",'C2-21 L1/L2/L9: chief-of-staff',bus_test),
('M7',broker,"(CREATIVE_DEFINITIONS.includes(role.definition) || role.definition === STAFF_TO) && !libraryOnly", "CREATIVE_DEFINITIONS.includes(role.definition) && !libraryOnly",'C2-21 L7/L7b: chief-of-staff',bus_test),
('M8',broker," role.publish.length === 0 ||\n (projects.some((p) => b.projects?.[p]?.kind === 'library') &&\n !definition.credentials.some((c) => c.service === 'gitea' && Array.isArray(c.scopes) && c.scopes.length > 0))", " role.publish.length === 0",'C2-21: a library grant with no declared scopes',bus_test),
('M9',grant," if (r.json.length !== 0 || (r.total !== null && r.total !== 0)) deny('grant-admin-not-dedicated');", " // M9: omit dedicated-admin reach check.",'^X-G2:',bus_test),
('M10',broker," // Step 1, one transaction: spend and recheck. scope-changed ends spent (D6);", " await api.userCreate({ username: 'early-write', email: '[email protected]', full_name: 'Early write', password: 'fx287-early-q7', must_change_password: false });\n // Step 1, one transaction: spend and recheck. scope-changed ends spent (D6);",'C2-21 L10:',bus_test),
('M11',grant,"...(Object.keys(library).length ? { library } : {})", "...(false ? { library } : {})",'C2-21 L10:',bus_test),
]
results=[]
for key,file,old,new,pattern,test in mutants:
p=Path(file); original=p.read_bytes(); digest=hashlib.sha256(original).hexdigest(); source=original.decode()
if source.count(old)!=1: raise RuntimeError(f'{key}: expected one mutation site, got {source.count(old)}')
try:
p.write_text(source.replace(old,new,1))
out=subprocess.run(['timeout','-k','10','180','node','--test',f'--test-name-pattern={pattern}',test],stdout=subprocess.PIPE,stderr=subprocess.STDOUT,text=True,env=os.environ)
(evidence/f'{key}.log').write_text(out.stdout)
passes=re.search(r'^ℹ pass (\d+)$',out.stdout,re.M); failures=re.search(r'^ℹ fail (\d+)$',out.stdout,re.M); skips=re.search(r'^ℹ skipped (\d+)$',out.stdout,re.M)
killed=out.returncode==1 and failures and int(failures[1])>0 and skips and int(skips[1])==0 and 'AssertionError' in out.stdout and not re.search(r'SyntaxError|ERR_MODULE_NOT_FOUND',out.stdout)
print(f'{key}: rc={out.returncode} pass={passes[1] if passes else "?"} fail={failures[1] if failures else "?"} skip={skips[1] if skips else "?"} {"KILLED" if killed else "INVALID/SURVIVED"}',flush=True)
results.append(f'{key} {"KILLED" if killed else "INVALID/SURVIVED"} rc={out.returncode}')
if not killed: sys.exit(1)
finally:
p.write_bytes(original)
if hashlib.sha256(p.read_bytes()).hexdigest()!=digest: raise RuntimeError(f'{key}: restoration mismatch')
(evidence/'mutants-summary.txt').write_text('\n'.join(results)+'\n')
@@ -0,0 +1,56 @@
69f91b4989a39bf09201a7b328f6c38e833e9897e310f026ce893d4caa2ec214 BUILD-LOG.md
d64255c7493e10e1d82949c2335e4fce47ad5f4130901941fdd091e224b9c2c8 agents/steelbeak/work/queue-287/BUILD.md
fb8615d02a3cdb29e96434bf97e3d6b26f447e9fa05cf26bf97c26f6c659b1c8 agents/steelbeak/work/queue-287/evidence/M1.log
df427effd5b4ce587229ccf258816b3a65fe93f25a1e3706f8f1bef9223dcc42 agents/steelbeak/work/queue-287/evidence/M10.log
777dd9500f451d9fbca215816efd5e1f94ba803ef1649a724b18ea3ec65b4d10 agents/steelbeak/work/queue-287/evidence/M11.log
08e3628a711afed00d475cb9f9b1ee2dffd3a480d8a78e91478783aaea55b8f3 agents/steelbeak/work/queue-287/evidence/M2.log
8d958f9bd3791d2e98392924ae93f335559ea2e35b72763be0847f72fc7539a9 agents/steelbeak/work/queue-287/evidence/M3.log
8a82af3e7b46206fbeb1eeb4d6976fe34be703de5fc3b09328b51d806a13fd53 agents/steelbeak/work/queue-287/evidence/M4.log
27841d51d2aa04fc8b23a2cdb0f481ef0c1b6a9ba689dc84d3fbf212276aa43a agents/steelbeak/work/queue-287/evidence/M5.log
422a05a364404dd89d0730605f637552dc7d95058e785cdb8b2916690021cbe3 agents/steelbeak/work/queue-287/evidence/M6.log
cdb11e0c732c8ee0aa6c4c7167b499fabb079a1fb3c0f64e09776efe1ce94ec9 agents/steelbeak/work/queue-287/evidence/M7.log
8c3d35bbdec28394133f17ce427122dcbb3a00859122a2ba3e220698b08c7ac6 agents/steelbeak/work/queue-287/evidence/M8.log
f697d8606f9a879b7afc16aedc722bf89cd56fb79d1947933b40354bbdcbc3cb agents/steelbeak/work/queue-287/evidence/M9.log
015595d7539fa0a3fdd8993ca5f6bb37eb3a4e90d21de9902490b68ce65790c6 agents/steelbeak/work/queue-287/evidence/bus-suite.log
1186834379a25300c4e7657dd560194346bfd960206d716d3ec80d083b874ac7 agents/steelbeak/work/queue-287/evidence/business-suite.log
fb407cecad775daeb964996f5286504478e78abb2639dfddad901da72a8299da agents/steelbeak/work/queue-287/evidence/cleanup-audit.txt
1b5994ef1fbceb0d85b7dc27b8c80c38763e9443d255f5e0ae8768a52cc49a6f agents/steelbeak/work/queue-287/evidence/cli-suite.log
b9df0f83ac9cdcbd9904d48e91c2c9b29704b6b1ed1d86bd5da8208d8ae6e5c5 agents/steelbeak/work/queue-287/evidence/docker-facts-r2.log
d0a408745b860ddcf0415f1eaa56f733117cf01eb911e89acc398d48ac6d3a12 agents/steelbeak/work/queue-287/evidence/docker-facts.log
e214557bb114e1ddcfc9fe1779664c5775e0006e1531beb42432cf994a6ee4dd agents/steelbeak/work/queue-287/evidence/docker-whole.log
67bb508ab388f24adbfb519edc22fe5438e80796e90f7c22e858a1360dca3682 agents/steelbeak/work/queue-287/evidence/install-whole.log
354a30815e25b949cba560d44dd373e7bd10a2151a7c8809a4de9eb64bbfede4 agents/steelbeak/work/queue-287/evidence/mutants-run.log
22d7df0a069ff63ebad0ae6d7d7d21f25727a85f31c5811b05d37d975c08bce9 agents/steelbeak/work/queue-287/evidence/mutants-summary.txt
5b0e61c389a337fb507f73254d0a60141ef36a8b0abd4f41e0dd20f81f1df247 agents/steelbeak/work/queue-287/evidence/red-new.log
c840276c920023dce945bcfeb3afd3b111d6abf6dcfb0ae65daa3fccbaa720ce agents/steelbeak/work/queue-287/evidence/red-replacement.log
9b5f6010e9f7c56834371b673710a95eb3bdbb256d77cc3bc8ab160c3301b40e agents/steelbeak/work/queue-287/evidence/shell-resume-summary.txt
c17fca8ffdf905a467e3a3f201876cc96719e691ff2b94c0366ecb54e8e3286f agents/steelbeak/work/queue-287/evidence/shell-summary.txt
f91fc95a173339137f981e24fbf630acfde957e32c3cd71e28304532083c6a77 agents/steelbeak/work/queue-287/evidence/test-auth.log
d681bd46560d9f359ccd70fdb2694e8b2a4457fe5cc70796e3044447c3fb9562 agents/steelbeak/work/queue-287/evidence/test-conductor.log
92ea8ce5e66d892fefacc76b87d140bcfb8c5a51dc77bdf4a47a6213ac5d2898 agents/steelbeak/work/queue-287/evidence/test-config.log
9b482b7a3f0672c872923c06e71d5a88ff085a08170905a6cabe1d2b6fc283b6 agents/steelbeak/work/queue-287/evidence/test-discord.log
87a89b628fcebbbb3e006105de2bc5b8b583aef007051ff824b846c7de8914f4 agents/steelbeak/work/queue-287/evidence/test-extension-package.log
b56e3912fc1657a859a4ced51859ad4083825d46d932b057868eae38b9668376 agents/steelbeak/work/queue-287/evidence/test-federation-acceptance.log
83dfaa21c4e4ad941088efab3f69e87ebd2f6bd3cc72ede938e21dc06da8a34f agents/steelbeak/work/queue-287/evidence/test-foundation.log
3e2f8024027592e77790db2bbe039803060915ebf37c7b67f04071a7e83d20dc agents/steelbeak/work/queue-287/evidence/test-land-work-bash.log
81aec0f6e855251882f3e87ac7cfbc4bd23ea3a8bd2f44577a2200fa8ae549fa agents/steelbeak/work/queue-287/evidence/test-land-work.log
5d9a170308927264d212e3dd00e87463a21d55811cf9b4cb51dce74d6a10fea5 agents/steelbeak/work/queue-287/evidence/test-push-check-bash.log
b59c448667010e92c3d2ef46ea5cb341971e31f71393d385efc63e1bd4fe0f8e agents/steelbeak/work/queue-287/evidence/test-queue-bash.log
9f9dd11e2631a24ad55aaf7becc61965c592b00da08a4e72b98e0384d3a13ec7 agents/steelbeak/work/queue-287/evidence/test-release-bash.log
a065947f07d70a4721b423da597968630bc8ec1659db14681af42bb3f974077d agents/steelbeak/work/queue-287/evidence/test-task-bash.log
851bc620c887bebf5d4458ca5ba45cd6e789f31aa1a12433c9a595de77993064 agents/steelbeak/work/queue-287/evidence/token-audit.txt
43f468e4a7b10abc8072c49aeaad78e93c38147afb7b6dbe551efed266c5df39 agents/steelbeak/work/queue-287/evidence/touched-r1.log
bbb37165d7d6e4f1d831bcb6399c2f06466525ff4d0a44c75ff1d814f193567b agents/steelbeak/work/queue-287/evidence/touched-r2.log
e87f8c8de6e58539563253962076d7b578c81b00a0dea36d5f32ad35dfe584e0 agents/steelbeak/work/queue-287/evidence/touched-r3.log
759970574c60a95dccc637284414aa5947934a100cf38114fbdc5c5d155da7df agents/steelbeak/work/queue-287/mutants.py
7a83e90c912c3cf25a119f91a59fcb10e672ad4a3199fefc6b6612b1f7574a28 docs/SESSIONS.md
901895aef8a1d4f2343c4a0e4fb1159136e1bbb61f05a15d8fcde5a7093baa59 packages/bus/src/broker.mjs
27d7d5322c2f12641e350867d3ac6f838b33832951e69f057e3a7a28b8ac4d5b packages/bus/src/gitea-grant.mjs
e18e9f4e5e28057487bf184478a97a6688c77693533ba5e8833e23163ade5da5 packages/bus/tests/fake-gitea-api.mjs
5294a667971479e4e5585a6797d5cfbe7c32698d339f7fe8fad25ab895bdc6f5 packages/bus/tests/gitea-grant-docker.test.mjs
cf7b0b8e6ee6f6c1a73ea3527df88cb43a8c129770c053abfd8963a059cff6d5 packages/bus/tests/gitea-grant.test.mjs
1e87b315d0e6676069314f37d0d7a731d76331977d1aadb17429af91239554b6 packages/bus/tests/library-credential.test.mjs
75f77b19027576f0e26b30f1c905837b0d115c9a12fc2fda53c5dac1345c7df4 packages/business/src/business.mjs
81a3b1c7b7f468b7e5db41f3a7524415ea3ed7b25a2544c999aeedc46293d312 packages/business/tests/business.test.mjs
5bfa627eed95dc7bdf6e2fc460a90db9c1cadfd6453cd9daa148c9761acac0bf packages/cli/tests/case2-install.test.mjs
e0678c27969d74d955641ee23f49f45b6152ef564db1ca05d1d7cd24cac2112b scripts/case2-install.sh
+4
View File
@@ -1667,3 +1667,7 @@ are never rewritten or removed; corrections are new entries.
- 2026-10-24 — rockerduck (a403aa78-b550-4bab-a613-7ea3fd75ec79), row286 / #1791 r6 under sage-4112: two fixture-value replacements only, exact secrets grep empty, touched6/6+24/24 and whole151/151+981/981, zero output fixture matches; no assertion/production/guard change, manifest prepared for round3.
2026-10-24T14:37:02Z | Sage (1ef1e4f8) | Row 286 / #1791 landed (mos-1576, sage-4100, sage-4112) | Boyd and Dewey approved r5 and r6; first push STOP secrets on two fixture values, r6 shortened them; gate GREEN 14:36:42Z (4251 tests); pushed 5d7801dc1; #1791 c31591 closed; case2 brief re-pinned for 225/229/284/287-290 (revs 3930-3936); brief template sage-4112 line
- 2026-10-24 — steelbeak — row 287 C2-21 (#1792), steelbeak/row287 at 18334d713: recovered inherited design and current code; canonical start initially refused assign lock; tests held pending Sage CLEAR, implementation not yet started.
- 2026-10-24 — steelbeak — row 287 C2-21 (#1792): canonical start steelbeak-287-start succeeded at queue rev 3955; inherited design and sage-4125 recorded, local root/webui npm ci passed, test drafts prepared, production untouched and all tests held pending Sage CLEAR.
- 2026-10-24 — steelbeak — row 287 C2-21 (#1792), builder complete under sage-4130: collaborator library grants and chief real read mint built; bus 998/business 283/CLI 989+FED1/Docker2/install27/all12 shell gates green, M1-M11 killed/restored, token/cleanup audits clean; preparing steelbeak/row287 push and manifest handoff to Boyd then Dewey, no live change or landing.
- 2026-10-24 — steelbeak — row 287 first-review follow-up: Boyd approved c2338ed58/3683b40... with two doc-only corrections; append-only BUILD correction distinguishes clean product/test diff from raw-log whitespace and records old-test numstats. Queue round1 open; missing-seat-credential request failure reported to Sage. Frozen product and manifest preserved; no tests or slot use.
+55 -24
View File
@@ -2756,22 +2756,24 @@ export class Broker {
: `\nThis makes a new key for ${g.agent} on ${g.host}.`);
return { body, block };
}
// Row 236 (#1742, #436b r5 section 2): the agent's role and definition, if the agent is a role of the
// business whose definition declares gitea and which has identity and a publish grant (199 D8). A
// creative role gets no grant, whatever its definition declares (mos-1163). Row 285 (#1790, sage-2921):
// nor does the chief of staff, whose definition declares gitea for a read grant only, since this path
// mints SEAT_SCOPES, write:repository included.
#giteaRole(business, agent) {
// Row 236: identity, publish and a Gitea declaration are required. mos-1163 keeps creative
// roles out of non-library grants and every host grant. mos-1320 item 2 and sage-4125 allow
// creative/chief targets only when all are library projects. sage-2921 refused the chief because
// SEAT_SCOPES included write:repository; library grants now use its definition's read scopes.
#giteaRole(business, agent, projects) {
const b = this.#business(business);
if (!Object.hasOwn(b.roles, agent)) fail('gitea-not-declared');
const role = b.roles[agent];
if (CREATIVE_DEFINITIONS.includes(role.definition) || role.definition === STAFF_TO) fail('gitea-not-declared');
const libraryOnly = projects.every((p) => b.projects?.[p]?.kind === 'library');
if ((CREATIVE_DEFINITIONS.includes(role.definition) || role.definition === STAFF_TO) && !libraryOnly) fail('gitea-not-declared');
const definition = Object.hasOwn(b.definitions ?? {}, role.definition) ? b.definitions[role.definition] : null;
if (
!definition?.credentials?.some((c) => c.service === 'gitea') ||
!role.identity?.gitea ||
!Array.isArray(role.publish) ||
role.publish.length === 0
role.publish.length === 0 ||
(projects.some((p) => b.projects?.[p]?.kind === 'library') &&
!definition.credentials.some((c) => c.service === 'gitea' && Array.isArray(c.scopes) && c.scopes.length > 0))
)
fail('gitea-not-declared');
return { b, role, definition };
@@ -2784,6 +2786,8 @@ export class Broker {
if (!role.publish.includes(project) || !upstream) fail('remote-not-granted');
const problem = forkProblem(upstream, role.identity);
if (problem) fail(problem);
if (b.projects[project].kind === 'library')
return { project, upstream, library: true, repository: upstream, level: CREATIVE_DEFINITIONS.includes(role.definition) ? 'write' : 'read' };
return { project, upstream, fork: forkUrl(upstream, role.identity.gitea.user) };
});
}
@@ -2797,7 +2801,7 @@ export class Broker {
// Section 2: the store checks and the raise binding. The caller owns the transaction. The chief
// can't shape the block: every line comes from the business, the slot and the store.
#giteaGrantBinding(business, g) {
const { b, role, definition } = this.#giteaRole(business, g.agent);
const { b, role, definition } = this.#giteaRole(business, g.agent, g.target.projects);
this.#grantableAgent(business, g.agent);
const targets = this.#giteaTargets(b, role, g.target.projects);
const meta = this.#giteaAdminMeta(business, role.identity.gitea.url);
@@ -2815,23 +2819,31 @@ export class Broker {
identitySha256: this.#giteaGrant.identitySha256(role, b.projects),
adminSha256: this.#giteaGrant.adminSha256(meta),
};
const others = role.publish.filter((p) => !g.target.projects.includes(p)).map((p) => b.projects?.[p]?.remote ?? p);
const others = role.publish.filter((p) => !g.target.projects.includes(p));
const forkOthers = others.filter((p) => b.projects?.[p]?.kind !== 'library').map((p) => b.projects?.[p]?.remote ?? p);
const libraryOthers = others.filter((p) => b.projects?.[p]?.kind === 'library');
const forks = targets.filter((t) => !t.library), repositories = targets.filter((t) => t.library);
const level = CREATIVE_DEFINITIONS.includes(role.definition) ? 'write' : 'read';
const scopes = repositories.length ? definition.credentials.find((c) => c.service === 'gitea').scopes : ['read:user', 'write:repository'];
const { gitea, email } = role.identity;
const block =
`\n\nGrant ${g.agent} a Gitea token: user ${gitea.user}, email ${email}, url ${gitea.url}.` +
`\nForks:` +
targets.map((t) => `\n${t.fork}`).join('') +
`\nScopes: ${['read:user', 'write:repository'].join(', ')}.` +
(forks.length ? `\nForks:` + forks.map((t) => `\n${t.fork}`).join('') : '') +
repositories.map((t) => `\nRepository: ${t.repository}, level ${t.level}.` + (t.level === 'write'
? `\nThe token can write every path of this repository. Only the bus's path check limits it to supporting folders.`
: `\nThe token can read every path of this repository. It can't push to it, because the user is a read collaborator.`)).join('') +
`\nScopes: ${scopes.join(', ')}.` +
`\nRotate by ${g.rotateBy}.` +
`\nKind: ${kind}.` +
`\nThe token can write every repository this user can write. Step 5 checks that this is only its forks.` +
(others.length ? `\nIt may also reach this user's existing forks of:` + others.map((u) => `\n${u}`).join('') : '');
`\nThe token can write every repository this user can write. Step 5 checks that this is only its forks${repositories.length ? ' and the repositories above at their level' : ''}.` +
(forkOthers.length ? `\nIt may also reach this user's existing forks of:` + forkOthers.map((u) => `\n${u}`).join('') : '') +
(libraryOthers.length ? `\nIt may also reach these repositories as a collaborator: ` + libraryOthers.map((p) => `${b.projects[p].remote} (${level})`).join(', ') : '');
return { body, block };
}
// Section 4 step 1's checks after the spend. Throws the code that ends the grant.
#giteaRecheck(business, decision, g) {
this.#scopeRecheck(business, decision, g);
const { b, role, definition } = this.#giteaRole(business, g.agent);
const { b, role, definition } = this.#giteaRole(business, g.agent, g.target.projects);
if (this.#giteaGrant.definitionSha256(definition) !== g.definitionSha256) fail('decision-mismatch');
if (this.#giteaGrant.identitySha256(role, b.projects) !== g.identitySha256) fail('decision-mismatch');
const meta = this.#giteaAdminMeta(business, role.identity.gitea.url);
@@ -3061,7 +3073,10 @@ export class Broker {
tokenName = this.#giteaGrant.tokenName(business, agent, decision);
step('mint', { tokenName });
minting = true;
const minted = await api.tokenCreate(user, tokenName);
const scopes = targets.some((t) => t.library)
? b.definitions[role.definition].credentials.find((c) => c.service === 'gitea').scopes
: undefined;
const minted = await api.tokenCreate(user, tokenName, scopes);
if (minted.status !== 201 || !giteaId(minted.json?.id) || typeof minted.json?.sha1 !== 'string') fail('gitea-failed');
tokenId = minted.json.id;
const token = this.#giteaGrant.checkToken(minted.json.sha1);
@@ -3069,9 +3084,16 @@ export class Broker {
stop();
// Step 4.
const seat = this.#giteaGrant.seat(meta.url, token, log);
const forks = [];
const forks = [], repositories = [];
for (const t of targets) {
const { owner, repo } = parseGiteaUrl(t.upstream, { repo: true });
if (t.library) {
const added = await api.collaboratorAdd(owner, repo, user, t.level);
stop();
if (added.status !== 204) fail('gitea-failed');
repositories.push({ project: t.project, repository: t.repository, level: t.level });
continue;
}
const f = await seat.forkGet(user, repo);
stop();
if (f.status === 200) {
@@ -3088,10 +3110,14 @@ export class Broker {
stop();
if (me.status !== 200) fail('gitea-reach-unknown');
if (lower(me.json?.login) !== lower(user) || me.json?.id !== userId) fail('gitea-user-foreign');
const upstreams = new Set();
const upstreams = new Set(), libraryReach = new Map();
for (const p of role.publish) {
const u = parseGiteaUrl(b.projects?.[p]?.remote, { repo: true });
if (u.url) upstreams.add(lower(`${u.owner}/${u.repo}`));
if (u.url) {
const name = lower(`${u.owner}/${u.repo}`);
if (b.projects[p].kind === 'library') libraryReach.set(name, CREATIVE_DEFINITIONS.includes(role.definition) ? 'write' : 'read');
else upstreams.add(name);
}
}
let seen = 0,
total = null,
@@ -3105,8 +3131,11 @@ export class Broker {
seen += r.json.length;
for (const repo of r.json) {
const parent = typeof repo?.parent?.full_name === 'string' ? lower(repo.parent.full_name) : null;
if (wider === null && (lower(repo?.owner?.login) !== lower(user) || !parent || !upstreams.has(parent)))
wider = typeof repo?.full_name === 'string' ? repo.full_name : 'unknown';
const level = libraryReach.get(lower(repo?.full_name));
const allowed = level
? repo.permissions?.admin === false && repo.permissions?.push === (level === 'write') && repo.permissions?.pull === true
: lower(repo?.owner?.login) === lower(user) && parent && upstreams.has(parent);
if (wider === null && !allowed) wider = typeof repo?.full_name === 'string' ? repo.full_name : 'unknown';
}
if (r.json.length < this.#giteaGrant.repoPage) break;
}
@@ -3122,11 +3151,13 @@ export class Broker {
step('commit', { previousTokenId: previous?.tokenId ?? null, tokenId, tokenName, previous });
stop();
const applied = (extra = {}) => {
this.#grantEvent(business, { action: 'grant.applied', method: 'gitea', grantDecision: decision, agent, user, userId, tokenId, forks, ...extra }, agent);
this.#grantEvent(business, { action: 'grant.applied', method: 'gitea', grantDecision: decision, agent, user, userId, tokenId, forks, ...(repositories.length ? { repositories } : {}), ...extra }, agent);
this.#grantNotice(
business,
decision,
`${agent} has a Gitea token as ${user} on ${meta.url} (decision ${decision}), forks: ${forks.join(', ')}.` +
`${agent} has a Gitea token as ${user} on ${meta.url} (decision ${decision})` +
(forks.length ? `, forks: ${forks.join(', ')}` : '') +
(repositories.length ? `, repositories: ${repositories.map((r) => `${r.repository} (${r.level})`).join(', ')}` : '') + '.' +
(extra.serviceLeft?.length ? ` The previous token ${extra.serviceLeft.join(', ')} is still live. Run: mosaic grants sweep gitea` : ''),
);
};
+10 -3
View File
@@ -18,7 +18,7 @@ import { BusError, GRANT_EVIDENCE } from './broker.mjs';
import { day, privateDir } from './credentials.mjs';
import { definitionSha256, grantTag } from './host-grant.mjs';
import { canonicalJson } from '../../business/src/util.mjs';
import { forkUrl, giteaName, ID_PATTERN, parseGiteaUrl } from '../../business/src/vocabulary.mjs';
import { forkUrl, giteaName, ID_PATTERN, parseGiteaUrl, CREATIVE_DEFINITIONS } from '../../business/src/vocabulary.mjs';
const deny = (code) => {
throw new BusError(code);
@@ -72,12 +72,17 @@ export function giteaGrantCheck(grant, today) {
// Section 2: over the agent's identity, its publish list and the remote of every project in it, since
// step 5 checks against all of them. `projects` is the business's project map.
export const identitySha256 = (role, projects) =>
sha256({
export const identitySha256 = (role, projects) => {
const library = Object.fromEntries((role.publish ?? []).filter((p) => projects?.[p]?.kind === 'library')
.map((p) => [p, CREATIVE_DEFINITIONS.includes(role.definition) ? 'write' : 'read']));
return sha256({
identity: role.identity ?? null,
publish: role.publish ?? [],
remotes: Object.fromEntries((role.publish ?? []).map((p) => [p, projects?.[p]?.remote ?? null])),
// C2-21: bind kind and level, preserving every legacy non-library binding byte for byte.
...(Object.keys(library).length ? { library } : {}),
});
};
// Section 2: over the admin slot's metadata, never the token.
export const adminSha256 = (meta) => sha256({ url: meta.url, account: meta.account, setAt: meta.setAt });
// Step 3: `mosaic-<tag>-<decision>`. grantTag already starts with "mosaic-".
@@ -263,6 +268,8 @@ export function giteaGrant({ configDir, ca = undefined, timeoutMs = CALL_MS, tra
tokenCreate: (user, name, scopes = SEAT_SCOPES, op = 'token-create') =>
go(op, 'POST', `/api/v1/users/${segment(user)}/tokens`, basic, { name, scopes: [...scopes] }),
tokenDelete: (user, idOrName) => go('token-delete', 'DELETE', `/api/v1/users/${segment(user)}/tokens/${segment(idOrName)}`, basic),
collaboratorAdd: (owner, repo, user, permission) =>
go('collaborator-add', 'PUT', `/api/v1/repos/${segment(owner)}/${segment(repo)}/collaborators/${segment(user)}`, { token }, { permission }),
};
};
return {
+13 -3
View File
@@ -22,9 +22,10 @@ export async function fakeGiteaApi(t, dir, { version = '1.27.1', admin = 'mosaic
const state = { version };
const lower = (x) => String(x).toLowerCase();
const userJson = (u) => ({ id: u.id, login: u.login, email: u.email, full_name: u.full_name, is_admin: u.is_admin, prohibit_login: u.prohibit_login });
const repoJson = (r) => ({
const repoJson = (r, user = null) => ({
id: r.id, name: r.name, full_name: `${r.owner}/${r.name}`, owner: { login: r.owner }, fork: Boolean(r.parent),
parent: r.parent ? { full_name: r.parent } : null,
permissions: { admin: lower(user) === lower(r.owner), push: lower(user) === lower(r.owner) || (r.permissions[lower(user)] ?? 'write') === 'write', pull: true },
});
const api = {
url: null,
@@ -52,7 +53,7 @@ export async function fakeGiteaApi(t, dir, { version = '1.27.1', admin = 'mosaic
users.delete(lower(login));
},
addRepo(owner, name, { parent = null, collaborators = [] } = {}) {
const r = { id: nextId++, owner, name, parent, collaborators: collaborators.map(lower) };
const r = { id: nextId++, owner, name, parent, collaborators: collaborators.map(lower), permissions: {} };
repos.push(r);
return r;
},
@@ -100,7 +101,7 @@ export async function fakeGiteaApi(t, dir, { version = '1.27.1', admin = 'mosaic
const all = reachable(me);
const limit = Number(query.get('limit') ?? 30);
const page = Number(query.get('page') ?? 1);
return [200, all.slice((page - 1) * limit, page * limit).map(repoJson), all.length];
return [200, all.slice((page - 1) * limit, page * limit).map((r) => repoJson(r, me.login)), all.length];
}
if (p[0] === 'users' && p.length === 2 && method === 'GET') {
const u = users.get(lower(p[1]));
@@ -144,6 +145,15 @@ export async function fakeGiteaApi(t, dir, { version = '1.27.1', admin = 'mosaic
return [204, null];
}
}
if (p[0] === 'repos' && p.length === 5 && p[3] === 'collaborators' && method === 'PUT') {
if (auth.kind !== 'token' || !me.is_admin || !who.token.scopes.includes('write:repository')) return [403, { message: 'forbidden' }];
const r = repos.find((x) => lower(x.owner) === lower(p[1]) && lower(x.name) === lower(p[2]));
if (!r || !users.has(lower(p[4]))) return [404, { message: 'not found' }];
if (!['read', 'write'].includes(body.permission)) return [422, { message: 'invalid permission' }];
if (!r.collaborators.includes(lower(p[4]))) r.collaborators.push(lower(p[4]));
r.permissions[lower(p[4])] = body.permission;
return [204, null];
}
if (p[0] === 'repos' && p.length === 3 && method === 'GET') {
const r = repos.find((x) => lower(x.owner) === lower(p[1]) && lower(x.name) === lower(p[2]));
return r ? [200, repoJson(r)] : [404, { message: 'not found' }];
+127 -2
View File
@@ -6,11 +6,12 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { randomBytes } from 'node:crypto';
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync } from 'node:fs';
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
import { request } from 'node:https';
import { createServer } from 'node:net';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { spawnSync } from 'node:child_process';
import { Broker, GRANT_EVIDENCE } from '../src/broker.mjs';
import { Credentials } from '../src/credentials.mjs';
@@ -58,6 +59,130 @@ function call(url, ca, method, path, { auth = null, body = null } = {}) {
});
}
// C2-21: independent of the executor. F-A and F-B gate production changes; F-C/F-D are observations.
test('C2-21 F-A/F-B: admin collaborator grant on another owner and seat permission reach; F-C/F-D protection observations', { skip, timeout: 300000 }, async (t) => {
// Capture the child's complete TAP stdout/stderr before any of it reaches this runner. The private
// scratch file lets the parent search actual minted values without putting one in an assertion.
if (!process.env.C2_GRANT_FACT_CAPTURE) {
const capture = mkdtempSync(join(tmpdir(), 'g287-capture-'));
t.after(() => rmSync(capture, { recursive: true, force: true }));
const file = join(capture, 'minted');
const childEnv = { ...process.env, C2_GRANT_FACT_CAPTURE: file };
delete childEnv.NODE_TEST_CONTEXT;
const child = spawnSync(process.execPath, ['--test', '--test-name-pattern=^C2-21 F-A/F-B:', fileURLToPath(import.meta.url)], {
encoding: 'utf8', timeout: 280000, env: childEnv,
});
const values = existsSync(file) ? JSON.parse(readFileSync(file, 'utf8')) : [];
const output = (child.stdout ?? '') + (child.stderr ?? '');
assert.ok(values.every((value) => !output.includes(value)), 'complete TAP stdout/stderr contain no minted token');
t.diagnostic(output);
t.diagnostic(`minted ${values.length}, hits 0 (complete TAP stdout/stderr)`);
assert.equal(child.status, 0, 'Docker facts child passed');
assert.equal(values.length, 4, 'all four scratch tokens were minted: facts actually ran');
return;
}
const dir = mkdtempSync(join(tmpdir(), 'g287-facts-'));
t.after(() => rmSync(dir, { recursive: true, force: true }));
const tlsDir = join(dir, 'tls'), tls = scratchCa(tlsDir);
chmodSync(tlsDir, 0o755);
chmodSync(join(tlsDir, 'server.key'), 0o644);
const ca = readFileSync(tls.ca, 'utf8'), port = await freePort();
const url = `https://127.0.0.1:${port}`, name = 'g287-' + randomBytes(6).toString('hex');
t.after(() => docker('rm', '-f', name));
const started = docker('run', '-d', '--rm', '--name', name, '-p', `127.0.0.1:${port}:3000`, '-v', `${tlsDir}:/certs:ro`,
'-e', 'GITEA__security__INSTALL_LOCK=true', '-e', 'GITEA__database__DB_TYPE=sqlite3',
'-e', 'GITEA__service__DISABLE_REGISTRATION=true', '-e', 'GITEA__server__PROTOCOL=https',
'-e', 'GITEA__server__CERT_FILE=/certs/server.pem', '-e', 'GITEA__server__KEY_FILE=/certs/server.key',
'-e', `GITEA__server__ROOT_URL=${url}/`, IMAGE);
assert.equal(started.status, 0, 'scratch container starts');
const end = Date.now() + 120000;
while ((await call(url, ca, 'GET', '/api/v1/version')).status !== 200) {
assert.ok(Date.now() < end, 'scratch Gitea answers in 120 seconds');
await sleep(500);
}
const gitea = (...args) => docker('exec', '-u', 'git', name, 'gitea', ...args);
const secrets = [], diagnostics = [];
const say = (line) => { diagnostics.push(line); t.diagnostic(line); };
for (const [user, admin] of [['mosaic-grant', true], ['writer', false], ['creative', false], ['chief', false]]) {
const r = gitea('admin', 'user', 'create', '--username', user, '--email', `${user}@example.test`,
'--password', randomBytes(16).toString('base64url'), '--must-change-password=false', ...(admin ? ['--admin'] : []));
assert.equal(r.status, 0, `create scratch ${user}`);
}
const mint = (user, scopes) => {
const r = gitea('admin', 'user', 'generate-access-token', '--username', user, '--token-name', 'facts', '--scopes', scopes.join(','), '--raw');
assert.equal(r.status, 0, `mint scratch ${user}`);
const value = r.stdout.trim();
assert.ok(/^[0-9a-f]{40}$/.test(value), 'minted token shape');
secrets.push(value);
writeFileSync(process.env.C2_GRANT_FACT_CAPTURE, JSON.stringify(secrets), { mode: 0o600 });
return value;
};
const adminToken = mint('mosaic-grant', ['write:admin', 'write:repository', 'write:user']);
const writerToken = mint('writer', ['read:user', 'write:repository']);
const creativeToken = mint('creative', ['read:user', 'write:repository']);
const chiefToken = mint('chief', ['read:repository', 'read:user']);
const made = await call(url, ca, 'POST', '/api/v1/admin/users/writer/repos', {
auth: 'token ' + adminToken, body: { name: 'books', auto_init: true, private: true },
});
assert.equal(made.status, 201, 'create repository owned by writer, not admin');
for (const [user, level, value, push] of [['creative', 'write', creativeToken, true], ['chief', 'read', chiefToken, false]]) {
const added = await call(url, ca, 'PUT', `/api/v1/repos/writer/books/collaborators/${user}`, {
auth: 'token ' + adminToken, body: { permission: level },
});
say(`F-A ${user} ${level}: PUT status ${added.status}`);
assert.equal(added.status, 204, 'F-A admin scopes can add collaborator to another owner');
const listed = await call(url, ca, 'GET', '/api/v1/user/repos', { auth: 'token ' + value });
assert.equal(listed.status, 200, 'F-B seat lists reachable repos');
const repository = listed.json?.find((r) => r.full_name === 'writer/books');
assert.ok(repository, 'F-B seat lists the granted repository');
say(`F-B ${user}: permissions ${JSON.stringify(repository.permissions)}`);
assert.deepEqual({ admin: repository.permissions?.admin, push: repository.permissions?.push, pull: repository.permissions?.pull },
{ admin: false, push, pull: true }, 'F-B exact admin/push/pull keys');
}
// No diagnostic includes a Git child result: HTTP auth is supplied only in that child's env.
const git = (args, user = 'writer', value = writerToken, cwd = dir) => spawnSync('git', args, {
cwd, encoding: 'utf8', timeout: 30000,
env: { ...process.env, GIT_CONFIG_NOSYSTEM: '1', GIT_CONFIG_GLOBAL: '/dev/null', GIT_TERMINAL_PROMPT: '0',
GIT_SSL_CAINFO: tls.ca, GIT_CONFIG_COUNT: '3', GIT_CONFIG_KEY_0: 'http.extraHeader',
GIT_CONFIG_VALUE_0: 'Authorization: Basic ' + Buffer.from(`${user}:${value}`).toString('base64'),
GIT_CONFIG_KEY_1: 'user.name', GIT_CONFIG_VALUE_1: 'Scratch writer',
GIT_CONFIG_KEY_2: 'user.email', GIT_CONFIG_VALUE_2: '[email protected]' },
});
const work = join(dir, 'work');
const branch = made.json.default_branch;
const clone = git(['clone', `${url}/writer/books.git`, work]);
const protection = await call(url, ca, 'POST', '/api/v1/repos/writer/books/branch_protections', {
auth: 'token ' + writerToken,
body: { branch_name: branch, rule_name: branch, enable_push: true, enable_push_whitelist: false,
enable_force_push: false, protected_file_patterns: 'Story/**' },
});
if (clone.status !== 0 || protection.status !== 201) {
say(`F-C/F-D not established: clone rc ${clone.status}, protection HTTP ${protection.status}`);
} else {
const commit = (path, text) => {
mkdirSync(join(work, path.split('/')[0]), { recursive: true });
writeFileSync(join(work, path), text);
const add = git(['add', path], 'writer', writerToken, work);
const made = git(['commit', '-m', 'scratch fact'], 'writer', writerToken, work);
return add.status === 0 && made.status === 0;
};
const story = commit('Story/chapter.md', 'scratch story\n');
const seatStory = story ? git(['push', 'origin', `HEAD:${branch}`], 'creative', creativeToken, work).status : null;
const ownerStory = story ? git(['push', 'origin', `HEAD:${branch}`], 'writer', writerToken, work).status : null;
say(`F-C protected Story/**: seat rc ${seatStory}, owner rc ${ownerStory}; established ${seatStory !== null && seatStory !== 0 && ownerStory === 0}`);
// Start from the actual remote tip even when the file-pattern observation failed.
const fetched = git(['fetch', 'origin', branch], 'writer', writerToken, work);
const reset = git(['reset', '--hard', 'FETCH_HEAD'], 'writer', writerToken, work);
const supporting = fetched.status === 0 && reset.status === 0 && commit('Notes/note.md', 'scratch supporting note\n');
const normal = supporting ? git(['push', 'origin', `HEAD:${branch}`], 'creative', creativeToken, work).status : null;
const force = normal === 0 ? git(['push', '--force', 'origin', `HEAD~1:${branch}`], 'creative', creativeToken, work).status : null;
say(`F-D protected branch: normal rc ${normal}, force rc ${force}; established ${normal === 0 && force !== null && force !== 0}`);
}
assert.ok(secrets.every((value) => diagnostics.every((line) => !line.includes(value))), 'diagnostics contain no minted token');
say(`minted ${secrets.length}, hits 0`);
});
test('X-G3: on scratch 1.27.1, a grant makes the user, mints, forks, checks reach, writes the file, and revoke deletes the token', { skip, timeout: 300000 }, async (t) => {
const dir = mkdtempSync(join(tmpdir(), 'g236-docker-'));
t.after(() => rmSync(dir, { recursive: true, force: true }));
@@ -69,7 +194,7 @@ test('X-G3: on scratch 1.27.1, a grant makes the user, mints, forks, checks reac
const ca = readFileSync(tls.ca, 'utf8');
const port = await freePort();
const url = `https://127.0.0.1:${port}`;
const name = 'g236-' + randomBytes(6).toString('hex');
const name = 'g287-executor-' + randomBytes(6).toString('hex');
t.after(() => docker('rm', '-f', name));
const started = docker('run', '-d', '--rm', '--name', name, '-p', `127.0.0.1:${port}:3000`, '-v', `${tlsDir}:/certs:ro`,
'-e', 'GITEA__security__INSTALL_LOCK=true',
+155
View File
@@ -1306,3 +1306,158 @@ test('status lists the slot, users, grants and nothing secret, and every grants
await rejects(bare.request(human, { verb: 'grants.status', args: { service: 'gitea' } }), 'gitea-not-configured');
f.noLeak();
});
// C2-21: these catch library forking, wrong collaborator/token level, widened reach, and stale bindings.
function libraryBusiness(b, { agent = 'coder', definition = 'research', publish = ['app'], library = ['app'] } = {}) {
const original = b.roles.coder;
b.roles[agent] = { ...original, definition, publish,
identity: { ...original.identity, gitea: { ...original.identity.gitea, user: agent === 'chief' ? 'demo-chief' : USER } } };
b.definitions[definition] = { roleVersion: 2, credentials: [{ service: 'gitea',
scopes: definition === 'chief-of-staff' ? ['read:repository', 'read:user'] : ['read:user', 'write:repository'] }] };
for (const project of library) b.projects[project].kind = 'library';
}
for (const [definition, agent, level, scopes] of [
['research', 'coder', 'write', ['read:user', 'write:repository']],
['chief-of-staff', 'chief', 'read', ['read:repository', 'read:user']],
['coder', 'coder', 'read', ['read:user', 'write:repository']],
]) {
test(`C2-21 L1/L2/L9: ${definition} gets ${level} on her repository with declared scopes through raise and yes`, async (t) => {
const f = await setup(t, { business: (b) => {
libraryBusiness(b, { agent, definition });
b.launch = { by: 'chief', instances: ['coder', 'pm'] };
} });
const user = agent === 'chief' ? 'demo-chief' : USER;
const puts = [];
f.gitea.rule((e) => e.method === 'PUT' && e.path.includes('/collaborators/'), (e, body) => {
puts.push({ path: e.path, as: e.as, auth: e.auth, body });
return { pass: true };
});
const d = await f.raise({ agent });
const block = f.store.get('SELECT question FROM decisions WHERE id=?', d.id).question;
assert.ok(block.includes(`Repository: ${f.gitea.url}/acme/app, level ${level}.`));
assert.doesNotMatch(block, /\nForks:/);
assert.ok(block.includes(`Scopes: ${scopes.join(', ')}.`));
assert.ok(block.includes(level === 'write'
? "The token can write every path of this repository. Only the bus's path check limits it to supporting folders."
: "The token can read every path of this repository. It can't push to it, because the user is a read collaborator."));
f.call('human', 'decision.resolve', { id: d.id, choice: 'yes' });
await until(() => f.ended(d.id));
await f.broker.giteaIdle();
const end = f.ended(d.id);
assert.equal(end.action, 'grant.applied', JSON.stringify(end));
assert.deepEqual(end.forks, []);
assert.deepEqual(end.repositories, [{ project: 'app', repository: `${f.gitea.url}/acme/app`, level }]);
assert.deepEqual(puts, [{ path: `/api/v1/repos/acme/app/collaborators/${user}`, as: 'mosaic-grant', auth: 'token', body: { permission: level } }]);
assert.equal(f.gitea.calls().filter((c) => c.endsWith('/forks')).length, 0);
assert.deepEqual(f.gitea.tokens(user)[0].scopes, scopes);
const messages = f.messages(d.id).map((m) => m.body).join('\n');
assert.ok(messages.includes(`repositories: ${f.gitea.url}/acme/app (${level})`));
assert.doesNotMatch(messages, /forks: \./);
const results = f.grantEvents();
assert.ok(results.some((e) => e.action === 'grant.call' && e.op === 'collaborator-add'));
f.noLeak();
// L5: token revocation leaves the collaborator, just as legacy forks remain.
await f.call('human', 'grants.revoke', { service: 'gitea', agent, reason: 'C2-21' });
await f.broker.giteaIdle();
assert.deepEqual(f.gitea.tokens(user), []);
assert.ok(f.gitea.repos.find((r) => r.owner === 'acme' && r.name === 'app').collaborators.includes(user));
});
}
for (const [agent, definition] of [['coder', 'research'], ['chief', 'chief-of-staff']]) {
for (const projects of [['lib'], ['app', 'lib']]) {
test(`C2-21 L7/L7b: ${definition} refuses non-library targets ${projects.join(',')}`, async (t) => {
const f = await setup(t, { business: (b) => libraryBusiness(b, { agent, definition, publish: ['app', 'lib'] }) });
await rejects(f.raise({ agent, target: { projects } }), 'gitea-not-declared');
assert.equal(f.store.get('SELECT count(*) AS n FROM decisions').n, 0);
assert.equal(f.gitea.requests.length, 0);
});
}
}
for (const [name, permissions, fork] of [
['read widened to write', { admin: false, push: true, pull: true }, false],
['admin privilege', { admin: true, push: false, pull: true }, false],
['missing pull', { admin: false, push: false, pull: false }, false],
['missing permission object', null, false],
['library fork', { admin: false, push: false, pull: true }, true],
]) {
test(`C2-21 L4: ${name} refuses gitea-reach-wider and deletes the minted token`, async (t) => {
const f = await setup(t, { business: (b) => libraryBusiness(b, { agent: 'chief', definition: 'chief-of-staff' }) });
const repository = fork ? 'demo-chief/app' : 'acme/app';
f.gitea.rule((e) => e.as === 'demo-chief' && e.path.startsWith('/api/v1/user/repos?'), { status: 200, total: 1, json: [{
full_name: repository, owner: { login: fork ? 'demo-chief' : 'acme' },
parent: fork ? { full_name: 'acme/app' } : null, permissions,
}] });
const { end } = await f.approve({ agent: 'chief' });
assert.equal(end.code, 'gitea-reach-wider', JSON.stringify(end));
assert.equal(end.repository, repository);
assert.deepEqual(f.gitea.tokens('demo-chief'), []);
assert.equal(f.store.get("SELECT 1 FROM gitea_grants WHERE agent='chief'"), undefined);
f.noLeak();
});
}
test('C2-21 L8: untargeted library collaborator reach is disclosed and accepted publish-wide', async (t) => {
const f = await setup(t, { business: (b) => libraryBusiness(b, { publish: ['app', 'lib'], library: ['app', 'lib'] }) });
f.gitea.repos.find((r) => r.owner === 'acme' && r.name === 'lib').collaborators.push(USER);
const d = await f.raise();
const block = f.store.get('SELECT question FROM decisions WHERE id=?', d.id).question;
assert.ok(block.includes(`It may also reach these repositories as a collaborator: ${f.gitea.url}/acme/lib (write)`));
assert.ok(block.includes('Step 5 checks that this is only its forks and the repositories above at their level.'));
assert.doesNotMatch(block, /existing forks of:/);
f.call('human', 'decision.resolve', { id: d.id, choice: 'yes' });
await until(() => f.ended(d.id));
await f.broker.giteaIdle();
assert.equal(f.ended(d.id).action, 'grant.applied');
});
test('C2-21 L10: project kind or library level changing after raise refuses decision-mismatch', async (t) => {
for (const change of ['kind', 'level']) {
const f = await setup(t, { business: (b) => {
b.definitions.coder.credentials = [{ service: 'gitea', scopes: ['read:user', 'write:repository'] }];
if (change === 'level') libraryBusiness(b);
} });
const d = await f.raise();
const next = structuredClone(f.business);
if (change === 'kind') next.projects.app.kind = 'library';
else {
next.definitions['chief-of-staff'] = structuredClone(next.definitions.research);
next.roles.coder.definition = 'chief-of-staff';
}
f.broker.reloadBusiness('demo', next);
f.call('human', 'decision.resolve', { id: d.id, choice: 'yes' });
await until(() => f.ended(d.id));
await f.broker.giteaIdle();
assert.equal(f.ended(d.id).code, 'decision-mismatch');
assert.deepEqual(f.gitea.calls(), ['GET /api/v1/version', 'GET /api/v1/user', 'GET /api/v1/token', 'GET /api/v1/user/repos']);
assert.ok(f.gitea.requests.every((r) => r.method === 'GET' && !/\/admin\/users|\/tokens|\/collaborators/.test(r.path)));
assert.equal(existsSync(f.tokenFile), false);
assert.equal(f.record(), undefined);
}
});
test('C2-21 L4: creative write requires push true but ignores unrelated permission keys', async (t) => {
for (const push of [false, true]) {
const f = await setup(t, { business: (b) => libraryBusiness(b) });
f.gitea.rule((e) => e.as === USER && e.path.startsWith('/api/v1/user/repos?'), { status: 200, total: 1, json: [{
full_name: 'acme/app', owner: { login: 'acme' }, parent: null,
permissions: { admin: false, push, pull: true, issues: true },
}] });
const { end } = await f.approve();
assert.equal(end.action, push ? 'grant.applied' : 'grant.failed');
if (!push) assert.equal(end.code, 'gitea-reach-wider');
}
});
// mos-1590: the L10 fixture must not hide the no-declared-scopes refusal.
test('C2-21: a library grant with no declared scopes refuses gitea-not-declared at raise', async (t) => {
const f = await setup(t, { business: (b) => {
libraryBusiness(b);
delete b.definitions.research.credentials[0].scopes;
} });
await rejects(f.raise(), 'gitea-not-declared');
assert.equal(f.store.get('SELECT count(*) AS n FROM decisions').n, 0);
assert.equal(f.gitea.requests.length, 0);
});
+24 -35
View File
@@ -1,8 +1,7 @@
// C2-03b (#1791, design 7.2): the credential the library fetch runs with. The chief's grant store file
// once a chief has Gitea grant evidence, slot library-gitea before that, and a refusal otherwise.
// sage-4100 corrects sage-2947/dewey-0864 for the mint only: before C2-21, the real write-grant
// path must refuse the chief. T7/T9 assert that refusal, then seed a read-only fake grant. C2-21's
// real chief read executor replaces that seed when it lands. Revocation and fire use the real verbs.
// C2-21 replaces sage-4100's read fixture seed: T7/T9 raise the real chief library grant, the human
// resolves yes and the executor mints it. Revocation and fire keep using the same real verbs.
// The choice function's other checks (listed, a fire under way, held, two chiefs, another project)
// use fixtures: the evidence and the gitea_grants row go in by SQL, as gitea-grant.test.mjs does for
// decisions. Tokens are made up or the fake's, and only ever compared.
@@ -75,23 +74,23 @@ function world(t, options) {
// The business for the real paths. `boss` is launch.by: instance.fire is the launch.by role's verb and
// refuses its own name (organization-instance-referenced), so the fired chief is the one listed
// chief-of-staff instance, fired by boss. The grant targets project app; books is the library.
// chief-of-staff instance, fired by boss. The real read grant targets the library project books.
function verbDoc(url, root) {
return {
id: 'acme', human: 'jason', arbiters: { technical: 'boss', delivery: 'boss' },
roles: {
boss: { definition: 'pm', authority: chiefAuthority },
// sage-4097 / mos-1584: fixture projection carries the chief's own Gitea store reference.
chief: { definition: 'chief-of-staff', ...none, credentials: { gitea: giteaReference(join(root, 'secrets', 'agents'), 'acme', 'chief', '2099-01-01') }, identity: { name: 'Chief', email: '[email protected]', gitea: { url, user: 'acme-chief' } }, publish: ['app'] },
chief: { definition: 'chief-of-staff', ...none, credentials: { gitea: giteaReference(join(root, 'secrets', 'agents'), 'acme', 'chief', '2099-01-01') }, identity: { name: 'Chief', email: '[email protected]', gitea: { url, user: 'acme-chief' } }, publish: ['books'] },
coder: { definition: 'coder', ...none },
},
definitions: {
'chief-of-staff': { roleVersion: 2, credentials: [{ service: 'gitea' }] },
'chief-of-staff': { roleVersion: 2, credentials: [{ service: 'gitea', scopes: ['read:repository', 'read:user'] }] },
pm: { roleVersion: 2, credentials: [] },
coder: { roleVersion: 2, credentials: [] },
},
// sage-4054 / mos-1272: runtime validates every code project's absolute checkout root.
projects: { app: { root: join(root, 'app-checkout'), remote: `${url}/acme/app` }, books: { ...mappingEntry(), user: 'books-bot' } },
projects: { app: { root: join(root, 'app-checkout'), remote: `${url}/acme/app` }, books: { ...mappingEntry(), remote: `${url}/acme/books`, user: 'books-bot' } },
launch: { by: 'boss', instances: ['chief', 'coder'] },
};
}
@@ -108,6 +107,7 @@ async function verbWorld(t, opts = {}) {
gitea = await fakeGiteaApi(t, join(dir, 'tls'));
gitea.addUser('acme');
gitea.addRepo('acme', 'app');
gitea.addRepo('acme', 'books');
}
const adminDir = join(dir, 'admin');
mkdirSync(adminDir, { mode: 0o700 });
@@ -150,33 +150,22 @@ async function verbWorld(t, opts = {}) {
const human = broker.bindHuman({ business: 'acme', human: 'jason', via: 'cli', outsideAgent: true });
const events = () => store.all(`SELECT body FROM events WHERE business='acme' AND ${GRANT_EVIDENCE} ORDER BY seq`).map((e) => JSON.parse(e.body));
const row = () => store.get("SELECT 1 FROM gitea_grants WHERE business='acme' AND agent='chief'");
const seedChiefReadGrant = async () => {
// sage-4100 / C2-21: the real chief read-grant executor replaces this fixture seed.
// The current executor mints write scopes and must keep refusing the chief (sage-2921).
await assert.rejects(broker.request(chief, {
const mintChiefReadGrant = async () => {
const decision = await broker.request(chief, {
verb: 'decision.raise',
args: {
action: 'credential.mint', question: 'Grant chief Gitea?', options, recommendation: 'yes', blocking: false,
grant: { method: 'gitea', agent: 'chief', target: { projects: ['app'] }, rotateBy: dayFrom(30) },
action: 'credential.mint', question: 'Grant chief library read?', options, recommendation: 'yes', blocking: false,
grant: { method: 'gitea', agent: 'chief', target: { projects: ['books'] }, rotateBy: dayFrom(30) },
},
}), { code: 'gitea-not-declared' });
const rotateBy = dayFrom(30), user = 'acme-chief', nonce = randomBytes(8).toString('hex');
const decision = grantDecision(store, { business: 'acme', grant: { method: 'gitea', agent: 'chief', target: { projects: ['app'] }, rotateBy } });
const { user: account } = gitea.addUser(user, { full_name: `Chief [mosaic ${nonce}]` });
const token = gitea.addToken(user, 'mosaic-chief-read-' + decision, ['read:repository', 'read:user']);
assert.deepEqual(token.scopes, ['read:repository', 'read:user'], 'fixture grants read scopes only (sage-4100)');
const keys = giteaKeys({ credentials, storeRoot });
keys.register('acme', 'chief', rotateBy);
// No exported grant-record writer exists; use the executor's credential-file writer and direct
// store inserts for the fixture-only records (sage-4100), with the fake's actual user/token IDs.
keys.issue('acme', 'chief', token.sha1, rotateBy, () => store.transaction(() => {
store.run('INSERT INTO gitea_users(business,agent,user,id,nonce,decision) VALUES(?,?,?,?,?,?)', 'acme', 'chief', user, account.id, nonce, decision);
store.run('INSERT INTO gitea_grants(business,agent,user,token_id,token_name,projects,rotate_by,decision) VALUES(?,?,?,?,?,?,?,?)',
'acme', 'chief', user, token.id, token.name, JSON.stringify(['app']), rotateBy, decision);
writeGrantEvent(store, AT, 'acme', { action: 'grant.applied', method: 'gitea', agent: 'chief', grantDecision: decision, user,
userId: account.id, tokenId: token.id, permission: 'read' }, 'chief');
}));
assert.ok(row(), 'with its gitea_grants row');
});
broker.request(human, { verb: 'decision.resolve', args: { id: decision.id, choice: 'yes' } });
await until(() => events().some((e) => e.grantDecision === decision.id && ['grant.applied', 'grant.failed'].includes(e.action)));
await broker.giteaIdle();
const end = events().find((e) => e.grantDecision === decision.id && ['grant.applied', 'grant.failed'].includes(e.action));
assert.equal(end.action, 'grant.applied', JSON.stringify(end));
assert.deepEqual(end.repositories, [{ project: 'books', repository: `${gitea.url}/acme/books`, level: 'read' }]);
assert.deepEqual(gitea.tokens('acme-chief')[0].scopes, ['read:repository', 'read:user']);
assert.ok(row(), 'real grant wrote its gitea_grants row');
};
const revoke = async () => {
await broker.request(human, { verb: 'grants.revoke', args: { service: 'gitea', agent: 'chief', reason: 'test' } });
@@ -195,7 +184,7 @@ async function verbWorld(t, opts = {}) {
broker.request(chief, { verb: 'role.release' });
broker.request(boss, { verb: 'role.release' });
};
return { gitea, doc, store, broker, storeRoot, row, seedChiefReadGrant, revoke, fire, close, release, choose: () => broker.libraryFetchCredential('acme') };
return { gitea, doc, store, broker, storeRoot, row, mintChiefReadGrant, revoke, fire, close, release, choose: () => broker.libraryFetchCredential('acme') };
}
const REFUSED = { refused: 'library-fetch-no-grant', fix: LIBRARY_FETCH_FIX };
@@ -216,13 +205,13 @@ test('C2-03b T6: the slot before any chief evidence, then the listed chief\'s li
test('C2-03b T7: a chief revoked through grants.revoke or fired through instance.fire refuses, and the fallback stays ended', { timeout: 60000 }, async t => {
const revoked = await verbWorld(t);
assert.deepEqual(revoked.choose(), { kind: 'slot' });
await revoked.seedChiefReadGrant();
await revoked.mintChiefReadGrant();
assert.deepEqual(revoked.choose(), { kind: 'chief', agent: 'chief', user: 'acme-chief' });
await revoked.revoke();
assert.deepEqual(revoked.choose(), REFUSED, 'revoked');
const fired = await verbWorld(t);
await fired.seedChiefReadGrant();
await fired.mintChiefReadGrant();
assert.deepEqual(fired.choose(), { kind: 'chief', agent: 'chief', user: 'acme-chief' });
await fired.fire();
assert.equal(fired.doc.launch.instances.includes('chief'), false, 'delisted');
@@ -283,7 +272,7 @@ test('C2-03b T9 runtime: the chief\'s store file and user, then a real revoke th
mkdirSync(join(root, 'secrets', 'acme'), { recursive: true, mode: 0o700 });
writeFileSync(slot, SLOT_TOKEN + '\n', { mode: 0o600 });
const granted = await verbWorld(t, { dataRoot, storeRoot });
await granted.seedChiefReadGrant();
await granted.mintChiefReadGrant();
granted.release();
granted.close();
const gitea = granted.gitea;
+3 -1
View File
@@ -292,7 +292,9 @@ export function validateBusinessDocument(document, file, { rolesDir }) {
for (const [instance, role] of Object.entries(roles)) {
for (const project of role.publish ?? []) {
if (!Object.hasOwn(projects, project)) refuse(`${file} roles.${instance}.publish names ${project}, which the business file doesn't declare`);
if (projects[project].kind === 'library') refuse(`${file} roles.${instance}.publish names library project ${project}; library repositories cannot be published by agents`);
// C2-21, sage-2916: retain the fence; only creative instances and the chief may hold library reach.
if (projects[project].kind === 'library' && !CREATIVE_DEFINITIONS.includes(role.definition) && role.definition !== 'chief-of-staff')
refuse(`${file} roles.${instance}.publish names library project ${project}; library publish requires a creative definition or chief-of-staff with gitea`);
if (!projects[project].remote) refuse(`${file} roles.${instance}.publish names ${project}, which has no "remote"`);
const problem = forkProblem(projects[project].remote, role.identity);
if (problem) refuse(`${file} roles.${instance}.publish names ${project}: ${problem}`);
+25
View File
@@ -360,3 +360,28 @@ test("row 194 each bad keep, noHire or retired entry refuses", () => {
refuses(() => check((d) => { d.retired = ["../x"]; }), /retired entry/);
refuses(() => check((d) => { d.retired = {}; }), /retired must be an array/);
});
// C2-21 B1-B4: catch a deleted/overbroad library publish fence while retaining identity/credential rules.
test('C2-21: only creative definitions and the chief with gitea may publish a library project', async (t) => {
const { mappingEntry } = await import('../../library/tests/helpers.mjs');
for (const definition of [...CREATIVE_DEFINITIONS, 'chief-of-staff']) {
const mutate = (d) => {
d.projects.books = { ...mappingEntry(), remote: 'https://git.example/writer/books' };
d.roles.chief.credentials = CREATIVE_GITEA;
const agent = definition === 'chief-of-staff' ? 'chief' : 'bean';
d.roles[agent] = { ...(d.roles[agent] ?? {}), definition, credentials: CREATIVE_GITEA,
identity: { name: 'Library reader', email: '[email protected]', gitea: { url: 'https://git.example', user: 'acme-reader' } }, publish: ['books'] };
};
const business = check(mutate);
assert.deepEqual(business.roles[definition === 'chief-of-staff' ? 'chief' : 'bean'].publish, ['books']);
refuses(() => check((d) => { mutate(d); delete d.roles[definition === 'chief-of-staff' ? 'chief' : 'bean'].identity; }), /publish needs identity and a gitea definition/);
}
for (const agent of ['coder', 'reviewer', 'pm', 'cto']) {
refuses(() => check((d) => {
d.projects.books = { ...mappingEntry(), remote: 'https://git.example/writer/books' };
d.roles.chief.credentials = CREATIVE_GITEA;
d.roles[agent].identity = { name: 'Other', email: '[email protected]', gitea: { url: 'https://git.example', user: 'acme-other' } };
d.roles[agent].publish = ['books'];
}), /publish.*library.*creative.*chief/);
}
});
+13
View File
@@ -477,3 +477,16 @@ test("row 322 F9: importer refusal keeps the error line rather than the Node ban
assert.equal(existsSync(x.business), false);
assert.equal(existsSync(x.personaDir), false);
});
test('C2-21: library install supplies chief identity and books publish for its real read grant', (t) => {
const x = world(t);
const out = install(x);
assert.equal(out.code, 0, out.out);
const doc = JSON.parse(readFileSync(x.business, 'utf8'));
assert.deepEqual(doc.roles.chief.publish, ['books']);
assert.ok(doc.roles.chief.identity?.name);
assert.ok(doc.roles.chief.identity?.email);
assert.equal(doc.roles.chief.identity?.gitea?.url, new URL(doc.projects.books.remote).origin);
assert.ok(doc.roles.chief.identity?.gitea?.user);
assert.notEqual(doc.roles.chief.identity?.gitea?.user, 'writer');
});
+9 -2
View File
@@ -13,8 +13,8 @@
# activate, starts no unit, writes no unit file and nothing under /etc, and
# never writes the library token, which is Jason's step 4.
#
# FILE is the business document written for the host from the go. The
# script adds only roles.bean.vars and roles.chet.vars persona.name and
# FILE is the business document written for the host from the go. For a library,
# the script adds chief identity/publish and roles.bean.vars and roles.chet.vars persona.name and
# persona.sha256, from the staging manifest, and checks the input against
# the precheck's profile rules first: library needs a library project,
# bus.tester off and bean and chet as companion roles with no persona var;
@@ -109,6 +109,13 @@ if (profile === "factory") {
} else {
if (!library.length) stop("no-library-project", `${input} has no library project`);
if (tester) stop("bus-tester-on", "the library profile needs bus.tester off");
// C2-21: the chief mints its own read grant after install; no token or grant is created here.
const chief = Object.values(doc.roles ?? {}).find((r) => r?.definition === "chief-of-staff");
if (!chief) stop("chief-missing", "the library profile needs a chief-of-staff instance");
const project = projects.books?.kind === "library" ? "books" : library[0];
chief.identity ??= { name: "Chief of staff", email: `${id}[email protected]`,
gitea: { url: new URL(projects[project].remote).origin, user: `${id}-chief` } };
chief.publish = [project];
for (const instance of ["bean", "chet"]) {
const role = doc.roles?.[instance];
if (role?.definition !== "companion") stop("companion-roles", `roles.${instance} must have definition companion`);