Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2fc27bce6b | ||
|
|
bcd93dc082 | ||
|
|
574ece13c1 | ||
|
|
7cea739de3 | ||
|
|
4de3cec273 |
No files matched your search
@@ -5411,3 +5411,7 @@ Row 299 (573, #1806, Gander, Filbert): before, the P10 band test timed a fault r
|
||||
2026-10-23T17:29:46Z | Gyro Vikunja571 sage-3542 Connections amendment | Branch gyro/editor-connections-design cut exactly from landed PRD1.17 30e6bb680; read REQ-LIB-5 in that PRD. Replaced stale J-b waits in parent and W4. Proposed W8 owns current authorized Markdown/DOCX links/backlinks list, labels, versioned refresh, leakage refusals and fixture tests. Build order under mos-1470/comment961 is W7 closed routing candidate, W8 landing, then W7 human first-real-import opening. No REQ-LIB-8 condition or acceptance change. Q234/mos-1363/S0 M1-M14 and post-10-27/MVP priority fixed. Static diff, twelve brief sections and append-only checks pass. No repo Node, tests, build, browser, queue mutation or live action. Fenton reviews after authorized docs branch push; Sage lands on approval.
|
||||
|
||||
2026-10-23T17:32:37Z | Gyro sage-3544 fenton-0016 N1/N2 delta | Before: W7 Gate/final real-conversion list omit the W8 build-order cross-reference and W8 human-view filtering cites agent settings. After: both locations explicitly put W8 landing before human opening as mos-1470 build order, not acceptance; comment/tracked-deletion links are not current story links and never appear in the human Connections view regardless of agent settings. New commit atop approved 5af060fb5, no amend. Static diff and append-only checks pass. Docs only, no repo Node/tests/live action. Fenton checks only this delta.
|
||||
|
||||
2026-10-23 | Boyd | Row 298 (#1804, Q228), before implementation completion | Took over Megavolt 7a141269c on boyd/row298 at 45dca69f6 under mos-1496/1502. Approved D1-D6 govern human-only grants.scope.set, empty fallback, in-scope close/evidence and spend recheck. All inherited code UNTESTED; no Node/tests until Sage CLEAR. Plan and static gaps recorded in agents/boyd/work/queue-298/BUILD.md; no live grant or credential access.
|
||||
|
||||
2026-10-23 | Boyd | Row 298 (#1804, Q228), targeted verification | On sage-3642 T1 lane B, five whole files serial: business scope 7/0, bus scope 14/0, scope runtime 1/0, CLI scope 4/0, project runtime 3/0 (29/0, no skips). Initial bus fixture typo recorded and corrected. M1-M10 killed by named assertions, source restored and final files green. Dewey dewey-0933 positive control and notes addressed; manifest and evidence in agents/boyd/work/queue-298/BUILD.md. Full package/script gate remains Sage's. No live scope set.
|
||||
@@ -0,0 +1,233 @@
|
||||
# Row 298 (#1804, Q228): chief-of-staff grant scope
|
||||
|
||||
Owner Boyd (48a3ae38), reviewer Dewey. Branch `boyd/row298`, base `45dca69f6`.
|
||||
Assignment: mos-1496/Q245. Handoff: Megavolt megavolt-0203, local `7a141269c`;
|
||||
cherry-picked as `4de3cec27` (design) and `7cea739de` (UNTESTED code).
|
||||
Brief: `docs/plans/2026-10-23_mvp-readiness-rows.md`, chief-of-staff grant scope.
|
||||
Megavolt's original design remains in `agents/megavolt/work/queue-298/BUILD.md`.
|
||||
|
||||
## Authority and accepted design
|
||||
|
||||
Sage sage-3594, restated in sage-3612: launch.grantScope, lenient read with empty
|
||||
fallback, strict human-only write. No general organization setter may write it.
|
||||
Per-method agents/projects or agents/hosts/commands; host-enroll never in scope.
|
||||
Host command arrays match whole tokens and exact lengths. `grants.scope.set`
|
||||
from the human CLI is Approve; no Console page or chief-raised scope change.
|
||||
Mos mos-1496/1502 requires the same human-shell guard as `mosaic access`.
|
||||
The broker closes an in-scope request via scope and writes grant.scope.used;
|
||||
missing evidence must refuse execution. Narrowing before spend fails
|
||||
scope-changed. Empty scope ships; only Jason sets the live value (Q188).
|
||||
|
||||
No credential files read, no live grants, no deployment, no queue writes. Push
|
||||
only boyd/*, additive commits, no amend/force-push or attribution trailers.
|
||||
Sage owns queue, shared test slot and landing. No Node/tests before CLEAR.
|
||||
|
||||
## Implementation plan and ledger
|
||||
|
||||
Using writing-plans and executing-plans inline; the approved brief and D1-D6
|
||||
are the spec. This packet is the requested plan/tracking location.
|
||||
|
||||
- [x] Read brief, roster, implementation and handoff; register session.
|
||||
- [x] Bring Megavolt's two commits onto Boyd's branch without changing history.
|
||||
- [x] Add independent CLI tests in `packages/cli/tests/grants-scope.test.mjs`:
|
||||
scope read/set requests; agent-marker rejection before stdin/transport;
|
||||
--tester rejection; missing revision, malformed/empty/oversize input.
|
||||
- [x] Strengthen new business/bus grant-scope tests for exact commands,
|
||||
scope evidence binding, host-enroll exclusion and spent scope-changed.
|
||||
- [ ] On Sage CLEAR, observe targeted failures before fixes; fix inherited
|
||||
implementation using the existing human proof and grant executors.
|
||||
- [ ] Update bus/CLI docs, append BUILD-LOG before/after evidence, manifest.
|
||||
- [ ] On Sage CLEAR run touched suites; record exact results and mutants.
|
||||
- [ ] Commit UNTESTED until verified; push boyd/row298 only when slot permits;
|
||||
report to Sage and Dewey, cc Mos for pushes/blockers. Dewey reviews manifest.
|
||||
|
||||
Pre-flight interfaces: business checkGrantScope/grantScopeOf/grantInScope feed
|
||||
the instance writer and broker. Runtime supplies a fresh scope from the org
|
||||
file and routes the human setter to the same organization writer. CLI callFor
|
||||
supplies refuseInsideAgent and humanTransport. The grant executors consume the
|
||||
broker's immutable close/evidence. Tests use scratch files/fake Gitea only.
|
||||
|
||||
Review focus: exact argv length, stale scope before spend, forged/missing scope
|
||||
evidence, malformed fallback, and CLI agent ancestry. Tests must cover each.
|
||||
|
||||
Static findings before any Boyd Node/test run:
|
||||
|
||||
- Handoff introduced commandPrefixes beyond the accepted D2 shape, allowing
|
||||
trailing args. Remove it; only exact command arrays are authorized.
|
||||
- scope.used carries a digest without validating it, and no org revision.
|
||||
Bind evidence to the canonical scope snapshot and grant/close sequence;
|
||||
preserve historical grants when scope is later changed, recheck at spend.
|
||||
- Gitea recheck throws inside the spend transaction and rolls back spend on
|
||||
scope-changed. Preserve spend and log failure for that case (D6).
|
||||
- The optional giteaStart?. assertion always passes when the API is absent;
|
||||
use the real runGiteaGrant entry point. Fake Gitea tokens() does exist.
|
||||
|
||||
## Existing test changes (mos-1272)
|
||||
|
||||
Approved by Mos mos-1510 via Sage sage-3624; edited exactly as approved:
|
||||
|
||||
| File | Actual numstat | Reason |
|
||||
| --- | --- | --- |
|
||||
| packages/bus/tests/project-runtime.test.mjs | +1 / -1 | Vocabulary import pin adds grantScopeOf and grantInScope. |
|
||||
|
||||
Other test work is confined to row 298's new files. No pre-existing CLI tests
|
||||
will change. Record final numstat here before review.
|
||||
|
||||
## Human proof and organization ownership
|
||||
|
||||
CLI: `packages/cli/src/cli.mjs` callFor runs refuseInsideAgent before constructing
|
||||
humanTransport, as `accessCommand` does. The setter takes no --tester flag.
|
||||
`packages/bus/src/human.mjs` verifyHuman checks the nonce, CLI path and every
|
||||
ancestor's markers/argv; `bindHuman` and #human in broker.mjs require the
|
||||
business human, via cli, outsideAgent true. Tests will pin this chain's caller
|
||||
refusal. The scope receipt records by/via/outsideAgent and old/new sha256.
|
||||
|
||||
Org file: `packages/business/src/organization.mjs` regular() requires a regular,
|
||||
non-symlink file owned by the process uid and not writable by group/other;
|
||||
write() uses revision checks, locked durable receipts and atomic rename.
|
||||
The OS owner uid can edit it directly; the agent sandbox does not mount the
|
||||
user's config (`packages/seat/src/sandbox.mjs`). This is the existing cooperative
|
||||
human/process boundary, not a hostile same-uid isolation claim.
|
||||
|
||||
Chief enrollment: broker instances.set calls #human and #mayHuman before
|
||||
add/change; credential issue/delete use #requestCredential's human gate.
|
||||
launch.by is an organization-file setting, excluded from general setters;
|
||||
only that chief may auto-approve within scope. Additional chief instances
|
||||
remain on the per-grant human approval path.
|
||||
|
||||
## Validation
|
||||
|
||||
UNTESTED. No Boyd Node/test run yet; waiting for Sage CLEAR.
|
||||
M1: bypass in-scope check. M2: bypass invalid-value fallback. M3: remove human
|
||||
setter guard. M4: drop grant.scope.used. All need named failing tests on gate.
|
||||
|
||||
## Session correction
|
||||
|
||||
The initial SESSIONS entry labels startup 19:13Z; startup was approximately
|
||||
19:08Z. Message header timestamps in boyd-0001 through 0004 were estimates;
|
||||
use T3's receipt timestamps as authoritative. Subsequent reports use date -u.
|
||||
|
||||
## Departures from Megavolt 7a141269c (Sage sage-3624)
|
||||
|
||||
1. Removed commandPrefixes; host scope commands match exact token arrays only.
|
||||
2. grantScope callback now supplies one organization read `{value,revision}`.
|
||||
grant.scope.used includes that revision, canonical scope snapshot and digest;
|
||||
#approvedGrant validates the digest, shape, method, agent and close sequence.
|
||||
It uses the immutable snapshot so later narrowing doesn't revoke issued grants.
|
||||
3. Gitea scope-changed is caught inside the spend transaction and logs a terminal
|
||||
failure without rolling back its spend. Other recheck failures keep their
|
||||
existing behavior. Host already catches recheck failures after spending.
|
||||
|
||||
Additional handoff corrections: replaced optional nonexistent giteaStart with
|
||||
runGiteaGrant; added successful host executor/revoke and enrollment-exclusion
|
||||
coverage; added real runtime writer coverage and independent CLI guard tests.
|
||||
|
||||
Sage scheduled push at 19:41Z with MOSAIC_GITEA_CREDENTIAL_FILE unset, then T1
|
||||
lane B around 19:47Z (after explicit CLEAR), four new files whole and serial,
|
||||
hard stop 20:00Z. Fethry lane A is disjoint. No test was run early. Production
|
||||
corrections are marked UNTESTED under that slot mandate; baseline regression
|
||||
checks against the inherited implementation will run only on CLEAR.
|
||||
|
||||
## Candidate preparation (before T1)
|
||||
|
||||
The three static corrections, CLI/runtime coverage and docs are prepared but
|
||||
UNTESTED. `git diff --check` is clean. The approved import-pin edit is exactly
|
||||
1 addition / 1 deletion. M1-M4 are encoded in mutants.py (not run). No Node,
|
||||
https git, full suite or webui build has run in Boyd's worktree.
|
||||
|
||||
Review package: product/test/documentation files in MANIFEST.sha256, excluding
|
||||
append-only project logs and the preserved predecessor packet. The manifest
|
||||
identifies exact bytes for Dewey; each later product change regenerates it and
|
||||
requires a review delta. Existing tests changed: only project-runtime, 1/1.
|
||||
|
||||
## Dewey static review dewey-0933
|
||||
|
||||
N1: Added a positive control to the malformed-evidence fixture. A valid event
|
||||
must pass scope authorization and reach decision-mismatch on the deliberately
|
||||
absent definition binding; invalid events remain not-authorized. No spend.
|
||||
N2: Additional named mutants cover spend recheck, exact argv length, Gitea spend
|
||||
rollback, launch.by eligibility and accepting any evidence row (results below).
|
||||
N3: At spend only grant scope membership is rechecked, not whether the raising
|
||||
chief is still launch.by. This implements sage-3594 Q5; changing the launcher
|
||||
later does not by itself invalidate a pending scope-approved request.
|
||||
N4: The scope shortcut returns the closed decision view to the chief without
|
||||
sending a separate close message; documented in the bus verb table.
|
||||
N5: grant.scope is logged after the organization file commit in a separate bus
|
||||
transaction. A crash between them leaves durable changes.jsonl as the receipt,
|
||||
as with other organization writers. The setter does not claim cross-store
|
||||
atomicity.
|
||||
|
||||
## T1 lane B results (sage-3642, 2026-10-23 19:23Z–19:29Z)
|
||||
|
||||
TMPDIR: `/mnt/storage/scratch/tmp/boyd298-4bzr6fn7`. Each whole file ran serially
|
||||
as `timeout -k 10 180 node --test --test-concurrency=1 FILE`, TMPDIR set to that
|
||||
scratch directory. Final restored candidate results:
|
||||
|
||||
| Whole file | Pass | Fail | Skip |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| business/tests/grant-scope.test.mjs | 7 | 0 | 0 |
|
||||
| bus/tests/grant-scope.test.mjs | 14 | 0 | 0 |
|
||||
| bus/tests/grant-scope-runtime.test.mjs | 1 | 0 | 0 |
|
||||
| cli/tests/grants-scope.test.mjs | 4 | 0 | 0 |
|
||||
| bus/tests/project-runtime.test.mjs | 3 | 0 | 0 |
|
||||
|
||||
29/0/0 total. `final-1.log` through `final-5.log` hold the final output. The
|
||||
runtime's reload-needs-restart notice is expected: the fixture's boot document
|
||||
has fewer credential references than its organization document, as in the
|
||||
existing instances runtime tests. No credential contents are loaded from the
|
||||
real user config.
|
||||
|
||||
Initial run: business 7/0; bus 13/1 due to Boyd's test typo, an out-of-scope
|
||||
`opts` reference in the missing-scope assertion. Corrected only that new-test
|
||||
line, then bus 14/0 and the remaining files green. Dewey N1 positive control
|
||||
was added and the whole bus file passed again; final rerun above followed all
|
||||
mutants. No production code changed during this test window.
|
||||
|
||||
Mutants: all whole files, serial, with timeout -k 10 and the same TMPDIR.
|
||||
`mutants.py --sage-clear sage-3642 --logs SCRATCH/review-mutants --deadline
|
||||
2026-10-23T19:38:00+00:00` restores exact original source after each run.
|
||||
M1/M2/M3/M4 are brief/Sage mutants; M5–M10 cover review focus and dewey-0933.
|
||||
Each exited 1 with named assertion failures, no loader/syntax kill.
|
||||
|
||||
| Mutant | Change | Pass/fail | Named killer |
|
||||
| --- | --- | --- | --- |
|
||||
| M1 | skip raise scope check | 11/3 | bus out-of-scope Gitea waits; empty/bad scope waits; host argv exclusion |
|
||||
| M2 | skip invalid-value fallback | 5/2 | business missing/bad scope fallback; bad file scope fallback |
|
||||
| M3 | remove #human from scope verb | 13/1 | bus Q3 caller refusals |
|
||||
| M4 | drop grant.scope.used write | 9/5 | bus Q4 protected evidence assertion; four executor waits also fail |
|
||||
| M5 | skip scope digest check | 13/1 | bus Q4 bad evidence: decision-mismatch instead of not-authorized |
|
||||
| M6 | rethrow narrowing, rolling spend back | 13/1 | bus Q5 Gitea spent assertion |
|
||||
| M7 | same without length check | 6/1 | business Q2 shorter command prefix exclusion |
|
||||
| M8 | no-op #scopeRecheck | 12/2 | bus Q5 Gitea and host scope-changed outcomes |
|
||||
| M9 | remove launch.by check | 13/1 | bus Q6 second chief stays open |
|
||||
| M10 | accept any matched scope evidence row | 13/1 | bus Q4 bad evidence, with positive control |
|
||||
|
||||
Mutation log sha256 (under scratch/review-mutants):
|
||||
|
||||
```text
|
||||
55e9973af5fca0088963e2ca11fd81b2fb46dfead813a577c0e0aad2d6269b68 M1.log
|
||||
abd1b784eff5ae1333a7b47085d3e199cad6d27a00e2584edf0b62417ad4c888 M2.log
|
||||
1a56085e548d1e244f0f88fe12a45f77c162375880794f9ef88b241ed3a1704d M3.log
|
||||
9c120f3f7128488edaf8bfd6d41bae712e69e76da4195a0d4dc0e8336b02038f M4.log
|
||||
1bba88a3752823454acdaa51abde6aad66fdd5488801b4aceda02a3b42c8123d M5.log
|
||||
a4ce3cacca9f7ea56b8cdfd503c76173a8e4ff1d8a7ec07032c93f0b51aa60d8 M6.log
|
||||
acecd5afc93d68d96d713f8a9a2dc08351831a0b7360a13b4e30a572ad4167bf M7.log
|
||||
fce7fa505ccf4f2d4190bd03c209fd19f1cf2770d3ae1246a3bd03697594019e M8.log
|
||||
a830a7964890dd04a2dcc7a932a1502f1dea06419a1eae6b91a85f7878dc5681 M9.log
|
||||
38ce3e7f69f6c6ffe10133397465d52538a930a830f76256a8b9280f8df78e4b M10.log
|
||||
```
|
||||
|
||||
Full touched-package suites and every scripts/test-*.sh remain for Sage's full gate; this CLEAR authorized only the five whole files. Candidate remains
|
||||
marked Untested for the full gate. No queue row has been changed by Boyd.
|
||||
|
||||
## Dewey final delta review (dewey-0935, 2026-10-23 19:31:55Z)
|
||||
|
||||
Dewey reviewed 574ece13c..bcd93dc08 with local git only, no Node: CLEAN,
|
||||
nothing blocks. Verified all 13 manifest entries, exact package-file coverage,
|
||||
project-runtime 1/1, M5-M10 mutation sites, all ten mutation log hashes and
|
||||
counts, named assertion kills without loader/syntax errors, final five logs
|
||||
29/0/0, and N1-N5 coverage. No production code delta in this review range.
|
||||
|
||||
Formal Gitea APPROVE on #1804 and queue record wait on the row 291 hold release
|
||||
and Sage's go. Sage's touched-package and every scripts/test-*.sh gate remains
|
||||
required. Boyd has released lane B and has no running test processes.
|
||||
@@ -0,0 +1,13 @@
|
||||
612488b705853e75e6f8952e41552526f4d07dd297d7619d608d434ed75121d6 packages/bus/README.md
|
||||
3677f403104c121d53eb50b874de7a5d8d22e038faac52a5ea5bad449d0d409c packages/bus/src/broker.mjs
|
||||
2e0ade9233404439cd4a629245600878da960f1126e7299aa2ccfb3aff86d784 packages/bus/src/runtime.mjs
|
||||
e7065db949cacfd4679a1031eebf5cde09ed196ce85612e8400ae563f02a0f18 packages/bus/tests/grant-scope-runtime.test.mjs
|
||||
4030cf584481a1db12d9e6f744015c48fdebd468335d065ead3e90180858d366 packages/bus/tests/grant-scope.test.mjs
|
||||
bc432d8d430433b9d84c00883584dc53d4b27afb5e93f739fe3ca28e80f01efa packages/bus/tests/project-runtime.test.mjs
|
||||
84d6dd15d5ca2926d8f7a34548c70c3e7a3c58e692f0b009414f1af35cb096cc packages/business/src/business.mjs
|
||||
1ac106522c8a115cb9479f2a6c5591fb9c77d138bbe93d2df10b6c4295645d0e packages/business/src/instance-settings.mjs
|
||||
fed4177325f65a2b9823a40a9c35c01d60c0520e3ff40b4574c3cf2c29cc096b packages/business/src/vocabulary.mjs
|
||||
b7f96e45b9ffca2a7ebc18dc446f0adcaac72d3900f098c60157cf6cbf1bfc13 packages/business/tests/grant-scope.test.mjs
|
||||
60fc267b7f8f9e83eeb0d0207c8abdef68d554f32b08d508e98dbf59b442a366 packages/cli/README.md
|
||||
b586907ca8f351f60dc19ec2683df75d2d70ddb12955d09fb14560c9d3bed3dc packages/cli/src/grants.mjs
|
||||
fb7c30b31facd887b03ff02dd6734d48a815978481b33bb21ec48604a51c5ff5 packages/cli/tests/grants-scope.test.mjs
|
||||
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Row 298 mutants. Run only with Sage's explicit CLEAR in Boyd's slot.
|
||||
|
||||
Each mutation is applied to this worktree, runs one whole new test file,
|
||||
then restores the exact original bytes, including after an interruption.
|
||||
Logs go to a caller-supplied scratch directory, never the repository root.
|
||||
"""
|
||||
import argparse
|
||||
import datetime
|
||||
import pathlib
|
||||
import subprocess
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parents[4]
|
||||
BROKER = ROOT / 'packages/bus/src/broker.mjs'
|
||||
VOCABULARY = ROOT / 'packages/business/src/vocabulary.mjs'
|
||||
BUS_TEST = 'packages/bus/tests/grant-scope.test.mjs'
|
||||
BUSINESS_TEST = 'packages/business/tests/grant-scope.test.mjs'
|
||||
|
||||
|
||||
def replace_once(source, old, new):
|
||||
if source.count(old) != 1:
|
||||
raise RuntimeError(f'expected exactly one mutant site, found {source.count(old)}')
|
||||
return source.replace(old, new, 1)
|
||||
|
||||
|
||||
def mutate(name, source):
|
||||
if name == 'M1':
|
||||
return replace_once(source,
|
||||
'if (!raise?.grant || !grantInScope(scope, raise.grant)) return view;',
|
||||
'if (!raise?.grant) return view;')
|
||||
if name == 'M2':
|
||||
return replace_once(source,
|
||||
'return scope === null ? { scope: GRANT_SCOPE_EMPTY, fallback: true } : { scope, fallback: false };',
|
||||
'return { scope: scope ?? value, fallback: false };')
|
||||
if name == 'M3':
|
||||
return replace_once(source,
|
||||
' #grantScopeVerb(s, verb, a) {\n this.#human(s);',
|
||||
' #grantScopeVerb(s, verb, a) {')
|
||||
if name == 'M4':
|
||||
old = """ this.#grantEvent(s.business, {
|
||||
action: 'grant.scope.used', grantDecision: d.id, decisionSeq: seq, method: raise.grant.method,
|
||||
agent: raise.grant.agent, scope, revision, scopeSha256: sha256(JSON.stringify(scope)),
|
||||
}, d.id);"""
|
||||
return replace_once(source, old, ' // MUTANT: scope evidence omitted.')
|
||||
if name == 'M5':
|
||||
return replace_once(source,
|
||||
"evidence.agent === grant.agent && evidence.scopeSha256 === sha256(JSON.stringify(checked.scope)) &&",
|
||||
"evidence.agent === grant.agent &&")
|
||||
if name == 'M6':
|
||||
start = source.index(" checked = this.#store.transaction(() => {\n const consumed = this.#consumeGrant")
|
||||
end = source.index('\n } catch (e) {', start)
|
||||
return source[:start] + " checked = this.#store.transaction(() => this.#giteaRecheck(business, decision, this.#consumeGrant(business, decision, 'gitea')));" + source[end:]
|
||||
if name == 'M7':
|
||||
return replace_once(source,
|
||||
'const same = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]);',
|
||||
'const same = (a, b) => a.every((x, i) => x === b[i]);')
|
||||
if name == 'M8':
|
||||
return replace_once(source,
|
||||
" if (this.#closed(decision)?.via === 'scope' && !grantInScope(this.#scopeNow(business).scope, g)) fail('scope-changed');",
|
||||
" // MUTANT: no spend-time scope recheck.")
|
||||
if name == 'M9':
|
||||
return replace_once(source,
|
||||
' if (s.role !== this.#business(s.business).launch?.by) return view;',
|
||||
' // MUTANT: any chief instance can use scope.')
|
||||
if name == 'M10':
|
||||
start = source.index(' const evidence = JSON.parse(row.body);', source.index(' #scopeUsed('))
|
||||
end = source.index('\n }', start)
|
||||
return source[:start] + ' return true;' + source[end:]
|
||||
raise ValueError(name)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--sage-clear', required=True, help='CLEAR message key; required before any Node run')
|
||||
parser.add_argument('--logs', type=pathlib.Path, required=True)
|
||||
parser.add_argument('--mutant', choices=['M1', 'M2', 'M3', 'M4', 'M5', 'M6', 'M7', 'M8', 'M9', 'M10', 'all'], default='all')
|
||||
parser.add_argument('--deadline', default='2026-10-23T19:38:00+00:00')
|
||||
args = parser.parse_args()
|
||||
deadline = datetime.datetime.fromisoformat(args.deadline)
|
||||
args.logs.mkdir(parents=True, exist_ok=True)
|
||||
names = ['M1', 'M2', 'M3', 'M4', 'M5', 'M6', 'M7', 'M8', 'M9', 'M10'] if args.mutant == 'all' else [args.mutant]
|
||||
for name in names:
|
||||
remaining = (deadline - datetime.datetime.now(datetime.timezone.utc)).total_seconds()
|
||||
if remaining <= 15:
|
||||
raise RuntimeError('Sage slot deadline reached; no new test run')
|
||||
path, test = (VOCABULARY, BUSINESS_TEST) if name in ['M2', 'M7'] else (BROKER, BUS_TEST)
|
||||
original = path.read_bytes()
|
||||
mutated = mutate(name, original.decode())
|
||||
log = args.logs / f'{name}.log'
|
||||
try:
|
||||
path.write_text(mutated)
|
||||
with log.open('w') as output:
|
||||
result = subprocess.run(['timeout', '-k', '10', str(int(min(180, remaining - 13))), 'node', '--test', '--test-concurrency=1', test],
|
||||
cwd=ROOT, stdout=output, stderr=subprocess.STDOUT,
|
||||
timeout=min(195, remaining - 1), check=False)
|
||||
print(f'{name}: exit={result.returncode} log={log}', flush=True)
|
||||
finally:
|
||||
path.write_bytes(original)
|
||||
# Exit 1 needs human inspection of the failing test names; errors are not kills.
|
||||
if result.returncode == 0:
|
||||
raise RuntimeError(f'{name} survived')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1593,3 +1593,7 @@ are never rewritten or removed; corrections are new entries.
|
||||
2026-10-23T17:48:43Z | Scrooge (629b6cd3) | Row 252 Console inbox Approve and Deny (#1753) | Rebased onto 306 landing as 580037105 (range-diff all =), slot run bus 907/0 webui 410/0 cli 943/0, in-review revs 3608/3609, Webby r1 APPROVE, J1-J19 specs to Sage; Sage landed 45ea3fa0d (done rev 3616). Part 1 mutant results were never in BUILD.md; they went to Sage by T3 (scrooge-0252-mutant-specs).
|
||||
2026-10-23 | Sage (1ef1e4f8) | Rows 308 and 252 landings, 571 Connections design amendment, Vikunja 597 | 308 (PRD 1.17, #1815) landed 30e6bb680 and 252 (#1753) landed 45ea3fa0d (replay of 580037105 onto 30e6bb680, range-diff all =), each after a full gate on the landed tree (252 also J1-J19, J11c and J18 equivalent); Gyro's 571 amendment (Fenton APPROVE) landed e8bb13df8; Vikunja 570, 435 and 597 done, canonical webui esbuild 0.28.2; queue revs 3589-3616 committed 59f013247
|
||||
2026-10-23 | Sage (1ef1e4f8) | Vikunja 571 W build rows; rows 13, 42, 257 and 289 check | Filed the design's ten W rows as queue rows 309-318 (revs 3617-3626, owners set to unassigned 3627-3636, queue commit 293b42fee pushed); Vikunja 571 done (comment 994). Row 13's candidate fd72d2689 is on refactor, gate is Monday's row 7 ledger run; 42 paused under mos-1184; 257 post-v1; 289 waits on 214 and 288. Slot released 17:56Z.
|
||||
|
||||
2026-10-23T19:13:00Z | Boyd (48a3ae38, Codex gpt-6.1-sol) | Row 298 (#1804, Q228) takeover from Megavolt under mos-1496 | Started on boyd/row298 at 45dca69f6; reading brief, D1-D6 and human-shell proof; awaiting Megavolt handoff. No Node/tests before Sage CLEAR, no queue writes.
|
||||
|
||||
2026-10-23T19:29:00Z | Boyd (48a3ae38) | Row 298 (#1804) targeted T1 lane B | Five whole files 29/0/0, ten mutants killed, source restored; reviewer delta prepared. Earlier startup timestamp corrected in Boyd BUILD.md. Full gate and queue/landing remain Sage's.
|
||||
@@ -226,6 +226,8 @@ an operator inspect content and delivery evidence after an uncertain result.
|
||||
| `hosts.remove` | `{host}`; the human or the tester. Deletes the record, writes `grant.host.removed` and removes the installer key. A `host_grants` record for the host is `host-in-use`, a guarded event naming a host line left on the host `host-line-left`. |
|
||||
| `hosts.revoke` | `{agent,host,reason}`; the human, the business's assistant, or the tester when `bus.tester` is exactly `true` (row 235, #1741, r6 section 6). Anyone else gets `revoke-refused`. The tester check reads `bus.tester` again at the call, `tester-dev-only`. One transaction deletes the `host_grants` record and writes `grant.revoked` with `hostLine:"pending"`. With no await between, the key leaves memory and its file goes, and a `host.run` or executor in flight aborts. Then `remove <user> <tag>` runs on the host and `grant.revoke.closed` records `hostLine:"removed"` or `hostLineLeft`. A revoke with no record still runs, with `granted:false`. Returns `{agent,host,tag,granted,keyFile,hostLine or hostLineLeft}`. A restart with a pending marker runs the remove once the bus is up; `hosts.status` lists its tag under `revoking`. |
|
||||
| `hosts.sweep` | `{host}`; the human or the tester (r6 X3). Runs `list <user>` and removes each listed tag this business recorded for the host in a `grant.step` or `grant.revoked` that no live grant holds. Another business's line and an unrecorded `mosaic-` line stay. Writes `grant.host.swept {host,removed,left}`; `hostLineLeft` then counts from it. Returns `{host,removed,hostLineLeft}`. |
|
||||
| `grants.scope` | `{}`; the proven human CLI only (row 298, #1804, Q228). Returns the organization revision, effective scope and fallback flag. `launch.grantScope` missing or invalid covers nothing. |
|
||||
| `grants.scope.set` | `{revision,scope}`; the proven human CLI only, itself Approve for the scope change. Agents, testers, readers and Console caps refuse. Strict scope shape; optimistic organization revision, durable organization receipt, and `grant.scope` with human proof and old/new scope digests. General organization setters cannot write this setting. Scope matches per-method agent and every project, or agent/host and exact command arrays; host enrollment is excluded. In-scope requests by `launch.by` close via `scope` with protected `grant.scope.used` evidence, and run the existing grant executor without per-grant Approve. The raise returns the closed decision view to the chief; this shortcut sends no separate decision-close message. Outside scope waits as before. Scope narrowing before spend fails `scope-changed`; every issued grant keeps its records and revoke path. Empty default ships; the live value is Jason's to set. |
|
||||
| `grants.status` | `{service:"gitea"}`; the human, the tester or the chief (row 236, #1742, #436b r5). The admin slot's url, account and rotate-by date with `rotationDue`, `adminTokenLeft`, the `gitea_users` and `gitea_grants` records, approved decisions not yet run (`spentAtNextStart`), grants `running` or `settlePending`, agents held as `tokenUnknown`, the `serviceLeft` entries a sweep retries, and revokes still `revoking`. No token. |
|
||||
| `grants.run` | `{decision}`; the human only. Starts the Gitea executor of a `credential.mint` decision with a gitea grant the human said yes to, as the yes itself does (X1). Step 0's refusals come back with no record: `not-authorized`, `decision-consumed`, `grant-token-unknown`, `grant-busy`, `gitea-not-configured`. Returns `{decision,agent,started:true}`; the outcome is a `grant.applied` or `grant.failed` and a notice. |
|
||||
| `grants.revoke` | `{service:"gitea",agent,reason}`; the human, the business's assistant, or the tester when `bus.tester` is exactly `true`. The token leaves memory and its file goes, an executor in flight aborts, then one transaction deletes the `gitea_grants` record and writes `grant.revoked` with the token ids to delete. The deletes run with the admin token after C1 to C4, and `grant.revoke.closed` records `serviceLeft`, a list. `credentials.revoke AGENT gitea` and `credentials.delete` do the same for a stack-made user; delete also deletes every token of the user and sets `prohibit_login` (fire). |
|
||||
|
||||
+39
-16
@@ -486,10 +486,9 @@ export class Broker {
|
||||
// sets bus.tester to exactly true; the bus runtime reads it live, so a hand edit counts at the next
|
||||
// tester lifecycle call and at the take. Without it the broker's own copy decides.
|
||||
this.#testerOn = testerOn ?? ((business) => this.#business(business).vars?.['bus.tester'] === true);
|
||||
// Row 298 (#1804, Q228): grantScope(business) is launch.grantScope as the organization file stands,
|
||||
// unchecked; the bus runtime reads it live, so the human's write counts at the next raise. Without
|
||||
// it the broker's own copy decides.
|
||||
this.#grantScope = grantScope ?? ((business) => this.#business(business).launch?.grantScope);
|
||||
// Row 298 (#1804, Q228): grantScope(business) reads {value, revision} atomically from the
|
||||
// organization file. Without a configured reader no scope can authorize a grant.
|
||||
this.#grantScope = grantScope ?? (() => ({ value: undefined, revision: null }));
|
||||
// Row 194 (#1692, D2.3): roleFile(name) loads role definition `name` from the roles directory as it
|
||||
// stands, for the hire checks. Without it a hire refuses not-configured.
|
||||
this.#roleFile = roleFile;
|
||||
@@ -1549,21 +1548,31 @@ export class Broker {
|
||||
// Row 298: the grant.scope.used record of scope close seq. Gitea and host grants only.
|
||||
#scopeUsed(business, decision, seq, grant) {
|
||||
if (grant.method !== 'gitea' && grant.method !== 'host') return false;
|
||||
return Boolean(this.#store.get(
|
||||
`SELECT 1 FROM events WHERE business=? AND ${GRANT_EVIDENCE} AND json_extract(body,'$.action')='grant.scope.used'
|
||||
const row = this.#store.get(
|
||||
`SELECT body FROM events WHERE business=? AND ${GRANT_EVIDENCE} AND json_extract(body,'$.action')='grant.scope.used'
|
||||
AND json_extract(body,'$.grantDecision')=? AND json_extract(body,'$.decisionSeq')=? AND json_extract(body,'$.method')=? LIMIT 1`,
|
||||
business, decision, seq, grant.method,
|
||||
));
|
||||
);
|
||||
if (!row) return false;
|
||||
const evidence = JSON.parse(row.body);
|
||||
const checked = grantScopeOf(evidence.scope);
|
||||
// Verify the immutable raise-time snapshot, not today's scope: scope changes do not
|
||||
// invalidate already issued grants. The executor rechecks today's scope before spend.
|
||||
return !checked.fallback && typeof evidence.revision === 'string' && /^[a-f0-9]{64}$/.test(evidence.revision) &&
|
||||
evidence.agent === grant.agent && evidence.scopeSha256 === sha256(JSON.stringify(checked.scope)) &&
|
||||
grantInScope(checked.scope, grant);
|
||||
}
|
||||
// Row 298: the scope the broker applies now. A missing key, a bad value or an unreadable file is the
|
||||
// empty scope, which covers nothing, so every grant waits for the human (brief item 1).
|
||||
// Row 298: the live organization snapshot. Missing/bad scope covers nothing. An unreadable
|
||||
// organization file or missing revision cannot authorize a scope close either.
|
||||
#scopeNow(business) {
|
||||
try {
|
||||
return grantScopeOf(this.#grantScope(business)).scope;
|
||||
const snapshot = this.#grantScope(business);
|
||||
return { ...grantScopeOf(snapshot?.value), revision: snapshot?.revision ?? null };
|
||||
} catch {
|
||||
return grantScopeOf(undefined).scope;
|
||||
return { ...grantScopeOf(undefined), revision: null };
|
||||
}
|
||||
}
|
||||
|
||||
// Row 298 (D4, sage-3594 Q2/Q4): after a gitea or host raise, in its transaction. When the raiser is
|
||||
// launch.by, the decision went to the human and the grant is inside the scope, the broker closes it
|
||||
// with the approval choice, via 'scope', writes grant.scope.used and starts the executor, as the
|
||||
@@ -1575,7 +1584,8 @@ export class Broker {
|
||||
const d = this.#store.get('SELECT id,route_to FROM decisions WHERE business=? AND id=?', s.business, view.id);
|
||||
if (d?.route_to !== 'human' || this.#closed(d.id)) return view;
|
||||
const raise = this.#raised(s.business, d.id);
|
||||
const scope = this.#scopeNow(s.business);
|
||||
const { scope, revision } = this.#scopeNow(s.business);
|
||||
if (typeof revision !== 'string' || !/^[a-f0-9]{64}$/.test(revision)) return view;
|
||||
if (!raise?.grant || !grantInScope(scope, raise.grant)) return view;
|
||||
const seq = Number(
|
||||
this.#store.run(
|
||||
@@ -1585,7 +1595,7 @@ export class Broker {
|
||||
);
|
||||
this.#grantEvent(s.business, {
|
||||
action: 'grant.scope.used', grantDecision: d.id, decisionSeq: seq, method: raise.grant.method,
|
||||
agent: raise.grant.agent, scopeSha256: sha256(JSON.stringify(scope)),
|
||||
agent: raise.grant.agent, scope, revision, scopeSha256: sha256(JSON.stringify(scope)),
|
||||
}, d.id);
|
||||
// As the resolve's: each executor rechecks all of this itself, so a rollback starts nothing.
|
||||
if (raise.grant.method === 'host') setImmediate(() => this.#runHostGrant(s.business, d.id));
|
||||
@@ -1595,7 +1605,7 @@ export class Broker {
|
||||
// Row 298 (D6, sage-3594 Q5): at the spend, a scope close needs its grant still inside the scope.
|
||||
// A narrowed scope ends it scope-changed, and the human's Approve is the way back.
|
||||
#scopeRecheck(business, decision, g) {
|
||||
if (this.#closed(decision)?.via === 'scope' && !grantInScope(this.#scopeNow(business), g)) fail('scope-changed');
|
||||
if (this.#closed(decision)?.via === 'scope' && !grantInScope(this.#scopeNow(business).scope, g)) fail('scope-changed');
|
||||
}
|
||||
// Row 252: the console answer's human.input for close row r. Same decision, same seq, same choice,
|
||||
// no role or run, and a three-string owner subject.
|
||||
@@ -2912,10 +2922,22 @@ export class Broker {
|
||||
} catch {}
|
||||
return code;
|
||||
}
|
||||
// Step 1, one transaction: the spend and the rechecks. A refusal rolls the spend back.
|
||||
// Step 1, one transaction: spend and recheck. scope-changed ends spent (D6);
|
||||
// other refusals retain the existing rollback behavior.
|
||||
let checked;
|
||||
try {
|
||||
checked = this.#store.transaction(() => this.#giteaRecheck(business, decision, this.#consumeGrant(business, decision, 'gitea')));
|
||||
checked = this.#store.transaction(() => {
|
||||
const consumed = this.#consumeGrant(business, decision, 'gitea');
|
||||
try {
|
||||
return this.#giteaRecheck(business, decision, consumed);
|
||||
} catch (e) {
|
||||
// D6: scope narrowing is a terminal, spent failure. Other recheck
|
||||
// refusals retain row 236's transaction/rollback behavior.
|
||||
if (!(e instanceof BusError) || e.code !== 'scope-changed') throw e;
|
||||
this.#giteaFailed(business, decision, agent, e.code);
|
||||
return { scopeChanged: true };
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
const code = e instanceof BusError ? e.code : 'gitea-failed';
|
||||
if (code !== 'not-authorized' && code !== 'decision-consumed') {
|
||||
@@ -2925,6 +2947,7 @@ export class Broker {
|
||||
}
|
||||
return code;
|
||||
}
|
||||
if (checked.scopeChanged) return 'scope-changed';
|
||||
return await this.#giteaSteps(business, decision, g, signal, log, api, meta, checked, stop);
|
||||
} finally {
|
||||
this.#giteaKeys.release(business, agent, signal);
|
||||
|
||||
@@ -369,11 +369,12 @@ export async function startBroker({
|
||||
return false;
|
||||
}
|
||||
},
|
||||
// Row 298 (#1804, Q228): launch.grantScope as the file stands now, unchecked. The broker checks
|
||||
// it; a throw here (an unreadable file) is the empty scope there.
|
||||
// Row 298 (#1804, Q228): one read binds launch.grantScope to the organization revision.
|
||||
// A missing organization reader or unreadable file cannot authorize an automatic grant.
|
||||
grantScope: (id) => {
|
||||
const b = settings ? settings.current(id) : businesses[id];
|
||||
return b?.launch?.grantScope;
|
||||
if (!settings) return { value: undefined, revision: null };
|
||||
const { document, revision } = instances.writer.read(id);
|
||||
return { value: document.launch?.grantScope, revision };
|
||||
},
|
||||
// Row 194 (#1692, D4.1, B8): bus.tester as the file sets it now; an unreadable file is off.
|
||||
testerOn: (id) => {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// Row 298: the real runtime routes the human scope verb to the durable
|
||||
// organization writer. Everything lives under a scratch config/data root.
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdirSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { startBroker } from '../src/runtime.mjs';
|
||||
import { businessDoc, rolesCopy, systemFor, writeJson } from '../../business/tests/helpers.mjs';
|
||||
|
||||
// Break caught: missing runtime setter routing or losing the organization receipt.
|
||||
test('row 298 runtime: human scope approval writes the file and organization history at the supplied revision', async t => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'scope-runtime-'));
|
||||
let runtime;
|
||||
t.after(async () => { await runtime?.close(); rmSync(root, { recursive: true, force: true }); });
|
||||
const rolesDir = rolesCopy(root);
|
||||
const file = writeJson(join(root, 'businesses/demo.json'), businessDoc(root, 'demo'));
|
||||
mkdirSync(join(root, 'data'), { mode: 0o700 });
|
||||
runtime = await startBroker({ dataRoot: join(root, 'data'),
|
||||
businesses: { demo: { id: 'demo', human: 'jason', arbiters: { technical: 'coder', delivery: 'coder' },
|
||||
roles: { coder: { authority: { withinRole: [], crossRole: [] } } } } },
|
||||
organization: { dir: root, rolesDir, system: systemFor(root) },
|
||||
});
|
||||
const human = runtime.broker.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true });
|
||||
const call = (verb, args = {}) => runtime.broker.request(human, { verb, args });
|
||||
const before = call('grants.scope');
|
||||
assert.deepEqual(before.scope, {});
|
||||
const scope = { gitea: { agents: ['coder'], projects: ['stack'] } };
|
||||
const saved = call('grants.scope.set', { revision: before.revision, scope });
|
||||
assert.deepEqual(JSON.parse(readFileSync(file, 'utf8')).launch.grantScope, scope);
|
||||
assert.deepEqual(call('grants.scope').scope, scope);
|
||||
assert.notEqual(saved.revision, before.revision);
|
||||
const entries = readFileSync(join(root, 'businesses/.history/demo/changes.jsonl'), 'utf8').trim().split('\n').map(JSON.parse);
|
||||
assert.equal(entries.at(-1).state, 'committed');
|
||||
assert.equal(entries.at(-1).actor, 'jason');
|
||||
assert.equal(entries.at(-1).changes[0].path, 'launch.grantScope');
|
||||
const bytes = readFileSync(file, 'utf8');
|
||||
assert.throws(() => call('grants.scope.set', { revision: before.revision, scope: {} }), e => e.code === 'revision-conflict');
|
||||
assert.equal(readFileSync(file, 'utf8'), bytes);
|
||||
});
|
||||
@@ -11,14 +11,14 @@ import { createHash } from 'node:crypto';
|
||||
import { mkdirSync, mkdtempSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { Broker, GRANT_EVIDENCE } from '../src/broker.mjs';
|
||||
import { Broker, GRANT_EVIDENCE, writeGrantEvent } from '../src/broker.mjs';
|
||||
import { Credentials } from '../src/credentials.mjs';
|
||||
import { giteaGrant, giteaKeys } from '../src/gitea-grant.mjs';
|
||||
import { hostGrant } from '../src/host-grant.mjs';
|
||||
import { hostGrant, grantKeys, fingerprint } from '../src/host-grant.mjs';
|
||||
import { Store } from '../src/store.mjs';
|
||||
import { checkGrantScope, grantScopeOf } from '../../business/src/vocabulary.mjs';
|
||||
import { fakeGiteaApi } from './fake-gitea-api.mjs';
|
||||
import { hostRecord } from './host-fixtures.mjs';
|
||||
import { hostRecord, grantDecision } from './host-fixtures.mjs';
|
||||
|
||||
const USER = 'demo-coder';
|
||||
const ENTRY = { address: '192.0.2.10', port: 22, user: 'jarvis', hostKeys: ['ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHostKeyForTests'] };
|
||||
@@ -34,7 +34,7 @@ const DEFINITIONS = {
|
||||
ops: { roleVersion: 2, credentials: [{ service: 'host' }] },
|
||||
};
|
||||
const GITEA = { gitea: { agents: ['coder'], projects: ['app'] } };
|
||||
const HOST = { host: { agents: ['ops'], hosts: ['web1'], commandPrefixes: [['systemctl', 'status']] } };
|
||||
const HOST = { host: { agents: ['ops'], hosts: ['web1'], commands: [['systemctl', 'status', 'nginx']] } };
|
||||
const REVISION = 'a'.repeat(64);
|
||||
async function until(fn, ms = 10000) {
|
||||
const end = Date.now() + ms;
|
||||
@@ -97,18 +97,28 @@ async function setup(t, opts = {}) {
|
||||
}
|
||||
return { revision: REVISION };
|
||||
};
|
||||
// Keep the real key generation/store/records; replace only remote host calls.
|
||||
const hostLines = new Set();
|
||||
const hostApi = {
|
||||
...hostGrant({ configDir, runRoot: join(configDir, 'secrets', 'run') }),
|
||||
keyscan: async () => ENTRY.hostKeys,
|
||||
install: async (_business, _host, _record, tag) => { hostLines.add(tag); },
|
||||
verify: async () => {},
|
||||
remove: async (_business, _host, _record, tag) => { hostLines.delete(tag); },
|
||||
};
|
||||
const broker = new Broker({
|
||||
store,
|
||||
businesses: { demo: business },
|
||||
secretCheck: (value) => credentials.assertClean(value),
|
||||
giteaGrant: grantApi,
|
||||
giteaKeys: giteaKeys({ credentials, storeRoot }),
|
||||
hostGrant: hostGrant({ configDir, runRoot: join(configDir, 'secrets', 'run') }),
|
||||
hostGrant: hostApi,
|
||||
hostKeys: opts.hostKeys ? grantKeys({ credentials, storeRoot }) : null,
|
||||
organization,
|
||||
grantScope: () => (typeof box.value === 'function' ? box.value() : box.value),
|
||||
grantScope: () => ({ value: typeof box.value === 'function' ? box.value() : box.value, revision: Object.hasOwn(opts, 'revision') ? opts.revision : REVISION }),
|
||||
});
|
||||
const caps = {};
|
||||
for (const role of ['chief', 'chief2', 'pm']) {
|
||||
for (const role of ['chief', 'chief2', 'pm', 'ops']) {
|
||||
caps[role] = broker.bindLaunch({ business: 'demo', role, run: role + '-run', harness: 'pi', address: role });
|
||||
broker.request(caps[role], { verb: 'role.claim' });
|
||||
}
|
||||
@@ -134,7 +144,7 @@ async function setup(t, opts = {}) {
|
||||
const closes = (decision) => store.all('SELECT * FROM decision_events WHERE decision=? ORDER BY seq', decision);
|
||||
const spent = (decision) => grantEvents().filter((e) => e.decision === decision && e.operation?.startsWith('grant.')).map((e) => e.operation);
|
||||
const record = () => store.get("SELECT * FROM gitea_grants WHERE business='demo' AND agent='coder'");
|
||||
return { store, broker, caps, box, orgCalls, call, raiseGitea, raiseHost, grantEvents, ended, closes, spent, record, gitea };
|
||||
return { store, broker, caps, box, orgCalls, call, raiseGitea, raiseHost, grantEvents, ended, closes, spent, record, gitea, hostLines };
|
||||
}
|
||||
|
||||
test('row 298 (Q3): only the human at the cli reads or sets the scope; agents, the tester, a reader and the Console refuse', async (t) => {
|
||||
@@ -180,6 +190,7 @@ test('row 298 (Q4): an in-scope Gitea grant is issued with no inbox item, logged
|
||||
const d = await f.raiseGitea();
|
||||
const [close] = f.closes(d.id);
|
||||
assert.deepEqual([close.op, close.by, close.choice, close.via], ['resolved', 'grant-scope', 'yes', 'scope']);
|
||||
assert.ok(f.grantEvents().some(e => e.action === 'grant.scope.used' && e.grantDecision === d.id), 'scope close must have protected evidence');
|
||||
const inbox = await f.call('human', 'inbox');
|
||||
assert.ok(!JSON.stringify(inbox).includes(d.id), 'the decision is not in the human inbox');
|
||||
await until(() => f.ended(d.id));
|
||||
@@ -187,12 +198,12 @@ test('row 298 (Q4): an in-scope Gitea grant is issued with no inbox item, logged
|
||||
assert.equal(f.ended(d.id).action, 'grant.applied');
|
||||
const used = f.grantEvents().find((e) => e.action === 'grant.scope.used');
|
||||
assert.deepEqual(used, {
|
||||
action: 'grant.scope.used', grantDecision: d.id, decisionSeq: close.seq, method: 'gitea', agent: 'coder', scopeSha256: sha256(JSON.stringify(GITEA)),
|
||||
action: 'grant.scope.used', grantDecision: d.id, decisionSeq: close.seq, method: 'gitea', agent: 'coder', scope: GITEA, revision: REVISION, scopeSha256: sha256(JSON.stringify(GITEA)),
|
||||
});
|
||||
assert.deepEqual(f.spent(d.id), ['grant.gitea']);
|
||||
assert.equal(f.record().decision, d.id);
|
||||
// The spend is row 236's: a second start finds it consumed.
|
||||
assert.equal(await f.broker.giteaStart?.('demo', d.id) ?? 'decision-consumed', 'decision-consumed');
|
||||
assert.equal(await f.broker.runGiteaGrant('demo', d.id), 'decision-consumed');
|
||||
const out = await f.call('human', 'grants.revoke', { service: 'gitea', agent: 'coder', reason: 'test' });
|
||||
assert.deepEqual(out.serviceLeft, []);
|
||||
assert.ok(f.grantEvents().some((e) => e.action === 'grant.revoked' && e.agent === 'coder'));
|
||||
@@ -237,10 +248,12 @@ test('row 298 (Q5): a scope narrowed between the raise and the spend ends the gr
|
||||
assert.deepEqual([f.ended(d.id).action, f.ended(d.id).code], ['grant.failed', 'scope-changed']);
|
||||
assert.equal(f.record(), undefined);
|
||||
assert.deepEqual(f.gitea.tokens(USER) ?? [], []);
|
||||
assert.deepEqual(f.spent(d.id), ['grant.gitea']);
|
||||
assert.equal(await f.broker.runGiteaGrant('demo', d.id), 'decision-consumed');
|
||||
});
|
||||
|
||||
test('row 298 (Q2): an in-scope host grant is spent with no Approve; ls never covers lsblk, nor a scope command extra arguments', async (t) => {
|
||||
const scope = { host: { agents: ['ops'], hosts: ['web1'], commands: [['ls']], commandPrefixes: [['systemctl', 'status']] } };
|
||||
const scope = { host: { agents: ['ops'], hosts: ['web1'], commands: [['ls'], ['systemctl', 'status', 'nginx']] } };
|
||||
const f = await setup(t, { scope });
|
||||
const d = await f.raiseHost();
|
||||
assert.equal(f.closes(d.id)[0].via, 'scope');
|
||||
@@ -261,4 +274,86 @@ test('row 298 (Q5): a host scope narrowed before the spend ends scope-changed',
|
||||
assert.equal(f.closes(d.id)[0].via, 'scope');
|
||||
await until(() => f.ended(d.id));
|
||||
assert.deepEqual([f.ended(d.id).action, f.ended(d.id).code], ['grant.failed', 'scope-changed']);
|
||||
assert.deepEqual(f.spent(d.id), ['grant.host']);
|
||||
});
|
||||
|
||||
// Break caught: accepting scope closes with no evidence or unbound evidence.
|
||||
test('row 298 Q4: missing scope evidence or a bad scope digest cannot authorize execution', async (t) => {
|
||||
const f = await setup(t, { scope: GITEA });
|
||||
const good = { scope: GITEA, scopeSha256: sha256(JSON.stringify(GITEA)), revision: REVISION };
|
||||
for (const evidence of [
|
||||
null,
|
||||
{ ...good, scopeSha256: '0'.repeat(64) },
|
||||
{ ...good, revision: null },
|
||||
{ ...good, scope: { gitea: 'all' } },
|
||||
{ ...good, agent: 'other' },
|
||||
{ ...good, method: 'host' },
|
||||
{ ...good, decisionSeq: 999999 },
|
||||
]) {
|
||||
const grant = { method: 'gitea', agent: 'coder', target: { projects: ['app'] }, rotateBy: dayFrom(30) };
|
||||
const id = grantDecision(f.store, { grant, close: { op: 'resolved', by: 'grant-scope', choice: 'yes', via: 'scope' } });
|
||||
if (evidence) writeGrantEvent(f.store, new Date().toISOString(), 'demo', {
|
||||
action: 'grant.scope.used', grantDecision: id, decisionSeq: f.closes(id)[0].seq,
|
||||
method: 'gitea', agent: 'coder', ...evidence,
|
||||
}, id);
|
||||
assert.equal(await f.broker.runGiteaGrant('demo', id), 'not-authorized');
|
||||
assert.deepEqual(f.spent(id), []);
|
||||
assert.equal(f.record(), undefined);
|
||||
}
|
||||
// Positive control: the same fixture with valid scope evidence reaches the
|
||||
// next grant check. Its intentionally absent definition binding refuses there.
|
||||
const id = grantDecision(f.store, {
|
||||
grant: { method: 'gitea', agent: 'coder', target: { projects: ['app'] }, rotateBy: dayFrom(30) },
|
||||
close: { op: 'resolved', by: 'grant-scope', choice: 'yes', via: 'scope' },
|
||||
});
|
||||
writeGrantEvent(f.store, new Date().toISOString(), 'demo', {
|
||||
action: 'grant.scope.used', grantDecision: id, decisionSeq: f.closes(id)[0].seq,
|
||||
method: 'gitea', agent: 'coder', ...good,
|
||||
}, id);
|
||||
assert.equal(await f.broker.runGiteaGrant('demo', id), 'decision-mismatch');
|
||||
assert.deepEqual(f.spent(id), []);
|
||||
assert.equal(f.record(), undefined);
|
||||
});
|
||||
|
||||
// Break caught: host grants bypassing records or losing the ordinary revoke path.
|
||||
test('row 298 T4/T8: an in-scope host grant issues, records and revokes through the existing executor', async (t) => {
|
||||
const f = await setup(t, { scope: HOST, hostKeys: true });
|
||||
const d = await f.raiseHost();
|
||||
assert.equal(f.closes(d.id)[0].via, 'scope');
|
||||
assert.ok(!JSON.stringify(await f.call('human', 'inbox')).includes(d.id));
|
||||
await until(() => f.ended(d.id));
|
||||
await f.broker.grantsIdle();
|
||||
assert.equal(f.ended(d.id).action, 'grant.applied');
|
||||
assert.equal(f.store.get("SELECT decision FROM host_grants WHERE agent='ops'").decision, d.id);
|
||||
assert.equal(f.hostLines.size, 1);
|
||||
await f.call('human', 'grants.scope.set', { revision: REVISION, scope: {} });
|
||||
// Historical approval still passes #hostAccess after scope is narrowed;
|
||||
// only the absent command executor refuses this call, before any sudo decision.
|
||||
await rejects(() => f.broker.requestHost(f.caps.ops,
|
||||
{ verb: 'host.run', args: { host: 'web1', argv: ['systemctl', 'status', 'nginx'] } }, null), 'host-not-configured');
|
||||
await f.call('human', 'hosts.revoke', { agent: 'ops', host: 'web1', reason: 'test' });
|
||||
assert.equal(f.store.get("SELECT decision FROM host_grants WHERE agent='ops'"), undefined);
|
||||
assert.equal(f.hostLines.size, 0);
|
||||
assert.ok(f.grantEvents().some(e => e.action === 'grant.revoked' && e.agent === 'ops'));
|
||||
});
|
||||
|
||||
// Break caught: a new host enrollment entering the scope shortcut.
|
||||
test('row 298 T6: host enrollment stays open for human Approve even with both methods in scope', async (t) => {
|
||||
const f = await setup(t, { scope: { ...GITEA, ...HOST } });
|
||||
const d = await f.call('chief', 'decision.raise', {
|
||||
action: 'credential.mint', question: 'Enroll host?', options, recommendation: 'yes', blocking: false,
|
||||
grant: { method: 'host-enroll', host: 'web2', address: ENTRY.address, port: ENTRY.port,
|
||||
user: ENTRY.user, sudo: true, fingerprints: ENTRY.hostKeys.map(fingerprint) },
|
||||
});
|
||||
assert.deepEqual(f.closes(d.id), []);
|
||||
assert.ok(JSON.stringify(await f.call('human', 'inbox')).includes(d.id));
|
||||
assert.deepEqual(f.grantEvents().filter(e => e.action === 'grant.scope.used'), []);
|
||||
});
|
||||
|
||||
// Break caught: using a scope without a verifiable organization revision.
|
||||
test('row 298: scope without an organization revision never closes automatically', async (t) => {
|
||||
const f = await setup(t, { scope: GITEA, revision: null });
|
||||
const d = await f.raiseGitea();
|
||||
assert.deepEqual(f.closes(d.id), []);
|
||||
assert.deepEqual(f.spent(d.id), []);
|
||||
});
|
||||
@@ -33,7 +33,7 @@ test('Broker keeps the sandbox client import closure: only the listed imports, n
|
||||
// Row 236 (#1742, #436b) reads giteaName and parseGiteaUrl for the Gitea grant path.
|
||||
// FED R3 r5 (row 274, #414) reads covers, a user's grant check, from it.
|
||||
// Row 219 reads THREAD_DEFINITIONS from the same vocabulary for creative thread admission.
|
||||
"import { ID_PATTERN, THREAD_DEFINITIONS, END_PROTECTED, forkProblem, forkUrl, credentialKind, validArgv, commandAllowed, HOST_ID, CREATIVE_DEFINITIONS, unhireableAction, giteaName, parseGiteaUrl, covers } from '../../business/src/vocabulary.mjs';",
|
||||
"import { ID_PATTERN, THREAD_DEFINITIONS, END_PROTECTED, forkProblem, forkUrl, credentialKind, validArgv, commandAllowed, HOST_ID, CREATIVE_DEFINITIONS, unhireableAction, giteaName, parseGiteaUrl, covers, grantScopeOf, grantInScope } from '../../business/src/vocabulary.mjs';",
|
||||
]);
|
||||
assert.doesNotMatch(source, /\bimport\s*\(/);
|
||||
assert.doesNotMatch(readFileSync(new URL('../src/landing.mjs', import.meta.url), 'utf8'), /\bimport\b/);
|
||||
|
||||
@@ -306,10 +306,9 @@ export function covers(grant, { business, project, instance }, now, { level } =
|
||||
// broker and the writer read these, and they live here because the session sandbox mounts this file
|
||||
// (seat/src/sandbox.mjs RUNNER_FILES) and the runner loads broker.mjs.
|
||||
//
|
||||
// Shape: { gitea?: { agents, projects }, host?: { agents, hosts, commands?, commandPrefixes? } }.
|
||||
// Each list is distinct and nonempty. A host entry needs commands or commandPrefixes. A commands
|
||||
// entry covers a requested prefix equal to it, token for token. A commandPrefixes entry also covers a
|
||||
// requested prefix that extends it with more whole tokens. No entry matches part of a token, so
|
||||
// Shape: { gitea?: { agents, projects }, host?: { agents, hosts, commands } }.
|
||||
// Each list is distinct and nonempty. A commands entry covers a requested prefix equal to it,
|
||||
// token for token and with equal length. No entry matches part of a token, so
|
||||
// ["ls"] never covers ["lsblk"]. Enrollment (host-enroll) is never in scope. {} is the empty scope.
|
||||
export const GRANT_SCOPE_EMPTY = Object.freeze({});
|
||||
const SCOPE_IDS = (value) =>
|
||||
@@ -333,15 +332,12 @@ export function checkGrantScope(value) {
|
||||
}
|
||||
if (value.host !== undefined) {
|
||||
const h = value.host;
|
||||
if (!SCOPE_PLAIN(h) || Object.keys(h).some((k) => !["agents", "hosts", "commands", "commandPrefixes"].includes(k))) return null;
|
||||
if (!SCOPE_PLAIN(h) || Object.keys(h).some((k) => !["agents", "hosts", "commands"].includes(k))) return null;
|
||||
if (!SCOPE_IDS(h.agents) || !Array.isArray(h.hosts) || !h.hosts.every((x) => typeof x === "string" && HOST_ID.test(x)) || !SCOPE_IDS(h.hosts)) return null;
|
||||
if (h.commands === undefined && h.commandPrefixes === undefined) return null;
|
||||
if (h.commands !== undefined && !SCOPE_ARGVS(h.commands)) return null;
|
||||
if (h.commandPrefixes !== undefined && !SCOPE_ARGVS(h.commandPrefixes)) return null;
|
||||
if (!SCOPE_ARGVS(h.commands)) return null;
|
||||
out.host = {
|
||||
agents: [...h.agents], hosts: [...h.hosts],
|
||||
...(h.commands !== undefined ? { commands: h.commands.map((argv) => [...argv]) } : {}),
|
||||
...(h.commandPrefixes !== undefined ? { commandPrefixes: h.commandPrefixes.map((argv) => [...argv]) } : {}),
|
||||
commands: h.commands.map((argv) => [...argv]),
|
||||
};
|
||||
}
|
||||
return out;
|
||||
@@ -366,11 +362,10 @@ export function grantInScope(scope, body) {
|
||||
if (body?.method === "host") {
|
||||
const h = scope.host;
|
||||
const same = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]);
|
||||
const extends_ = (argv, prefix) => argv.length >= prefix.length && prefix.every((x, i) => x === argv[i]);
|
||||
return Boolean(h) && h.agents.includes(body.agent) && h.hosts.includes(body.host) &&
|
||||
Array.isArray(body.commands) && body.commands.length > 0 &&
|
||||
body.commands.every((argv) =>
|
||||
(h.commands ?? []).some((c) => same(argv, c)) || (h.commandPrefixes ?? []).some((c) => extends_(argv, c)));
|
||||
h.commands.some((c) => same(argv, c)));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -15,7 +15,7 @@ const roots = [];
|
||||
after(() => roots.forEach(root => rmSync(root, { recursive: true, force: true })));
|
||||
|
||||
const GITEA = { gitea: { agents: ["coder"], projects: ["app", "lib"] } };
|
||||
const HOST = { host: { agents: ["ops"], hosts: ["web1"], commands: [["systemctl", "status", "nginx"]], commandPrefixes: [["ls"]] } };
|
||||
const HOST = { host: { agents: ["ops"], hosts: ["web1"], commands: [["systemctl", "status", "nginx"], ["ls"]] } };
|
||||
const gitea = (over = {}) => ({ method: "gitea", agent: "coder", target: { projects: ["app"] }, ...over });
|
||||
const host = (commands, over = {}) => ({ method: "host", agent: "ops", host: "web1", commands, ...over });
|
||||
|
||||
@@ -46,7 +46,7 @@ test("row 298: checkGrantScope takes the D2 shape and refuses anything else", ()
|
||||
{ gitea: { agents: ["coder"], projects: ["app"], extra: 1 } }, { host: { agents: ["ops"], hosts: ["web1"] } },
|
||||
{ host: { agents: ["ops"], hosts: ["web_1"], commands: [["ls"]] } }, { host: { agents: ["ops"], hosts: ["web1"], commands: [[]] } },
|
||||
{ host: { agents: ["ops"], hosts: ["web1"], commands: [["ls"], ["ls"]] } }, { host: { agents: ["ops"], hosts: ["web1"], commands: [["bin/ls"]] } },
|
||||
{ host: { agents: ["ops"], hosts: ["web1"], commandPrefixes: [["ls", ""]] } }, { "host-enroll": {} },
|
||||
{ host: { agents: ["ops"], hosts: ["web1"], commandPrefixes: [["ls"]] } }, { "host-enroll": {} },
|
||||
])
|
||||
assert.equal(checkGrantScope(bad), null, JSON.stringify(bad));
|
||||
});
|
||||
@@ -75,9 +75,9 @@ test("row 298 (sage-3594 Q2): host commands match whole argv tokens, and enrollm
|
||||
// A commands entry covers only itself: no extra trailing arguments, no shorter prefix.
|
||||
assert.equal(grantInScope(HOST, host([["systemctl", "status", "nginx", "--now"]])), false);
|
||||
assert.equal(grantInScope(HOST, host([["systemctl", "status"]])), false);
|
||||
// A commandPrefixes entry covers more whole tokens after it, never part of a token.
|
||||
// No scope entry permits extending the requested command prefix.
|
||||
assert.equal(grantInScope(HOST, host([["ls"]])), true);
|
||||
assert.equal(grantInScope(HOST, host([["ls", "-l", "/var"]])), true);
|
||||
assert.equal(grantInScope(HOST, host([["ls", "-l", "/var"]])), false);
|
||||
assert.equal(grantInScope(HOST, host([["lsblk"]])), false);
|
||||
assert.equal(grantInScope(HOST, host([["ls"], ["lsblk"]])), false);
|
||||
assert.equal(grantInScope(HOST, host([["ls"]], { host: "web2" })), false);
|
||||
|
||||
@@ -1080,6 +1080,37 @@ reads the admin token on stdin, never argv; `admin rotate gitea --rotate-by DATE
|
||||
gitea --finish` and `admin remove gitea` keep the slot. run and admin are the human's alone. Nothing
|
||||
prints a token. A usage error exits 4 and a bus refusal 2.
|
||||
|
||||
`mosaic grants scope --business ID` reads the chief of staff's effective scope,
|
||||
its organization revision, and whether a bad value fell back to empty (Q228,
|
||||
row 298). `mosaic grants scope set --business ID --revision REV` reads scope JSON
|
||||
from stdin. This human-shell command is Approve for the scope change; it refuses
|
||||
agent callers through the same guard and human transport as `mosaic access`.
|
||||
Neither command accepts `--tester`. No Console page or chief-raised scope-change
|
||||
request exists. A stale revision refuses; malformed scope input writes nothing.
|
||||
|
||||
The setting is `launch.grantScope` in the business organization file. It defaults
|
||||
to `{}`, which covers nothing; a missing or invalid value has the same effect.
|
||||
Only Jason sets the live value. The supported shapes are:
|
||||
|
||||
```json
|
||||
{
|
||||
"gitea": { "agents": ["coder"], "projects": ["app"] },
|
||||
"host": { "agents": ["ops"], "hosts": ["web1"], "commands": [["systemctl", "status", "nginx"]] }
|
||||
}
|
||||
```
|
||||
|
||||
Either method may be omitted. All lists are distinct and nonempty. Every target
|
||||
project, agent, host and command must be listed for its method. Command arrays
|
||||
match token for token with equal lengths: `["ls"]` covers neither `["lsblk"]`
|
||||
nor `["ls", "-l"]`. Host enrollment always needs per-grant Approve.
|
||||
An in-scope request from the enrolled chief is closed and issued by the protected
|
||||
grant executor without an inbox approval; other requests wait for Approve.
|
||||
All grants retain their records and revoke path. Narrowing the scope before a
|
||||
grant is spent fails that grant with `scope-changed`; existing issued grants
|
||||
remain revocable. Setting `{}` removes the setting and restores per-grant Approve.
|
||||
Scope changes have organization receipts and a `grant.scope` event with the
|
||||
human proof and old/new digests; each automatic approval has `grant.scope.used`.
|
||||
|
||||
## Access (FED R5, #414)
|
||||
|
||||
```
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
// Row 298/Q228: the real CLI parser and human-shell refusal. Transport is a
|
||||
// recorder; no socket, config, credential or live grant is accessed.
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { main } from "../src/cli.mjs";
|
||||
import { humanEnv, io } from "./helpers.mjs";
|
||||
|
||||
const AGENT_MARKERS = ["MOSAIC_BUS_CAP", "MOSAIC_RUN_ID", "MOSAIC_AGENT_RUN", "CLAUDECODE", "CLAUDE_CODE_ENTRYPOINT", "CODEX_THREAD_ID", "PI_AGENT_DIR"];
|
||||
const REVISION = "a".repeat(64);
|
||||
const SCOPE = { gitea: { agents: ["coder"], projects: ["app"] } };
|
||||
const READ = ["grants", "scope", "--business", "acme"];
|
||||
const SET = ["grants", "scope", "set", "--business", "acme", "--revision", REVISION];
|
||||
function setup(env = humanEnv(), stdin = JSON.stringify(SCOPE)) {
|
||||
const x = io(env);
|
||||
x.stdin.end(stdin);
|
||||
const calls = [];
|
||||
const deps = {
|
||||
system: { dataRoot: "/nonexistent" },
|
||||
transport: (business) => async (verb, args) => {
|
||||
calls.push({ business, verb, args });
|
||||
return { revision: REVISION, scope: SCOPE, fallback: false };
|
||||
},
|
||||
testerTransport: () => async () => { throw new Error("tester transport must never run"); },
|
||||
};
|
||||
return { x, calls, deps };
|
||||
}
|
||||
|
||||
// Break caught: dropping scope dispatch or misrouting the writer's revision.
|
||||
test("row 298 CLI: scope reads and scope set sends JSON and the observed revision", async () => {
|
||||
const f = setup();
|
||||
await main(READ, f.x, f.deps);
|
||||
assert.deepEqual(f.calls, [{ business: "acme", verb: "grants.scope", args: {} }]);
|
||||
assert.deepEqual(JSON.parse(f.x.out.text), { revision: REVISION, scope: SCOPE, fallback: false });
|
||||
const w = setup();
|
||||
await main(SET, w.x, w.deps);
|
||||
assert.deepEqual(w.calls, [{ business: "acme", verb: "grants.scope.set", args: { revision: REVISION, scope: SCOPE } }]);
|
||||
const empty = setup(humanEnv(), "{}");
|
||||
await main(SET, empty.x, empty.deps);
|
||||
assert.deepEqual(empty.calls[0].args.scope, {});
|
||||
});
|
||||
|
||||
// Break caught: a new grants path bypassing access's refuseInsideAgent guard.
|
||||
test("row 298 CLI: every access agent marker refuses scope set before transport", async () => {
|
||||
for (const marker of AGENT_MARKERS) {
|
||||
const f = setup({ ...humanEnv(), [marker]: "agent" });
|
||||
await assert.rejects(main(SET, f.x, f.deps), (e) => e.exitCode === 3 && /human shell/.test(e.message), marker);
|
||||
assert.deepEqual(f.calls, []);
|
||||
assert.equal(f.x.out.text, "");
|
||||
}
|
||||
});
|
||||
|
||||
// Break caught: --tester selecting a nonhuman transport for this approval.
|
||||
test("row 298 CLI: --tester cannot read or change the scope", async () => {
|
||||
for (const args of [READ, SET]) {
|
||||
const f = setup();
|
||||
await assert.rejects(main([...args, "--tester"], f.x, f.deps), (e) => e.exitCode === 4);
|
||||
assert.deepEqual(f.calls, []);
|
||||
}
|
||||
});
|
||||
|
||||
// Break caught: a missing revision or invalid stdin being sent as an approval.
|
||||
test("row 298 CLI: missing revision and empty, invalid or oversized JSON refuse without a call", async () => {
|
||||
for (const [args, input, code] of [
|
||||
[SET.slice(0, -2), "{}", 4],
|
||||
[SET, "", 4],
|
||||
[SET, " \n", 4],
|
||||
[SET, "{bad", 4],
|
||||
[SET, " ".repeat(65537), 2],
|
||||
]) {
|
||||
const f = setup(humanEnv(), input);
|
||||
await assert.rejects(main(args, f.x, f.deps), (e) => e.exitCode === code);
|
||||
assert.deepEqual(f.calls, []);
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user