Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a4bef6c13 | ||
|
|
ec86f2c847 | ||
|
|
4ec279dc43 | ||
|
|
3d2569ce25 | ||
|
|
3a20f29d66 | ||
|
|
c5f96836d5 | ||
|
|
99930362e4 | ||
|
|
579720e84a | ||
|
|
c25799928d |
No files matched your search
@@ -5453,3 +5453,19 @@ Row 299 (573, #1806, Gander, Filbert): before, the P10 band test timed a fault r
|
||||
2026-10-24 | Goldie | Row 323 / #1819 second red STOP (mos-1560, sage-3864) | Isolated revoke-window whole-file run1 on unchanged571376c30 failed0/1 after20.126s, same no RESULT at ask167/test194. Start00:24:22Z, load1.46/1.68/2.70, busyhelperCLEAR, fresh scratch goldieBObDQ9. No run2/3/CLI retry/mutant/restored run; no source/old-test edit. Sage goldie-0018 has log/load/error; slot returned, awaits base-dd6cb5872 root cause. Remote remains unverified571.
|
||||
|
||||
2026-10-24 | Goldie | Row 323 / #1819 accepted evidence (mos-1561, sage-3869) | Sage traced revoke-window reds to37-byte TMPDIR making109-byte tools.sock >107 limit (session.mjs278), not Codex. Fresh31-byte /scratch/tmp/XXXXXX confirmation passes1/1 in0.84s. On unchanged571376c30 serialized timeout-k10: business261/261, new7/7, tester-switch mutant killed/restored SHA85e6b9ac, restored new7/7. Every log starts UTC/load/helperCLEAR; all positive runs0 fail/skip/cancel. CLI whole acceptance comes from Sage gate under mos-1561; no local CLI rerun. Evidence-only notes/log commit follows, no old test/source changes.
|
||||
|
||||
2026-10-24 | Goldie | Row325 / #1821 before (mos-1563/1564) | Real .mosaic/project.json idstack disagrees with Case1 project's mosaic-stack key; row323 scratch fixture omitted project file and only warned. New goldie/row325 at99930362e changes only that JSON key and expressly authorized old assertion key; new real-byte project-copy validation tests require valid and reject old key. Mutant prepared; no tests before Sage CLEAR, no queue writes/host actions.
|
||||
|
||||
2026-10-24 | Goldie | Row325 / #1821 after (sage-3882 CLEAR) | Only Case1 projects key becomesstack; only old test70 expected key updated under mos-1564. New3-case test copies real .mosaic/project.json bytes, requires CLI project valid, rejects old key. Serialized shortTMP timeout-k10: new3/3, business264/264, old-key mutant killed/restored SHA9e43bfef, restored new3/3; all positive0 fail/skip/cancel. Manifest4/4 verifies including unchanged project document. Logs/load lines and mutant diff in BUILD.md; no queue writes/host action, Gladstone review/Sage gate pending.
|
||||
|
||||
2026-10-24 — Poe, row 208 / 193d (#1706), before: building Console shared list,
|
||||
share controls and per-grant viewer read-only against approved design e2d161e2e.
|
||||
Base ec86f2c84. Isolated tests and UI-guard mutants pending Sage CLEAR; no live action.
|
||||
|
||||
2026-10-24 — Poe, row 208 / 193d (#1706), after: Console shared list, share/unshare
|
||||
controls and live per-grant viewer UI implemented. Webui 418/418 and plugins
|
||||
119/119 green under sage-3925 CLEAR; four UI guard mutants killed with viewer
|
||||
bus bypass refusals. Prior old-test draft regression fixed under sage-3918;
|
||||
missing root oauth4webapi corrected by pinned npm ci under sage-3924. Evidence:
|
||||
agents/poe/work/queue-208/BUILD.md. Independent reviews and Sage's global gate
|
||||
remain; no live state, schema or bus policy change.
|
||||
@@ -0,0 +1,10 @@
|
||||
# Bentina
|
||||
|
||||
Builder seat, Codex gpt-6.1-sol, started by Sage under Q246 and mos-1566. Owns row 157, I2: the install images, the
|
||||
install config key, the endpoint helper and the egress proxy (docs/plans/2026-10-18_containerized-install-rows.md),
|
||||
reassigned from Researcher so the coding runs on Codex.
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no credentials of its
|
||||
own; it pushes only its own `bentina/*` branches through the repository credential helper (mos-0981), never `refactor`,
|
||||
`main` or another seat's branch. Claude seats orchestrate and review the install path (mos-1542 rule 4). Registered by
|
||||
Sage on 2026-10-24 so the queue can name the seat as row owner (store.mjs isSeatDir).
|
||||
@@ -0,0 +1,74 @@
|
||||
# Row 323 round 1 verdict: goldie/row323 6a0f0f4f8 (duckworth-0300)
|
||||
|
||||
Request: sage-3865 (early look at draft 571376c30) and sage-3870 (candidate 6a0f0f4f8, base dd6cb5872,
|
||||
manifest 903ef0225f2f…cda79f). Brief: docs/plans/2026-10-23_case1-business-file-brief.md, section "Brief:
|
||||
the Case 1 factory business file".
|
||||
Method: static. Git reads of local objects only. No node, no tests, no fetch, no queue reads.
|
||||
|
||||
## Verdict: APPROVE (static), with one condition
|
||||
|
||||
- **The manifest matches.** MANIFEST.sha256 hashes to 903ef0225f2f…cda79f, and both of its blobs match at
|
||||
6a0f0f4f8: the JSON 85e6b9ac… and the test aecd1c0f…. Neither file changed between 571376c30 and
|
||||
6a0f0f4f8. The rest of that range is BUILD.md, logs, BUILD-LOG and SESSIONS. dd6cb5872 is an ancestor.
|
||||
No existing test or source file changed.
|
||||
- **Fields against the brief and the cited rulings:**
|
||||
- id `mosaic-stack` (DESIGN section 7). `vars.bus.tester` true. There is no `library` key and no
|
||||
library project.
|
||||
- human `jason`. I can't read mos-1554 myself, so this rests on Sage's relay in sage-3823. BUILD.md is
|
||||
explicit that this is the only literal value that comes from mos-1554.
|
||||
- The project root /srv/mosaic/src/mosaic-stack is under the DESIGN home /srv/mosaic, and the value
|
||||
comes from sage-3822. It passes the normalized-absolute check at business.mjs:274. A missing project
|
||||
file there only gives a warning in validate (cli.mjs:73-77), so the path doesn't need to exist at
|
||||
install.
|
||||
- Roles are chief (chief-of-staff) and assistant. The assistant is needed because checkLaunch requires
|
||||
`launch.instances` to be nonempty and to exclude the launcher (business.mjs:186-190). BUILD.md names
|
||||
that check, as the brief asks. Both arbiters are chief, under sage-3820. launch.by is chief, which
|
||||
F7 (case1-accept.sh:545-559) reads.
|
||||
- Claude only. `plugins.harness` is [claude-code] and both roles use harness claude-code. There is no
|
||||
pi.
|
||||
- No tracker, no credentials, no library, and no credential value or reference anywhere in the file.
|
||||
Q247 covers the tracker by omission.
|
||||
- **The model, provider and effort have sources.**
|
||||
- The model claude-opus-5-5 is Sage's ruling in sage-3822.
|
||||
- The provider mapping anthropic/claude-opus-5-5 matches packages/business/README.md:185.
|
||||
- `thinking: high` is one of the claude-code LEVELS (index.mjs:28) and matches the chief convention at
|
||||
session-launch.test.mjs:83.
|
||||
- With no `launch.limits`, there is no ceiling: limitHit (launch.mjs:137-155) checks only the limits
|
||||
that are defined. That is a valid choice, and N1 records it.
|
||||
- **The tests read the shipped bytes.**
|
||||
- DOCUMENT is `REPO/docs/guides/case1-business.mosaic-stack.json`. Case 1 writes those exact bytes,
|
||||
loads them through loadBusiness, checks the bytes didn't change, and then asserts every field.
|
||||
- The validate, install, boot and refusal cases copy the document and change only the project root,
|
||||
under sage-3820. Since an absent root only warns, the copy differs from the shipped file only in a
|
||||
way validate doesn't refuse.
|
||||
- The brief's three tests are all there:
|
||||
- (1) The loader case.
|
||||
- (2) The real case2-install.sh factory profile with `--business-file`, with only stack-copy stubbed.
|
||||
- (3) `bus-tester-off`, the literal library key through the final validate (sage-3820), and the
|
||||
library project through `profile-library-business`.
|
||||
- The boot case also covers the 324 boundary: bootConfig gives no trackers, credentials.status() is
|
||||
[], and the reader's credentials are [].
|
||||
- **The tester-switch mutant is a real kill target.**
|
||||
- mutant.py asserts that the needle `"bus.tester": true` occurs exactly once, flips only that value,
|
||||
and restores in a finally block with a byte-equality check.
|
||||
- The value is asserted at test :66. mutant.log shows `not ok 1`, `false !== true`, `# fail 1`, and the
|
||||
restored sha256 85e6b9ac… equals the manifest.
|
||||
- The flip would also make the install case at :95 fail, because the install refuses with
|
||||
bus-tester-off.
|
||||
- **The run, as Goldie reported it (sage-3869 CLEAR, 31-byte TMPDIRs, on 571376c30). I didn't rerun it.**
|
||||
- The new file passed 7/7, the business suite 261/261, and the restored file 7/7 again.
|
||||
- The mutant was killed.
|
||||
- The earlier revoke-window red is the tools.sock length limit (session.mjs:278, dda2d07b4). It fails
|
||||
the same way on the bare base dd6cb5872, so it isn't this row's.
|
||||
|
||||
## Condition
|
||||
|
||||
- C1. Under mos-1561, the cli suite counts only from Sage's full gate on 6a0f0f4f8. This approval
|
||||
depends on that gate passing with no new failures.
|
||||
|
||||
## Notes
|
||||
|
||||
- N1 (doc). `launch` has no `limits`, so the business has no session ceiling. If Mos wants a cap on .53,
|
||||
it's a later launch-settings edit and doesn't need a new file.
|
||||
- N2 (doc). An install on .53 with no project file at /srv/mosaic/src/mosaic-stack still passes. It
|
||||
warns "no project variables" (cli.mjs:76), which the run sheet could mention.
|
||||
@@ -0,0 +1,48 @@
|
||||
# Row 325 / #1821 — Case 1 project key
|
||||
|
||||
Goldie, 2026-10-24. Branch goldie/row325, base99930362e4b8a64d5425b10ca087b2d1f93ae538. Separate worktree /home/jwoltje/.t3/worktrees/mosaic-stack/goldie-row325. Brief pinned at60d78e44e:docs/plans/2026-10-24_case1-project-key-brief.md, section Brief: re-key the Case1 project to stack. Mos-1563 fixes the business document and preserves project id. Reviewer Gladstone; Sage owns review posting/landing. No queue writes authorized or performed.
|
||||
|
||||
## Defect and change
|
||||
|
||||
The real .mosaic/project.json declares id stack; the Case1 business document incorrectly keyed its project mosaic-stack. Real business validation checks equality at packages/business/src/cli.mjs:80, so the install refused once the host held the project file. Row323's scratch execution root omitted that file and exercised only the warning/absent branch, missing the deployment mismatch.
|
||||
|
||||
Only the projects object's key changes to stack in docs/guides/case1-business.mosaic-stack.json. Business id stays mosaic-stack, root stays /srv/mosaic/src/mosaic-stack, and every other field is unchanged. No .mosaic/project.json edit or product/schema/role change.
|
||||
|
||||
The new packages/business/tests/case1-project-key.test.mjs reads the repo's real .mosaic/project.json bytes, asserts exactly one matching Case1 key, copies those bytes unchanged into a scratch project root, and calls the actual business CLI validate path with a temporary system/business config. Positive output must report the project valid, not absent. Negative coverage re-keys the same fixture to mosaic-stack and requires exit2 plus the real project-id mismatch message; missing-file warnings cannot stand in for validation.
|
||||
|
||||
Mos-1564 (mos-1272) authorizes exactly one pre-existing test line: case1-business-file.test.mjs:70's expected key changes to stack; all other bytes/assertions remain unchanged. No other old test is touched. This is explicitly the obsolete expectation caused by the product correction, not a relaxed assertion.
|
||||
|
||||
## Verification plan / state
|
||||
|
||||
No tests run before Sage CLEAR. Static node --check, JSON parse and diff checks only. On CLEAR, one run at a time: new test file whole, business suite whole (includes the one updated old test), one old-key mutant, restored whole new file to verify restoration. Every run gets fresh `export TMPDIR=$(mktemp -d /mnt/storage/scratch/tmp/XXXXXX)`, timeout-k10, and the established UTC/load/busy-helper log header. Stop/report on any other old-test red; no further old-test edits. First unexpected new-test red is recorded/reported under the continuing mos-1272 rule.
|
||||
|
||||
mutant.py changes only the projects key back to mosaic-stack, runs the new exact-key assertion, requires expected exit1/fail1, then restores/checks bytes in finally. Prepared, not run. Sage's landing gate supplies CLI suite counts.
|
||||
|
||||
The dependency setup is an ignored root node_modules symlink to /mnt/storage/src/mosaic-stack/node_modules; no dependency installation/pin change. MANIFEST.sha256 covers business JSON, the one old test, new test, and real project document as an unchanged reference. Final review report goes to both Sage and Gladstone with tip/base/manifest. No host contact, credential access, queue write or refactor push.
|
||||
|
||||
## Verification — sage-3882 CLEAR
|
||||
|
||||
All four runs were serial on the same final source bytes, using timeout-k10, fresh31-byte TMPDIRs from /mnt/storage/scratch/tmp/XXXXXX, and date-u/uptime/busy-helper headers. Node v26.8.1. No first-run new-test red and no old-test red. No CLI suite run; Sage's landing gate supplies that scope.
|
||||
|
||||
| Run / log | UTC start | Load averages | Busy | Result / TMPDIR |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| test-new.log | 2026-10-24 01:13:40 | 1.34,1.60,2.50 | CLEAR | Whole new file3/3 pass, 172.161691ms; hchZu2 |
|
||||
| test-business.log | 2026-10-24 01:13:50 | 1.36,1.59,2.49 | CLEAR | Whole business suite264/264 pass, 6478.924028ms; yWVbwY |
|
||||
| mutant-driver.log + mutant.log | 2026-10-24 01:14:04 | 1.36,1.58,2.47 | CLEAR | Old-key mutant killed, targeted new test0/1, expected exit1; driver exit0; YsOE6N |
|
||||
| test-restored.log | 2026-10-24 01:14:14 | 1.15,1.53,2.44 | CLEAR | Restored new file3/3 pass, 169.948442ms; Y43Jju |
|
||||
|
||||
All TMPDIRs are under /mnt/storage/scratch/tmp/. New/restored/mutant driver limits180s, business600s, each timeout-k10. Positive runs have0 failures/skips/cancellations. The business suite includes the entire old case1-business-file.test.mjs, with only the mos-1564-approved key assertion changed.
|
||||
|
||||
Mutant diff in business JSON:
|
||||
|
||||
```diff
|
||||
"projects": {
|
||||
- "stack": {
|
||||
+ "mosaic-stack": {
|
||||
```
|
||||
|
||||
The exact-key new test fails deepStrictEqual (actual mosaic-stack, expected stack); raw evidence is mutant.log. mutant.py restores in finally and checks exact bytes. Restored JSON SHA256 `9e43bfef326e568488aaa4f0d8882e9b932750aeb39f587a113a226bfb30cf0f`; all4 MANIFEST.sha256 entries verify, including unchanged .mosaic/project.json. Whole new file passed again after restoration. git diff --check passed.
|
||||
|
||||
Final source accounting: business JSON1+/1-, only projects key; old test1+/1-, only expected key at70; new test file with3 cases; no other old-test line touched. Business id/root remain unchanged. Root-cause gap is now covered by byte-identical real project fixture plus required valid result and old-key refusal. No host action, credentials, queue writes, refactor push or unapproved scope change.
|
||||
|
||||
Logs have trailing whitespace normalized for git diff --check, with substantive output/results unchanged. The slot is returned in the report accompanying the pushed candidate; Gladstone first review and Sage landing gate remain pending.
|
||||
@@ -0,0 +1,4 @@
|
||||
9e43bfef326e568488aaa4f0d8882e9b932750aeb39f587a113a226bfb30cf0f docs/guides/case1-business.mosaic-stack.json
|
||||
9c3bf7f349d0ee8626743e1388b9a596c7f3c274beaedad5106db5f2ff8f65e7 packages/business/tests/case1-business-file.test.mjs
|
||||
b543ac6afbfb336c5566f359ff357904b36f413d5f75fcd54a5a8b72aef1dbd6 packages/business/tests/case1-project-key.test.mjs
|
||||
044714a37c267b70279977ea8ca429df7a28463271b884afd79a01bc27afa259 .mosaic/project.json
|
||||
@@ -0,0 +1,6 @@
|
||||
Sat Oct 24 01:14:04 UTC 2026
|
||||
20:14:04 up 10 days, 11:33, 2 users, load average: 1.36, 1.58, 2.47
|
||||
CLEAR
|
||||
TMPDIR=/mnt/storage/scratch/tmp/YsOE6N bytes=31
|
||||
old-key mutant: exit 1
|
||||
restored sha256 9e43bfef326e568488aaa4f0d8882e9b932750aeb39f587a113a226bfb30cf0f
|
||||
@@ -0,0 +1,40 @@
|
||||
TAP version 13
|
||||
# Subtest: row 325: the Case 1 project's sole key matches the repository project id
|
||||
not ok 1 - row 325: the Case 1 project's sole key matches the repository project id
|
||||
---
|
||||
duration_ms: 2.058961
|
||||
type: 'test'
|
||||
location: '/home/jwoltje/.t3/worktrees/mosaic-stack/goldie-row325/packages/business/tests/case1-project-key.test.mjs:38:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
Expected values to be strictly deep-equal:
|
||||
+ actual - expected
|
||||
|
||||
[
|
||||
+ 'mosaic-stack'
|
||||
- 'stack'
|
||||
]
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected:
|
||||
0: 'stack'
|
||||
actual:
|
||||
0: 'mosaic-stack'
|
||||
operator: 'deepStrictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/goldie-row325/packages/business/tests/case1-project-key.test.mjs:42:10)
|
||||
Test.runInAsyncScope (node:async_hooks:226:14)
|
||||
Test.run (node:internal/test_runner/test:1402:25)
|
||||
Test.start (node:internal/test_runner/test:1262:17)
|
||||
startSubtestAfterBootstrap (node:internal/test_runner/harness:387:17)
|
||||
...
|
||||
1..1
|
||||
# tests 1
|
||||
# suites 0
|
||||
# pass 0
|
||||
# fail 1
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 44.416722
|
||||
@@ -0,0 +1,26 @@
|
||||
"""Row 325 old-project-key mutant; run only in Sage's test slot."""
|
||||
import hashlib
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
root = Path(__file__).resolve().parents[4]
|
||||
source = root / 'docs/guides/case1-business.mosaic-stack.json'
|
||||
fixed = source.read_bytes()
|
||||
needle = b'"projects": {\n "stack": {'
|
||||
assert fixed.count(needle) == 1
|
||||
try:
|
||||
source.write_bytes(fixed.replace(needle, b'"projects": {\n "mosaic-stack": {'))
|
||||
result = subprocess.run([
|
||||
'timeout', '-k', '10', '90', 'node', '--test', '--test-reporter=tap',
|
||||
'--test-name-pattern=row 325: the Case 1 project',
|
||||
'packages/business/tests/case1-project-key.test.mjs',
|
||||
], cwd=root, env=os.environ, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
Path(__file__).with_suffix('.log').write_text(result.stdout)
|
||||
print(f'old-key mutant: exit {result.returncode}', flush=True)
|
||||
assert result.returncode == 1, result.stdout
|
||||
assert '# fail 1\n' in result.stdout and 'mosaic-stack' in result.stdout, result.stdout
|
||||
finally:
|
||||
source.write_bytes(fixed)
|
||||
assert source.read_bytes() == fixed
|
||||
print('restored sha256 ' + hashlib.sha256(fixed).hexdigest(), flush=True)
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,32 @@
|
||||
Sat Oct 24 01:13:40 UTC 2026
|
||||
20:13:40 up 10 days, 11:33, 2 users, load average: 1.34, 1.60, 2.50
|
||||
CLEAR
|
||||
TMPDIR=/mnt/storage/scratch/tmp/hchZu2 bytes=31
|
||||
TAP version 13
|
||||
# Subtest: row 325: the Case 1 project's sole key matches the repository project id
|
||||
ok 1 - row 325: the Case 1 project's sole key matches the repository project id
|
||||
---
|
||||
duration_ms: 4.262251
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: row 325: real business validate reports the copied repository project valid
|
||||
ok 2 - row 325: real business validate reports the copied repository project valid
|
||||
---
|
||||
duration_ms: 67.962164
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: row 325: the old mosaic-stack project key refuses against those same project bytes
|
||||
ok 3 - row 325: the old mosaic-stack project key refuses against those same project bytes
|
||||
---
|
||||
duration_ms: 57.926665
|
||||
type: 'test'
|
||||
...
|
||||
1..3
|
||||
# tests 3
|
||||
# suites 0
|
||||
# pass 3
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 172.161691
|
||||
@@ -0,0 +1,32 @@
|
||||
Sat Oct 24 01:14:14 UTC 2026
|
||||
20:14:14 up 10 days, 11:34, 2 users, load average: 1.15, 1.53, 2.44
|
||||
CLEAR
|
||||
TMPDIR=/mnt/storage/scratch/tmp/Y43Jju bytes=31
|
||||
TAP version 13
|
||||
# Subtest: row 325: the Case 1 project's sole key matches the repository project id
|
||||
ok 1 - row 325: the Case 1 project's sole key matches the repository project id
|
||||
---
|
||||
duration_ms: 4.401838
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: row 325: real business validate reports the copied repository project valid
|
||||
ok 2 - row 325: real business validate reports the copied repository project valid
|
||||
---
|
||||
duration_ms: 60.555647
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: row 325: the old mosaic-stack project key refuses against those same project bytes
|
||||
ok 3 - row 325: the old mosaic-stack project key refuses against those same project bytes
|
||||
---
|
||||
duration_ms: 63.16989
|
||||
type: 'test'
|
||||
...
|
||||
1..3
|
||||
# tests 3
|
||||
# suites 0
|
||||
# pass 3
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 169.948442
|
||||
@@ -0,0 +1,10 @@
|
||||
# Poe
|
||||
|
||||
Builder seat, Codex gpt-6.1-sol, started by Sage under Q246 and mos-1566. Owns row 208, 193d: the Console's shared
|
||||
list, share controls and viewer read-only (docs/plans/2026-10-20_human-users-build.md), reassigned from Morgana so the
|
||||
coding runs on Codex.
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no credentials of its
|
||||
own; it pushes only its own `poe/*` branches through the repository credential helper (mos-0981), never `refactor`,
|
||||
`main` or another seat's branch. Claude seats orchestrate and review guards (mos-1542 rule 4). Registered by Sage on
|
||||
2026-10-24 so the queue can name the seat as row owner (store.mjs isSeatDir).
|
||||
@@ -0,0 +1,122 @@
|
||||
# Row 208 / 193d — Console shared threads and viewer reads
|
||||
|
||||
Base ec86f2c84, branch poe/row208; Gitea #1706. Approved design: e2d161e2e,
|
||||
sha256 dd7fcb419ed8510e9d8d74b569183386d0942af8b68eecc0bbb5e182e677b523.
|
||||
|
||||
## Before
|
||||
|
||||
FED R6 supplies per-session user transport and the user-only page, row 192 SSO,
|
||||
and row 206 supplies schema 3l shares and bus refusals. No bus/schema changes needed.
|
||||
The webui channel adapter must forward the three existing share verbs.
|
||||
|
||||
## Choices and acceptance
|
||||
|
||||
Use the existing chat HTTP write fences and per-session transport for shares.
|
||||
Expose only live collaborator grant project/instance scopes as UI hints, never
|
||||
identity or bus authority. Re-read hints when rendering a user page; mixed grants
|
||||
must hide writes on viewer-only threads even when another project allows writes.
|
||||
Shared threads always read without session management, composer, resume or sharing.
|
||||
Keep the exact governance notice on user thread/share pages. Targets are typed
|
||||
owner/user principal names or *, avoiding a new user directory read API.
|
||||
|
||||
Add new isolated HTTP/broker and browser coverage. Remove each UI guard in mutants
|
||||
and show the real broker still refuses viewer create, send, open and share writes.
|
||||
Every test execution requires Sage CLEAR; use scratch TMPDIR, deadlines, npm ci,
|
||||
and MOSAIC_REQUIRE_EDITOR_BUILD=1. No live state or network destinations.
|
||||
|
||||
## Tracking
|
||||
|
||||
Queue next from linked tree refused as designed; canonical next matched pinned brief.
|
||||
Claim poe-0001 initially refused on Sage's queue lock; retry the same operation id.
|
||||
|
||||
Claim poe-0001 succeeded at canonical queue revision 3807 (in-progress).
|
||||
During implementation, three string replacements briefly targeted canonical
|
||||
packages/webui/src/public/{bus.js,user.js,chat.js} due to an incorrect command cwd.
|
||||
Checked the complete diffs, surgically reversed exactly those replacements, and
|
||||
applied them to this worktree. Sage independently confirmed canonical packages/
|
||||
has no remaining diff (sage-3914). No tests or live actions occurred there.
|
||||
|
||||
poe-0002 requested the test slot. sage-3914 approved the exact serialized plan,
|
||||
but CLEAR is pending Bentina returning her slot. No node/npm/browser commands
|
||||
before CLEAR. New tests are thread-sharing.test.mjs with thread-sharing-fixture.mjs;
|
||||
mutants.mjs runs four served-source mutants with assertion kills plus bus bypass.
|
||||
|
||||
## First verification (sage-3917 CLEAR)
|
||||
|
||||
Scratch logs: /mnt/storage/scratch/tmp/YU1G5B/.
|
||||
Syntax 12/12 exit 0; npm-ci.log exit 0; editor-build.log exit 0;
|
||||
thread-sharing.tap 7/7 exit 0. mutants.log and poe-208-mutants.json: four
|
||||
absence-test assertion kills (exit 1 each), four bus bypass proofs (exit 0 each),
|
||||
runner exit 0. Each mutant's individual log is poe-208-NAME-kill.tap or -bypass.tap.
|
||||
Full serialized webui.tap stopped on an existing-test red: 44 pass, 1 fail before
|
||||
termination; runner exit 1. Plugins not run. Slot returned to Sage via poe-0003.
|
||||
|
||||
Existing failure: chat-browser.test.mjs:123, uncertain writes/read failure draft
|
||||
preservation, line 133 expects #chat-body after no-bus-host. The new current.thread
|
||||
is not retained before list reads succeed, unlike old current.threads. A transient
|
||||
read failure therefore hides the existing owner conversation. Proposed fix retains
|
||||
same-thread metadata across refresh, never stale user grant scopes. No old tests
|
||||
changed and no further execution pending Sage's ruling/CLEAR.
|
||||
|
||||
## Approved correction (sage-3918)
|
||||
|
||||
The old test is correct; this change broke its transient-failure draft contract.
|
||||
Retain current.thread and shared only when the old id equals the requested id.
|
||||
User chatGrants still start empty and are read fresh, so a stale hint is never
|
||||
carried forward. A real 403/404 clears thread/shared metadata plus cached history,
|
||||
drafts and operation forms. chat-browser.test.mjs remains byte-identical.
|
||||
Fix applied statically; Sage will CLEAR the four mutants, full webui and plugins
|
||||
suites after Bentina returns the slot. No node/npm/browser commands before then.
|
||||
|
||||
Clarification during static review: retain the existing distinction between a
|
||||
thread-level refusal and a local bad-session selector (old row 272 B1 test).
|
||||
All 403/404 responses clear visible thread/shared metadata, as before; thread-level
|
||||
thread-not-owned/thread-not-found/access-refused also erase sibling cached history
|
||||
and drafts. A bad-session selector never erases valid sibling drafts/recovery keys.
|
||||
This preserves the existing invalidation contract rather than broadening the fix.
|
||||
|
||||
sage-3920 CLEAR: first mutant rerun stopped on a new-test failure, not a valid
|
||||
mutant kill. /mnt/storage/scratch/tmp/8UlAhS/poe-208-composer-kill.tap shows null
|
||||
old.thread on initial /chat: old?.id and undefined route id compared equal.
|
||||
Fixed by requiring old to exist before either same-id metadata carry. This is a
|
||||
new defect in the approved correction, no old test change. Rerunning the plan.
|
||||
|
||||
## Second slot outcome (sage-3920)
|
||||
|
||||
Scratch /mnt/storage/scratch/tmp/8UlAhS/.
|
||||
Initial mutants.log exit 1 was the new null-previous-state defect above. Fixed;
|
||||
mutants-r2.log exit 0: all four assertion kills exit 1, all four bypass proofs
|
||||
exit 0 (individual logs overwritten by the successful rerun; summary JSON kept).
|
||||
Full serialized webui.tap stopped on old tests: 114 top-level pass, 5 fail,
|
||||
runner exit 1. The original draft-preservation failure and row 272 B1 both pass.
|
||||
The new old-test failure is environmental: decide-sso.test.mjs cannot import
|
||||
root dependency oauth4webapi (ERR_MODULE_NOT_FOUND); all five failures share it.
|
||||
packages/webui npm ci installs its editor tooling, not root runtime dependencies.
|
||||
No tests changed; plugins not run. Request root npm ci from pinned lockfile in
|
||||
next slot before rerunning webui and plugins. Slot returned via poe-0004.
|
||||
|
||||
sage-3924 ruled the second red an environment gap, not a product defect. Approved
|
||||
root npm ci --ignore-scripts from the pinned lock, full webui then plugins,
|
||||
serialized. sage-3925 CLEAR starts that sequence. Push only after green; send
|
||||
base/tip and every changed path's SHA-256 to Sage and Gladstone.
|
||||
|
||||
## Final builder verification (sage-3925 CLEAR)
|
||||
|
||||
Root npm ci --ignore-scripts exit 0 from pinned lock.
|
||||
Full webui: exit 0, 418 pass, 0 fail, 0 skipped (271964 ms).
|
||||
Plugins: exit 0, 119 pass, 0 fail, 0 skipped (823 ms).
|
||||
Logs /mnt/storage/scratch/tmp/Et9xAa/{root-npm-ci.log,webui.tap,plugins.tap}.
|
||||
Slot returned immediately after plugins via poe-0005. All new acceptance cases
|
||||
are included in the full webui run. Four latest UI mutants and bus bypass proofs
|
||||
remain green at /mnt/storage/scratch/tmp/8UlAhS/mutants-r2.log and
|
||||
poe-208-mutants.json. No old tests changed. No bus authority/schema changes.
|
||||
|
||||
Acceptance: owner and collaborator can share/unshare own writable threads;
|
||||
shared lists preserve the plain-list shape and show owner names safely as text;
|
||||
shared threads cannot compose, resume, mutate sessions or reshare. Viewer old
|
||||
own threads retain read history with no write controls. Mixed grants and live
|
||||
downgrades hide writes only where coverage requires; revoked shares clear the
|
||||
view. Direct browser POSTs bypass all UI checks and the real broker still
|
||||
refuses viewer create/send/open/share. Existing exact governance notice retained.
|
||||
Global scripts/test-*.sh gate and independent reviewer approval remain Sage's
|
||||
landing gate; this builder does not claim those or any live deployment.
|
||||
@@ -0,0 +1,28 @@
|
||||
// Run only under Sage's CLEAR, with scratch TMPDIR and MOSAIC_REQUIRE_EDITOR_BUILD=1.
|
||||
// Mutants alter only the fixture-served copy of chat.js; repository source is untouched.
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
const file = 'packages/webui/tests/thread-sharing.test.mjs';
|
||||
const results = [];
|
||||
for (const mutant of ['composer', 'open', 'share', 'create']) {
|
||||
for (const [stage, pattern, expected] of [
|
||||
['kill', '193d viewer UI has no', 1],
|
||||
['bypass', '193d bypassed UI checks', 0],
|
||||
]) {
|
||||
const run = spawnSync(process.execPath, ['--test', '--test-concurrency=1', '--test-name-pattern=' + pattern, file], {
|
||||
env: { ...process.env, POE_UI_MUTANT: mutant }, encoding: 'utf8', timeout: 90000,
|
||||
});
|
||||
const log = join(process.env.TMPDIR, `poe-208-${mutant}-${stage}.tap`);
|
||||
writeFileSync(log, (run.stdout ?? '') + (run.stderr ?? ''));
|
||||
const killedByAssertion = /ERR_ASSERTION/.test(run.stdout ?? '');
|
||||
const passed = run.status === expected && (stage !== 'kill' || killedByAssertion);
|
||||
results.push({ mutant, stage, exit: run.status, signal: run.signal, passed, log });
|
||||
console.log(JSON.stringify(results.at(-1)));
|
||||
if (!passed) {
|
||||
writeFileSync(join(process.env.TMPDIR, 'poe-208-mutants.json'), JSON.stringify(results, null, 2));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
writeFileSync(join(process.env.TMPDIR, 'poe-208-mutants.json'), JSON.stringify(results, null, 2));
|
||||
@@ -1617,3 +1617,8 @@ are never rewritten or removed; corrections are new entries.
|
||||
2026-10-24T00:22Z | Goldie (a13dd6d5-5e73-4b7d-8318-02abeff24940) | Row 323 / #1819 first slot stop | Commit571376c30 new7/7/business261/261; CLI existing row306 revoke-window red no RESULT, suite stopped (partial631 pass/1 fail). No old-test changes/reruns/mutant; manifest unchanged. Sage goldie-0016 has failure evidence and slot back; awaits ruling.
|
||||
2026-10-24T00:25Z | Goldie (a13dd6d5-5e73-4b7d-8318-02abeff24940) | Row 323 / #1819 mos-1560 stop | First isolated revoke-window whole-file rerun failed0/1 at20.126s under load1.46/1.68/2.70/helperCLEAR. Second-red rule: stopped with no further tests/edits, manifest unchanged, remote571 unverified. Log and load lines in BUILD.md; Sage notified goldie-0018, slot returned.
|
||||
2026-10-24T00:38Z | Goldie (a13dd6d5-5e73-4b7d-8318-02abeff24940) | Row 323 / #1819 accepted verification | mos-1561/sage-3869: short31-byte TMPDIR fixes socket-length refusal; revoke-window1/1, business261/261, new7/7, mutant killed/bytes restored, new7/7 again. Every run logs time/load/helper; no local CLI rerun, Sage gate supplies it. Notes/logs evidence only; Duckworth review/in-review move next.
|
||||
2026-10-24T00:41Z | Duckworth (Opus 5.5) | row 323 round 1 static review on 6a0f0f4f8 (sage-3865, sage-3870) | APPROVE (static) C1 Sage full gate cli, manifest 903ef022 matches, fields sourced, tests read shipped bytes, tester mutant real, N1-N2 doc (duckworth-0300) |
|
||||
2026-10-24T01:13Z | Goldie (a13dd6d5-5e73-4b7d-8318-02abeff24940) | Row325 / #1821 Case1 project key | New goldie/row325 worktree at99930362e; pinned brief60d78e44e. Draft JSON key correction, new real-project-byte tests, sole old line70 key edit under mos-1564. Static checks only; requesting Sage CLEAR, no queue writes.
|
||||
2026-10-24T01:14Z | Goldie (a13dd6d5-5e73-4b7d-8318-02abeff24940) | Row325 / #1821 verified | sage-3882: new3/3, business264/264, one key mutant killed/restored, new3/3 again, per-run fresh shortTMP/load/helperCLEAR. Four manifest entries verify; sole old assertion key change, no queue writes. Candidate push/report to Sage and Gladstone next.
|
||||
2026-10-24 | Poe | Q246 / mos-1566, row 208 / 193d (#1706), poe/row208 | Started; approved design hash verified; implementation and isolated acceptance pending.
|
||||
2026-10-24 | Poe | Row 208 / 193d (#1706), poe/row208 | Builder acceptance green: webui 418/418, plugins 119/119, four UI mutants killed with bus bypass proofs; candidate prepared for Gladstone then Duckworth guard review.
|
||||
@@ -7,7 +7,7 @@
|
||||
"technical": "chief"
|
||||
},
|
||||
"projects": {
|
||||
"mosaic-stack": {
|
||||
"stack": {
|
||||
"root": "/srv/mosaic/src/mosaic-stack"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# Case 1 business file: project key must match the project file (#414 stage 2)
|
||||
|
||||
Written 2026-10-24 by Sage for ops-0288 and Mos's ruling mos-1563.
|
||||
|
||||
## Brief: re-key the Case 1 project to stack
|
||||
|
||||
**Why.** The .53 stage 2 run on 99930362e stopped at step 8 (ops-0288). `case2-install.sh --profile factory`
|
||||
refused with:
|
||||
`validate: mosaic business: project file /srv/mosaic/src/mosaic-stack/.mosaic/project.json has id stack, but
|
||||
business mosaic-stack declares it as mosaic-stack`. Row 323's `docs/guides/case1-business.mosaic-stack.json`
|
||||
keys its one project `mosaic-stack`. The repo's `.mosaic/project.json` has id `stack`, and the loader requires
|
||||
the key to equal that id (packages/business/src/cli.mjs:80, packages/cli/src/config.mjs:67). Row 323's test
|
||||
missed it because it validates against a scratch root with no project file, which only warns (cli.mjs:74-78).
|
||||
|
||||
**Ruling (mos-1563).** Fix the business file. Don't touch the project id: `stack` is read elsewhere.
|
||||
|
||||
**What to change.**
|
||||
1. In `docs/guides/case1-business.mosaic-stack.json`, rename the `projects` key from `mosaic-stack` to
|
||||
`stack`. Keep its `root` (`/srv/mosaic/src/mosaic-stack`). Business id stays `mosaic-stack`. Change nothing
|
||||
else in the file.
|
||||
2. A new test file, `packages/business/tests/case1-project-key.test.mjs`, that validates the committed case1
|
||||
file against the repo's own `.mosaic/project.json`, so a mismatch fails in CI instead of on a host:
|
||||
- read the repo's `.mosaic/project.json` and assert the case1 file has exactly one project whose key equals
|
||||
its `id`;
|
||||
- run the business `validate` path (the same one cli.mjs:80 runs) with that project's root pointed at a
|
||||
scratch copy holding the repo's real `.mosaic/project.json` bytes, and assert it passes with the project
|
||||
reported `valid`, not `absent`;
|
||||
- a negative case: the same run with the key set back to `mosaic-stack` refuses with the cli.mjs:80
|
||||
message.
|
||||
3. The one old-test line this change breaks, `packages/business/tests/case1-business-file.test.mjs:70`
|
||||
(`assert.deepEqual(b.projects, { "mosaic-stack": ... })`), changes its key to `stack` and nothing else. This
|
||||
edit has Mos's go (mos-1564, under mos-1272). Don't change any other line of any old test. If another old
|
||||
test reds, stop and report it.
|
||||
|
||||
**Gate.** Use `export TMPDIR=$(mktemp -d /mnt/storage/scratch/tmp/XXXXXX)` (BRIEF-TEMPLATE Gate). On my CLEAR
|
||||
only: the new test file whole, then the packages/business suite whole. One mutant: set the case1 key back to
|
||||
`mosaic-stack`, show the new test red, restore and check the bytes. The cli suite counts from my landing gate.
|
||||
|
||||
**Boundaries.** No host action, no .53 contact, no credential reads, no queue writes, no push to refactor.
|
||||
Work in a new worktree off refactor at 99930362e or later, on branch `goldie/row325`. Push the branch and
|
||||
report the tip, base and manifest to Sage and to the reviewer. Record design choices in
|
||||
agents/goldie/work/queue-325/BUILD.md.
|
||||
|
||||
**Acceptance.** At a landed commit, `case2-install.sh --business mosaic-stack --business-file
|
||||
docs/guides/case1-business.mosaic-stack.json --profile factory` passes step 8's validate against
|
||||
`/srv/mosaic/src/mosaic-stack/.mosaic/project.json`, and the new test fails if the key and the project id
|
||||
drift apart again.
|
||||
@@ -0,0 +1,78 @@
|
||||
# Host run sheet: five operator gaps from the .53 and Run B installs (#414 stage 2)
|
||||
|
||||
Written 2026-10-24 by Sage for Mos's mos-1566 item 1, from ops-0282, ops-0288, ops-0291, ops-0292,
|
||||
Duckworth's 323 N2 and Mos's rulings mos-1563 and mos-1565.
|
||||
|
||||
## Brief: five run-sheet gaps
|
||||
|
||||
**Why.** Five things stopped or slowed operators on the .53 stage 2 install and 433 Run B. Each is a gap
|
||||
in `docs/guides/host-run-sheet.md` (checked at 3d2569ce2), not a product bug. The code they describe has
|
||||
landed. This row writes them down.
|
||||
|
||||
**What to write.** Edit only `docs/guides/host-run-sheet.md`. Source every command, path and message from
|
||||
the code or the receipts named here, and cite the line you read, the way the sheet already does
|
||||
(`scripts/stack-release.sh:14`). Don't invent a flag, path or exit code.
|
||||
|
||||
1. **B2-2, the Console proof file format** (DEFERRED.md "ops-0282 B2-2", ops-0282). In section 9's .54
|
||||
library table (:321), the proof file row's File cell gives mode and owner but no format. Add it: one
|
||||
base64url line of at least 32 characters, `[A-Za-z0-9_-]` only (packages/webui/src/serve.mjs:264). The
|
||||
Console exits 2 at start otherwise (:267). `case2-unit.sh` doesn't open the file, so a bad proof fails
|
||||
only at Console start. Read serve.mjs:255-270 yourself and match what it checks, including whether a
|
||||
trailing newline is accepted.
|
||||
2. **N2, a missing project file only warns** (Duckworth 323 round 1 N2, ROUND1-6a0f0f4f8.md). When the
|
||||
project root a business file names has no `.mosaic/project.json`, `mosaic business validate` passes
|
||||
with the warning "no project file at ... it has no project variables" (packages/business/src/cli.mjs:74-78).
|
||||
An install on such a host passes step 7 with the project's variables missing. Say so where the sheet
|
||||
describes `case2-install.sh` (near :278), and tell the operator to treat that warning as a stop.
|
||||
3. **The pre-install validate step** (mos-1563, ops-0288). Before `case2-install.sh`, check by hand that
|
||||
each `projects` key in the business file equals the `id` in `<that project's root>/.mosaic/project.json`
|
||||
in the stack's user's clone, and that the file exists. The loader refuses a mismatch
|
||||
(packages/business/src/cli.mjs:80) at step 7, after the copy is made. The check must use only what the
|
||||
host has at that point: coreutils, grep, or the pinned node under `/opt/mosaic-stack/node-<v>/bin`
|
||||
(section 2), run as the stack's user. Give the expected output for a match and say a mismatch is a
|
||||
stop: fix the business file, not the project id (mos-1563).
|
||||
4. **The retry path after a failed first install** (mos-1565, ops-0291). Section 6 has no retry form.
|
||||
What happened on .53: a refused `case2-install.sh` left `<home>/.config/mosaic-dev/config.json` behind
|
||||
(bootstrap ran at step 1), and the first refusal also left an empty `businesses/` dir in that config
|
||||
dir. The operator then used the upgrade form's fetch, which writes the bundle to
|
||||
`refs/remotes/origin/refactor` and doesn't move `refs/heads/refactor`, so `stack-copy.sh copy` refused
|
||||
with "not reachable from refs/heads/refactor". Mos's ruling: move the clones aside and rerun section
|
||||
6's first-install form. Write that as a short "If the first install refuses" block in section 6:
|
||||
- which paths move aside: root's checkout `/root/mosaic-stack`, the stack's user's clone
|
||||
`/srv/mosaic/src/mosaic-stack`, and the stack's user's config dir. Read `scripts/case2-install.sh`
|
||||
(bootstrap at step 1, :172, :200, :210-211) and `scripts/bootstrap.sh` to list what an install
|
||||
writes before it can refuse, and name only those paths;
|
||||
- move, never delete: `mv` to a name with a UTC timestamp suffix, as root, keeping owner and mode;
|
||||
- what the operator must re-place afterwards if a credential slot under that config dir was already
|
||||
filled (section 9). If nothing in section 9 can be filled before the first install, say that;
|
||||
- then rerun the first-install block from its first line. Never use the upgrade form's fetch on a
|
||||
clone that a first install hasn't finished with.
|
||||
5. **Checking a backup's SHA256SUMS by hand** (ops-0292). The sheet has no backup check today. A backup set
|
||||
under `<dataRoot>/backups/stack/<activation>/` holds `SHA256SUMS` with paths relative to `bus/`
|
||||
(scripts/stack-release.sh:482 writes `(cd "$o/bus" && sha256sum bus.sqlite) > "$o/SHA256SUMS"`), and
|
||||
the tool checks it with `(cd "$o/bus" && sha256sum -c --quiet ../SHA256SUMS)` (:502, :2244). Running
|
||||
`sha256sum -c SHA256SUMS` from the backup dir itself fails (rc 1, "FAILED"), because `bus.sqlite` is
|
||||
in `bus/`. Add a short paragraph after the upgrade exits in section 6 that gives the `bus/` form for an
|
||||
operator who wants to check a backup before a rollback or restore. Read :475-510 to confirm the
|
||||
layout first.
|
||||
|
||||
Keep the sheet's voice: short sentences, the path and line for each claim, no em dashes. Don't renumber
|
||||
sections. Don't touch any other file except your BUILD.md.
|
||||
|
||||
**Test.** None ships. Before the review request, show that no test or script reads the sheet:
|
||||
`git grep -n host-run-sheet -- 'packages/*/tests/*' 'scripts/*'` prints nothing, with a positive control
|
||||
that the same command form finds a path you know a test reads. Also grep the tests for a 64-hex pin next
|
||||
to the sheet's name. If any test reads the sheet, stop and tell Sage.
|
||||
|
||||
**Gate.** Doc only (decision 137). No suite run is needed from you, and no CLEAR. Sage's landing gate
|
||||
runs every suite (mos-1455). The reviewer checks every new claim against the cited line.
|
||||
|
||||
**Boundaries.** No host action, no .53, .54 or 1013 contact, no credential reads, no push to refactor.
|
||||
Claimant queue moves are yours (start, in-review). Work in a new worktree off refactor at 3d2569ce2 or
|
||||
later, on branch `goldie/row-run-sheet`. Push the branch and report the tip, base and manifest to Sage
|
||||
and to the reviewer. Record choices in `agents/goldie/work/queue-<row>/BUILD.md`.
|
||||
|
||||
**Acceptance.** An operator following the sheet can see the proof file format, treats the missing
|
||||
project file warning as a stop, checks the project key before install, has a written retry path after a
|
||||
refused first install, and can check a backup by hand with the form that works. Every new claim cites a
|
||||
line that says it.
|
||||
@@ -48,6 +48,17 @@ What this piece deliberately leaves alone, and where that work goes.
|
||||
Who says yes, and on what evidence: the command to run, the output to
|
||||
expect, or the comment to read.
|
||||
|
||||
Every seat, Claude or Codex, runs the suites with a short TMPDIR:
|
||||
`export TMPDIR=$(mktemp -d /mnt/storage/scratch/tmp/XXXXXX)`, 31 bytes. A
|
||||
launch refuses when its run's `tools.sock` path passes 107 bytes
|
||||
(`packages/seat/src/session.mjs:278`). Tests build that path under TMPDIR
|
||||
and drop the launcher log, so the refusal shows only as a 20-second "no
|
||||
RESULT". `packages/cli/tests/revoke-window.test.mjs` needs TMPDIR at 35
|
||||
bytes or less. A seat-named TMPDIR such as `.../tmp/goldieXXXXXX` (37)
|
||||
fails it on any commit (row 323, mos-1561). A builder whose cli suite
|
||||
can't run says so in the row, and the cli suite counts from the lead's
|
||||
landing gate.
|
||||
|
||||
### Handoffs
|
||||
|
||||
Every seat hands off directly (DECISIONS Q210, mos-1221). When the owner
|
||||
|
||||
@@ -742,3 +742,17 @@ one base64url line of at least 32 characters, `[A-Za-z0-9_-]` only (packages/web
|
||||
exits 2 at start otherwise (:267). case2-unit.sh doesn't open the file, so a short or malformed proof fails
|
||||
only at Console start. That's what stopped Run B P1 (B2-1, ops-01's 24-byte fixture). Fix: add the format to
|
||||
the File cell of that row. Doc only, no test reads it; bound to the next doc row that edits the run sheet.
|
||||
|
||||
## Row 323: revoke-window hides a socket-path refusal as a 20-second "no RESULT"
|
||||
|
||||
Sage, for Mos's mos-1560/mos-1561. packages/cli/tests/revoke-window.test.mjs failed on every commit in
|
||||
Goldie's environment (base dd6cb5872 included) and passed in Sage's. The cause is TMPDIR length, not Codex.
|
||||
The test's root is `$TMPDIR/mosaic-revoke-window-XXXXXX`, and a run's `tools.sock` sits at
|
||||
`<root>/data/launches/acme/r<12 hex>/tools.sock`. newRun refuses past 107 bytes
|
||||
(packages/seat/src/session.mjs:278). Measured lengths: `.../tmp/sageXXXXXX` (35 bytes) gives 107 and passes,
|
||||
`.../tmp/goldieXXXXXX` (37 bytes) gives 109 and fails. Sage reproduced it with a 37-byte TMPDIR: 0/1 after
|
||||
20235 ms, the same "no RESULT". The test passes `log: () => {}` to the launcher, so the refusal never shows.
|
||||
The guard is right. The test is the weak part: its prefix leaves no margin, and a launch refusal reads as a
|
||||
timeout. Fix: shorten the prefix and fail on a launch-failed log line instead of waiting 20 s. That's an
|
||||
old-test change, so it needs Mos's go (mos-1272). Bound to the next row that edits revoke-window.test.mjs.
|
||||
Until then, docs/plans/BRIEF-TEMPLATE.md's Gate section sets TMPDIR to `mktemp -d /mnt/storage/scratch/tmp/XXXXXX`.
|
||||
+5
-4
@@ -33,7 +33,7 @@ Gaps found while working go to `docs/plans/DEFERRED.md`, not here.
|
||||
|
||||
<!-- mosaic-queue:begin -->
|
||||
|
||||
Generated from `docs/plans/queue.json` revision 3765 by `queue render`. Do not edit between the markers; change the queue with `scripts/mosaic queue`.
|
||||
Generated from `docs/plans/queue.json` revision 3788 by `queue render`. Do not edit between the markers; change the queue with `scripts/mosaic queue`.
|
||||
|
||||
| # | Piece | Owner | Issues | State | After | Gate | Brief | Note |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
@@ -78,7 +78,7 @@ Generated from `docs/plans/queue.json` revision 3765 by `queue render`. Do not e
|
||||
| 39 | Slice 1 S4: the mosaic CLI, inbox, decide, tasks, agents, trail | rocko; reviewers darkwing, filbert | #1521 | done | 37 done | filbert approves on #1521, darkwing approves the packages/discord change and the broker host; cli tests, test-discord, every test-*.sh green (sage) | `docs/plans/2026-10-04_slice-1.md` § Slice 1 S4: the `mosaic` CLI (inbox, decide, tasks, agents, trail) | Follow-ups from round 1 (sage): (a) one host that boots every business, since writer.lock allows one broker per dataRoot while the unit is per business; accepted for slice 1 with one business. (b) the queue's PATH_RE refuses '@' in manifest paths, so the S4 unit template was renamed to mosaic-bus.service.in; packages/discord/systemd/[email protected] would hit the same refusal in any future candidate manifest. |
|
||||
| 40 | Slice 1 S5: WebUI inbox, tasks, agents and trails; CHAT-03 Gate E | dewey; reviewers darkwing, filbert | #1522 | done | 39 done | darkwing and filbert approve on #1522; webui, conversation, control-board and every test-*.sh green; row 5 Gate E is Jason's (sage) | `docs/plans/2026-10-04_slice-1.md` § Slice 1 S5: WebUI (inbox, tasks, agents and trails; CHAT-03 Gate E) | — |
|
||||
| 41 | Slice 1 S6: meta-harness and launching; the PM moves off T3 | filbert; reviewers darkwing | #1523 | done | 34 done, 36 done, 38 done | darkwing approves on #1523; harness and seat tests, every test-*.sh; recorded PM launch of a coder (sage) | `docs/plans/2026-10-04_slice-1.md` § Slice 1 S6: meta-harness and launching (the PM moves off T3) | — |
|
||||
| 42 | Slice 1 S7: acceptance, five pieces in a row | sage | #1524 | in-progress | 40 done, 41 done | Jason, from the five trails: only gated decisions reached him, every step left a trail (jason) | `docs/plans/2026-10-04_slice-1.md` § Slice 1 S7: acceptance, five pieces in a row | mos-1184 (2026-10-21T16:58Z): pieces 1-5 are Jason's requests to pm through the Console or mosaic talk pm, a route Q194 suspends until Jason says the Stack is operational. The pieces are paused, the 10-23 23:59Z date is dropped and no new date is set. mos-1148's C1/C2 rules wait with them. The 10-24 window goes ahead without the pieces (sage-1622). |
|
||||
| 42 | Slice 1 S7: acceptance, five pieces in a row | sage | #1524 | in-progress | 40 done, 41 done | Jason, from the five trails: only gated decisions reached him, every step left a trail (jason) | `docs/plans/2026-10-04_slice-1.md` § Slice 1 S7: acceptance, five pieces in a row | mos-1559 (00:00Z 10-24): the 10-23 23:59Z date passed with no pieces sent. Under the mos-1148 default the unopened pieces stay unopened. New latest-by date: Vikunja 442 due 2026-10-26 18:00Z. Pieces stay paused under Q194 until Jason says the Stack is operational; Mos raises them with Jason in the 10-24 session. |
|
||||
| 43 | S2b: gated approvals are single-use (authorize records consumption) | rocko; reviewers darkwing | #1525 | done | 37 done | darkwing approves on #1525; Sage's integration gate (sage) | `docs/plans/2026-10-05_s2b-single-use-approvals.md` § S2b: gated approvals are single-use (authorize records consumption) | 2026-10-05: S2 is in the tree at 38828a2c. Rocko's README edit (ae643fba, decision 62 human-proof note) is an uncommitted change on packages/bus/README.md and belongs in this row's candidate. |
|
||||
| 44 | S2c: a within-role message cites a decision without consuming it | rocko; reviewers darkwing | #1526 | done | 43 done | darkwing approves on #1526; Sage's integration gate (sage) | `docs/plans/2026-10-05_s2c-message-decision-citation.md` § S2c: a within-role message cites a decision without consuming it | — |
|
||||
| 45 | S4 follow-up: notifier retry limit, journal types and the round 2 test gaps | rocko; reviewers darkwing, filbert | #1527 | done | — | filbert and darkwing approve on #1527; every node package suite and every test-*.sh green on Sage's gate rerun (sage) | `docs/plans/2026-10-09_s4-follow-up-and-cohort.md` § S4 follow-up: notifier retry limit, journal types and the round 2 test gaps | lead decision 72; follows row 39 (2f5303c1) |
|
||||
@@ -359,8 +359,9 @@ Generated from `docs/plans/queue.json` revision 3765 by `queue render`. Do not e
|
||||
| 320 | Case 1/2 host fixes for the 433 P1 rerun: hostcheck item 5 mode 750, run sheet bundle verify, case2-unit --claude @, stack-copy unreadable cwd, notify.json step and precheck, mosaic wrapper step (10-27 path) | gladstone; reviewers morgana | #1816 | required; done | — | Morgana approves. Touched test files run whole (case1-hostcheck, case2-unit, case2-install, stack-copy) and the 8 named mutants are killed; Sage's full landing gate runs every package suite and every scripts/test-*.sh. After landing Mos sends ops-01 the new N. (sage) | `docs/plans/2026-10-23_host-fixes-1816-brief.md` § Brief: hostcheck item 5, bundle verify, case2-unit @, stack-copy cwd | LANDED 094124b4e 21:53Z (replay of faf55618e onto 45e3ca7bd, code paths patch-id equal, log appends in one final commit). F1-F6 hostcheck item 5, bundle verify, case2-unit scoped paths and cwd, precheck notify decision, run sheet 9a notify dirs 0700, wrapper step. Morgana APPROVE r2 c31403 rev 3715. Full gate GREEN 21:53:00Z, 0 fail (mos-1453/1455/1456). Vikunja 614 done. 433 P1 reruns on this N (mos-1532). |
|
||||
| 321 | F7, F10, F11: run sheet wrapper step umask and node PATH under sudo; adopt step after a first install; --no-rehearsal on the first upgrade; wait for healthz 200 before a precheck (433, ops-0271/0274/0275); docs only | gladstone; reviewers boyd | #1817 | required; done | — | Boyd (Codex) approves. A git grep with a positive control shows no test reads the changed run sheet lines; BUILD.md records the diff and the grep. Doc only, lands with any row (mos-1544). (sage) | `docs/plans/2026-10-23_host-fixes-1816-brief.md` § Brief: F7, the wrapper step's PATH and umask under sudo | LANDED 5283d3745, 3fd5307e1, 3569bf54f: cherry-picks of b5d4d7903, d9400f0d3, 9aae73ade with equal patch-ids. Doc only. Pushed tip 3569bf54f 23:32:54Z 10-23. Boyd APPROVE c31450 rev 3757, grep with positive control in BUILD.md. Rode the full gate GREEN 23:32:28Z on 3569bf54f. Live unchanged 45dca69f6. |
|
||||
| 322 | F8/F9: case2-install.sh runs the library persona import from the stack copy, and its refusal carries the error line (433 Run B, ops-0273) | goldie; reviewers morgana | #1818 | required; done | — | Morgana approves (install path). The new case2-install test fails on 094124b4e and passes on the candidate; a mutant restoring REPO at :185 is killed; Sage's full landing gate runs every package suite and scripts/test-*.sh. Then ops-01 reruns Run B P1. (sage) | `docs/plans/2026-10-23_host-fixes-1816-brief.md` § Brief: F8 and F9, the library persona import runs without the clone's node_modules | LANDED 467b05fe2, cherry-pick of 7b4ea4217 on 860afc2b6; added and removed lines identical, patch-id differs only by BUILD-LOG/SESSIONS append context (both sides kept). Pushed with tip 3569bf54f 23:32:54Z 10-23. Morgana APPROVE c31451 rev 3758, N1-N3 non-blocking. Sage full gate GREEN 23:32:28Z on 3569bf54f, all suites 0 fail, every scripts/test-*.sh rc 0. Next: ops-01 Run B P1 rerun on 467b05fe2 (mos-1553). Live unchanged 45dca69f6. |
|
||||
| 323 | Case 1 factory business file for .53: docs/guides/case1-business.mosaic-stack.json plus a loader and factory-check test (#414 stage 2, ops-0277) | goldie; reviewers duckworth | #1819 | in-progress | — | Sage full gate: all package suites except queue plus every scripts/test-*.sh (mos-1455); Duckworth review; new test file and one bus.tester mutant on CLEAR (sage) | `docs/plans/2026-10-23_case1-business-file-brief.md` § Brief: the Case 1 factory business file | — |
|
||||
| 324 | Tracker optional in the business file (Q247) | goldie; reviewers duckworth | #1820 | in-progress | — | New tests only. Absent tracker: loader and validate pass, the bus boots with no credential-file error and no @sync. Present tracker unchanged. A mutant that skips the presence check is killed. Then whole touched test files, then the full landing gate. (sage) | `docs/plans/2026-10-23_optional-tracker-brief.md` § Brief: tracker becomes optional in the business file | — |
|
||||
| 323 | Case 1 factory business file for .53: docs/guides/case1-business.mosaic-stack.json plus a loader and factory-check test (#414 stage 2, ops-0277) | goldie; reviewers duckworth | #1819 | done | — | Sage full gate: all package suites except queue plus every scripts/test-*.sh (mos-1455); Duckworth review; new test file and one bus.tester mutant on CLEAR (sage) | `docs/plans/2026-10-23_case1-business-file-brief.md` § Brief: the Case 1 factory business file | LANDED 571376c30 + evidence 6a0f0f4f8 (ff from dd6cb5872), tip 99930362e. Duckworth approve c31477 rev 3778 on 6a0f0f4f8. Sage gate green 00:54Z on 6a0f0f4f8: all package suites 0 fail, every scripts/test-*.sh rc 0. Root cause of the revoke-window red: TMPDIR length, tools.sock over 107 bytes (session.mjs:278). Brief template rule and DEFERRED entry in c25799928 (mos-1561). N1/N2 doc only. |
|
||||
| 324 | Tracker optional in the business file (Q247) | goldie; reviewers duckworth | #1820 | done | — | New tests only. Absent tracker: loader and validate pass, the bus boots with no credential-file error and no @sync. Present tracker unchanged. A mutant that skips the presence check is killed. Then whole touched test files, then the full landing gate. (sage) | `docs/plans/2026-10-23_optional-tracker-brief.md` § Brief: tracker becomes optional in the business file | LANDED 7e59ba8b3 + evidence dbdfcfd52 (ff from 3569bf54f), pushed with tip dd6cb5872 00:06:58Z 10-24. Duckworth APPROVE c31470 rev 3770 (static, N1 doc only). Goldie: new 5/5 and 4/4, business.test 21/21, presence mutant killed. Sage full gate GREEN 00:05:49Z on 64ea54083: every package suite 0 fail 0 skip (business 254, cli 986, bus 924, plugins 119, webui 411), every scripts/test-*.sh rc 0. Live unchanged 45dca69f6. Run B N1 for ops-01 P2/P3 (mos-1558). |
|
||||
| 325 | Case 1 business file: re-key project to stack to match .mosaic/project.json, plus a project-file test (#414 stage 2, ops-0288, mos-1563) | goldie; reviewers gladstone | #1821 | in-review, round 1, request posted | — | New test file and packages/business whole on CLEAR; one key mutant killed; Gladstone review; Sage full landing gate (mos-1455) (sage) | `docs/plans/2026-10-24_case1-project-key-brief.md` § Brief: re-key the Case 1 project to stack | — |
|
||||
|
||||
<!-- mosaic-queue:end -->
|
||||
|
||||
|
||||
+766
-17
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 1,
|
||||
"canonicalRoot": "/mnt/storage/src/mosaic-stack",
|
||||
"revision": 3765,
|
||||
"revision": 3788,
|
||||
"rows": [
|
||||
{
|
||||
"id": 1,
|
||||
@@ -2549,10 +2549,10 @@
|
||||
"seat": "sage",
|
||||
"op": "sage-42-start-q118"
|
||||
},
|
||||
"note": "mos-1184 (2026-10-21T16:58Z): pieces 1-5 are Jason's requests to pm through the Console or mosaic talk pm, a route Q194 suspends until Jason says the Stack is operational. The pieces are paused, the 10-23 23:59Z date is dropped and no new date is set. mos-1148's C1/C2 rules wait with them. The 10-24 window goes ahead without the pieces (sage-1622).",
|
||||
"note": "mos-1559 (00:00Z 10-24): the 10-23 23:59Z date passed with no pieces sent. Under the mos-1148 default the unopened pieces stay unopened. New latest-by date: Vikunja 442 due 2026-10-26 18:00Z. Pieces stay paused under Q194 until Jason says the Stack is operational; Mos raises them with Jason in the 10-24 session.",
|
||||
"blockedReason": null,
|
||||
"createdAt": "2026-10-05T02:51:49.652Z",
|
||||
"updatedAt": "2026-10-21T16:58:26.582Z",
|
||||
"updatedAt": "2026-10-24T00:01:20.990Z",
|
||||
"updatedBy": "sage"
|
||||
},
|
||||
{
|
||||
@@ -32646,7 +32646,7 @@
|
||||
"closes": [
|
||||
1819
|
||||
],
|
||||
"state": "in-progress",
|
||||
"state": "done",
|
||||
"previousState": null,
|
||||
"required": false,
|
||||
"requiredSince": null,
|
||||
@@ -32661,16 +32661,70 @@
|
||||
"reviewers": [
|
||||
"duckworth"
|
||||
],
|
||||
"review": null,
|
||||
"claim": {
|
||||
"seat": "goldie",
|
||||
"op": "goldie-0323-start-1"
|
||||
"review": {
|
||||
"rounds": [
|
||||
{
|
||||
"n": 1,
|
||||
"op": "goldie-0323-review-1",
|
||||
"by": "goldie",
|
||||
"at": "2026-10-24T00:39:25.465Z",
|
||||
"issue": 1819,
|
||||
"candidate": {
|
||||
"kind": "commit",
|
||||
"digest": "6a0f0f4f8065c2171044f28c309dba4602414397",
|
||||
"text": null
|
||||
},
|
||||
"request": "comment",
|
||||
"attempts": [
|
||||
{
|
||||
"op": "goldie-0323-review-1",
|
||||
"by": "goldie",
|
||||
"at": "2026-10-24T00:39:25.465Z",
|
||||
"state": "failed",
|
||||
"comment": null,
|
||||
"transport": {
|
||||
"outcome": "failed",
|
||||
"status": null,
|
||||
"comment": null,
|
||||
"detail": "pre-send: the credential or account check failed"
|
||||
},
|
||||
"resolutions": []
|
||||
},
|
||||
{
|
||||
"op": "sage-3871",
|
||||
"by": "sage",
|
||||
"at": "2026-10-24T00:42:07.619Z",
|
||||
"state": "posted",
|
||||
"comment": 31475,
|
||||
"transport": {
|
||||
"outcome": "posted",
|
||||
"status": 201,
|
||||
"comment": 31475,
|
||||
"detail": "created"
|
||||
},
|
||||
"resolutions": []
|
||||
}
|
||||
],
|
||||
"duplicateRisk": false,
|
||||
"receipts": [
|
||||
{
|
||||
"reviewer": "duckworth",
|
||||
"op": "duckworth-323-verdict-r1",
|
||||
"at": "2026-10-24T00:44:15.270Z",
|
||||
"verdict": "approve",
|
||||
"comment": 31477,
|
||||
"candidate": "6a0f0f4f8065c2171044f28c309dba4602414397"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"note": null,
|
||||
"claim": null,
|
||||
"note": "LANDED 571376c30 + evidence 6a0f0f4f8 (ff from dd6cb5872), tip 99930362e. Duckworth approve c31477 rev 3778 on 6a0f0f4f8. Sage gate green 00:54Z on 6a0f0f4f8: all package suites 0 fail, every scripts/test-*.sh rc 0. Root cause of the revoke-window red: TMPDIR length, tools.sock over 107 bytes (session.mjs:278). Brief template rule and DEFERRED entry in c25799928 (mos-1561). N1/N2 doc only.",
|
||||
"blockedReason": null,
|
||||
"createdAt": "2026-10-23T22:50:41.494Z",
|
||||
"updatedAt": "2026-10-23T23:03:02.369Z",
|
||||
"updatedBy": "goldie"
|
||||
"updatedAt": "2026-10-24T01:00:54.851Z",
|
||||
"updatedBy": "sage"
|
||||
},
|
||||
{
|
||||
"id": 324,
|
||||
@@ -32682,7 +32736,7 @@
|
||||
"closes": [
|
||||
1820
|
||||
],
|
||||
"state": "in-progress",
|
||||
"state": "done",
|
||||
"previousState": null,
|
||||
"required": false,
|
||||
"requiredSince": null,
|
||||
@@ -32697,16 +32751,163 @@
|
||||
"reviewers": [
|
||||
"duckworth"
|
||||
],
|
||||
"review": null,
|
||||
"review": {
|
||||
"rounds": [
|
||||
{
|
||||
"n": 1,
|
||||
"op": "goldie-0324-review-1",
|
||||
"by": "goldie",
|
||||
"at": "2026-10-23T23:46:09.632Z",
|
||||
"issue": 1820,
|
||||
"candidate": {
|
||||
"kind": "commit",
|
||||
"digest": "dbdfcfd52caccee915c25c9979771fcb233d6139",
|
||||
"text": null
|
||||
},
|
||||
"request": "comment",
|
||||
"attempts": [
|
||||
{
|
||||
"op": "goldie-0324-review-1",
|
||||
"by": "goldie",
|
||||
"at": "2026-10-23T23:46:09.632Z",
|
||||
"state": "failed",
|
||||
"comment": null,
|
||||
"transport": {
|
||||
"outcome": "failed",
|
||||
"status": null,
|
||||
"comment": null,
|
||||
"detail": "pre-send: the credential or account check failed"
|
||||
},
|
||||
"resolutions": []
|
||||
},
|
||||
{
|
||||
"op": "sage-3852",
|
||||
"by": "sage",
|
||||
"at": "2026-10-23T23:48:13.612Z",
|
||||
"state": "posted",
|
||||
"comment": 31469,
|
||||
"transport": {
|
||||
"outcome": "posted",
|
||||
"status": 201,
|
||||
"comment": 31469,
|
||||
"detail": "created"
|
||||
},
|
||||
"resolutions": []
|
||||
}
|
||||
],
|
||||
"duplicateRisk": false,
|
||||
"receipts": [
|
||||
{
|
||||
"reviewer": "duckworth",
|
||||
"op": "duckworth-324-verdict-r1",
|
||||
"at": "2026-10-23T23:53:49.526Z",
|
||||
"verdict": "approve",
|
||||
"comment": 31470,
|
||||
"candidate": "dbdfcfd52caccee915c25c9979771fcb233d6139"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"claim": null,
|
||||
"note": "LANDED 7e59ba8b3 + evidence dbdfcfd52 (ff from 3569bf54f), pushed with tip dd6cb5872 00:06:58Z 10-24. Duckworth APPROVE c31470 rev 3770 (static, N1 doc only). Goldie: new 5/5 and 4/4, business.test 21/21, presence mutant killed. Sage full gate GREEN 00:05:49Z on 64ea54083: every package suite 0 fail 0 skip (business 254, cli 986, bus 924, plugins 119, webui 411), every scripts/test-*.sh rc 0. Live unchanged 45dca69f6. Run B N1 for ops-01 P2/P3 (mos-1558).",
|
||||
"blockedReason": null,
|
||||
"createdAt": "2026-10-23T23:12:07.446Z",
|
||||
"updatedAt": "2026-10-24T00:08:57.664Z",
|
||||
"updatedBy": "sage"
|
||||
},
|
||||
{
|
||||
"id": 325,
|
||||
"piece": "Case 1 business file: re-key project to stack to match .mosaic/project.json, plus a project-file test (#414 stage 2, ops-0288, mos-1563)",
|
||||
"owner": "goldie",
|
||||
"issues": [
|
||||
1821
|
||||
],
|
||||
"closes": [
|
||||
1821
|
||||
],
|
||||
"state": "in-review",
|
||||
"previousState": null,
|
||||
"required": false,
|
||||
"requiredSince": null,
|
||||
"gate": "New test file and packages/business whole on CLEAR; one key mutant killed; Gladstone review; Sage full landing gate (mos-1455)",
|
||||
"gateOwner": "sage",
|
||||
"brief": {
|
||||
"path": "docs/plans/2026-10-24_case1-project-key-brief.md",
|
||||
"anchor": "Brief: re-key the Case 1 project to stack",
|
||||
"blob": "8d91484c6731c7f7ceaf197a483c95024dee7448"
|
||||
},
|
||||
"after": [],
|
||||
"reviewers": [
|
||||
"gladstone"
|
||||
],
|
||||
"review": {
|
||||
"rounds": [
|
||||
{
|
||||
"n": 1,
|
||||
"op": "goldie-0325-review-1",
|
||||
"by": "goldie",
|
||||
"at": "2026-10-24T01:18:04.364Z",
|
||||
"issue": 1821,
|
||||
"candidate": {
|
||||
"kind": "commit",
|
||||
"digest": "c5f96836d5ffa6cc0160da7360321706bef86a17",
|
||||
"text": null
|
||||
},
|
||||
"request": "comment",
|
||||
"attempts": [
|
||||
{
|
||||
"op": "goldie-0325-review-1",
|
||||
"by": "goldie",
|
||||
"at": "2026-10-24T01:18:04.364Z",
|
||||
"state": "failed",
|
||||
"comment": null,
|
||||
"transport": {
|
||||
"outcome": "failed",
|
||||
"status": null,
|
||||
"comment": null,
|
||||
"detail": "pre-send: the credential or account check failed"
|
||||
},
|
||||
"resolutions": []
|
||||
},
|
||||
{
|
||||
"op": "sage-3884",
|
||||
"by": "sage",
|
||||
"at": "2026-10-24T01:20:34.463Z",
|
||||
"state": "posted",
|
||||
"comment": 31482,
|
||||
"transport": {
|
||||
"outcome": "posted",
|
||||
"status": 201,
|
||||
"comment": 31482,
|
||||
"detail": "created"
|
||||
},
|
||||
"resolutions": []
|
||||
}
|
||||
],
|
||||
"duplicateRisk": false,
|
||||
"receipts": [
|
||||
{
|
||||
"reviewer": "gladstone",
|
||||
"op": "gladstone-325-verdict-r1",
|
||||
"at": "2026-10-24T01:22:42.822Z",
|
||||
"verdict": "approve",
|
||||
"comment": 31484,
|
||||
"candidate": "c5f96836d5ffa6cc0160da7360321706bef86a17"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"claim": {
|
||||
"seat": "goldie",
|
||||
"op": "goldie-0324-start-1"
|
||||
"op": "goldie-0325-start-1"
|
||||
},
|
||||
"note": null,
|
||||
"blockedReason": null,
|
||||
"createdAt": "2026-10-23T23:12:07.446Z",
|
||||
"updatedAt": "2026-10-23T23:15:58.025Z",
|
||||
"updatedBy": "goldie"
|
||||
"createdAt": "2026-10-24T01:09:57.971Z",
|
||||
"updatedAt": "2026-10-24T01:22:42.822Z",
|
||||
"updatedBy": "gladstone"
|
||||
}
|
||||
],
|
||||
"log": [
|
||||
@@ -131357,6 +131558,554 @@
|
||||
"receipt": "ok sage-3847 rev 3765 row 321 in-review→done round 1"
|
||||
},
|
||||
"viewSha": "a0c965a44bbfae9c701bb3edef7dae7e3b76110bd834db6a412b6bc2891d0708"
|
||||
},
|
||||
{
|
||||
"rev": 3766,
|
||||
"op": "goldie-0324-review-1",
|
||||
"verb": "move",
|
||||
"args": {
|
||||
"id": 324,
|
||||
"to": "in-review",
|
||||
"reason": null,
|
||||
"candidate": "dbdfcfd52caccee915c25c9979771fcb233d6139",
|
||||
"evidence": null,
|
||||
"issue": null
|
||||
},
|
||||
"by": "goldie",
|
||||
"at": "2026-10-23T23:46:09.632Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 324,
|
||||
"from": "in-progress",
|
||||
"to": "in-review",
|
||||
"round": 1,
|
||||
"issue": 1820,
|
||||
"candidate": {
|
||||
"kind": "commit",
|
||||
"digest": "dbdfcfd52caccee915c25c9979771fcb233d6139",
|
||||
"text": null
|
||||
},
|
||||
"receipt": "ok goldie-0324-review-1 rev 3766 row 324 in-progress→in-review round 1 on #1820; request goldie-0324-review-1 requesting"
|
||||
},
|
||||
"viewSha": "a7f646d3c079b4791bf24687f6dd9c0779bfd24804f52caa94a1b472a21d9f3d"
|
||||
},
|
||||
{
|
||||
"rev": 3767,
|
||||
"op": "goldie-0324-review-1.outcome",
|
||||
"verb": "review-outcome",
|
||||
"args": {
|
||||
"id": 324,
|
||||
"attempt": "goldie-0324-review-1",
|
||||
"outcome": "failed",
|
||||
"status": null,
|
||||
"comment": null,
|
||||
"detail": "pre-send: the credential or account check failed"
|
||||
},
|
||||
"by": "goldie",
|
||||
"at": "2026-10-23T23:47:00.333Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 324,
|
||||
"round": 1,
|
||||
"attempt": "goldie-0324-review-1",
|
||||
"state": "failed",
|
||||
"receipt": "ok goldie-0324-review-1.outcome rev 3767 row 324 round 1 request goldie-0324-review-1 failed"
|
||||
},
|
||||
"viewSha": "6302ae6f4c2eb7c16d3ab4b7f76c71c7f95fac2d7512588f15bb3ab37cd05ad7"
|
||||
},
|
||||
{
|
||||
"rev": 3768,
|
||||
"op": "sage-3852",
|
||||
"verb": "review-request",
|
||||
"args": {
|
||||
"id": 324
|
||||
},
|
||||
"by": "sage",
|
||||
"at": "2026-10-23T23:48:13.612Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 324,
|
||||
"round": 1,
|
||||
"attempt": "sage-3852",
|
||||
"state": "requesting",
|
||||
"receipt": "ok sage-3852 rev 3768 row 324 round 1 request sage-3852 requesting"
|
||||
},
|
||||
"viewSha": "abb1f0659abe54b896133bdda093a9d01e0a5bc8048f7b6022da19d4f7d8ac0e"
|
||||
},
|
||||
{
|
||||
"rev": 3769,
|
||||
"op": "sage-3852.outcome",
|
||||
"verb": "review-outcome",
|
||||
"args": {
|
||||
"id": 324,
|
||||
"attempt": "sage-3852",
|
||||
"outcome": "posted",
|
||||
"status": 201,
|
||||
"comment": 31469,
|
||||
"detail": "created"
|
||||
},
|
||||
"by": "sage",
|
||||
"at": "2026-10-23T23:49:03.433Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 324,
|
||||
"round": 1,
|
||||
"attempt": "sage-3852",
|
||||
"state": "posted",
|
||||
"receipt": "ok sage-3852.outcome rev 3769 row 324 round 1 request sage-3852 posted comment 31469"
|
||||
},
|
||||
"viewSha": "6850671baeae0739abb6bc9518722ffa91c9c1efd32e6a07dafe6703e29ecfc0"
|
||||
},
|
||||
{
|
||||
"rev": 3770,
|
||||
"op": "duckworth-324-verdict-r1",
|
||||
"verb": "review-record",
|
||||
"args": {
|
||||
"id": 324,
|
||||
"verdict": "approve",
|
||||
"comment": 31470,
|
||||
"candidate": "dbdfcfd52caccee915c25c9979771fcb233d6139"
|
||||
},
|
||||
"by": "duckworth",
|
||||
"at": "2026-10-23T23:53:49.526Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 324,
|
||||
"round": 1,
|
||||
"verdict": "approve",
|
||||
"receipt": "ok duckworth-324-verdict-r1 rev 3770 row 324 round 1 approve by duckworth (comment 31470)"
|
||||
},
|
||||
"viewSha": "02bd1d4e4b3c98af3517d3d621d8435790cd70c393d3fc988d206eed49e1ae6e"
|
||||
},
|
||||
{
|
||||
"rev": 3771,
|
||||
"op": "sage-3855",
|
||||
"verb": "note",
|
||||
"args": {
|
||||
"id": 42,
|
||||
"text": "mos-1559 (00:00Z 10-24): the 10-23 23:59Z date passed with no pieces sent. Under the mos-1148 default the unopened pieces stay unopened. New latest-by date: Vikunja 442 due 2026-10-26 18:00Z. Pieces stay paused under Q194 until Jason says the Stack is operational; Mos raises them with Jason in the 10-24 session."
|
||||
},
|
||||
"by": "sage",
|
||||
"at": "2026-10-24T00:01:20.990Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 42,
|
||||
"field": "note",
|
||||
"receipt": "ok sage-3855 rev 3771 row 42 note"
|
||||
},
|
||||
"viewSha": "a397c2a14b85071a6c6ef17f8c6ec79e45135a8d34229fe8b8039aa90fa07595"
|
||||
},
|
||||
{
|
||||
"rev": 3772,
|
||||
"op": "sage-3856",
|
||||
"verb": "note",
|
||||
"args": {
|
||||
"id": 324,
|
||||
"text": "LANDED 7e59ba8b3 + evidence dbdfcfd52 (ff from 3569bf54f), pushed with tip dd6cb5872 00:06:58Z 10-24. Duckworth APPROVE c31470 rev 3770 (static, N1 doc only). Goldie: new 5/5 and 4/4, business.test 21/21, presence mutant killed. Sage full gate GREEN 00:05:49Z on 64ea54083: every package suite 0 fail 0 skip (business 254, cli 986, bus 924, plugins 119, webui 411), every scripts/test-*.sh rc 0. Live unchanged 45dca69f6. Run B N1 for ops-01 P2/P3 (mos-1558)."
|
||||
},
|
||||
"by": "sage",
|
||||
"at": "2026-10-24T00:08:03.422Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 324,
|
||||
"field": "note",
|
||||
"receipt": "ok sage-3856 rev 3772 row 324 note"
|
||||
},
|
||||
"viewSha": "b3cc913bb83c54bd8fe8ac9b6ffcb76a1dcc9805171cb7e81a056016db9babf0"
|
||||
},
|
||||
{
|
||||
"rev": 3773,
|
||||
"op": "sage-3857",
|
||||
"verb": "move",
|
||||
"args": {
|
||||
"id": 324,
|
||||
"to": "done",
|
||||
"reason": null,
|
||||
"candidate": null,
|
||||
"evidence": null,
|
||||
"issue": null
|
||||
},
|
||||
"by": "sage",
|
||||
"at": "2026-10-24T00:08:57.664Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 324,
|
||||
"from": "in-review",
|
||||
"to": "done",
|
||||
"round": 1,
|
||||
"issue": null,
|
||||
"candidate": null,
|
||||
"receipt": "ok sage-3857 rev 3773 row 324 in-review→done round 1"
|
||||
},
|
||||
"viewSha": "27401b14330cff219803f2767761922a8082b7575dbcd873406da14865fe2f5a"
|
||||
},
|
||||
{
|
||||
"rev": 3774,
|
||||
"op": "goldie-0323-review-1",
|
||||
"verb": "move",
|
||||
"args": {
|
||||
"id": 323,
|
||||
"to": "in-review",
|
||||
"reason": null,
|
||||
"candidate": "6a0f0f4f8065c2171044f28c309dba4602414397",
|
||||
"evidence": null,
|
||||
"issue": null
|
||||
},
|
||||
"by": "goldie",
|
||||
"at": "2026-10-24T00:39:25.465Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 323,
|
||||
"from": "in-progress",
|
||||
"to": "in-review",
|
||||
"round": 1,
|
||||
"issue": 1819,
|
||||
"candidate": {
|
||||
"kind": "commit",
|
||||
"digest": "6a0f0f4f8065c2171044f28c309dba4602414397",
|
||||
"text": null
|
||||
},
|
||||
"receipt": "ok goldie-0323-review-1 rev 3774 row 323 in-progress→in-review round 1 on #1819; request goldie-0323-review-1 requesting"
|
||||
},
|
||||
"viewSha": "e5443f2cffe39261fc91f3aaf6d9966778a9f6e75d8c6c99793eb9cdbd27a092"
|
||||
},
|
||||
{
|
||||
"rev": 3775,
|
||||
"op": "goldie-0323-review-1.outcome",
|
||||
"verb": "review-outcome",
|
||||
"args": {
|
||||
"id": 323,
|
||||
"attempt": "goldie-0323-review-1",
|
||||
"outcome": "failed",
|
||||
"status": null,
|
||||
"comment": null,
|
||||
"detail": "pre-send: the credential or account check failed"
|
||||
},
|
||||
"by": "goldie",
|
||||
"at": "2026-10-24T00:40:43.664Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 323,
|
||||
"round": 1,
|
||||
"attempt": "goldie-0323-review-1",
|
||||
"state": "failed",
|
||||
"receipt": "ok goldie-0323-review-1.outcome rev 3775 row 323 round 1 request goldie-0323-review-1 failed"
|
||||
},
|
||||
"viewSha": "1fb5e5dec6a0c383d06ddd9a2d1f775706fef061c549ab9b3a6a7c875fadf320"
|
||||
},
|
||||
{
|
||||
"rev": 3776,
|
||||
"op": "sage-3871",
|
||||
"verb": "review-request",
|
||||
"args": {
|
||||
"id": 323
|
||||
},
|
||||
"by": "sage",
|
||||
"at": "2026-10-24T00:42:07.619Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 323,
|
||||
"round": 1,
|
||||
"attempt": "sage-3871",
|
||||
"state": "requesting",
|
||||
"receipt": "ok sage-3871 rev 3776 row 323 round 1 request sage-3871 requesting"
|
||||
},
|
||||
"viewSha": "7f80ed332d55d7bdeef940273c50cbd9683a420d418e23ef0151f573793dfc47"
|
||||
},
|
||||
{
|
||||
"rev": 3777,
|
||||
"op": "sage-3871.outcome",
|
||||
"verb": "review-outcome",
|
||||
"args": {
|
||||
"id": 323,
|
||||
"attempt": "sage-3871",
|
||||
"outcome": "posted",
|
||||
"status": 201,
|
||||
"comment": 31475,
|
||||
"detail": "created"
|
||||
},
|
||||
"by": "sage",
|
||||
"at": "2026-10-24T00:43:04.250Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 323,
|
||||
"round": 1,
|
||||
"attempt": "sage-3871",
|
||||
"state": "posted",
|
||||
"receipt": "ok sage-3871.outcome rev 3777 row 323 round 1 request sage-3871 posted comment 31475"
|
||||
},
|
||||
"viewSha": "6c034db294c905c3735ad996fdaf28da3ec1fbee48d9d70aeb122745ffdda1f3"
|
||||
},
|
||||
{
|
||||
"rev": 3778,
|
||||
"op": "duckworth-323-verdict-r1",
|
||||
"verb": "review-record",
|
||||
"args": {
|
||||
"id": 323,
|
||||
"verdict": "approve",
|
||||
"comment": 31477,
|
||||
"candidate": "6a0f0f4f8065c2171044f28c309dba4602414397"
|
||||
},
|
||||
"by": "duckworth",
|
||||
"at": "2026-10-24T00:44:15.270Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 323,
|
||||
"round": 1,
|
||||
"verdict": "approve",
|
||||
"receipt": "ok duckworth-323-verdict-r1 rev 3778 row 323 round 1 approve by duckworth (comment 31477)"
|
||||
},
|
||||
"viewSha": "f776c94408564a716ca7efbc70ad058cb2b8704d4c8a6dfedb66beee4810f2c1"
|
||||
},
|
||||
{
|
||||
"rev": 3779,
|
||||
"op": "sage-3873",
|
||||
"verb": "note",
|
||||
"args": {
|
||||
"id": 323,
|
||||
"text": "LANDED 571376c30 + evidence 6a0f0f4f8 (ff from dd6cb5872), tip 99930362e. Duckworth approve c31477 rev 3778 on 6a0f0f4f8. Sage gate green 00:54Z on 6a0f0f4f8: all package suites 0 fail, every scripts/test-*.sh rc 0. Root cause of the revoke-window red: TMPDIR length, tools.sock over 107 bytes (session.mjs:278). Brief template rule and DEFERRED entry in c25799928 (mos-1561). N1/N2 doc only."
|
||||
},
|
||||
"by": "sage",
|
||||
"at": "2026-10-24T01:00:03.010Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 323,
|
||||
"field": "note",
|
||||
"receipt": "ok sage-3873 rev 3779 row 323 note"
|
||||
},
|
||||
"viewSha": "3bdae29d120d73111f7a31bd3a2457c9916b14ff609bff845d037b65da3cbce9"
|
||||
},
|
||||
{
|
||||
"rev": 3780,
|
||||
"op": "sage-3874",
|
||||
"verb": "move",
|
||||
"args": {
|
||||
"id": 323,
|
||||
"to": "done",
|
||||
"reason": null,
|
||||
"candidate": null,
|
||||
"evidence": null,
|
||||
"issue": null
|
||||
},
|
||||
"by": "sage",
|
||||
"at": "2026-10-24T01:00:54.851Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 323,
|
||||
"from": "in-review",
|
||||
"to": "done",
|
||||
"round": 1,
|
||||
"issue": null,
|
||||
"candidate": null,
|
||||
"receipt": "ok sage-3874 rev 3780 row 323 in-review→done round 1"
|
||||
},
|
||||
"viewSha": "f3418b4c61363a419ac03600b3feb6cc9a4bbce4405e8980bf2787801672809f"
|
||||
},
|
||||
{
|
||||
"rev": 3781,
|
||||
"op": "sage-3879",
|
||||
"verb": "add",
|
||||
"args": {
|
||||
"piece": "Case 1 business file: re-key project to stack to match .mosaic/project.json, plus a project-file test (#414 stage 2, ops-0288, mos-1563)",
|
||||
"gate": "New test file and packages/business whole on CLEAR; one key mutant killed; Gladstone review; Sage full landing gate (mos-1455)",
|
||||
"brief": "docs/plans/2026-10-24_case1-project-key-brief.md#Brief: re-key the Case 1 project to stack",
|
||||
"issues": [
|
||||
1821
|
||||
],
|
||||
"note": null,
|
||||
"owner": "goldie",
|
||||
"gateOwner": "sage",
|
||||
"after": null,
|
||||
"reviewers": [
|
||||
"gladstone"
|
||||
],
|
||||
"required": null
|
||||
},
|
||||
"by": "sage",
|
||||
"at": "2026-10-24T01:09:57.971Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 325,
|
||||
"from": null,
|
||||
"to": "queued",
|
||||
"brief": {
|
||||
"path": "docs/plans/2026-10-24_case1-project-key-brief.md",
|
||||
"anchor": "Brief: re-key the Case 1 project to stack",
|
||||
"blob": "8d91484c6731c7f7ceaf197a483c95024dee7448"
|
||||
},
|
||||
"receipt": "ok sage-3879 rev 3781 row 325 none→queued"
|
||||
},
|
||||
"viewSha": "c59e1401c4689a96fa467c787bd9691b729eda3e2af4679188235eeb6d233094"
|
||||
},
|
||||
{
|
||||
"rev": 3782,
|
||||
"op": "sage-3880",
|
||||
"verb": "move",
|
||||
"args": {
|
||||
"id": 325,
|
||||
"to": "briefed",
|
||||
"reason": null,
|
||||
"candidate": null,
|
||||
"evidence": null,
|
||||
"issue": null
|
||||
},
|
||||
"by": "sage",
|
||||
"at": "2026-10-24T01:10:53.568Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 325,
|
||||
"from": "queued",
|
||||
"to": "briefed",
|
||||
"round": null,
|
||||
"issue": null,
|
||||
"candidate": null,
|
||||
"receipt": "ok sage-3880 rev 3782 row 325 queued→briefed"
|
||||
},
|
||||
"viewSha": "41afd165d5a71f6af386de17bf8f6e9c6fe107dbe99956b5f7a86731fa361228"
|
||||
},
|
||||
{
|
||||
"rev": 3783,
|
||||
"op": "goldie-0325-start-1",
|
||||
"verb": "move",
|
||||
"args": {
|
||||
"id": 325,
|
||||
"to": "in-progress",
|
||||
"reason": null,
|
||||
"candidate": null,
|
||||
"evidence": null,
|
||||
"issue": null
|
||||
},
|
||||
"by": "goldie",
|
||||
"at": "2026-10-24T01:16:58.930Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 325,
|
||||
"from": "briefed",
|
||||
"to": "in-progress",
|
||||
"round": null,
|
||||
"issue": null,
|
||||
"candidate": null,
|
||||
"receipt": "ok goldie-0325-start-1 rev 3783 row 325 briefed→in-progress"
|
||||
},
|
||||
"viewSha": "d768e4bf91d9908b5697649267a4393f7c4c7a9db0d05ec377906e318d522d87"
|
||||
},
|
||||
{
|
||||
"rev": 3784,
|
||||
"op": "goldie-0325-review-1",
|
||||
"verb": "move",
|
||||
"args": {
|
||||
"id": 325,
|
||||
"to": "in-review",
|
||||
"reason": null,
|
||||
"candidate": "c5f96836d5ffa6cc0160da7360321706bef86a17",
|
||||
"evidence": null,
|
||||
"issue": null
|
||||
},
|
||||
"by": "goldie",
|
||||
"at": "2026-10-24T01:18:04.364Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 325,
|
||||
"from": "in-progress",
|
||||
"to": "in-review",
|
||||
"round": 1,
|
||||
"issue": 1821,
|
||||
"candidate": {
|
||||
"kind": "commit",
|
||||
"digest": "c5f96836d5ffa6cc0160da7360321706bef86a17",
|
||||
"text": null
|
||||
},
|
||||
"receipt": "ok goldie-0325-review-1 rev 3784 row 325 in-progress→in-review round 1 on #1821; request goldie-0325-review-1 requesting"
|
||||
},
|
||||
"viewSha": "23408b96e46a3d6f5f80470448c475622261ebc99fa6ed3cd3d0339e79f4e69b"
|
||||
},
|
||||
{
|
||||
"rev": 3785,
|
||||
"op": "goldie-0325-review-1.outcome",
|
||||
"verb": "review-outcome",
|
||||
"args": {
|
||||
"id": 325,
|
||||
"attempt": "goldie-0325-review-1",
|
||||
"outcome": "failed",
|
||||
"status": null,
|
||||
"comment": null,
|
||||
"detail": "pre-send: the credential or account check failed"
|
||||
},
|
||||
"by": "goldie",
|
||||
"at": "2026-10-24T01:19:00.773Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 325,
|
||||
"round": 1,
|
||||
"attempt": "goldie-0325-review-1",
|
||||
"state": "failed",
|
||||
"receipt": "ok goldie-0325-review-1.outcome rev 3785 row 325 round 1 request goldie-0325-review-1 failed"
|
||||
},
|
||||
"viewSha": "a098b29709b88484d98350f44750a37a621751a982c47fe0863ae9b6c938d224"
|
||||
},
|
||||
{
|
||||
"rev": 3786,
|
||||
"op": "sage-3884",
|
||||
"verb": "review-request",
|
||||
"args": {
|
||||
"id": 325
|
||||
},
|
||||
"by": "sage",
|
||||
"at": "2026-10-24T01:20:34.463Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 325,
|
||||
"round": 1,
|
||||
"attempt": "sage-3884",
|
||||
"state": "requesting",
|
||||
"receipt": "ok sage-3884 rev 3786 row 325 round 1 request sage-3884 requesting"
|
||||
},
|
||||
"viewSha": "44e292c9544263d2093cacc4f643bf452c9bdd21c72c64d0ac5a169cb8d900e8"
|
||||
},
|
||||
{
|
||||
"rev": 3787,
|
||||
"op": "sage-3884.outcome",
|
||||
"verb": "review-outcome",
|
||||
"args": {
|
||||
"id": 325,
|
||||
"attempt": "sage-3884",
|
||||
"outcome": "posted",
|
||||
"status": 201,
|
||||
"comment": 31482,
|
||||
"detail": "created"
|
||||
},
|
||||
"by": "sage",
|
||||
"at": "2026-10-24T01:21:30.422Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 325,
|
||||
"round": 1,
|
||||
"attempt": "sage-3884",
|
||||
"state": "posted",
|
||||
"receipt": "ok sage-3884.outcome rev 3787 row 325 round 1 request sage-3884 posted comment 31482"
|
||||
},
|
||||
"viewSha": "25a06deff4a77b1cf1a889e39edfbdc2e73a9dd3f93653e4b91b0e6400549348"
|
||||
},
|
||||
{
|
||||
"rev": 3788,
|
||||
"op": "gladstone-325-verdict-r1",
|
||||
"verb": "review-record",
|
||||
"args": {
|
||||
"id": 325,
|
||||
"verdict": "approve",
|
||||
"comment": 31484,
|
||||
"candidate": "c5f96836d5ffa6cc0160da7360321706bef86a17"
|
||||
},
|
||||
"by": "gladstone",
|
||||
"at": "2026-10-24T01:22:42.822Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 325,
|
||||
"round": 1,
|
||||
"verdict": "approve",
|
||||
"receipt": "ok gladstone-325-verdict-r1 rev 3788 row 325 round 1 approve by gladstone (comment 31484)"
|
||||
},
|
||||
"viewSha": "422f9329c44229356521749dc31d56b1130fbee51659c18a58657003cf4880a5"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -67,7 +67,7 @@ test("row 323: committed Case 1 document loads unchanged through the business lo
|
||||
assert.equal(b.tracker, null);
|
||||
assert.equal(Object.hasOwn(document(), "tracker"), false);
|
||||
assert.equal(Object.hasOwn(document(), "library"), false);
|
||||
assert.deepEqual(b.projects, { "mosaic-stack": { root: "/srv/mosaic/src/mosaic-stack" } });
|
||||
assert.deepEqual(b.projects, { "stack": { root: "/srv/mosaic/src/mosaic-stack" } });
|
||||
assert.deepEqual(Object.keys(b.roles), ["chief", "assistant"]);
|
||||
assert.deepEqual(b.plugins.harness, ["claude-code"]);
|
||||
assert.equal(b.launch.by, "chief");
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// Row 325 (#1821): factory validation must load the repository's actual project document.
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { REPO, REPO_ROLES, systemConfig, tmp, writeJson } from "./helpers.mjs";
|
||||
|
||||
const CASE1 = join(REPO, "docs/guides/case1-business.mosaic-stack.json");
|
||||
const PROJECT = join(REPO, ".mosaic/project.json");
|
||||
const document = () => JSON.parse(readFileSync(CASE1, "utf8"));
|
||||
|
||||
function fixture(t, { oldKey = false } = {}) {
|
||||
const root = tmp();
|
||||
t.after(() => rmSync(root, { recursive: true, force: true }));
|
||||
const doc = document();
|
||||
assert.equal(Object.keys(doc.projects).length, 1);
|
||||
const [key] = Object.keys(doc.projects);
|
||||
const projectRoot = join(root, "checkout");
|
||||
const projectFile = join(projectRoot, ".mosaic/project.json");
|
||||
const bytes = readFileSync(PROJECT);
|
||||
mkdirSync(join(projectRoot, ".mosaic"), { recursive: true });
|
||||
writeFileSync(projectFile, bytes);
|
||||
assert.deepEqual(readFileSync(projectFile), bytes, "fixture must use the real project's unmodified bytes");
|
||||
const entry = { ...doc.projects[key], root: projectRoot };
|
||||
doc.projects = { [oldKey ? "mosaic-stack" : key]: entry };
|
||||
const config = systemConfig(root);
|
||||
writeJson(join(root, "config/businesses/mosaic-stack.json"), doc);
|
||||
return {
|
||||
project: JSON.parse(bytes), projectFile,
|
||||
validate: () => spawnSync(process.execPath, [join(REPO, "packages/business/src/cli.mjs"), "validate", "mosaic-stack"], {
|
||||
env: { ...process.env, HOME: root, MOSAIC_CONFIG: config, MOSAIC_ROLES_DIR: REPO_ROLES },
|
||||
encoding: "utf8", timeout: 30000,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
test("row 325: the Case 1 project's sole key matches the repository project id", () => {
|
||||
const doc = document();
|
||||
const project = JSON.parse(readFileSync(PROJECT, "utf8"));
|
||||
assert.equal(project.id, "stack");
|
||||
assert.deepEqual(Object.keys(doc.projects), [project.id]);
|
||||
assert.equal(doc.id, "mosaic-stack");
|
||||
assert.equal(doc.projects[project.id].root, "/srv/mosaic/src/mosaic-stack");
|
||||
});
|
||||
|
||||
test("row 325: real business validate reports the copied repository project valid", (t) => {
|
||||
const x = fixture(t);
|
||||
const r = x.validate();
|
||||
assert.equal(r.status, 0, r.stdout + r.stderr);
|
||||
const result = JSON.parse(r.stdout);
|
||||
assert.equal(result.business, "mosaic-stack");
|
||||
assert.deepEqual(result.projects, { [x.project.id]: "valid" });
|
||||
assert.doesNotMatch(r.stderr, /no project file|project variables/);
|
||||
});
|
||||
|
||||
test("row 325: the old mosaic-stack project key refuses against those same project bytes", (t) => {
|
||||
const x = fixture(t, { oldKey: true });
|
||||
const r = x.validate();
|
||||
assert.equal(r.status, 2, r.stdout + r.stderr);
|
||||
assert.equal(r.stdout, "");
|
||||
assert.match(r.stderr, /has id stack, but business mosaic-stack declares it as mosaic-stack/);
|
||||
assert.ok(r.stderr.includes(x.projectFile));
|
||||
assert.doesNotMatch(r.stderr, /no project file/);
|
||||
});
|
||||
@@ -9,6 +9,9 @@ const VERBS = new Set([
|
||||
"thread.messages",
|
||||
"thread.send",
|
||||
"thread.open",
|
||||
"thread.shares",
|
||||
"thread.share",
|
||||
"thread.unshare",
|
||||
"session.list",
|
||||
"session.create",
|
||||
"session.clear",
|
||||
|
||||
@@ -1091,3 +1091,16 @@ During Q175's read-only interim both buttons show as unavailable and point to
|
||||
`mosaic halt` / `mosaic resume` in the instance CLI. There is no halt-only send
|
||||
exception or alternate authentication path. Landing the UI and activating it
|
||||
remain separately gated.
|
||||
|
||||
Row 208 (193d): Chat includes a **Shared with me** list. Shared threads are
|
||||
read-only, including for the business owner. On your own writable threads,
|
||||
**Share read-only** accepts `user:ID`, the business owner's name, or `*` for
|
||||
everyone with coverage; **Unshare** ends that target's share. A share grants
|
||||
no project or instance access of its own.
|
||||
|
||||
Viewers can read shared threads and their old own threads. Their pages omit
|
||||
creation, composer, resume, share and session mutation controls. Mixed grants
|
||||
apply separately to each project and instance; a collaborator grant elsewhere
|
||||
does not enable writes here. Grant hints refresh on each user chat render and
|
||||
background refresh, while the bus decides every operation from live grants.
|
||||
The existing user notice names the governance records visible to the owner.
|
||||
@@ -18,6 +18,16 @@ export function consoleAccess({ load, business }) {
|
||||
} catch { return null; }
|
||||
};
|
||||
return Object.freeze({
|
||||
// UI hints only: no grants belonging to another user, receipts or subjects escape.
|
||||
chatGrants: session => {
|
||||
try {
|
||||
if (lookup(session.provider, session.sub) !== session.user) return [];
|
||||
const doc = load(), b = business();
|
||||
return doc.grants.filter(g => g.user === session.user && g.business === b &&
|
||||
covers(g, { business: b, project: g.projects[0], instance: g.instances[0] }, new Date(), { level: 'collaborator' }))
|
||||
.map(g => ({ projects: [...g.projects], instances: [...g.instances] }));
|
||||
} catch { return []; }
|
||||
},
|
||||
// The user id this (provider, sub) signs in as, or null.
|
||||
find: ({ provider, sub }) => lookup(provider, sub),
|
||||
// Is this session's user still the holder of its own subject, with a live grant?
|
||||
|
||||
@@ -22,6 +22,7 @@ const STATUS = {
|
||||
'unknown-verb': 503, 'channel-operation': 503,
|
||||
'tester-off': 403, 'tester-required': 403, 'tester-refused': 403,
|
||||
// FED R6 (#1800): the broker's refusals of a granted user.
|
||||
'share-not-covered': 403, 'share-exists': 409, 'share-not-found': 404,
|
||||
'access-refused': 403, 'access-revoked': 401,
|
||||
};
|
||||
function safeMessage(code) {
|
||||
@@ -72,7 +73,7 @@ export async function chatHTTP(req, res, { path, search, chat, named, send, prev
|
||||
if (one(req, 'sec-fetch-site') !== 'same-origin') return refuse(403, 'cross-site');
|
||||
}
|
||||
try {
|
||||
const match = /^\/api\/chat\/threads\/([^/]+)\/(messages|open|sessions|clear|cancel|operation)$/.exec(path);
|
||||
const match = /^\/api\/chat\/threads\/([^/]+)\/(messages|open|sessions|clear|cancel|operation|shares|share|unshare)$/.exec(path);
|
||||
if (match) {
|
||||
thread = match[1];
|
||||
if (!id(thread)) throw Error();
|
||||
@@ -89,6 +90,9 @@ export async function chatHTTP(req, res, { path, search, chat, named, send, prev
|
||||
if (!exact(v, ['project', 'name', 'instance'], ['project', 'name']) || !id(v.project) || !name(v.name) ||
|
||||
(Object.hasOwn(v, 'instance') && (typeof v.instance !== 'string' || !KEY.test(v.instance)))) throw Error();
|
||||
action = 'createThread'; args = v;
|
||||
} else if (['share', 'unshare'].includes(match?.[2])) {
|
||||
if (!exact(v, ['to']) || typeof v.to !== 'string' || !v.to.length || v.to.length > 200 || /[\r\n\0]/.test(v.to)) throw Error();
|
||||
action = match[2]; args = { thread, to: v.to };
|
||||
} else if (match?.[2] === 'sessions') {
|
||||
if (!exact(v, ['name', 'op']) || !name(v.name) || !KEY.test(v.op ?? '')) throw Error();
|
||||
action = 'createSession'; args = { thread, name: v.name, op: v.op };
|
||||
@@ -109,9 +113,12 @@ export async function chatHTTP(req, res, { path, search, chat, named, send, prev
|
||||
} else if (path === '/api/chat/projects') {
|
||||
query(search, []); action = 'projects'; args = {};
|
||||
} else if (path === '/api/chat/threads') {
|
||||
const q = query(search, ['project']);
|
||||
const q = query(search, ['project', 'shared']);
|
||||
if (q.has('shared') && q.get('shared') !== 'true') throw Error();
|
||||
if (q.has('project') && !id(q.get('project'))) throw Error();
|
||||
action = 'threads'; args = q.has('project') ? { project: q.get('project') } : {};
|
||||
action = 'threads'; args = { ...(q.has('project') ? { project: q.get('project') } : {}), ...(q.has('shared') ? { shared: true } : {}) };
|
||||
} else if (match?.[2] === 'shares') {
|
||||
query(search, []); action = 'shares'; args = { thread };
|
||||
} else if (match?.[2] === 'sessions') {
|
||||
query(search, []); action = 'sessions'; args = { thread };
|
||||
} else if (match?.[2] === 'operation') {
|
||||
@@ -155,6 +162,13 @@ export async function chatHTTP(req, res, { path, search, chat, named, send, prev
|
||||
} : {};
|
||||
return json(res, 200, { rows: value, ...page, at: new Date().toISOString() });
|
||||
}
|
||||
if (action === 'share' || action === 'unshare') {
|
||||
if (value?.thread !== thread || value?.to !== args.to || value?.op !== action) {
|
||||
line('error=outcome-unknown'); return refuse(503, 'outcome-unknown', 'unknown');
|
||||
}
|
||||
line(`thread=${thread} op=${action}`);
|
||||
return json(res, 200, { thread, to: value.to, op: value.op });
|
||||
}
|
||||
if (!value || (action === 'open' ? value.opened !== thread : !id(value.id)) ||
|
||||
(action === 'createThread' && (typeof value.instance !== 'string' || !KEY.test(value.instance)))) {
|
||||
line('error=outcome-unknown');
|
||||
|
||||
@@ -24,7 +24,10 @@ export function createChat({ call, owner = null }) {
|
||||
return Object.freeze({
|
||||
projects: () => channel('project.list', {}),
|
||||
createProject: ({ name, repo }) => channel('project.create', { name, ...(repo === undefined ? {} : { repo }) }),
|
||||
threads: ({ project } = {}) => channel('thread.list', project === undefined ? {} : { project }),
|
||||
threads: ({ project, shared } = {}) => channel('thread.list', { ...(project === undefined ? {} : { project }), ...(shared === true ? { shared: true } : {}) }),
|
||||
shares: ({ thread }) => channel('thread.shares', { thread }),
|
||||
share: ({ thread, to }) => channel('thread.share', { thread, to }),
|
||||
unshare: ({ thread, to }) => channel('thread.unshare', { thread, to }),
|
||||
createThread: ({ project, name, instance }) => channel('thread.create', { project, name, ...(instance === undefined ? {} : { instance }) }),
|
||||
sessions: ({ thread }) => channel('session.list', { thread }),
|
||||
createSession: ({ thread, name, op }) => channel('session.create', { thread, name, op }),
|
||||
|
||||
@@ -1232,7 +1232,7 @@
|
||||
window.MosaicChat?.leave(path);
|
||||
if (path === '/chat' || path.startsWith('/chat/')) {
|
||||
// Row 177 (#1675): the tester Console writes to its own threads.
|
||||
await window.MosaicChat.render({ path, container: $('bus-view'), canSend: canSend || isTester, navigated });
|
||||
await window.MosaicChat.render({ path, container: $('bus-view'), canSend: canSend || isTester, tester: isTester, navigated });
|
||||
if (g !== gen || out()) return;
|
||||
shown = route; schedule(); return;
|
||||
}
|
||||
|
||||
@@ -57,23 +57,35 @@
|
||||
const disabledSession = pending => disabled(pending || selectedInactive());
|
||||
const disabled = pending => !current.canSend || out() || pending ? ' disabled' : '';
|
||||
const stamp = at => `<time>${txt(at)}</time>`;
|
||||
// Per-grant UI hints; all writes still use the caller's bus principal.
|
||||
const scopeWrites = (project, instance) => !current.user || current.chatGrants.some(g =>
|
||||
(g.projects.includes('*') || g.projects.includes(project)) &&
|
||||
(instance === undefined || g.instances.includes('*') || g.instances.includes(instance)));
|
||||
const threadWrites = () => !!current.thread && !current.shared && scopeWrites(current.thread?.project, current.thread?.instance);
|
||||
function sharing(t) {
|
||||
if (!current.canSend || !threadWrites() || current.tester) return '';
|
||||
const f = form('share:' + t.id);
|
||||
return `<section aria-label="Thread sharing"><h2>Share read-only</h2><p>Share with a user (user:id), the business owner's name, or * for everyone granted on this thread.</p><ul>${(current.shares ?? []).map(r => `<li>${txt(r.to)} <button class="btn" type="button" data-unshare="${esc(r.to)}"${disabled(f.pending)}>Unshare</button></li>`).join('')}</ul><form id="chat-share-form"><label>Share with<input id="chat-share-target" value="${esc(f.to ?? '')}" maxlength="200" required></label><button class="btn" type="submit"${disabled(f.pending)}>Share read-only</button></form><p role="status">${txt(f.receipt)}</p></section>`;
|
||||
}
|
||||
function creation(kind, key) {
|
||||
const f = form(key), project = kind === 'project';
|
||||
if (current.user && (project || !scopeWrites(key))) return '';
|
||||
const repo = project && current.repos.length ? `<label>Repository (optional)<select id="chat-repo" data-chat-field="repo"><option value="">No repository link</option>${current.repos.map(r => `<option value="${esc(r)}"${r === f.repo ? ' selected' : ''}>${txt(r)}</option>`).join('')}</select></label>` : '';
|
||||
const choices = current.instances ?? [];
|
||||
const choices = (current.instances ?? []).filter(r => scopeWrites(key, r.instance));
|
||||
const picker = project ? '' : `<label>Thread with<select id="chat-instance" data-chat-field="instance" required${disabled(f.pending || !!current.instanceError || !choices.length)}><option value="">Choose who this thread is with</option>${choices.map(row => `<option value="${esc(row.instance)}"${row.instance === f.instance ? ' selected' : ''}>${txt(row.name === row.instance ? row.instance : `${row.name} (${row.instance})`)}</option>`).join('')}</select></label>`;
|
||||
const unavailable = !project && (!!current.instanceError || !choices.some(row => row.instance === f.instance));
|
||||
const status = !project && current.instanceError ? current.instanceError : f.receipt || (!project && !choices.length ? 'No thread instances are configured.' : '');
|
||||
return `<form id="chat-${kind}-form" class="chat-create" data-key="${esc(key)}"><h2>New ${kind}</h2><label>Name<input id="chat-${kind}-name" data-chat-field="name" value="${esc(f.name)}" maxlength="200" required${disabled(f.pending)}></label>${repo}${picker}<button class="btn" type="submit"${disabled(f.pending || unavailable)}>${f.pending ? 'Creating…' : `Create ${kind}`}</button><p role="status">${txt(status)}</p></form>`;
|
||||
}
|
||||
function threads(rows) {
|
||||
return rows.length ? `<ul class="chat-thread-list">${rows.map(t => `<li>${link('threads', t.id, t.name)} <span class="muted">${txt(t.instance)}</span></li>`).join('')}</ul>` : '<p>No threads yet.</p>';
|
||||
return rows.length ? `<ul class="chat-thread-list">${rows.map(t => `<li>${link('threads', t.id, t.name)} <span class="muted">${txt(t.instance)}${t.shared ? ` · Shared by ${txt(t.owner)}` : ''}</span></li>`).join('')}</ul>` : '<p>No threads yet.</p>';
|
||||
}
|
||||
const sessionPath = (thread, session) => `#/chat/threads/${thread}/sessions/${session}`;
|
||||
const countLabel = value => !value ? 'unavailable (count collector not installed)' :
|
||||
value.known === true && Number.isSafeInteger(value.count) && value.count >= 0 ? String(value.count) :
|
||||
`unknown (${value.reason ?? 'not-collected'})`;
|
||||
function sessionControls(t) {
|
||||
if (!threadWrites()) return '<p>Read-only thread.</p>';
|
||||
if (current.legacyDefault) return '<p role="status">Sessions are not available for this account.</p>';
|
||||
const c = current, selected = c.sessions.find(r => r.id === c.session), f = form('session:' + t.id);
|
||||
const clear = form('clear:' + c.key);
|
||||
@@ -84,7 +96,7 @@
|
||||
const s = state(current.key);
|
||||
const messages = s.rows.map(m => `<li data-message="${esc(m.id)}"><div class="chat-message-meta"><strong>${txt(m.from_role)}</strong> ${stamp(m.at)}</div><div class="chat-body">${txt(m.body)}</div></li>`).join('');
|
||||
const receipt = s.receipt;
|
||||
return `<section class="chat-conversation" data-thread="${esc(t.id)}" data-session="${esc(current.session)}"><p>${link('projects', t.project, current.projects.find(p => p.id === t.project)?.name ?? 'Project')} / ${txt(t.instance)}</p>${heading(t.name)}${sessionControls(t)}<p class="muted">Reading never wakes the assistant. An accepted message is not an answer.</p><button class="btn" type="button" id="chat-resume"${disabledSession(s.pending)}>Resume assistant</button><ol class="chat-log" tabindex="0" aria-label="Thread conversation">${messages || '<li class="muted">No messages yet.</li>'}</ol><div class="chat-paging">${s.more ? '<p>Showing earlier history. Read forward to reach the current conversation.</p><button class="btn" type="button" id="chat-next">Read next messages</button>' : '<p class="muted">Caught up to the last read.</p>'}<button class="btn" type="button" id="chat-new"${s.follow ? ' hidden' : ''}>New messages — go to end</button></div><form id="chat-send-form"><label for="chat-body">Message this thread</label><textarea id="chat-body" rows="4"${disabledSession(s.pending)}>${esc(s.draft)}</textarea><button class="btn primary" type="submit"${disabledSession(s.pending)}>Send</button></form><p id="chat-receipt" role="status">${txt(receipt)}</p></section>`;
|
||||
return `<section class="chat-conversation" data-thread="${esc(t.id)}" data-session="${esc(current.session)}"><p>${link('projects', t.project, current.projects.find(p => p.id === t.project)?.name ?? 'Project')} / ${txt(t.instance)}</p>${heading(t.name)}${sessionControls(t)}${sharing(t)}<p class="muted">Reading never wakes the assistant. An accepted message is not an answer.</p>${threadWrites() ? `<button class="btn" type="button" id="chat-resume"${disabledSession(s.pending)}>Resume assistant</button>` : ''}<ol class="chat-log" tabindex="0" aria-label="Thread conversation">${messages || '<li class="muted">No messages yet.</li>'}</ol><div class="chat-paging">${s.more ? '<p>Showing earlier history. Read forward to reach the current conversation.</p><button class="btn" type="button" id="chat-next">Read next messages</button>' : '<p class="muted">Caught up to the last read.</p>'}<button class="btn" type="button" id="chat-new"${s.follow ? ' hidden' : ''}>New messages — go to end</button></div>${threadWrites() ? `<form id="chat-send-form"><label for="chat-body">Message this thread</label><textarea id="chat-body" rows="4"${disabledSession(s.pending)}>${esc(s.draft)}</textarea><button class="btn primary" type="submit"${disabledSession(s.pending)}>Send</button></form>` : ''}<p id="chat-receipt" role="status">${txt(receipt)}</p></section>`;
|
||||
}
|
||||
function capture() {
|
||||
if (!current) return null;
|
||||
@@ -98,16 +110,16 @@
|
||||
let html = '<div class="chat-area"><p><a href="#/chat">Chat projects</a></p>';
|
||||
if (c.error) html += `<p class="chat-error banner err" role="alert">${txt(c.error)}</p>`;
|
||||
if (c.kind === 'thread') {
|
||||
const t = c.threads.find(t => t.id === c.id);
|
||||
const t = c.thread;
|
||||
if (t && (c.session || c.legacyDefault) && !c.refused) html += conversation(t);
|
||||
else html += heading('Thread unavailable');
|
||||
} else if (c.kind === 'project') {
|
||||
const p = c.projects.find(p => p.id === c.id);
|
||||
html += heading(p?.name ?? 'Project unavailable');
|
||||
if (p && !c.refused) html += threads(c.threads) + creation('thread', c.id);
|
||||
if (p && !c.refused) html += threads(c.threads.filter(t => !t.shared)) + '<h2>Shared with me</h2>' + threads(c.threads.filter(t => t.shared)) + creation('thread', c.id);
|
||||
} else {
|
||||
html += heading('Chat') + '<p>Business projects and your named assistant threads.</p><div class="chat-projects">' +
|
||||
c.projects.map(p => `<section class="panel"><h2>${link('projects', p.id, p.name)}</h2>${p.repo ? `<p class="muted">Repository: ${txt(p.repo)}</p>` : ''}${threads(c.threads.filter(t => t.project === p.id))}</section>`).join('') + (c.projects.length ? '' : '<p>No projects yet.</p>') + '</div>' + creation('project', 'projects');
|
||||
c.projects.map(p => `<section class="panel"><h2>${link('projects', p.id, p.name)}</h2>${p.repo ? `<p class="muted">Repository: ${txt(p.repo)}</p>` : ''}${threads(c.threads.filter(t => t.project === p.id && !t.shared))}</section>`).join('') + (c.projects.length ? '' : '<p>No projects yet.</p>') + '</div><h2>Shared with me</h2>' + threads(c.threads.filter(t => t.shared)) + creation('project', 'projects');
|
||||
}
|
||||
if (!c.canSend) html += '<p class="muted">Creating and sending require signed-in send mode. This Console only reads.</p>';
|
||||
c.container.innerHTML = html + '</div>';
|
||||
@@ -132,7 +144,7 @@
|
||||
err.code === 'thread-not-found' ? 'Thread not found.' :
|
||||
err.code === 'session-not-found' ? 'Session not found in this thread.' : 'Chat could not be read. Refresh to try again; your draft is kept.';
|
||||
}
|
||||
async function render({ path, container, canSend, navigated = false, next = false }) {
|
||||
async function render({ path, container, canSend, user = false, tester = false, navigated = false, next = false }) {
|
||||
if (out()) return;
|
||||
const match = /^\/chat(?:\/(projects|threads)\/([0-9a-f-]+)(?:\/sessions\/([0-9a-f-]+))?)?$/.exec(path);
|
||||
controller?.abort(); controller = new AbortController();
|
||||
@@ -144,23 +156,46 @@
|
||||
paint(); return;
|
||||
}
|
||||
const old = current;
|
||||
current = { container, kind, id, canSend, projects: old?.projects ?? [], threads: old?.threads ?? [], repos: old?.repos ?? [], instances: old?.instances ?? [], instanceError: kind === 'project' ? 'Reading thread instances…' : '', error: '', refused: false, legacyDefault: false, sessions: old && old.id === id ? old.sessions ?? [] : [], session: match[3] ?? (old && old.id === id ? old.session : null), key: match[3] ? id + '/' + match[3] : old && old.id === id ? old.key : id };
|
||||
current = { container, kind, id, canSend, user, tester, chatGrants: [], thread: old && old.id === id ? old.thread : null, shared: old && old.id === id ? old.shared : false, projects: old?.projects ?? [], threads: old?.threads ?? [], repos: old?.repos ?? [], instances: old?.instances ?? [], instanceError: kind === 'project' ? 'Reading thread instances…' : '', error: '', refused: false, legacyDefault: false, sessions: old && old.id === id ? old.sessions ?? [] : [], session: match[3] ?? (old && old.id === id ? old.session : null), key: match[3] ? id + '/' + match[3] : old && old.id === id ? old.key : id };
|
||||
if (navigated || !old) container.innerHTML = '<p class="chat-loading" role="status">Reading chat…</p>';
|
||||
try {
|
||||
if (user) {
|
||||
const config = await fetch('/api/config', { cache: 'no-store', redirect: 'manual', signal });
|
||||
if (config.type === 'opaqueredirect' || config.status === 401) { signOut(); return; }
|
||||
if (!config.ok) throw new ChatError('invalid-response');
|
||||
const hints = await config.json();
|
||||
if (g !== generation || out()) return;
|
||||
current.chatGrants = Array.isArray(hints.chatGrants) ? hints.chatGrants : [];
|
||||
current.canSend = hints.send === true;
|
||||
}
|
||||
// Resolve the explicit session before touching history/cursors. Missing selectors never
|
||||
// silently fall back to a sibling. The saved default is used only for thread-only routes.
|
||||
const [projects, listed, catalog] = await Promise.all([
|
||||
const [projects, listed, shared] = await Promise.all([
|
||||
request('projects', undefined, signal),
|
||||
request('threads' + (kind === 'project' ? '?project=' + id : ''), undefined, signal),
|
||||
kind === 'thread' ? request(`threads/${id}/sessions`, undefined, signal).catch(err => {
|
||||
// A denied optional catalog does not revoke an existing default-thread grant.
|
||||
// Explicit selectors and every other failure keep their refusal/error state.
|
||||
if (err.code === 'access-refused' && !match[3]) return null;
|
||||
throw err;
|
||||
}) : null,
|
||||
tester ? { rows: [] } : request('threads?shared=true' + (kind === 'project' ? '&project=' + id : ''), undefined, signal).catch(err => {
|
||||
if (['signed-out', 'access-revoked'].includes(err.code)) throw err;
|
||||
return { rows: [] };
|
||||
}),
|
||||
]);
|
||||
if (g !== generation || out()) return;
|
||||
current.projects = projects.rows; current.threads = listed.rows;
|
||||
current.projects = projects.rows;
|
||||
const own = new Set(listed.rows.map(t => t.id));
|
||||
current.threads = [...listed.rows, ...shared.rows.filter(t => !own.has(t.id)).map(t => ({ ...t, shared: true }))];
|
||||
current.thread = current.threads.find(t => t.id === id);
|
||||
current.shared = current.thread?.shared === true;
|
||||
const catalog = kind === 'thread' && !current.shared ? await request(`threads/${id}/sessions`, undefined, signal).catch(err => {
|
||||
if (err.code === 'access-refused' && !match[3]) return null;
|
||||
throw err;
|
||||
}) : null;
|
||||
if (g !== generation || out()) return;
|
||||
if (current.shared && match[3]) throw new ChatError('session-not-found', 404);
|
||||
current.shares = [];
|
||||
if (kind === 'thread' && current.canSend && threadWrites() && !tester) {
|
||||
const shares = await request(`threads/${id}/shares`, undefined, signal).catch(() => ({ rows: [] }));
|
||||
if (g !== generation || out()) return;
|
||||
current.shares = shares.rows;
|
||||
}
|
||||
let s = null, page = null, after = null;
|
||||
if (kind === 'thread' && catalog === null) {
|
||||
current.legacyDefault = true; current.sessions = []; current.session = null;
|
||||
@@ -208,13 +243,13 @@
|
||||
if (g !== generation || out()) return;
|
||||
if (!listed.rows.every(row => typeof row?.instance === 'string' && /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,159}$/.test(row.instance) && typeof row.name === 'string'))
|
||||
throw new ChatError('invalid-response');
|
||||
current.instances = listed.rows; current.instanceError = '';
|
||||
current.instances = listed.rows.filter(r => scopeWrites(id, r.instance)); current.instanceError = '';
|
||||
const f = form(id);
|
||||
if (f.instance && !listed.rows.some(row => row.instance === f.instance)) {
|
||||
if (f.instance && !current.instances.some(row => row.instance === f.instance)) {
|
||||
f.instance = ''; f.instanceChosen = true;
|
||||
f.receipt = 'The selected instance is no longer available. Choose who this thread is with.';
|
||||
}
|
||||
if (!f.instanceChosen && !f.instance && listed.rows.length === 1) f.instance = listed.rows[0].instance;
|
||||
if (!f.instanceChosen && !f.instance && current.instances.length === 1) f.instance = current.instances[0].instance;
|
||||
} catch (err) {
|
||||
if (err.name === 'AbortError' || out() || g !== generation) return;
|
||||
current.instances = [];
|
||||
@@ -244,7 +279,7 @@
|
||||
for (const key of forms.keys()) if (key.includes(id)) forms.delete(key);
|
||||
}
|
||||
if (err.status === 403 || err.status === 404) {
|
||||
current.projects = []; current.threads = []; current.refused = true;
|
||||
current.projects = []; current.threads = []; current.thread = null; current.shared = false; current.refused = true;
|
||||
}
|
||||
paint(navigated);
|
||||
}
|
||||
@@ -258,6 +293,7 @@
|
||||
document.addEventListener('input', event => {
|
||||
if (!current || out()) return;
|
||||
if (event.target.id === 'chat-operation') { const f = form('clear:' + current.key); if (f.pending) return; f.op = event.target.value; f.record = null; const button = document.getElementById('chat-cancel'); if (button) button.disabled = !current.canSend || f.pending || !f.op; }
|
||||
if (event.target.id === 'chat-share-target') form('share:' + current.id).to = event.target.value;
|
||||
if (event.target.id === 'chat-body') state(current.key ?? current.id).draft = event.target.value;
|
||||
const f = event.target.closest('.chat-create');
|
||||
if (f && event.target.dataset.chatField) {
|
||||
@@ -275,6 +311,8 @@
|
||||
async function write(action, key, value, path) {
|
||||
if (!current || !current.canSend || out()) return;
|
||||
const isThread = action === 'send' || action === 'open';
|
||||
if ((isThread || action === 'createSession') && !threadWrites()) return;
|
||||
if (action === 'createProject' && current.user || action === 'createThread' && !scopeWrites(value.project, value.instance)) return;
|
||||
const targetThread = current.id;
|
||||
const s = isThread ? state(key) : form(key);
|
||||
if (s.pending || isThread && selectedInactive()) return;
|
||||
@@ -314,7 +352,9 @@
|
||||
document.addEventListener('submit', event => {
|
||||
if (!current || !event.target.id.startsWith('chat-')) return;
|
||||
event.preventDefault();
|
||||
if (event.target.id === 'chat-send-form') {
|
||||
if (event.target.id === 'chat-share-form') {
|
||||
void changeShare('share', form('share:' + current.id).to ?? '');
|
||||
} else if (event.target.id === 'chat-send-form') {
|
||||
const id = current.id, session = current.session, key = current.key;
|
||||
void write('send', key, { body: state(key).draft, ...(session ? { session } : {}) }, `threads/${id}/messages`);
|
||||
} else if (event.target.id === 'chat-session-form') {
|
||||
@@ -336,6 +376,7 @@
|
||||
});
|
||||
document.addEventListener('click', event => {
|
||||
if (!current || out()) return;
|
||||
if (event.target.dataset.unshare) void changeShare('unshare', event.target.dataset.unshare);
|
||||
if (event.target.id === 'chat-resume') void write('open', current.key, current.session ? { session: current.session } : {}, `threads/${current.id}/open`);
|
||||
if (event.target.id === 'chat-cancel') void cancelClear();
|
||||
if (event.target.id === 'chat-clear') void clearContext();
|
||||
@@ -347,6 +388,23 @@
|
||||
location.hash = sessionPath(current.id, event.target.value);
|
||||
}
|
||||
});
|
||||
async function changeShare(op, to) {
|
||||
if (!current?.canSend || !threadWrites() || current.tester || out()) return;
|
||||
const id = current.id, f = form('share:' + id);
|
||||
if (f.pending || !to.trim()) return;
|
||||
f.pending = true; f.receipt = ''; paint();
|
||||
try {
|
||||
const r = await request(`threads/${id}/${op}`, { to });
|
||||
if (r.thread !== id || r.to !== to || r.op !== op) throw new ChatError('outcome-unknown');
|
||||
f.receipt = op === 'share' ? 'Shared read-only.' : 'Share ended.';
|
||||
if (op === 'share') f.to = '';
|
||||
} catch (e) {
|
||||
f.receipt = e.sent === false ? `Share refused (${inert(e.code)}).` : 'Outcome unknown. Read shares before trying again; nothing was retried.';
|
||||
} finally {
|
||||
f.pending = false;
|
||||
if (!out() && current?.id === id) { await refresh(); paint(); }
|
||||
}
|
||||
}
|
||||
const clearText = r => r.state === 'cancelled' ? `Operation ${r.op}: ${r.retired === true ? 'startup cancelled; old session remains retired' : 'replacement cancelled; old session active'}. Cancelled inputs are not replayed.` : `Operation ${r.op}: ${r.state}; successor ${r.successor}; start ${r.start ?? 'pending'}${r.refusal ? '; ' + r.refusal : ''}. No stop is forced.`;
|
||||
async function checkClear(thread, key, f) {
|
||||
if (!/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,159}$/.test(f.op ?? '')) { f.receipt = 'Enter a valid operation key.'; paint(); return; }
|
||||
@@ -361,7 +419,7 @@
|
||||
}
|
||||
async function cancelClear() {
|
||||
const c = current, key = c.key, f = form('clear:' + key);
|
||||
if (!c.canSend || out() || f.pending || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,159}$/.test(f.op ?? '')) return;
|
||||
if (!c.canSend || !threadWrites() || out() || f.pending || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,159}$/.test(f.op ?? '')) return;
|
||||
if (!window.confirm(`Cancel clear operation ${f.op}? Before retirement this restores the old session to active. After retirement it only cancels successor startup. Cancelled messages are never replayed, and nothing is un-retired.`)) return;
|
||||
const op = f.op;
|
||||
f.pending = true; paint();
|
||||
@@ -379,7 +437,7 @@
|
||||
}
|
||||
async function clearContext() {
|
||||
const c = current, key = c.key, selected = c.sessions.find(r => r.id === c.session), f = form('clear:' + key);
|
||||
if (!c.canSend || out() || f.pending || selectedInactive() || !Number.isSafeInteger(selected?.revision)) return;
|
||||
if (!c.canSend || !threadWrites() || out() || f.pending || selectedInactive() || !Number.isSafeInteger(selected?.revision)) return;
|
||||
if (!window.confirm(`Clear context for "${selected.name}"? This ends this session after a safe stop and starts fresh context. History and the old checkout (including unpushed work) are retained. The successor uses a fresh approved checkout; old work is not copied. This can wait for safe stop or capacity.`)) return;
|
||||
// A lost answer retains this key. Only status reads are allowed afterward.
|
||||
if (f.op && !['cancelled', 'failed'].includes(f.record?.state)) { await checkClear(c.id, key, f); return; }
|
||||
|
||||
@@ -9,10 +9,11 @@
|
||||
return path.startsWith('/chat') ? path : '/chat';
|
||||
};
|
||||
function show(navigated) {
|
||||
if (ready && window.MosaicChat) void window.MosaicChat.render({ path: route(), container: $('chat'), canSend, navigated });
|
||||
if (ready && window.MosaicChat) void window.MosaicChat.render({ path: route(), container: $('chat'), canSend, user: true, navigated });
|
||||
}
|
||||
document.addEventListener('mosaic-signed-out', () => { $('signed-out').hidden = false; $('sign-out').hidden = true; });
|
||||
window.addEventListener('hashchange', () => show(true));
|
||||
setInterval(() => { if (!document.hidden) show(false); }, 5000);
|
||||
fetch('/api/config', { cache: 'no-store', redirect: 'manual' }).then(async res => {
|
||||
if (res.type === 'opaqueredirect' || res.status === 401) { document.dispatchEvent(new Event('mosaic-signed-out')); return; }
|
||||
const config = await res.json();
|
||||
|
||||
@@ -632,7 +632,7 @@ export async function startServer({ host = '127.0.0.1', port = 7330, board = DEF
|
||||
if (send !== null && ((preview === null && sso === null) || typeof send !== 'function')) throw new Error('--send needs the preview mode (--public-host, --auth-header and --allow-user) or --sso');
|
||||
const server = createServer(async (req, res) => {
|
||||
// A granted user's request rebinds these below; every other request keeps the owner's.
|
||||
let { bus, reads, send, settingsWrite, projectFiles, projectFile, projectSave, chat, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite, processing, upstream, files } = owner, granted = false;
|
||||
let { bus, reads, send, settingsWrite, projectFiles, projectFile, projectSave, chat, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite, processing, upstream, files } = owner, granted = false, chatGrants = [];
|
||||
res.setHeader('x-content-type-options', 'nosniff');
|
||||
res.setHeader('referrer-policy', 'no-referrer');
|
||||
// Under --sso the Sign out form's POST is redirected to the provider's end-session endpoint, and
|
||||
@@ -700,6 +700,7 @@ export async function startServer({ host = '127.0.0.1', port = 7330, board = DEF
|
||||
if (userOptions === null) return json(res, 503, { error: 'not-configured', message: 'the Console has no user transport' });
|
||||
({ bus, send, settingsWrite, projectFiles, projectFile, projectSave, chat, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite, processing } = userOptions(session));
|
||||
reads = null; upstream = null; files = USER_FILES; granted = true;
|
||||
chatGrants = sso.chatGrants?.(session) ?? [];
|
||||
}
|
||||
}
|
||||
if (named && preview !== null) {
|
||||
@@ -839,7 +840,7 @@ export async function startServer({ host = '127.0.0.1', port = 7330, board = DEF
|
||||
// `send`: the page shows the composer only where a send can pass (the public name, --send on).
|
||||
// `edit`: the editor shows Save only where a save can pass, the same test as the route's.
|
||||
// `user` and `federation` (FED R6) only when there is one, so the owner's config is unchanged.
|
||||
if (path === '/api/config') return json(res, 200, { board: upstream, ...(tester ? { tester: true } : {}), ...(sso ? { sso: true } : {}), ...(granted ? { user: true } : {}), ...(hub ? { federation: hub } : {}), send: !tester && Boolean(send) && named, decide: !tester && !granted && Boolean(send) && Boolean(decisionWrite) && named && sso !== null, edit: !tester && !granted && Boolean(send) && Boolean(projectSave) && named });
|
||||
if (path === '/api/config') return json(res, 200, { board: upstream, ...(tester ? { tester: true } : {}), ...(sso ? { sso: true } : {}), ...(granted ? { user: true, chatGrants } : {}), ...(hub ? { federation: hub } : {}), send: !tester && Boolean(send) && named, decide: !tester && !granted && Boolean(send) && Boolean(decisionWrite) && named && sso !== null, edit: !tester && !granted && Boolean(send) && Boolean(projectSave) && named });
|
||||
if (path === '/healthz') return json(res, 200, { ok: true, ...(sso ? { sso: sso.health() } : {}) });
|
||||
if (path === '/favicon.ico') { res.writeHead(204); return res.end(); }
|
||||
const file = files.get(path);
|
||||
|
||||
@@ -352,6 +352,7 @@ export async function createSso(config, { secrets, fetch: customFetch = null, lo
|
||||
|
||||
return {
|
||||
publicOrigin, publicHost, sessions, logins, handle, session, returnURL,
|
||||
chatGrants: session => users?.chatGrants?.(session) ?? [],
|
||||
health: () => Object.fromEntries([...providers].map(([id, e]) => [id, e.state])),
|
||||
formTargets: () => [...new Set([...providers.values()].filter(e => e.as?.end_session_endpoint).map(e => new URL(e.as.end_session_endpoint).origin))],
|
||||
summary: () => config.providers.map(p => ({ id: p.id, issuer: p.issuer, subjects: p.subjects.length })),
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
// Row 208: isolated real broker + chat HTTP; no config, slot, provider or live host.
|
||||
import { createServer, request } from 'node:http';
|
||||
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { Broker } from '../../bus/src/broker.mjs';
|
||||
import { Store } from '../../bus/src/store.mjs';
|
||||
import { createChat } from '../src/chat.mjs';
|
||||
import { chatHTTP } from '../src/chat-http.mjs';
|
||||
import { consoleAccess } from '../src/access.mjs';
|
||||
import { BusReadError } from '../src/bus.mjs';
|
||||
|
||||
export const mutants = {
|
||||
composer: ["${threadWrites() ? `<form id=\"chat-send-form\"", "${true ? `<form id=\"chat-send-form\""],
|
||||
open: ['${threadWrites() ? `<button class="btn" type="button" id="chat-resume"', '${true ? `<button class="btn" type="button" id="chat-resume"'],
|
||||
share: ["if (!current.canSend || !threadWrites() || current.tester) return '';", "if (!current.canSend || current.tester) return '';"],
|
||||
create: ["if (current.user && (project || !scopeWrites(key))) return '';", "if (current.user && project) return '';"],
|
||||
};
|
||||
export async function sharingFixture(t, { owner = false, mutant = process.env.POE_UI_MUTANT } = {}) {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'poe-sharing-'));
|
||||
const store = new Store(dir), calls = [], lines = [];
|
||||
const access = { accessVersion: 1, revision: 1, users: [
|
||||
{ id: 'alice', home: 'local', subjects: [{ provider: 'fake', sub: 'alice-sub' }] },
|
||||
{ id: 'bob', home: 'local', subjects: [{ provider: 'fake', sub: 'bob-sub' }] },
|
||||
], grants: ['alice', 'bob'].map(user => ({ id: 'g-' + user, user, business: 'demo', projects: ['*'], instances: ['assistant'], level: 'collaborator' })) };
|
||||
const broker = new Broker({ store, access: () => access, businesses: { demo: {
|
||||
id: 'demo', human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' },
|
||||
roles: { chief: { definition: 'chief-of-staff', authority: { withinRole: ['message.send'], crossRole: [] } }, assistant: { definition: 'assistant', authority: { withinRole: ['message.send'], crossRole: [] } } },
|
||||
definitions: { 'chief-of-staff': {}, assistant: { messaging: JSON.parse(readFileSync(new URL('../../../roles/assistant.json', import.meta.url))).messaging } },
|
||||
} } });
|
||||
const human = broker.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true });
|
||||
const invoke = (cap, verb, args = {}) => broker.request(cap, { verb, args });
|
||||
const alice = () => broker.bindUser({ business: 'demo', user: 'alice' });
|
||||
const bob = () => broker.bindUser({ business: 'demo', user: 'bob' });
|
||||
const project = invoke(human, 'project.create', { name: 'Viewer project' }).id;
|
||||
const other = invoke(human, 'project.create', { name: 'Collaborator project' }).id;
|
||||
const own = invoke(alice(), 'thread.create', { project, instance: 'assistant', name: 'Old own thread' }).id;
|
||||
const writable = invoke(alice(), 'thread.create', { project: other, instance: 'assistant', name: 'Writable thread' }).id;
|
||||
const shared = invoke(bob(), 'thread.create', { project, instance: 'assistant', name: 'Shared thread <img src=x onerror=alert(1)>' }).id;
|
||||
invoke(alice(), 'thread.send', { thread: own, body: 'Own history' });
|
||||
invoke(bob(), 'thread.send', { thread: shared, body: 'Shared history' });
|
||||
invoke(bob(), 'thread.share', { thread: shared, to: 'user:alice' });
|
||||
access.grants[0].level = 'viewer';
|
||||
const hints = consoleAccess({ load: () => access, business: () => 'demo' });
|
||||
const principal = () => owner ? human : alice();
|
||||
const chat = createChat({ call: async (verb, args) => {
|
||||
calls.push([verb, args]);
|
||||
if (verb === 'plugins') return { plugins: { channel: ['webui'] } };
|
||||
try { return invoke(principal(), verb, args); }
|
||||
catch (e) { throw new BusReadError(e.code); }
|
||||
} });
|
||||
const source = readFileSync(new URL('../src/public/chat.js', import.meta.url), 'utf8');
|
||||
let chatSource = source;
|
||||
if (mutant) {
|
||||
const [before, after] = mutants[mutant] ?? [];
|
||||
if (!before || !source.includes(before)) throw Error('mutant anchor missing: ' + mutant);
|
||||
chatSource = source.replace(before, after);
|
||||
}
|
||||
const server = createServer(async (req, res) => {
|
||||
const url = new URL(req.url, 'http://fixture');
|
||||
if (url.pathname === '/api/config') {
|
||||
res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' });
|
||||
return res.end(JSON.stringify({ send: true, user: !owner, chatGrants: hints.chatGrants({ user: 'alice', provider: 'fake', sub: 'alice-sub' }) }));
|
||||
}
|
||||
if (url.pathname.startsWith('/api/chat/')) return chatHTTP(req, res, {
|
||||
path: url.pathname, search: url.search, chat, named: true, send: true,
|
||||
preview: { sso: true, user: owner ? 'jason' : 'alice', origin: origin() }, log: line => lines.push(line),
|
||||
});
|
||||
if (url.pathname === '/chat.js') { res.writeHead(200, { 'content-type': 'text/javascript' }); return res.end(chatSource); }
|
||||
if (url.pathname === '/') {
|
||||
res.writeHead(200, { 'content-type': 'text/html' });
|
||||
return res.end('<!doctype html><title>Sharing fixture</title><div id="view"></div><script src="/chat.js"></script>');
|
||||
}
|
||||
res.writeHead(404); res.end();
|
||||
});
|
||||
await new Promise(r => server.listen(0, '127.0.0.1', r));
|
||||
const origin = () => `http://127.0.0.1:${server.address().port}`;
|
||||
t.after(async () => { await new Promise(r => server.close(r)); store.close(); rmSync(dir, { recursive: true, force: true }); });
|
||||
const http = (path, value, extra = {}) => new Promise((resolve, reject) => {
|
||||
const body = value === undefined ? undefined : JSON.stringify(value);
|
||||
const req = request(origin() + path, { method: body ? 'POST' : 'GET', headers: body ? { origin: origin(), 'sec-fetch-site': 'same-origin', 'content-type': 'application/json', ...extra } : extra }, res => {
|
||||
let data = ''; res.on('data', chunk => { data += chunk; });
|
||||
res.on('end', () => resolve({ status: res.statusCode, body: JSON.parse(data) }));
|
||||
}); req.on('error', reject); req.end(body);
|
||||
});
|
||||
return { store, access, broker, human, alice, bob, invoke, project, other, own, writable, shared, calls, lines, origin: origin(), http, hints, source, chatSource };
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { sharingFixture } from './thread-sharing-fixture.mjs';
|
||||
import { browser } from './browser.mjs';
|
||||
import { startServer } from '../src/serve.mjs';
|
||||
import { consoleBus } from '../src/console-bus.mjs';
|
||||
|
||||
async function page(t, f) {
|
||||
const b = await browser(); t.after(() => b.close());
|
||||
await b.navigate(f.origin);
|
||||
const render = async path => {
|
||||
await b.evaluate(`(async()=>{location.hash=${JSON.stringify('#' + path)};await MosaicChat.render({path:${JSON.stringify(path)},container:document.getElementById('view'),canSend:true,user:${!f.owner},navigated:true});return true;})()`);
|
||||
};
|
||||
const wait = expr => b.evaluate(`(async()=>{for(let i=0;i<100;i++){if(${expr})return true;await new Promise(r=>setTimeout(r,30));}return false;})()`);
|
||||
return { b, render, wait };
|
||||
}
|
||||
const counts = f => Object.fromEntries(['threads', 'messages', 'thread_opens', 'thread_shares'].map(table => [table, f.store.get(`SELECT count(*) n FROM ${table}`).n]));
|
||||
|
||||
test('193d HTTP shares use the caller, plain/shared lists, exact args and write fences', async t => {
|
||||
const f = await sharingFixture(t);
|
||||
let r = await f.http('/api/chat/threads?shared=true');
|
||||
assert.equal(r.status, 200); assert.deepEqual(r.body.rows.map(x => x.id), [f.shared]);
|
||||
assert.equal(r.body.rows[0].owner, 'user:bob');
|
||||
r = await f.http('/api/chat/threads');
|
||||
assert.deepEqual(r.body.rows.map(x => x.id), [f.own, f.writable]);
|
||||
assert.ok(r.body.rows.every(x => !Object.hasOwn(x, 'owner')), 'plain list shape unchanged');
|
||||
assert.equal((await f.http(`/api/chat/threads/${f.shared}/messages`)).status, 200);
|
||||
const before = counts(f);
|
||||
for (const [path, body] of [
|
||||
['/api/chat/threads', { project: f.project, instance: 'assistant', name: 'forbidden' }],
|
||||
[`/api/chat/threads/${f.own}/messages`, { body: 'forbidden' }],
|
||||
[`/api/chat/threads/${f.own}/open`, {}],
|
||||
[`/api/chat/threads/${f.own}/share`, { to: 'user:bob' }],
|
||||
[`/api/chat/threads/${f.own}/unshare`, { to: 'user:bob' }],
|
||||
]) {
|
||||
r = await f.http(path, body);
|
||||
assert.equal(r.status, 403, path); assert.equal(r.body.error, 'access-refused'); assert.equal(r.body.sent, false);
|
||||
}
|
||||
assert.equal((await f.http(`/api/chat/threads/${f.own}/shares`)).body.error, 'access-refused');
|
||||
assert.deepEqual(counts(f), before);
|
||||
f.access.grants[0].level = 'collaborator';
|
||||
r = await f.http(`/api/chat/threads/${f.own}/share`, { to: 'user:bob' });
|
||||
assert.equal(r.status, 200); assert.deepEqual(r.body, { thread: f.own, to: 'user:bob', op: 'share' });
|
||||
assert.deepEqual((await f.http(`/api/chat/threads/${f.own}/shares`)).body.rows.map(x => x.to), ['user:bob']);
|
||||
assert.equal((await f.http(`/api/chat/threads/${f.own}/unshare`, { to: 'user:bob' })).status, 200);
|
||||
for (const [path, body] of [
|
||||
['/api/chat/threads?shared=false', undefined], ['/api/chat/threads?shared=true&shared=true', undefined],
|
||||
[`/api/chat/threads/${f.own}/share`, { to: 'user:bob', owner: 'jason' }],
|
||||
[`/api/chat/threads/${f.own}/share`, { to: 'user:bob', verb: 'thread.send' }],
|
||||
[`/api/chat/threads/${f.own}/shares?to=user:bob`, undefined],
|
||||
]) assert.equal((await f.http(path, body)).status, 400, path);
|
||||
assert.equal((await f.http(`/api/chat/threads/${f.own}/share`, { to: '*' }, { origin: 'http://other.test' })).status, 403);
|
||||
assert.equal((await f.http(`/api/chat/threads/${f.own}/share`, { to: '*' }, { 'sec-fetch-site': 'cross-site' })).status, 403);
|
||||
f.invoke(f.bob(), 'thread.unshare', { thread: f.shared, to: 'user:alice' });
|
||||
assert.equal((await f.http(`/api/chat/threads/${f.shared}/messages`)).body.error, 'thread-not-owned');
|
||||
});
|
||||
|
||||
test('193d config supplies only the signed-in user live collaborator scopes and updates on downgrade', async t => {
|
||||
const f = await sharingFixture(t);
|
||||
f.access.grants.push({ ...f.access.grants[0], id: 'mixed', level: 'collaborator', projects: [f.other] });
|
||||
const session = { user: 'alice', name: 'alice', provider: 'fake', sub: 'alice-sub' };
|
||||
let web;
|
||||
const sso = { get publicHost() { return `127.0.0.1:${web.address().port}`; }, get publicOrigin() { return `http://${this.publicHost}`; },
|
||||
formTargets: () => [], session: () => session, chatGrants: s => f.hints.chatGrants(s) };
|
||||
web = await startServer({ port: 0, sso, log: () => {}, userOptions: () => consoleBus(async () => [], { send: true }) });
|
||||
t.after(() => new Promise(r => web.close(r)));
|
||||
const read = async () => (await fetch(`http://127.0.0.1:${web.address().port}/api/config`)).json();
|
||||
let config = await read();
|
||||
assert.equal(config.user, true); assert.deepEqual(config.chatGrants, [{ projects: [f.other], instances: ['assistant'] }]);
|
||||
assert.equal(config.send, true, 'send mode is separate from individual grant levels');
|
||||
f.access.grants[2].level = 'viewer';
|
||||
assert.deepEqual((await read()).chatGrants, []);
|
||||
f.access.grants[2].level = 'collaborator'; f.access.grants[2].expires = new Date(Date.now() - 1000).toISOString();
|
||||
assert.deepEqual((await read()).chatGrants, []);
|
||||
assert.deepEqual(f.hints.chatGrants({ ...session, sub: 'bob-sub' }), []);
|
||||
});
|
||||
|
||||
test('193d viewer UI has no composer, resume, share, session mutations or create controls', async t => {
|
||||
const f = await sharingFixture(t); f.owner = false;
|
||||
const { b, render } = await page(t, f);
|
||||
await render('/chat');
|
||||
assert.match(await b.evaluate('document.getElementById("view").textContent'), /Shared with me/);
|
||||
assert.equal(await b.evaluate('!!document.querySelector("#chat-project-form")'), false);
|
||||
await render('/chat/projects/' + f.project);
|
||||
assert.equal(await b.evaluate('!!document.querySelector("#chat-thread-form")'), false);
|
||||
for (const thread of [f.own, f.shared]) {
|
||||
await render('/chat/threads/' + thread);
|
||||
assert.match(await b.evaluate('document.querySelector(".chat-log").textContent'), /history/);
|
||||
for (const selector of ['#chat-send-form', '#chat-resume', '#chat-share-form', '#chat-session-form', '#chat-clear', '#chat-cancel']) {
|
||||
assert.equal(await b.evaluate(`!!document.querySelector(${JSON.stringify(selector)})`), false, `${thread}: ${selector}`);
|
||||
}
|
||||
}
|
||||
assert.equal(await b.evaluate('!!document.querySelector("img")'), false, 'shared name remains text');
|
||||
const sharedSessions = f.calls.filter(([verb, args]) => verb === 'session.list' && args.thread === f.shared);
|
||||
assert.equal(sharedSessions.length, 0, 'shared read never requests an owner-only catalog');
|
||||
});
|
||||
|
||||
test('193d mixed grants keep viewer project read-only and collaborator own thread writable; downgrade hides controls', async t => {
|
||||
const f = await sharingFixture(t); f.owner = false;
|
||||
f.access.grants.push({ ...f.access.grants[0], id: 'mixed', level: 'collaborator', projects: [f.other] });
|
||||
const { b, render, wait } = await page(t, f);
|
||||
await render('/chat/threads/' + f.own);
|
||||
assert.equal(await b.evaluate('!!document.querySelector("#chat-send-form")'), false);
|
||||
await render('/chat/threads/' + f.writable);
|
||||
assert.equal(await b.evaluate('!!document.querySelector("#chat-send-form")'), true);
|
||||
assert.equal(await b.evaluate('!!document.querySelector("#chat-share-form")'), true);
|
||||
await b.evaluate('(()=>{const x=document.getElementById("chat-share-target");x.value="user:bob";x.dispatchEvent(new Event("input",{bubbles:true}));document.querySelector("#chat-share-form button").click();return true;})()');
|
||||
assert.equal(await wait('!!document.querySelector("[data-unshare]")'), true);
|
||||
assert.equal(f.store.get("SELECT count(*) n FROM thread_shares WHERE thread=? AND op='share'", f.writable).n, 1);
|
||||
await b.evaluate('document.querySelector("[data-unshare]").click(),true');
|
||||
assert.equal(await wait('!document.querySelector("[data-unshare]")'), true);
|
||||
f.access.grants[2].level = 'viewer';
|
||||
await render('/chat/threads/' + f.writable);
|
||||
assert.equal(await b.evaluate('!!document.querySelector("#chat-send-form,#chat-share-form,#chat-resume")'), false);
|
||||
// Loss of a share clears visible history on the next render.
|
||||
await render('/chat/threads/' + f.shared);
|
||||
f.invoke(f.bob(), 'thread.unshare', { thread: f.shared, to: 'user:alice' });
|
||||
await render('/chat/threads/' + f.shared);
|
||||
assert.equal(await b.evaluate('!!document.querySelector(".chat-log")'), false);
|
||||
});
|
||||
|
||||
test('193d owner has share controls on own threads and shared threads remain read-only', async t => {
|
||||
const f = await sharingFixture(t, { owner: true }); f.owner = true;
|
||||
const own = f.invoke(f.human, 'thread.create', { project: f.project, instance: 'assistant', name: 'Owner thread' }).id;
|
||||
f.invoke(f.bob(), 'thread.share', { thread: f.shared, to: 'jason' });
|
||||
const { b, render } = await page(t, f);
|
||||
await render('/chat/threads/' + own);
|
||||
assert.equal(await b.evaluate('!!document.querySelector("#chat-share-form")'), true);
|
||||
await render('/chat/threads/' + f.shared);
|
||||
assert.match(await b.evaluate('document.querySelector(".chat-log").textContent'), /Shared history/);
|
||||
assert.equal(await b.evaluate('!!document.querySelector("#chat-share-form,#chat-send-form,#chat-resume")'), false);
|
||||
});
|
||||
|
||||
// Run under each served-source mutant after the normal absence test kills it.
|
||||
// Every browser POST bypasses all local UI checks and still uses the viewer transport.
|
||||
test('193d bypassed UI checks still reach a refusing bus for every viewer write', async t => {
|
||||
const f = await sharingFixture(t); f.owner = false;
|
||||
const { b, render } = await page(t, f);
|
||||
await render('/chat/threads/' + f.own);
|
||||
const before = counts(f);
|
||||
for (const [path, value] of [
|
||||
['threads', { project: f.project, instance: 'assistant', name: 'mutant create' }],
|
||||
[`threads/${f.own}/messages`, { body: 'mutant send' }],
|
||||
[`threads/${f.own}/open`, {}], [`threads/${f.own}/share`, { to: 'user:bob' }],
|
||||
]) {
|
||||
const r = JSON.parse(await b.evaluate(`(async()=>{const r=await fetch('/api/chat/'+${JSON.stringify(path)},{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(${JSON.stringify(value)})});return JSON.stringify({status:r.status,body:await r.json()});})()`));
|
||||
assert.equal(r.status, 403, path); assert.equal(r.body.error, 'access-refused'); assert.equal(r.body.sent, false);
|
||||
}
|
||||
assert.deepEqual(counts(f), before);
|
||||
for (const verb of ['thread.create', 'thread.send', 'thread.open', 'thread.share']) assert.ok(f.calls.some(([v]) => v === verb), `${verb} reached the bus`);
|
||||
});
|
||||
|
||||
|
||||
test('193d collaborator shares stay read-only and never expose reshare or wake controls', async t => {
|
||||
const f = await sharingFixture(t); f.owner = false;
|
||||
f.access.grants[0].level = 'collaborator';
|
||||
const { b, render } = await page(t, f);
|
||||
await render('/chat/threads/' + f.shared);
|
||||
assert.equal(await b.evaluate('!!document.querySelector("#chat-share-form,#chat-send-form,#chat-resume,#chat-session-form")'), false);
|
||||
for (const [action, body] of [['messages', { body: 'shared write' }], ['open', {}], ['share', { to: '*' }]]) {
|
||||
const r = await f.http(`/api/chat/threads/${f.shared}/${action}`, body);
|
||||
assert.equal(r.status, 403); assert.equal(r.body.error, 'thread-not-owned');
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user