Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1321f8f90 | ||
|
|
e855f82e18 | ||
|
|
f06a1f78cf | ||
|
|
ad84c1e45c |
No files matched your search
@@ -5411,3 +5411,10 @@ Row 299 (573, #1806, Gander, Filbert): before, the P10 band test timed a fault r
|
||||
2026-10-23T17:29:46Z | Gyro Vikunja571 sage-3542 Connections amendment | Branch gyro/editor-connections-design cut exactly from landed PRD1.17 30e6bb680; read REQ-LIB-5 in that PRD. Replaced stale J-b waits in parent and W4. Proposed W8 owns current authorized Markdown/DOCX links/backlinks list, labels, versioned refresh, leakage refusals and fixture tests. Build order under mos-1470/comment961 is W7 closed routing candidate, W8 landing, then W7 human first-real-import opening. No REQ-LIB-8 condition or acceptance change. Q234/mos-1363/S0 M1-M14 and post-10-27/MVP priority fixed. Static diff, twelve brief sections and append-only checks pass. No repo Node, tests, build, browser, queue mutation or live action. Fenton reviews after authorized docs branch push; Sage lands on approval.
|
||||
|
||||
2026-10-23T17:32:37Z | Gyro sage-3544 fenton-0016 N1/N2 delta | Before: W7 Gate/final real-conversion list omit the W8 build-order cross-reference and W8 human-view filtering cites agent settings. After: both locations explicitly put W8 landing before human opening as mos-1470 build order, not acceptance; comment/tracked-deletion links are not current story links and never appear in the human Connections view regardless of agent settings. New commit atop approved 5af060fb5, no amend. Static diff and append-only checks pass. Docs only, no repo Node/tests/live action. Fenton checks only this delta.
|
||||
|
||||
## 2026-10-23 — Lena, row 296 / #1802 FED R7 start
|
||||
|
||||
Before: R4 and R6 landed in base 45dca69f6; no scratch two-instance acceptance script.
|
||||
Plan: preserve Quackerjack handoff c6ab9bdc9, add isolated script/CLI test harness with real
|
||||
Console proof in transient user services, map assertions and two mutants for Filbert.
|
||||
Untested: all Node/tests and shared gate wait for Sage CLEAR; no production or live state changes.
|
||||
@@ -0,0 +1,266 @@
|
||||
# Row 296: FED R7, scratch two-instance acceptance (#1802)
|
||||
|
||||
## Start
|
||||
|
||||
- Started 2026-10-23 18:53:27Z, on Sage's sage-3583 (mos-1489).
|
||||
- Base: origin/refactor 45dca69f6 (pinned; local copy used, no fetch: fetch runs the credential node script, which
|
||||
is held during Darkwing's re-pin window).
|
||||
- Branch: quackerjack/row296 in worktree quackerjack-fed-r5-row296.
|
||||
- Brief: docs/plans/2026-10-23_mvp-readiness-rows.md at 45dca69f6, section "Brief: FED R7, scratch two-instance
|
||||
acceptance" (line 77).
|
||||
- Constraints from Sage: scratch roots only (mktemp -d under /mnt/storage/scratch/tmp), never ~/.mosaic-dev or
|
||||
the live bus. No push, claim, queue write, node or test run until CLEAR.
|
||||
|
||||
## What the brief asks for (summary)
|
||||
|
||||
- One script test under scripts/ with its harness in packages/*/tests/, as row 190's tests are.
|
||||
- Two configs, two data roots, two Consoles, one fake OP. Consoles start outside agent ancestry in a transient
|
||||
user unit. They never use the live stack's units, ports or sockets. The script removes its units on exit (trap).
|
||||
- Test (design section 7 R7 gate): Jason's test identity, granted collaborator on one hub project, signs in,
|
||||
makes a thread, sends to the granted instance, gets the reply in the thread, is refused on another project and
|
||||
on board reads, is revoked, is refused again, and the run ends. The factory shows nothing of the hub.
|
||||
- Mutants: grant project list widened to `*`; revocation claim end skipped.
|
||||
- Gate: Filbert approves the manifest; the script test and every scripts/test-*.sh green on Sage's gate; no unit,
|
||||
port or socket of the script outlives the run. R8 (live window) stays row 209.
|
||||
|
||||
## Open reads before design
|
||||
|
||||
- R4 and R6 candidates (the brief says it builds on their combined candidate). Confirm what they expose: the
|
||||
hub sign-in, grant and revocation paths, and the Console's board-read refusal.
|
||||
- Row 190's test harness (several instances on one uid): how it starts units and cleans up.
|
||||
- Transient user unit support on this host (systemd-run --user), and how the existing tests detect it.
|
||||
|
||||
## Status
|
||||
|
||||
- 18:53Z: worktree created, brief read. No code yet.
|
||||
|
||||
## Handoff 2026-10-23 19:10:09Z (Mos ruling Q245, mos-1503)
|
||||
|
||||
Row 296 moves to Lena (Codex seat, thread db27faca-85fb-4ca0-9fc7-78d740d3fa3a, branch lena/row296). Quackerjack
|
||||
stops here: no builds, no claim, no queue writes. This design is the handoff; Lena continues it.
|
||||
|
||||
Base: 45dca69f6. R4 landed as 2efd127ce (ancestor of the base, checked with merge-base --is-ancestor). R6 landed as
|
||||
d3059c2c3 (row 294). R7 is claimable once Darkwing's window ends (Sage, sage-3592).
|
||||
|
||||
What I verified by reading (not by running):
|
||||
- packages/cli/src/access.mjs:65 reads `system.federation?.peers ?? []` (the R6 seam).
|
||||
- packages/bus/src/user-cli.mjs exists at base (the user-cli transport).
|
||||
- Row 190's tests (packages/cli/tests/stack-instances.test.mjs, stack-release.test.mjs T56, stack-release-fixture.mjs)
|
||||
drive stand-in units and fixtures. They do not start real transient user units. Not a model for R7's Consoles.
|
||||
- Real transient-unit launch pattern: packages/conversation/src/cohort.mjs (`scopeAvailable()` and `ScopeLauncher`,
|
||||
systemd-run --user --scope -p Delegate=yes --unit=...; MANAGER_ENV = XDG_RUNTIME_DIR, DBUS_SESSION_BUS_ADDRESS).
|
||||
- Test-only spawn of systemd-run in packages/conversation/tests is the nearest precedent for start/stop cleanup.
|
||||
|
||||
What I did NOT verify (so treat as open): the hub sign-in, grant, revocation and board-read paths in R4/R6 code; the
|
||||
Console startup flags; whether the fake OP can be a local stand-in; the trap-based unit cleanup.
|
||||
|
||||
Design (proposal, not yet checked against code):
|
||||
1. scripts/test-federation-acceptance.sh: the entry point. Creates a mktemp -d scratch root under
|
||||
/mnt/storage/scratch/tmp (never ~/.mosaic-dev, never the live bus). trap removes every unit it started and the
|
||||
scratch root. Refuses to run if a transient unit name collides.
|
||||
2. Two configs (instance A = hub-side granted seat, instance B = collaborator's home), each with its own
|
||||
MOSAIC_CONFIG, data root and federation.self; federation.peers names the other.
|
||||
3. One fake OP: a local stand-in that issues Jason's test identity. Its port and socket are per-run, not the live ones.
|
||||
4. Two Consoles, each in its own transient user unit (systemd-run --user --unit=mosaic-r7-<run>-<A|B>); outside agent
|
||||
ancestry; ports picked per run.
|
||||
5. Test flow (the R7 gate): identity -> grant on one hub project -> sign in -> thread -> send to granted instance ->
|
||||
reply in thread -> refused on another project -> refused on board reads -> revoke -> refused after revoke -> run
|
||||
ends. Factory output must show nothing of the hub.
|
||||
6. Mutants: (a) grant project list widened to `*`, must fail the other-project refusal; (b) revocation claim end
|
||||
skipped, must fail the refusal-after-revoke check.
|
||||
|
||||
Unresolved decisions for Lena (I did not decide these):
|
||||
- Whether the harness lives in packages/cli/tests (matching row 190) or packages/conversation/tests (matching
|
||||
cohort.mjs). Brief says packages/*/tests/; pick the one that owns the Console start path.
|
||||
- Fake OP: in-process HTTP stand-in versus file-backed identity. No preference recorded.
|
||||
- Port selection: fixed range per run vs OS-assigned. Brief forbids the live ports; it does not pick a range.
|
||||
|
||||
R2 and R5 learning:
|
||||
- R2 (row 203, #1701) landed as a4da4e6df. I built it. Relevant to R7: validateFederation in
|
||||
scripts/mosaic-config.mjs rejects unknown keys and bad ids, and test-config.sh has an FED block
|
||||
(expect_fed_refusal). Use federation.self/peers exactly as row 203 validates them.
|
||||
Mutant runner pattern: one textual edit that must occur exactly once, then a named test must fail; restore from git.
|
||||
Name filter gotcha: `node --test --test-name-pattern` skips subtests whose parent does not match; include the
|
||||
parent name (T57 for row 203).
|
||||
- R5: I did not build R5 in this session and have no first-hand notes on it. Ask Sage for R5's landing sha and notes.
|
||||
|
||||
Status: design written; no code, no test runs, no claim, no push of the test files. Next owner: Lena.
|
||||
|
||||
## Lena continuation, 2026-10-23
|
||||
|
||||
Owner: Lena, thread db27faca-85fb-4ca0-9fc7-78d740d3fa3a. Branch lena/row296.
|
||||
Base remains 45dca69f6. Quackerjack's handoff c6ab9bdc9 preserved by cherry-pick ad84c1e45.
|
||||
Issue #1802 exists. Reviewer Filbert. Sage owns queue state, shared test slot and landing.
|
||||
Status: Untested. All Node, tests and credential-helper fetch/push held for Sage CLEAR (sage-3615).
|
||||
The earlier canonical `queue next lena` read finished with `nothing` and a rendered-view warning;
|
||||
no queue state was changed. Lena's owner registration is Sage's action.
|
||||
|
||||
### Design decisions sent to Sage (lena-0003)
|
||||
|
||||
- Harness under packages/cli/tests, beside row 190's instance proof; shell entry under scripts/.
|
||||
- Existing HTTPS fakeOidc helper, its generated scratch CA trusted through NODE_EXTRA_CA_CERTS
|
||||
by each real Console CLI. One fake OP, two client ids; no TLS verification bypass.
|
||||
- Kernel-assigned loopback ports (0), discovered from each Console's startup log. Public origins
|
||||
are distinct *.example.test HTTPS names; HTTP requests model the loopback reverse proxy with
|
||||
exact Host and Origin. This proves the Console boundary, not a deployed HTTPS front end (R8).
|
||||
- Transient user service units, not scopes: a scope leaves ancestry beneath the caller; the real
|
||||
user proof walks ancestry. Each Console runs the existing cli.mjs serve --sso --send verb.
|
||||
- Real startHost broker process and createLauncher. Only the harness adapter is a test stand-in,
|
||||
as in thread-revoke.test.mjs. A real sandboxed run replies through the broker.
|
||||
- Scratch-only setup and access grant/revoke use real human CLI transport from a transient service.
|
||||
No injected principal verifier, direct bindHuman/bindUser, or simulated user transport.
|
||||
- Every service has a unique per-run prefix and is recorded before launch. Shell EXIT/INT/TERM
|
||||
traps stop only these services, wait for collection and verify socket and port closure.
|
||||
- Claim-end assertion reads append-only claim history independently of the launcher's final
|
||||
shutdown so the claim-end mutant cannot be hidden by endLaunch's cleanup.
|
||||
|
||||
### Implementation plan
|
||||
|
||||
Goal: an executable scratch proof of federation section 7's R7 gate, without live state.
|
||||
Architecture: shell lifecycle owner, Node test driver, test-only host fixture, existing real Console CLI.
|
||||
Tech: Bash, Node ESM/node:test, systemd user transient services, existing fake OP and bwrap adapter.
|
||||
Spec: docs/plans/2026-10-23_mvp-readiness-rows.md, FED R7, plus preserved handoff above.
|
||||
|
||||
Constraints: scratch only under /mnt/storage/scratch/tmp; no live units/ports/sockets/configs or
|
||||
credential reads; no production behavior/policy changes; no old test edits without Mos via Sage.
|
||||
|
||||
Review focus: partial startup cleanup; signals; unit name collision; mismatched hub/factory sessions;
|
||||
claim end versus process exit. Each must have explicit assertions or a negative cleanup exercise.
|
||||
|
||||
- [ ] Write acceptance driver assertions: Jason test sub binds as collaborator (owner is another
|
||||
fixture identity), one allowed project/instance, thread send and correlated RESULT, private
|
||||
project creation/read refused, board reads refused, factory has no hub objects, revoke denies
|
||||
session and re-login, claim history records access-revoked and runner ends.
|
||||
- [ ] Write shell lifecycle: scratch HOME/TMPDIR, unique unit ledger, bounded command timeout,
|
||||
traps stop/collect only this run's units, verify recorded sockets and ports no longer listen.
|
||||
- [ ] Write host fixture using startHost(bootConfig), createLauncher with fakeHarnesses and the
|
||||
existing thread adapter. Prepare two named configs through bootstrap before fixture edits;
|
||||
supply only generated test client slots. Setup/grant/revoke through real human transport.
|
||||
- [ ] On Sage CLEAR run only the new script, fix failures in new files and record exact results.
|
||||
- [ ] Prepare explicit once-only mutant patches: R5 projects widened to ['*']; R4 #claimEnd call
|
||||
skipped. On Sage CLEAR prove each gives an assertion failure, restore bytes in finally.
|
||||
- [ ] Publish manifest (paths, hashes, assertion mapping, old-test numstat), request Filbert review,
|
||||
push additive commits to lena/row296 only. Sage runs shared suites and gate and lands the row.
|
||||
|
||||
Pre-existing test changes: none (numstat: empty).
|
||||
|
||||
### Source inspection corrections before first execution
|
||||
|
||||
- The real webui chat is channel-gated. Scratch businesses enable only harness pi and channel webui;
|
||||
the loader fixture re-exports the unchanged webui channel. The adapter replacement is exactly
|
||||
fakeHarnesses' pi.adapter export; no product file gains a fixture branch.
|
||||
- startHost creates a real broker child and a real Unix proof socket; it is hosted in the test
|
||||
driver. The two Console services run the exact same-tree webui CLI, which is required by
|
||||
verifyPrincipal. Human setup runs the exact same-tree human-cli through humanTransport.
|
||||
- The earlier in-process host proposal did not need a separate host fixture: startHost exposes
|
||||
the production launcher API directly, so the only helper file is federation-human.mjs.
|
||||
- Read-only SQLite inspects claim evidence while the broker remains the sole writer. The test
|
||||
also checks the registered runner and engine pids/start times are gone after revocation.
|
||||
- `--kill-after-start` kills the test process after both services have listened; Node teardown
|
||||
hooks cannot run. The shell trap owns service cleanup independently. This is an expected-red
|
||||
exercise; acceptance requires its kill marker, nonzero exit and successful cleanup line.
|
||||
- Main driver environment is a whitelist (PATH, scratch HOME/TMPDIR, systemd manager transport,
|
||||
R7 run ids). Console/human services use env -i. No host preload or credential env is carried.
|
||||
- No Node or test has run. Bash syntax and git diff --check passed during preparation.
|
||||
|
||||
### Binding design correction: sage-3625, 2026-10-23 19:17:35Z
|
||||
|
||||
Sage rejected the transient-unit approach: "no transient systemd user units used to satisfy the
|
||||
human proof"; such a unit makes the ancestry walk miss the development agent, and creates host
|
||||
state under mos-1438. This direction replaces the brief/handoff's unit requirement for this build.
|
||||
No unit was created by the superseded draft, and no Node/test execution occurred.
|
||||
|
||||
Current design, replacing the earlier proposals above:
|
||||
|
||||
- Consoles are ordinary spawned child processes in their own process groups; no systemd commands,
|
||||
units, user manager or manager environment are involved.
|
||||
- Test-only federation-host.mjs follows channel-fixture.mjs's mock.module pattern. It re-exports
|
||||
real readProcess and replaces verifyHuman's readProcess argument with real process fields plus
|
||||
ppid=1. This is explicitly a stand-in for Jason's shell, NOT a real ancestry acceptance proof.
|
||||
Real pid/uid/startTime/nonce/path checks still run; verifyPrincipal still checks the Console's
|
||||
actual direct parent path for user calls. No product source receives this override.
|
||||
- Each fixture host runs real startBroker and createLauncher. Its local host API is the existing
|
||||
thread-revoke.test.mjs pattern, with only the pi adapter replaced via fakeHarnesses.
|
||||
- Setup/grant/revoke are plain children using real CLI wrappers against that fixture proof seam.
|
||||
- Shell records each detached group's leader pid and start time, checks both before signalling,
|
||||
and verifies no live group member, TCP listener or Unix listener remains. Killed-driver mode
|
||||
bypasses every Node teardown hook and still leaves cleanup to the shell trap.
|
||||
- Read-only claim evidence and actual runner/engine exit remain independent assertions.
|
||||
- Gate limitation: this proves the revised scratch integration, not real outside-agent ancestry,
|
||||
transient unit cleanup, TLS proxy deployment or live federation. R8 remains row 209.
|
||||
- R5 landing found in git log: ddfb29af7 (#1700).
|
||||
- Targeted Node/tests remain held. Sage permits the first lena/row296 push in the 19:41Z gap,
|
||||
with MOSAIC_GITEA_CREDENTIAL_FILE unset. 226 and 283 have earlier slots.
|
||||
|
||||
Current files: shell entry; federation-acceptance.test.mjs; fixtures/federation-host.mjs and
|
||||
fixtures/federation-human.mjs; this BUILD; mutant runner; manifest; append-only logs; preserved
|
||||
Quackerjack BUILD at ad84c1e45. No pre-existing test modification (numstat empty).
|
||||
|
||||
### First candidate, 2026-10-23
|
||||
|
||||
Implementation is prepared under sage-3625, not yet executed. Static checks: Bash parser accepts
|
||||
scripts/test-federation-acceptance.sh; Python ast.parse accepts mutants.py; git diff --check clean.
|
||||
No test outcome is claimed. The root node_modules links the existing canonical install so the
|
||||
future targeted run needs no install or network. No webui editor build is involved.
|
||||
|
||||
M1 now requires the user project-list leak assertion (`R7 ungranted project must not appear`),
|
||||
so it proves exposure through the actual Console transport rather than only inspecting grant
|
||||
configuration. The private-project thread-create refusal and exact grant-list check follow it.
|
||||
M2 requires `R7 revoke must end the claim as access-revoked` before any host teardown.
|
||||
|
||||
Sage opened the push gap early in sage-3644 (19:23:04Z). The first candidate is committed as
|
||||
Untested, for push only to lena/row296 with MOSAIC_GITEA_CREDENTIAL_FILE unset. Node, acceptance,
|
||||
negative cleanup exercise and mutants still wait for the test slot. Full suites/gate are Sage's.
|
||||
|
||||
### Filbert static review, filbert-296-first-push-static, 19:28:51Z
|
||||
|
||||
Review pinned f06a1f78c against 45dca69f6. Filbert verified all nine manifest hashes from Git
|
||||
objects, no product/pre-existing test changes, no systemd calls, fixture-only ancestry override,
|
||||
real path/uid/startTime/nonce checks, group ledger identity checks and child-only CA trust.
|
||||
He ran nothing. The verdict remains pending execution and gate evidence.
|
||||
|
||||
- C1 confirmed by inspection: subprocess.run(timeout=280) kills the shell on timeout and can
|
||||
bypass its trap, while the shell's own deadline plus cleanup can exceed 280 seconds. Removed
|
||||
the outer Python timeout (the simpler alternative Filbert explicitly allowed). Python now
|
||||
waits for the shell's bounded run and EXIT/TERM cleanup. Source restoration remains in finally.
|
||||
- N1 retained limitation: a SIGKILL between spawn and the pid/startTime ledger append can leave
|
||||
an unrecorded group. The killed-driver exercise begins after both Consoles are recorded and
|
||||
does not cover that gap. Cleanup claims apply to recorded groups; no universal crash-proof
|
||||
process creation claim is made.
|
||||
- N2 addressed before the mutant run: the named readonly claim-history assertion now runs after
|
||||
revoke and before waiting for the run's end. A skipped #claimEnd therefore cannot first fail
|
||||
the run-end wait. The final latest-op check still runs after engine/runner exit. If M2 instead
|
||||
fails an earlier condition, its runner must say NOT KILLED; diagnose the first failure on the
|
||||
same pinned candidate, retain the intended assertion, and rerun on Sage CLEAR. Never accept a
|
||||
timeout or change the mutant's expected assertion merely to mark it killed.
|
||||
- N3 deferred cosmetic fixture detail: wrap() returns fn(value) without await, so async rejection
|
||||
skips its code mapping. This is not an authorization or cleanup change; the rejection still
|
||||
propagates, and no verdict relies on that fixture mapping.
|
||||
- N4: corrected the exact-grant assertion's indentation in the new test.
|
||||
- N5: prepare() first calls bootstrap --instance, then overwrites that bootstrap-generated
|
||||
config inside the unique disposable scratch HOME with fixture Console/provider settings.
|
||||
This is fixture-only setup, not a product/bootstrap overwrite path or a live-config operation.
|
||||
There remains one selected config per scratch instance, with the same marked data root.
|
||||
|
||||
Still Untested. No Node, acceptance, mutant, negative cleanup or CLI suite run. Static validation
|
||||
only; execution waits for Sage's test window. Follow-up push needs a currently authorized gap.
|
||||
|
||||
### Push receipt and N2 ordering proof, 2026-10-23 19:31Z
|
||||
|
||||
Sage sage-3659 authorized push before 19:38Z. e855f82e1846f094927c3de69aea1a801253736a
|
||||
was pushed to origin/lena/row296 with MOSAIC_GITEA_CREDENTIAL_FILE unset. No Node/tests ran.
|
||||
|
||||
N2, filbert-296-c1-ack: this path is synchronous after a successful reload. accessCommand awaits
|
||||
reload at access.mjs:289; reload awaits the access.reload transport at :297. broker.mjs:4645
|
||||
calls #revokeLost before constructing its reply. #revokeLost (:1025–1034) calls #claimEnd inside
|
||||
Store.transaction, which executes COMMIT before returning (store.mjs:282–302). server.mjs:160–161
|
||||
calls broker.request and then writes the response. Therefore the claim record is committed before
|
||||
the successful CLI call returns; no asynchronous sweep is required for this assertion. A refused
|
||||
reload can be reported by the CLI with exit 0, but then immediate revocation evidence is absent
|
||||
and this test must fail; that is not a race the test should hide. No polling change is needed.
|
||||
|
||||
T3 package comparison (sage-3659): the four planned runs execute only the new dedicated
|
||||
federation-acceptance.test.mjs and its fixture host/human children. They do not execute stack-copy,
|
||||
stack-release or the whole CLI suite. Existing helpers are imported; no old test module is run.
|
||||
Asked Researcher to confirm against row216. Full CLI suite remains a later Sage gate requirement,
|
||||
outside the targeted T3 command list. All test resources are unique scratch state.
|
||||
@@ -0,0 +1,67 @@
|
||||
# Row 296 / #1802 — C1 follow-up candidate (Untested)
|
||||
|
||||
Base: 45dca69f6. Branch: lena/row296. Reviewer: Filbert. Gate owner: Sage.
|
||||
Handoff: c6ab9bdc9 preserved as ad84c1e45, including
|
||||
agents/quackerjack/work/queue-296/BUILD.md. Every changed path is in MANIFEST.sha256,
|
||||
except this manifest and the checksum file itself. No product file or pre-existing test changed.
|
||||
Pre-existing test numstat: empty. BUILD-LOG.md and docs/SESSIONS.md are append-only additions.
|
||||
|
||||
Binding ruling: sage-3625 replaces the brief's transient-unit start with plain child Consoles and
|
||||
explicit test stand-ins. **This candidate does not prove genuine outside-agent ancestry.**
|
||||
It creates no transient systemd unit and makes no systemd CLI call. R8 remains row 209.
|
||||
|
||||
## What is real, and what is a stand-in
|
||||
|
||||
- Two configs created by bootstrap --instance hub/factory, two marked data roots, real Console
|
||||
cli.mjs serve --sso --send, real broker runtime and launcher, real path-bound human-cli and
|
||||
user-cli wrappers, real access loader, CLI writes/reload, grant filters and revocation.
|
||||
- One existing HTTPS fakeOidc server, two issuer/client identities. Its generated CA is trusted
|
||||
only through NODE_EXTRA_CA_CERTS in this test's child environment, never any trust store.
|
||||
- federation-host.mjs replaces verifyHuman's readProcess argument with actual process fields plus
|
||||
ppid=1, exactly the test pattern Sage named. This stands in for Jason's shell on setup,
|
||||
grant/revoke and both Consoles' transports. The actual Console parent/path check remains.
|
||||
There is no product parameter, environment switch or altered verifier default.
|
||||
- fakeHarnesses replaces only the pi harness adapter() with a scratch shell invoking the existing
|
||||
thread-adapter.mjs. The existing webui channel is re-exported unchanged. No network model runs.
|
||||
- Public HTTPS origins are modelled through HTTP loopback requests with matching Host/Origin;
|
||||
reverse-proxy TLS deployment is outside this scratch proof.
|
||||
|
||||
## Acceptance map
|
||||
|
||||
All checks live in `FED R7 scratch two-instance acceptance` in
|
||||
packages/cli/tests/federation-acceptance.test.mjs.
|
||||
|
||||
| Requirement | Assertion/evidence |
|
||||
| --- | --- |
|
||||
| Isolated instances | different config paths/data roots/ports; scratch HOME/TMPDIR; two named markers from bootstrap; port 0, forbidden live ports 7330–7332 checked |
|
||||
| Jason is collaborator, not owner | owner melanie; access user jason-test, home factory, fake sub jason-test-sub; hub /api/config user=true |
|
||||
| One project, one granted instance | actual access grant CLI, projects=[shared], instances=[assistant]; user project list contains shared only |
|
||||
| Thread and reply | create thread on shared, POST message, correlated RESULT with in_reply_to=sent.id, from_role=assistant, to_role=user, body contains question |
|
||||
| Other project refused | private project absent from listing; create on private returns 403 access-refused |
|
||||
| Board reads refused | /api/board, /api/bus/inbox and /api/bus/tasks return 403 owner-only |
|
||||
| Factory sees no hub objects | Jason sub refused at factory; factory owner sees factory-only project and no threads; hub cookie gets 401 at factory |
|
||||
| Revoke takes effect | actual access revoke CLI; existing hub session 401 and re-login 403 |
|
||||
| Claim and process ended | live run before revoke; end log exitCode=0; no registered sessions; engine and runner pid/startTime gone; readonly claim history has revoke/access-revoked before the process-end wait, latest op=revoke afterward |
|
||||
| Normal cleanup | EXIT trap terminates only recorded pid/startTime-checked groups; checks no live group member, no recorded TCP/Unix listener, no other scratch Unix listener, removes scratch directory/socket files |
|
||||
| Killed-driver cleanup | --kill-after-start prints both-Consoles-started marker then SIGKILLs test driver; shell performs the same independent cleanup; expected nonzero run with successful cleanup line |
|
||||
|
||||
## Verification commands (held for Sage CLEAR)
|
||||
|
||||
- scripts/test-federation-acceptance.sh — must exit 0, one acceptance test passes, cleanup line present.
|
||||
- scripts/test-federation-acceptance.sh --kill-after-start — must exit nonzero, print the explicit
|
||||
interruption marker and `R7 cleanup: no child groups, ports or sockets remain; no units created`.
|
||||
- python3 agents/lena/work/queue-296/mutants.py M1 — once-only R5 grant projects -> ['*']; must fail
|
||||
`R7 ungranted project must not appear` and verify cleanup, then restore original source bytes. Python relies on the shell's own run deadline and cleanup; no outer killing timeout.
|
||||
- python3 agents/lena/work/queue-296/mutants.py M2 — once-only R4 claim end omitted; must fail
|
||||
`R7 revoke must end the claim as access-revoked` before shutdown, verify cleanup and restore bytes.
|
||||
- Sage's gate: the new script plus every scripts/test-*.sh; full applicable suites per Sage's slot.
|
||||
|
||||
Status: no Node, acceptance or mutant run. Static Bash syntax, Python parsing and diff checks only.
|
||||
The script never silently skips a missing tool/sandbox/provider: prerequisites fail the acceptance.
|
||||
Ordinary CLI suites skip the dedicated test until invoked by its owning script.
|
||||
|
||||
Static-review follow-up: filbert-296-first-push-static. C1 removes Python's outer timeout so it
|
||||
cannot SIGKILL the shell before EXIT cleanup. N2 moves the named claim-history assertion before
|
||||
the process-end wait. N1's spawn-to-ledger SIGKILL gap is retained and documented in BUILD.md;
|
||||
--kill-after-start exercises the fully recorded groups. N3 remains a cosmetic fixture limitation.
|
||||
N4 indentation corrected. N5 scratch-bootstrap config overwrite is explicitly fixture-only.
|
||||
@@ -0,0 +1,9 @@
|
||||
6e0193ba91cbd20edd3acfbc57ae9af2f016e6d2c3587b434a9085b3a02c493c BUILD-LOG.md
|
||||
72029331a22f33433ad14ff08fe8b24d59f79e784009b12fd9c5b74c9e524f13 docs/SESSIONS.md
|
||||
87239327014c49abbe130c17881bb48e13d96545bf9f3e09b6f4aed46e11453b agents/quackerjack/work/queue-296/BUILD.md
|
||||
6f32d7e5e0c2c1ee086949a68b176a9d1fefb8ad0df562f5ce72b0c01d21e7a0 agents/lena/work/queue-296/BUILD.md
|
||||
bd6e9fd9e3f7aa0ee6702401776ef7bd3e39562059a11b87aaaf2a427904381b agents/lena/work/queue-296/mutants.py
|
||||
89a15e62aa6f951596b764993afeff7e33eeaa3a4f45d31599eba7d27a4000ad packages/cli/tests/federation-acceptance.test.mjs
|
||||
86b76565f5afe9efdd076ef61cc54e40c447d235f77b95818d0d601ce036effa packages/cli/tests/fixtures/federation-host.mjs
|
||||
b12a9bad4be083f2439c3363738b9763fa0061228f84d501a385ade48fea90e4 packages/cli/tests/fixtures/federation-human.mjs
|
||||
4008361f062a2f21b6075c500060196e10a48351fd7d95b25d681f36b4fa801a scripts/test-federation-acceptance.sh
|
||||
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env python3
|
||||
"""R7 mutants. Run only on Sage CLEAR in Lena's isolated candidate.
|
||||
|
||||
Every replacement must occur once. Product bytes are restored even on timeout
|
||||
or failure. A killed mutant must contain the named assertion in the script's
|
||||
output; startup errors and timeout alone are not evidence. The shell owns its
|
||||
own timeout and EXIT/TERM cleanup. Do not put a killing Python timeout around it.
|
||||
"""
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
repo = Path(__file__).resolve().parents[4]
|
||||
mutants = {
|
||||
'M1': (
|
||||
'packages/cli/src/access.mjs',
|
||||
'projects: resolveProjects(list(parsed.values["--projects"]), rows),',
|
||||
'projects: ["*"],',
|
||||
'R7 ungranted project must not appear',
|
||||
),
|
||||
'M2': (
|
||||
'packages/bus/src/broker.mjs',
|
||||
"this.#claimEnd({ business: t.business }, h, 'revoke', null, 'access-revoked', by);",
|
||||
"/* R7 mutant: skip ending the revoked claim */",
|
||||
'R7 revoke must end the claim as access-revoked',
|
||||
),
|
||||
}
|
||||
if len(sys.argv) != 2 or sys.argv[1] not in mutants:
|
||||
raise SystemExit('usage: mutants.py M1|M2')
|
||||
name = sys.argv[1]
|
||||
file, before, after, assertion = mutants[name]
|
||||
path = repo / file
|
||||
original = path.read_bytes()
|
||||
text = original.decode()
|
||||
if text.count(before) != 1:
|
||||
raise SystemExit(f'{name}: anchor count {text.count(before)}, expected 1')
|
||||
try:
|
||||
path.write_text(text.replace(before, after))
|
||||
result = subprocess.run(['scripts/test-federation-acceptance.sh'], cwd=repo,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
text=True)
|
||||
print(result.stdout, end='')
|
||||
if result.returncode == 0 or assertion not in result.stdout:
|
||||
raise SystemExit(f'{name}: NOT KILLED by expected assertion')
|
||||
if 'R7 cleanup: no child groups, ports or sockets remain; no units created' not in result.stdout:
|
||||
raise SystemExit(f'{name}: cleanup not verified')
|
||||
print(f'{name}: KILLED by {assertion}')
|
||||
finally:
|
||||
path.write_bytes(original)
|
||||
if path.read_bytes() != original:
|
||||
raise SystemExit(f'{name}: restore failed')
|
||||
@@ -0,0 +1,87 @@
|
||||
# Row 296: FED R7, scratch two-instance acceptance (#1802)
|
||||
|
||||
## Start
|
||||
|
||||
- Started 2026-10-23 18:53:27Z, on Sage's sage-3583 (mos-1489).
|
||||
- Base: origin/refactor 45dca69f6 (pinned; local copy used, no fetch: fetch runs the credential node script, which
|
||||
is held during Darkwing's re-pin window).
|
||||
- Branch: quackerjack/row296 in worktree quackerjack-fed-r5-row296.
|
||||
- Brief: docs/plans/2026-10-23_mvp-readiness-rows.md at 45dca69f6, section "Brief: FED R7, scratch two-instance
|
||||
acceptance" (line 77).
|
||||
- Constraints from Sage: scratch roots only (mktemp -d under /mnt/storage/scratch/tmp), never ~/.mosaic-dev or
|
||||
the live bus. No push, claim, queue write, node or test run until CLEAR.
|
||||
|
||||
## What the brief asks for (summary)
|
||||
|
||||
- One script test under scripts/ with its harness in packages/*/tests/, as row 190's tests are.
|
||||
- Two configs, two data roots, two Consoles, one fake OP. Consoles start outside agent ancestry in a transient
|
||||
user unit. They never use the live stack's units, ports or sockets. The script removes its units on exit (trap).
|
||||
- Test (design section 7 R7 gate): Jason's test identity, granted collaborator on one hub project, signs in,
|
||||
makes a thread, sends to the granted instance, gets the reply in the thread, is refused on another project and
|
||||
on board reads, is revoked, is refused again, and the run ends. The factory shows nothing of the hub.
|
||||
- Mutants: grant project list widened to `*`; revocation claim end skipped.
|
||||
- Gate: Filbert approves the manifest; the script test and every scripts/test-*.sh green on Sage's gate; no unit,
|
||||
port or socket of the script outlives the run. R8 (live window) stays row 209.
|
||||
|
||||
## Open reads before design
|
||||
|
||||
- R4 and R6 candidates (the brief says it builds on their combined candidate). Confirm what they expose: the
|
||||
hub sign-in, grant and revocation paths, and the Console's board-read refusal.
|
||||
- Row 190's test harness (several instances on one uid): how it starts units and cleans up.
|
||||
- Transient user unit support on this host (systemd-run --user), and how the existing tests detect it.
|
||||
|
||||
## Status
|
||||
|
||||
- 18:53Z: worktree created, brief read. No code yet.
|
||||
|
||||
## Handoff 2026-10-23 19:10:09Z (Mos ruling Q245, mos-1503)
|
||||
|
||||
Row 296 moves to Lena (Codex seat, thread db27faca-85fb-4ca0-9fc7-78d740d3fa3a, branch lena/row296). Quackerjack
|
||||
stops here: no builds, no claim, no queue writes. This design is the handoff; Lena continues it.
|
||||
|
||||
Base: 45dca69f6. R4 landed as 2efd127ce (ancestor of the base, checked with merge-base --is-ancestor). R6 landed as
|
||||
d3059c2c3 (row 294). R7 is claimable once Darkwing's window ends (Sage, sage-3592).
|
||||
|
||||
What I verified by reading (not by running):
|
||||
- packages/cli/src/access.mjs:65 reads `system.federation?.peers ?? []` (the R6 seam).
|
||||
- packages/bus/src/user-cli.mjs exists at base (the user-cli transport).
|
||||
- Row 190's tests (packages/cli/tests/stack-instances.test.mjs, stack-release.test.mjs T56, stack-release-fixture.mjs)
|
||||
drive stand-in units and fixtures. They do not start real transient user units. Not a model for R7's Consoles.
|
||||
- Real transient-unit launch pattern: packages/conversation/src/cohort.mjs (`scopeAvailable()` and `ScopeLauncher`,
|
||||
systemd-run --user --scope -p Delegate=yes --unit=...; MANAGER_ENV = XDG_RUNTIME_DIR, DBUS_SESSION_BUS_ADDRESS).
|
||||
- Test-only spawn of systemd-run in packages/conversation/tests is the nearest precedent for start/stop cleanup.
|
||||
|
||||
What I did NOT verify (so treat as open): the hub sign-in, grant, revocation and board-read paths in R4/R6 code; the
|
||||
Console startup flags; whether the fake OP can be a local stand-in; the trap-based unit cleanup.
|
||||
|
||||
Design (proposal, not yet checked against code):
|
||||
1. scripts/test-federation-acceptance.sh: the entry point. Creates a mktemp -d scratch root under
|
||||
/mnt/storage/scratch/tmp (never ~/.mosaic-dev, never the live bus). trap removes every unit it started and the
|
||||
scratch root. Refuses to run if a transient unit name collides.
|
||||
2. Two configs (instance A = hub-side granted seat, instance B = collaborator's home), each with its own
|
||||
MOSAIC_CONFIG, data root and federation.self; federation.peers names the other.
|
||||
3. One fake OP: a local stand-in that issues Jason's test identity. Its port and socket are per-run, not the live ones.
|
||||
4. Two Consoles, each in its own transient user unit (systemd-run --user --unit=mosaic-r7-<run>-<A|B>); outside agent
|
||||
ancestry; ports picked per run.
|
||||
5. Test flow (the R7 gate): identity -> grant on one hub project -> sign in -> thread -> send to granted instance ->
|
||||
reply in thread -> refused on another project -> refused on board reads -> revoke -> refused after revoke -> run
|
||||
ends. Factory output must show nothing of the hub.
|
||||
6. Mutants: (a) grant project list widened to `*`, must fail the other-project refusal; (b) revocation claim end
|
||||
skipped, must fail the refusal-after-revoke check.
|
||||
|
||||
Unresolved decisions for Lena (I did not decide these):
|
||||
- Whether the harness lives in packages/cli/tests (matching row 190) or packages/conversation/tests (matching
|
||||
cohort.mjs). Brief says packages/*/tests/; pick the one that owns the Console start path.
|
||||
- Fake OP: in-process HTTP stand-in versus file-backed identity. No preference recorded.
|
||||
- Port selection: fixed range per run vs OS-assigned. Brief forbids the live ports; it does not pick a range.
|
||||
|
||||
R2 and R5 learning:
|
||||
- R2 (row 203, #1701) landed as a4da4e6df. I built it. Relevant to R7: validateFederation in
|
||||
scripts/mosaic-config.mjs rejects unknown keys and bad ids, and test-config.sh has an FED block
|
||||
(expect_fed_refusal). Use federation.self/peers exactly as row 203 validates them.
|
||||
Mutant runner pattern: one textual edit that must occur exactly once, then a named test must fail; restore from git.
|
||||
Name filter gotcha: `node --test --test-name-pattern` skips subtests whose parent does not match; include the
|
||||
parent name (T57 for row 203).
|
||||
- R5: I did not build R5 in this session and have no first-hand notes on it. Ask Sage for R5's landing sha and notes.
|
||||
|
||||
Status: design written; no code, no test runs, no claim, no push of the test files. Next owner: Lena.
|
||||
@@ -1593,3 +1593,4 @@ are never rewritten or removed; corrections are new entries.
|
||||
2026-10-23T17:48:43Z | Scrooge (629b6cd3) | Row 252 Console inbox Approve and Deny (#1753) | Rebased onto 306 landing as 580037105 (range-diff all =), slot run bus 907/0 webui 410/0 cli 943/0, in-review revs 3608/3609, Webby r1 APPROVE, J1-J19 specs to Sage; Sage landed 45ea3fa0d (done rev 3616). Part 1 mutant results were never in BUILD.md; they went to Sage by T3 (scrooge-0252-mutant-specs).
|
||||
2026-10-23 | Sage (1ef1e4f8) | Rows 308 and 252 landings, 571 Connections design amendment, Vikunja 597 | 308 (PRD 1.17, #1815) landed 30e6bb680 and 252 (#1753) landed 45ea3fa0d (replay of 580037105 onto 30e6bb680, range-diff all =), each after a full gate on the landed tree (252 also J1-J19, J11c and J18 equivalent); Gyro's 571 amendment (Fenton APPROVE) landed e8bb13df8; Vikunja 570, 435 and 597 done, canonical webui esbuild 0.28.2; queue revs 3589-3616 committed 59f013247
|
||||
2026-10-23 | Sage (1ef1e4f8) | Vikunja 571 W build rows; rows 13, 42, 257 and 289 check | Filed the design's ten W rows as queue rows 309-318 (revs 3617-3626, owners set to unassigned 3627-3636, queue commit 293b42fee pushed); Vikunja 571 done (comment 994). Row 13's candidate fd72d2689 is on refactor, gate is Monday's row 7 ledger run; 42 paused under mos-1184; 257 post-v1; 289 waits on 214 and 288. Slot released 17:56Z.
|
||||
- 2026-10-23 — lena (Codex, db27faca): row 296 / #1802 FED R7; recovered Quackerjack design on lena/row296, inspecting acceptance paths; Node/tests held pending Sage CLEAR, no live changes.
|
||||
@@ -0,0 +1,234 @@
|
||||
// FED R7 / #1802. Run through scripts/test-federation-acceptance.sh: it owns
|
||||
// the child process groups and scratch lifetime. Ordinary CLI suites skip it.
|
||||
// sage-3625: federation-host.mjs supplies a labelled readProcess ancestry
|
||||
// stand-in for setup, grant/revoke and both Console transports. This is NOT
|
||||
// a genuine outside-agent human proof. Product transports/grant checks stay real.
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { spawn, spawnSync } from 'node:child_process';
|
||||
import { closeSync, cpSync, existsSync, mkdirSync, openSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { request } from 'node:http';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { businessDoc, launchDoc, writeJson } from '../../business/tests/helpers.mjs';
|
||||
import { REPO_SKILLS } from '../../harness/tests/helpers.mjs';
|
||||
import { DatabaseSync } from 'node:sqlite';
|
||||
import { SESSION_COOKIE } from '../../webui/src/sso.mjs';
|
||||
import { fakeOidc } from '../../webui/tests/helpers/fake-oidc.mjs';
|
||||
import { socketPath, REPO } from '../src/config.mjs';
|
||||
import { startTimeOf } from '../src/host.mjs';
|
||||
import { launchLogFile, readSessions } from '../../seat/src/session.mjs';
|
||||
|
||||
const root = process.env.MOSAIC_R7_ROOT;
|
||||
const helper = fileURLToPath(new URL('./fixtures/federation-human.mjs', import.meta.url));
|
||||
const adapterModule = fileURLToPath(new URL('./fixtures/thread-adapter.mjs', import.meta.url));
|
||||
const webCli = join(REPO, 'packages/webui/src/cli.mjs');
|
||||
const cli = join(REPO, 'packages/cli/src/cli.mjs');
|
||||
const pause = ms => new Promise(r => setTimeout(r, ms));
|
||||
const json = path => JSON.parse(readFileSync(path, 'utf8'));
|
||||
|
||||
function run(cmd, args, options = {}) {
|
||||
const r = spawnSync(cmd, args, { encoding: 'utf8', timeout: 30000, ...options });
|
||||
assert.equal(r.status, 0, `${cmd} ${args.join(' ')}: ${r.stderr ?? ''} ${r.error?.message ?? ''}`);
|
||||
return r.stdout;
|
||||
}
|
||||
async function until(read, label, ms = 20000) {
|
||||
const end = Date.now() + ms;
|
||||
while (Date.now() < end) {
|
||||
const value = await read();
|
||||
if (value) return value;
|
||||
await pause(50);
|
||||
}
|
||||
assert.fail(`R7 assertion: ${label}`);
|
||||
}
|
||||
|
||||
function http(instance, path, { cookie, body, method = body === undefined ? 'GET' : 'POST' } = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const text = body === undefined ? undefined : JSON.stringify(body);
|
||||
const headers = { host: new URL(instance.origin).host };
|
||||
if (cookie) headers.cookie = cookie;
|
||||
if (text !== undefined) Object.assign(headers, { origin: instance.origin, 'sec-fetch-site': 'same-origin', 'content-type': 'application/json', 'content-length': Buffer.byteLength(text) });
|
||||
const req = request({ host: '127.0.0.1', port: instance.port, path, method, headers, timeout: 5000 }, res => {
|
||||
let data = '';
|
||||
res.on('data', c => data += c);
|
||||
res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body: data && String(res.headers['content-type']).startsWith('application/json') ? JSON.parse(data) : data }));
|
||||
});
|
||||
req.on('error', reject);
|
||||
req.on('timeout', () => req.destroy(new Error('R7 HTTP timeout')));
|
||||
req.end(text);
|
||||
});
|
||||
}
|
||||
let serial = 0;
|
||||
function child(instance, label, command) {
|
||||
const log = join(root, `${instance.name}-${label}.log`);
|
||||
const fd = openSync(log, 'a', 0o600);
|
||||
let proc;
|
||||
try { proc = spawn(command[0], command.slice(1), { detached: true, env: instance.env, stdio: ['ignore', fd, fd] }); }
|
||||
finally { closeSync(fd); }
|
||||
proc.on('error', e => { console.error(`R7 child ${label}: ${e.message}`); });
|
||||
assert.ok(proc.pid, `R7 ${label} spawned`);
|
||||
const start = startTimeOf(proc.pid);
|
||||
assert.ok(start, `R7 ${label} start time recorded`);
|
||||
// EXIT trap owns the detached group even if Node's test driver is killed.
|
||||
writeFileSync(join(root, 'groups'), `${proc.pid} ${start}\n`, { flag: 'a' });
|
||||
proc.unref();
|
||||
return { log, proc };
|
||||
}
|
||||
// Jason-shell ancestry stand-in is in the fixture host; transport/path are real.
|
||||
function human(instance, verb, args = {}) {
|
||||
const out = join(root, `human-${++serial}.json`);
|
||||
run(process.execPath, [helper, socketPath(instance.dataRoot), instance.name, verb, JSON.stringify(args), out], { env: instance.env });
|
||||
return json(out);
|
||||
}
|
||||
// R5/R4 CLI with that same labelled fixture proof, never a live human shell.
|
||||
function access(instance, args) {
|
||||
return run(process.execPath, [cli, 'access', ...args, '--business', instance.name], { env: instance.env });
|
||||
}
|
||||
|
||||
function prepare(name, iss, ca) {
|
||||
const home = process.env.HOME;
|
||||
const env = { HOME: home, PATH: process.env.PATH, TMPDIR: process.env.TMPDIR };
|
||||
run(join(REPO, 'scripts/bootstrap.sh'), ['--instance', name], { env });
|
||||
const config = join(home, '.config', `mosaic-${name}`, 'config.json');
|
||||
const system = json(config), dataRoot = system.dataRoot, configDir = dirname(config);
|
||||
const fixtureRoot = join(root, name);
|
||||
mkdirSync(fixtureRoot, { recursive: true });
|
||||
const doc = businessDoc(fixtureRoot, name);
|
||||
doc.human = 'melanie';
|
||||
doc.plugins = { harness: ['pi'], channel: ['webui'] };
|
||||
doc.roles.chief.vars = { harness: 'pi', model: 'claude-opus-5-5', thinking: 'high' };
|
||||
doc.roles.assistant = { definition: 'assistant', vars: { harness: 'pi', model: 'claude-sonnet-5-5', thinking: 'high' } };
|
||||
doc.launch = launchDoc('chief', ['coder', 'assistant'], { opus: 1, sonnet: 2 });
|
||||
doc.launch.threadIdleSeconds = 3600;
|
||||
for (const role of Object.values(doc.roles)) role.vars = { harness: 'pi', model: 'glm-5.3-flash', thinking: 'high', ...role.vars };
|
||||
writeJson(join(configDir, 'businesses', `${name}.json`), doc);
|
||||
const origin = `https://${name}.example.test`;
|
||||
system.console = { sso: { publicOrigin: origin, providers: [{ id: 'test-op', issuer: iss.issuer, clientId: iss.clientId,
|
||||
clientSecret: { slot: name }, algs: ['RS256'], subjects: [{ sub: 'owner-sub', name: 'melanie' }] }] } };
|
||||
system.federation = { self: name, peers: [{ id: name === 'hub' ? 'factory' : 'hub', origin: `https://${name === 'hub' ? 'factory' : 'hub'}.example.test`, label: name === 'hub' ? 'Factory' : 'Hub' }] };
|
||||
writeJson(config, system);
|
||||
writeJson(join(configDir, 'access.json'), { accessVersion: 1, revision: 1, users: [], grants: [] });
|
||||
// Generated fake OP material only; never copies a host slot or credential.
|
||||
writeJson(join(configDir, 'secrets/mosaic-stack', name), iss.clientSecret);
|
||||
cpSync(REPO_SKILLS, join(dataRoot, 'skills-enabled'), { recursive: true });
|
||||
const projectRoot = doc.projects.stack.root;
|
||||
const gitEnv = { ...env, GIT_CONFIG_NOSYSTEM: '1', GIT_CONFIG_GLOBAL: '/dev/null' };
|
||||
const git = (...args) => run('git', ['-c', 'user.name=R7 fixture', '-c', '[email protected]', '-C', projectRoot, ...args], { env: gitEnv });
|
||||
git('init', '-q', '-b', 'main');
|
||||
writeFileSync(join(projectRoot, 'file.txt'), `${name} fixture\n`);
|
||||
git('add', 'file.txt'); git('commit', '-q', '-m', 'scratch fixture');
|
||||
const adapter = join(fixtureRoot, 'adapter.sh');
|
||||
writeFileSync(adapter, `exec '${process.execPath}' '${adapterModule}'\n`);
|
||||
return { name, origin, configDir, dataRoot, adapter, env: { ...env, MOSAIC_CONFIG: config, NODE_EXTRA_CA_CERTS: ca }, port: null };
|
||||
}
|
||||
|
||||
async function signIn(instance, op, iss, sub) {
|
||||
const start = await http(instance, '/auth/login');
|
||||
assert.equal(start.status, 303, JSON.stringify(start.body));
|
||||
const location = new URL(start.headers.location);
|
||||
const { code } = op.grant(iss, location.href, { sub });
|
||||
const cookie = start.headers['set-cookie'][0].split(';')[0];
|
||||
const result = await http(instance, `/auth/callback?${new URLSearchParams({ code, state: location.searchParams.get('state') })}`, { cookie });
|
||||
const session = (result.headers['set-cookie'] ?? []).find(c => c.startsWith(`${SESSION_COOKIE}=`) && !c.includes('Max-Age=0'))?.split(';')[0];
|
||||
return { ...result, cookie: session };
|
||||
}
|
||||
const logs = i => existsSync(launchLogFile(i.dataRoot, i.name)) ? readFileSync(launchLogFile(i.dataRoot, i.name), 'utf8').trim().split('\n').filter(Boolean).map(JSON.parse) : [];
|
||||
|
||||
function claimHistory(instance, run) {
|
||||
const store = new DatabaseSync(join(instance.dataRoot, 'bus/bus.sqlite'), { readOnly: true });
|
||||
try { return store.prepare('SELECT op, reason FROM thread_claims WHERE holder_run=? ORDER BY seq').all(run); }
|
||||
finally { store.close(); }
|
||||
}
|
||||
|
||||
test('FED R7 scratch two-instance acceptance', { skip: !root && 'run scripts/test-federation-acceptance.sh', timeout: 180000 }, async t => {
|
||||
const op = await fakeOidc();
|
||||
t.after(() => op.close());
|
||||
const hubIssuer = op.issuer('hub'), factoryIssuer = op.issuer('factory');
|
||||
assert.notEqual(hubIssuer.clientId, factoryIssuer.clientId);
|
||||
const ca = join(root, 'fake-op-ca.pem'); writeFileSync(ca, op.cert);
|
||||
assert.ok(![7330, 7331, 7332].includes(Number(new URL(op.base).port)));
|
||||
writeFileSync(join(root, 'ports'), `${new URL(op.base).port}\n`, { flag: 'a' });
|
||||
const hub = prepare('hub', hubIssuer, ca), factory = prepare('factory', factoryIssuer, ca);
|
||||
assert.notEqual(hub.dataRoot, factory.dataRoot);
|
||||
assert.notEqual(hub.env.MOSAIC_CONFIG, factory.env.MOSAIC_CONFIG);
|
||||
for (const i of [hub, factory]) {
|
||||
const ready = join(root, `${i.name}-host.ready`);
|
||||
const host = child(i, 'host', [process.execPath, '--experimental-test-module-mocks',
|
||||
fileURLToPath(new URL('./fixtures/federation-host.mjs', import.meta.url)), i.name, i.adapter, ready]);
|
||||
await until(() => existsSync(ready), `${i.name} fixture host ready; ${host.log}`);
|
||||
writeFileSync(join(root, 'sockets'), `${socketPath(i.dataRoot)}\n`, { flag: 'a' });
|
||||
}
|
||||
const shared = human(hub, 'project.create', { name: 'hub-shared', repo: 'stack' }).id;
|
||||
const privateProject = human(hub, 'project.create', { name: 'hub-private' }).id;
|
||||
human(factory, 'project.create', { name: 'factory-only' });
|
||||
access(hub, ['user', 'add', 'jason-test', '--home', 'factory', '--subject', 'test-op:jason-test-sub', '--revision', '1']);
|
||||
access(hub, ['grant', '--user', 'jason-test', '--projects', shared, '--instances', 'assistant', '--level', 'collaborator', '--revision', '2']);
|
||||
for (const i of [hub, factory]) {
|
||||
const { log } = child(i, 'console', [process.execPath, webCli, 'serve', '--port', '0', '--business', i.name, '--board', 'http://127.0.0.1:1', '--sso', '--send']);
|
||||
i.port = Number(await until(() => existsSync(log) && readFileSync(log, 'utf8').match(/Mosaic Console: http:\/\/127\.0\.0\.1:(\d+)\//)?.[1], `${i.name} Console started`));
|
||||
assert.ok(![7330, 7331, 7332].includes(i.port));
|
||||
writeFileSync(join(root, 'ports'), `${i.port}\n`, { flag: 'a' });
|
||||
}
|
||||
assert.notEqual(hub.port, factory.port);
|
||||
if (process.env.MOSAIC_R7_INTERRUPT === '1') {
|
||||
console.log('R7 interruption: both Consoles started; killing test driver');
|
||||
process.kill(process.pid, 'SIGKILL');
|
||||
await pause(60000);
|
||||
}
|
||||
const signed = await signIn(hub, op, hubIssuer, 'jason-test-sub');
|
||||
assert.equal(signed.status, 303, JSON.stringify(signed.body)); assert.ok(signed.cookie);
|
||||
const cookie = signed.cookie;
|
||||
const config = await http(hub, '/api/config', { cookie });
|
||||
assert.equal(config.status, 200); assert.equal(config.body.user, true);
|
||||
const projects = await http(hub, '/api/chat/projects', { cookie });
|
||||
assert.equal(projects.status, 200, JSON.stringify(projects.body));
|
||||
assert.deepEqual(projects.body.rows.map(p => p.id), [shared], 'R7 ungranted project must not appear');
|
||||
const created = await http(hub, '/api/chat/threads', { cookie, body: { project: shared, instance: 'assistant', name: 'Jason scratch thread' } });
|
||||
assert.equal(created.status, 200, JSON.stringify(created.body));
|
||||
const thread = created.body.id;
|
||||
const sent = await http(hub, `/api/chat/threads/${thread}/messages`, { cookie, body: { body: 'R7 scratch question' } });
|
||||
assert.equal(sent.status, 200, JSON.stringify(sent.body));
|
||||
const reply = await until(async () => {
|
||||
const r = await http(hub, `/api/chat/threads/${thread}/messages`, { cookie });
|
||||
assert.equal(r.status, 200, JSON.stringify(r.body));
|
||||
return r.body.rows.find(m => m.class === 'RESULT' && m.in_reply_to === sent.body.id);
|
||||
}, 'granted instance replies in Jason thread');
|
||||
assert.equal(reply.from_role, 'assistant'); assert.equal(reply.to_role, 'user');
|
||||
assert.match(reply.body, /R7 scratch question/);
|
||||
const refused = await http(hub, '/api/chat/threads', { cookie, body: { project: privateProject, instance: 'assistant', name: 'must refuse' } });
|
||||
assert.equal(refused.status, 403); assert.equal(refused.body.error, 'access-refused');
|
||||
assert.deepEqual(json(join(hub.configDir, 'access.json')).grants[0].projects, [shared], 'R7 grant must name exactly one project');
|
||||
for (const path of ['/api/board', '/api/bus/inbox', '/api/bus/tasks']) {
|
||||
const r = await http(hub, path, { cookie });
|
||||
assert.equal(r.status, 403, path); assert.equal(r.body.error, 'owner-only', path);
|
||||
}
|
||||
const factoryLogin = await signIn(factory, op, factoryIssuer, 'jason-test-sub');
|
||||
assert.equal(factoryLogin.status, 403); assert.equal(factoryLogin.cookie, undefined);
|
||||
const factoryOwner = await signIn(factory, op, factoryIssuer, 'owner-sub');
|
||||
assert.equal(factoryOwner.status, 303); assert.ok(factoryOwner.cookie);
|
||||
const factoryProjects = await http(factory, '/api/chat/projects', { cookie: factoryOwner.cookie });
|
||||
assert.equal(factoryProjects.status, 200);
|
||||
assert.deepEqual(factoryProjects.body.rows.map(p => p.name), ['factory-only']);
|
||||
const factoryThreads = await http(factory, '/api/chat/threads', { cookie: factoryOwner.cookie });
|
||||
assert.equal(factoryThreads.status, 200); assert.deepEqual(factoryThreads.body.rows, []);
|
||||
const hubCookieAtFactory = await http(factory, '/api/chat/projects', { cookie });
|
||||
assert.equal(hubCookieAtFactory.status, 401);
|
||||
const [launch] = logs(hub).filter(e => e.event === 'launch' && e.thread === thread);
|
||||
assert.ok(launch, 'R7 sandboxed run launched');
|
||||
const liveRun = readSessions(hub.dataRoot).find(s => s.run === launch.run);
|
||||
assert.ok(liveRun, 'R7 run remains live before revoke');
|
||||
access(hub, ['revoke', '--user', 'jason-test', '--revision', '3']);
|
||||
const after = await http(hub, '/api/chat/projects', { cookie });
|
||||
assert.equal(after.status, 401); assert.equal(after.body.error, 'sign-in required');
|
||||
assert.equal((await signIn(hub, op, hubIssuer, 'jason-test-sub')).status, 403);
|
||||
// Pin the claim before waiting for process exit: a missing broker revocation
|
||||
// must fail this assertion, even if it also prevents the runner from ending.
|
||||
assert.ok(claimHistory(hub, launch.run).some(r => r.op === 'revoke' && r.reason === 'access-revoked'),
|
||||
'R7 revoke must end the claim as access-revoked');
|
||||
const ended = await until(() => logs(hub).find(e => e.event === 'end' && e.run === launch.run), 'revoked run ended');
|
||||
assert.equal(ended.exitCode, 0, JSON.stringify(ended));
|
||||
assert.deepEqual(readSessions(hub.dataRoot), []);
|
||||
await until(() => startTimeOf(liveRun.pid) !== liveRun.startTime && startTimeOf(liveRun.runnerPid) !== liveRun.runnerStartTime, 'revoked engine and runner processes gone');
|
||||
assert.equal(claimHistory(hub, launch.run).at(-1).op, 'revoke', 'R7 shutdown must not replace missing revocation with release');
|
||||
assert.deepEqual(human(factory, 'thread.list'), []);
|
||||
});
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
// FED R7 fixture, sage-3625: the ancestry walk is a stand-in for Jason's
|
||||
// shell. Keep this override in a test-only process, never in product code.
|
||||
// As channel-fixture.mjs, real pid/uid/argv/startTime/nonce checks remain;
|
||||
// verifyHuman sees ppid=1 instead of walking the development agent's tree.
|
||||
import { mock } from 'node:test';
|
||||
const humanUrl = new URL('../../../bus/src/human.mjs', import.meta.url).href;
|
||||
const real = await import(humanUrl);
|
||||
mock.module(humanUrl, { namedExports: {
|
||||
readProcess: real.readProcess,
|
||||
verifyHuman: (proof, options) => real.verifyHuman(proof, {
|
||||
...options, readProcess: pid => ({ ...real.readProcess(pid), ppid: 1 }),
|
||||
}),
|
||||
} });
|
||||
const { startBroker } = await import('../../../bus/src/runtime.mjs');
|
||||
const { BusError } = await import('../../../bus/src/broker.mjs');
|
||||
const { loadSystem, bootConfig } = await import('../../src/config.mjs');
|
||||
const { createLauncher } = await import('../../src/launcher.mjs');
|
||||
const { fakeHarnesses } = await import('../helpers.mjs');
|
||||
const { writeFileSync } = await import('node:fs');
|
||||
const [business, adapter, ready] = process.argv.slice(2);
|
||||
const system = loadSystem();
|
||||
const runtime = await startBroker(bootConfig({ system, businessId: business }));
|
||||
const hooks = [], cleanups = [];
|
||||
const ops = {
|
||||
identity: m => runtime.identity(m.cap),
|
||||
authorizeLaunch: m => runtime.authorizeLaunch(m.cap, m.instance),
|
||||
refuse: m => runtime.refuse(m.cap, m.code, m.subject),
|
||||
endLaunch: m => runtime.endLaunch(m.record),
|
||||
credentialStatus: m => runtime.credentialStatus(m.business, m.role),
|
||||
threadsState: m => runtime.threadsState(m.business),
|
||||
threadIdle: m => runtime.threadIdle(m.record),
|
||||
mayLaunch: m => runtime.mayLaunch(m),
|
||||
landHeld: m => runtime.landHeld(m.record),
|
||||
landHolders: m => runtime.landHolders(m.business),
|
||||
landRelease: m => runtime.landRelease(m.record),
|
||||
sessionClears: m => runtime.sessionClears(m.business),
|
||||
lifecycleRequests: m => runtime.lifecycleRequests(m.business),
|
||||
};
|
||||
const wrap = fn => async value => {
|
||||
try { return fn(value); }
|
||||
catch (e) { throw Object.assign(new Error(e.message), { code: e instanceof BusError ? e.code : 'fixture-host-refused' }); }
|
||||
};
|
||||
const host = {
|
||||
business, path: runtime.path, dataRoot: system.dataRoot,
|
||||
op: wrap(m => { if (!Object.hasOwn(ops, m.op)) throw new BusError('invalid-host-request'); return ops[m.op](m); }),
|
||||
bindLaunch: wrap(record => runtime.bindLaunch(record)),
|
||||
beforeClose: fn => void hooks.push(fn),
|
||||
};
|
||||
const launcher = createLauncher({ host, system,
|
||||
makeLoader: await fakeHarnesses({ after: fn => cleanups.push(fn) }, { pi: adapter }, write => write('channel', 'webui',
|
||||
`export * from ${JSON.stringify(new URL('../../../plugins/channel/webui/index.mjs', import.meta.url).href)};`)),
|
||||
readOnly: [new URL('./thread-adapter.mjs', import.meta.url).pathname],
|
||||
harnessVersion: '0.85.1', sessionDefaults: { pollInterval: 600000 }, threadTickMs: 100,
|
||||
});
|
||||
let closing = false;
|
||||
async function close() {
|
||||
if (closing) return;
|
||||
closing = true;
|
||||
for (const fn of hooks) await fn(0);
|
||||
await runtime.close();
|
||||
for (const fn of cleanups) await fn();
|
||||
process.exit(0);
|
||||
}
|
||||
process.once('SIGTERM', close);
|
||||
process.once('SIGINT', close);
|
||||
await launcher.listen();
|
||||
writeFileSync(ready, 'ready\n', { mode: 0o600 });
|
||||
@@ -0,0 +1,7 @@
|
||||
// R7 fixture: actual human-cli path with the fixture host's labelled ancestry override.
|
||||
// This stands in for Jason's shell (sage-3625); it does not prove real human ancestry.
|
||||
import { writeFileSync } from 'node:fs';
|
||||
import { humanTransport } from '../../src/transport.mjs';
|
||||
const [socket, business, verb, args, out] = process.argv.slice(2);
|
||||
const result = await humanTransport({ socket, business })(verb, JSON.parse(args));
|
||||
writeFileSync(out, JSON.stringify(result), { mode: 0o600 });
|
||||
Executable
+111
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env bash
|
||||
# FED R7 / #1802. Scratch acceptance with labelled ancestry stand-in (sage-3625).
|
||||
# No systemd units. All children run in owned process groups, removed on exit.
|
||||
set -euo pipefail
|
||||
R7_INTERRUPT=0
|
||||
case "${1:-}" in
|
||||
'') ;;
|
||||
--kill-after-start) R7_INTERRUPT=1 ;;
|
||||
*) echo 'usage: scripts/test-federation-acceptance.sh [--kill-after-start]' >&2; exit 2 ;;
|
||||
esac
|
||||
[ "$#" -le 1 ] || exit 2
|
||||
for R7_TOOL in ss rg timeout node ps awk; do
|
||||
command -v "$R7_TOOL" >/dev/null || { echo "R7 requires $R7_TOOL" >&2; exit 1; }
|
||||
done
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")/.."
|
||||
R7_ROOT="$(mktemp -d /mnt/storage/scratch/tmp/r7.XXXXXXXX)"
|
||||
mkdir -p "$R7_ROOT/home" "$R7_ROOT/tmp"
|
||||
touch "$R7_ROOT/groups" "$R7_ROOT/ports" "$R7_ROOT/sockets"
|
||||
|
||||
cleanup() {
|
||||
local result=$? pid start raw port socket listeners cleanup_failed=0 attempt
|
||||
local -a fields
|
||||
trap - EXIT INT TERM
|
||||
if [ -n "${R7_DRIVER:-}" ]; then
|
||||
kill -TERM "$R7_DRIVER" 2>/dev/null || true
|
||||
wait "$R7_DRIVER" 2>/dev/null || true
|
||||
fi
|
||||
# A detached child is its group's leader. Confirm pid/start time/group before
|
||||
# signalling only groups in this run's ledger; never a live stack process.
|
||||
while read -r pid start; do
|
||||
[ -n "$pid" ] || continue
|
||||
if [ -r "/proc/$pid/stat" ]; then
|
||||
raw="$(cat "/proc/$pid/stat")"
|
||||
read -r -a fields <<< "${raw##*) }"
|
||||
if [ "${fields[19]}" != "$start" ] || [ "${fields[2]}" != "$pid" ]; then
|
||||
echo "R7 cleanup failed: group identity changed: $pid" >&2
|
||||
result=1; cleanup_failed=1
|
||||
continue
|
||||
fi
|
||||
kill -TERM -- "-$pid" 2>/dev/null || true
|
||||
for ((attempt=0; attempt<800; attempt++)); do
|
||||
ps -eo pgid=,stat= | awk -v g="$pid" '$1==g && $2 !~ /^Z/ {found=1} END {exit !found}' || break
|
||||
sleep 0.05
|
||||
done
|
||||
kill -KILL -- "-$pid" 2>/dev/null || true
|
||||
for ((attempt=0; attempt<40; attempt++)); do
|
||||
ps -eo pgid=,stat= | awk -v g="$pid" '$1==g && $2 !~ /^Z/ {found=1} END {exit !found}' || break
|
||||
sleep 0.05
|
||||
done
|
||||
fi
|
||||
# Zombies hold no tools/listeners and are reaped by their parent. The test
|
||||
# requires no live member of an owned group, including orphan descendants.
|
||||
if ps -eo pgid=,stat= | awk -v g="$pid" '$1==g && $2 !~ /^Z/ {found=1} END {exit !found}'; then
|
||||
echo "R7 cleanup failed: group $pid still has a live process" >&2
|
||||
result=1; cleanup_failed=1
|
||||
fi
|
||||
done < "$R7_ROOT/groups"
|
||||
while IFS= read -r port; do
|
||||
[ -n "$port" ] || continue
|
||||
if ! listeners="$(ss -H -ltn "sport = :$port")"; then
|
||||
echo "R7 cleanup failed: cannot inspect port $port" >&2
|
||||
result=1; cleanup_failed=1
|
||||
elif [ -n "$listeners" ]; then
|
||||
echo "R7 cleanup failed: port $port still listens" >&2
|
||||
result=1; cleanup_failed=1
|
||||
fi
|
||||
done < "$R7_ROOT/ports"
|
||||
while IFS= read -r socket; do
|
||||
[ -n "$socket" ] || continue
|
||||
if ! listeners="$(ss -H -lx)"; then
|
||||
echo "R7 cleanup failed: cannot inspect socket $socket" >&2
|
||||
result=1; cleanup_failed=1
|
||||
elif printf '%s\n' "$listeners" | rg -F -- "$socket" >/dev/null; then
|
||||
echo "R7 cleanup failed: socket $socket still listens" >&2
|
||||
result=1; cleanup_failed=1
|
||||
fi
|
||||
done < "$R7_ROOT/sockets"
|
||||
if listeners="$(ss -H -lx)"; then
|
||||
if printf '%s\n' "$listeners" | rg -F -- "$R7_ROOT/" >/dev/null; then
|
||||
echo 'R7 cleanup failed: another scratch Unix listener remains' >&2
|
||||
result=1; cleanup_failed=1
|
||||
fi
|
||||
else
|
||||
result=1; cleanup_failed=1
|
||||
fi
|
||||
if [ "$result" -ne 0 ]; then
|
||||
for raw in "$R7_ROOT"/*.log; do
|
||||
[ -f "$raw" ] || continue
|
||||
echo "R7 diagnostic: ${raw##*/}" >&2
|
||||
tail -40 "$raw" >&2
|
||||
done
|
||||
fi
|
||||
rm -rf -- "$R7_ROOT"
|
||||
if [ -e "$R7_ROOT" ]; then
|
||||
echo 'R7 cleanup failed: scratch directory remains' >&2
|
||||
result=1; cleanup_failed=1
|
||||
fi
|
||||
if [ "$cleanup_failed" -eq 0 ]; then echo 'R7 cleanup: no child groups, ports or sockets remain; no units created'; fi
|
||||
exit "$result"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
# No host configuration, preload hooks, credentials or agent env in fixtures.
|
||||
# The ancestry stand-in lives only in federation-host.mjs, per sage-3625.
|
||||
timeout -k 10 210 env -i PATH="$PATH" HOME="$R7_ROOT/home" TMPDIR="$R7_ROOT/tmp" \
|
||||
MOSAIC_R7_ROOT="$R7_ROOT" MOSAIC_R7_INTERRUPT="$R7_INTERRUPT" \
|
||||
node --test --test-concurrency=1 packages/cli/tests/federation-acceptance.test.mjs &
|
||||
R7_DRIVER=$!
|
||||
wait "$R7_DRIVER"
|
||||
R7_DRIVER=
|
||||
Reference in new issue
Block a user