Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6887825e42 | ||
|
|
164e166479 |
No files matched your search
@@ -5322,3 +5322,5 @@ Gander: after Duckworth-0100 design approval with C1-C5, prepared the two host-s
|
||||
2026-10-22 14:45 UTC | Gander row253 focused27 on finalaca16404c base | Fresh0604d91fc / manifest573c51cf:27 whole files604/0/0, no timeout/retry, case134/0 in99.281s, case243/0 in66.418s, stack-release includingT46221/0 in583.453s. N9 sandbox bind ran and passed, no focused skips. Seven source blobs unchanged; sweep0 run-created leftovers, pre-existing infrastructure receipts retained. Whole gate and M1-M40 await separate Sage CLEAR after Duckworth rebase check. No live action.
|
||||
|
||||
2026-10-22 14:46 UTC | Gander correction to the row253 focused27 entry immediately above | stack-release whole was218/0/0, not221/0. The27-file sum604/0/0 and duration583.453s are unchanged; raw files/results.json and SUMMARY.json give the exact per-file totals.
|
||||
|
||||
2026-10-22 18:11:58 UTC | Gander row276 #1781 prepared, gate pending | Before: row253 left5legacy Case1 AS_USER calls unsupervised across sudo pty sessions. After: candidate uses its unchanged supervisor with per-call inner/outer budgets, separate streams, strict display-only failure trailer, fake-only paired process-model cases and15unique mutants. Duckworth0182 B1 corrected the plan before product code;0183 cleared build. Parse-only Bash checks pass; no Node, behavioral test, host, unit or config action. Runtime gate and publication await Sage.
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"base": "48e450ce8",
|
||||
"removed_assertions": [
|
||||
" assert.deepEqual([...new Set(cmds)].sort(), [CLI, PRECHECK, CLAUDE, NODE].sort());"
|
||||
],
|
||||
"authority": "mos-1326",
|
||||
"all_other_original_assertion_lines_preserved": true,
|
||||
"command_level_talk_and_precheck_checks_unchanged": true
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
# Row276 build, gate pending
|
||||
|
||||
Base48e450ce8, branchgander/row276. Claim rev3255, gander-276-start-1. Brief b0d0b7f76; Mos1324 starts build, Sage2893 approves plan and authorizes local commits. Duckworth0182 B1 corrected the exit-status plan before product code;0183 approves that correction with C1-C4. PLAN.md records decisions. No runtime/Node check has run.
|
||||
|
||||
All5legacy calls use legacy_run: talk320/330 with unchanged conversation300 and concurrent F5 scan; precheck200/210 preserves its existing180s Claude probe; versions5/7; PM list10/12. Both scripts keep the exact old supervisor payload. Case2 code changes only its setuid-member header. Header N2 names inner-cleanup refusal for a uid-changing unkillable member. Root capture/tools N1 counterparts are M10/M11.
|
||||
|
||||
Supervisor0 is the sole pass authority. The dropped-uid builtin-only Node runner exits its original command status. Supervisor87 stays87; only its ffail display number may come from the strict final newline-terminated decimal1..255 trailer. A0trailer under87 gives display87, never exited0. Missing/bad256/999/leading-zero/sign/no-newline records also give87 and the fixed reason. Private display files live in root's mktemp WORK directory, explicitly0700, never in user data. Root reads at most65536stderr bytes using a validated system tail. NODE is invoked only after uid drop; ownership fence means non-user-writable, not non-executable by the user. Test stand-ins retain0755.
|
||||
|
||||
F3/F4 retain the old displayed code and failure strings. F4's stripped stderr follows its stdout when merged; its precheck contract is whole-check success/failure plus last diagnostic, and no existing test asserts interleaving. F6/F7/F10 now require supervisor0 before interpreting stdout, intentionally tightening their old pipeline behavior. Existing content/refusal assertions remain. The one existing wiring assertion now requires five supervised Bash launcher calls plus the two isolated metadata/probe calls; direct talk/precheck argv assertions remain unchanged.
|
||||
|
||||
Tests are written before source, but intentionally unrun because Sage holds all execution until CLEAR. New tests cover each call under exec/hop, exact inner reasons/no-leftovers, stalled outer reason with explicit R3 fixture cleanup, streams, child statuses1/7/81/137, forged trailers before/after actual status, grammar edge refusals, bounded stderr and stdout that looks successful under supervisor failure. Existing case1/case2 whole files are the gate scope because they import the shared fake module. No actual sudo, host or live service is used. Static SUPERVISOR-SHAS and UNIQUENESS verify bytes/targets. Bash-n on source and15variants passes; Node parse/behavior remains pending.
|
||||
|
||||
Gate request: intended RED/GREEN against old script; case1-accept.test.mjs andcase2-unit.test.mjs whole under timeout-k10 900 initially; M1-M15 with passing fresh controls, exact restores and no timeout counted as kill; all10scripts/test-*.sh whole under SHELL-LIMITS.json, based on the longest retained row253/196/278 receipts (ceil3x+120). Busy check before every file. TMPDIR from mktemp-d /mnt/storage/scratch/tmp/gXXX. First unexpected failure/survivor/timeout stops for Sage/Mos. Report named skips,timing,full counts and cwd sweep. No passing claim until that gate.
|
||||
|
||||
Manifest excludes BUILD-LOG for a doc-only append if Sage needs it. DEFERRED's conditional closure lists F5,N1,N2 individually; R3 and532 remain. No new dependency installation, live state, credentials, ports or units touched. Push waits for a Sage gap. No queue writes after claim.
|
||||
|
||||
|
||||
## mos-1326 wiring assertion authorization
|
||||
|
||||
packages/cli/tests/case1-accept.test.mjs:726. The old direct-command shape cannot express the required supervisor. All other old assertion lines are byte-identical, including command-level talk and precheck argv checks. ASSERTION-PRESERVATION.json records the exact static comparison. Structured sudo-argv.jsonl records every argv element before fake path translation. The replacement compares full arrays, never a substring. Its only variable text is the random message marker, separately pinned by the unchanged original talk assertion. Both supervisor bytes and literal bounds are pinned.
|
||||
|
||||
Old:
|
||||
```js
|
||||
assert.deepEqual([...new Set(cmds)].sort(), [CLI, PRECHECK, CLAUDE, NODE].sort());
|
||||
```
|
||||
|
||||
New:
|
||||
```js
|
||||
assert.equal(asMosaic.length,7,'two metadata/probe calls and five legacy calls');
|
||||
assert.deepEqual([...new Set(cmds)].sort(), ['/usr/bin/bash']);
|
||||
// mos-1326: assert every argv element, not flattened/substr command detection.
|
||||
const source=read(SCRIPT);
|
||||
const supervisor=source.split("<<'MOSAIC_TESTER_SUPERVISOR'")[1].split('\n').slice(1).join('\n').split('\nMOSAIC_TESTER_SUPERVISOR')[0]+'\n';
|
||||
assert.equal(createHash('sha256').update(supervisor).digest('hex'),'17b881513c35107e26f78c3722bc4a2927d206b197597be0bdbb2c8a8588bb4c');
|
||||
const runner=/LEGACY_COMMAND_RUNNER='([\s\S]*?)'\nlegacy_display_rc/.exec(source)[1];
|
||||
const f7=/F7_JS='([\s\S]*?)';\ncheck_f7/.exec(source)[1];
|
||||
const raw=read(join(h.hc,'sudo-argv.jsonl')).trim().split('\n').map(line=>JSON.parse(line));
|
||||
const legacy=raw.filter(a=>a[0]==='-u'&&a[1]==='mosaic'&&a[4].startsWith('PATH='));
|
||||
const marker=legacy[0][20].split(':')[0];
|
||||
const argv=(tag,inner,command)=>['-u','mosaic','-H','env',`PATH=/opt/mosaic-stack/node-${NODE_PIN}/bin:/usr/local/bin:/usr/bin:/bin`,
|
||||
'/usr/bin/bash','-c',supervisor,'--',String(inner),'/usr/bin/setsid','/usr/bin/sleep',NODE,'-e',runner,'--',tag,...command];
|
||||
assert.deepEqual(legacy,[
|
||||
argv('talk',320,[CLI,'talk','chief',marker+': this is the Case 1 acceptance; reply with one short line.','--business','mosaic-stack','--tester','--wait','300']),
|
||||
argv('precheck',200,[PRECHECK,'--business','mosaic-stack','--profile','factory']),
|
||||
argv('claude-version',5,[CLAUDE,'--version']),
|
||||
argv('pm-list',10,[NODE,'-e',f7,'--',`${HOME}/.config/mosaic-dev/businesses/mosaic-stack.json`,CLI,'mosaic-stack']),
|
||||
argv('node-version',5,[NODE,'--version']),
|
||||
]);
|
||||
```
|
||||
|
||||
The existing target-uid config query already requires executing this same NODE before any F check, so a root-only executable mode that prevents the dropped uid running it fails preflight. No new root NODE execution is introduced.
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"docs/plans/DEFERRED.md": "f8af6a2e563caaf99d7e25663370f0ef2842cf5b02d26f3837db8ece3e4fc974",
|
||||
"packages/cli/tests/case-sudo-fixture.mjs": "eac5c83f43056bdeb5781101307e17d4bc01f03e97e02f4f5e930436d7e43535",
|
||||
"packages/cli/tests/case1-accept.test.mjs": "fe4bc897497d83ef247b229cd49acf78b1a607af44e803a8a0a866115ff8cf87",
|
||||
"scripts/case1-accept.sh": "46245e2bc19c8bcab2ac4fbda16cc72f2dfbf656e4bad8db707bb5a254694900",
|
||||
"scripts/case2-unit.sh": "d2709a1da6c9a03f4c8b31bf57ad00df4c0ae62a5cbc0e7588cdceb9dc0e2aa1"
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"whole_files": [
|
||||
"packages/cli/tests/case1-accept.test.mjs",
|
||||
"packages/cli/tests/case2-unit.test.mjs"
|
||||
],
|
||||
"shells": [
|
||||
"scripts/test-auth.sh",
|
||||
"scripts/test-conductor.sh",
|
||||
"scripts/test-config.sh",
|
||||
"scripts/test-discord.sh",
|
||||
"scripts/test-extension-package.sh",
|
||||
"scripts/test-foundation.sh",
|
||||
"scripts/test-land-work.sh",
|
||||
"scripts/test-queue.sh",
|
||||
"scripts/test-release.sh",
|
||||
"scripts/test-task.sh"
|
||||
],
|
||||
"mutants": [
|
||||
"M1",
|
||||
"M2",
|
||||
"M3",
|
||||
"M4",
|
||||
"M5",
|
||||
"M6",
|
||||
"M7",
|
||||
"M8",
|
||||
"M9",
|
||||
"M10",
|
||||
"M11",
|
||||
"M12",
|
||||
"M13",
|
||||
"M14",
|
||||
"M15"
|
||||
],
|
||||
"red_pattern": "row276 talk legacy inner timeout",
|
||||
"node_seconds": 900,
|
||||
"estimate_minutes": "12-18; no row276 measured runtime yet"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# Row276 manifest, unrun candidate
|
||||
|
||||
Source 164e166479378caf0702ab8b1eb3871b54219da3, base48e450ce8, branchgander/row276. Five-path manifest digest b0fdd91183d7e1911ed327e32c03b8de7f0efd4c86bd143fa0ad54390358ee63. BUILD-LOG excluded for append-only doc sweep. SOURCE.json pins committed blobs; no Node or behavior run. Duckworth reviews candidate and15mutants (changes.json), including mos-1326 exact wiring and preserved old assertions. Gate scope and stop rules in BUILD.md; next publication waits for Sage gap.
|
||||
@@ -0,0 +1,5 @@
|
||||
f8af6a2e563caaf99d7e25663370f0ef2842cf5b02d26f3837db8ece3e4fc974 docs/plans/DEFERRED.md
|
||||
eac5c83f43056bdeb5781101307e17d4bc01f03e97e02f4f5e930436d7e43535 packages/cli/tests/case-sudo-fixture.mjs
|
||||
fe4bc897497d83ef247b229cd49acf78b1a607af44e803a8a0a866115ff8cf87 packages/cli/tests/case1-accept.test.mjs
|
||||
46245e2bc19c8bcab2ac4fbda16cc72f2dfbf656e4bad8db707bb5a254694900 scripts/case1-accept.sh
|
||||
d2709a1da6c9a03f4c8b31bf57ad00df4c0ae62a5cbc0e7588cdceb9dc0e2aa1 scripts/case2-unit.sh
|
||||
@@ -0,0 +1,23 @@
|
||||
# Supervised legacy Case calls implementation plan
|
||||
|
||||
Goal: finish row253 F5 on base48e450ce8, brief docs/plans/2026-10-22_case-sudo-legacy-followup.md (b0d0b7f76), row276/#1781. Reviewer Duckworth. No live host, units, config, ports or credentials. Tests and Node wait for Sage CLEAR; publication waits for a gap.
|
||||
|
||||
Architecture: keep TESTER_SUPERVISOR byte-identical in both scripts. A case1 legacy launcher uses the validated system Bash/setsid/sleep tools after sudo drops uid, with a separate trusted GNU-timeout outer bound and private notice channel. stdout stays streamed to the caller, stderr stays separate. No root import of copy code. All five original calls are covered; no exception remains.
|
||||
|
||||
Bounds (inner/outer seconds): talk320/330, with its unchanged --wait300 conversation and concurrent F5 scan; precheck200/210, preserving its existing timeout180 Claude probe; Claude version5/7; PM launches10/12; Node version5/7. Test extraction substitutes shorter bounds, with tests pinning the production values. Root stderr notice distinguishes outer-timeout from child137. Existing supervisor codes/reasons apply.
|
||||
|
||||
Exit-status compatibility (Duckworth0182 B1): the inline builtin-only Node runner exits with its command's own status. Supervisor0 is the only success authority. On87 (inner-command), root may parse a final stderr trailer only to save a display code for ffail text; legacy_run still returns87 even if that trailer says0. A missing/bad trailer gives display87 plus the fixed inner-command reason, never pass. All other supervisor reasons ignore the trailer. Root saves display-only status in a private WORK file so background talk/command substitutions can report original exit numbers without changing their pass condition. F3/F4 use that number only in existing failure text; no old assertion changes.
|
||||
|
||||
Trailer grammar: one last, newline-terminated line `MOSAIC_LEGACY_STATUS=<decimal>` with canonical decimal0..255 (0 or nonzero-leading up to3 digits). The runner writes it after command stderr and exits with the actual status. A child/escaped descendant may spoof display diagnostics, but can never change supervisor failure into success. Tests cover an earlier forged0 then actual7, an appended forged0 after a real7, malformed/missing records and statuses137/81.
|
||||
|
||||
Root bounds trailer processing to the last65536bytes through a root-validated system tail; oversized diagnostics are explicitly truncated. stdout remains streamed. stderr's retained tail is emitted without only the final valid trailer. On supervisor0 the trailer is ignored as status; on87 it can alter display only. F6/F7/F10 now check supervisor rc before considering stdout, while retaining their original successful output. They capture stderr privately; F4 merges emitted stdout and stripped stderr at its existing capture, so stderr is emitted after buffered capture. Talk keeps its two files. The NODE runner executes only after uid drop. Its existing F9 root-only path check means uid ownership/no group-or-other write, not owner-only execute; activation's executable mode remains required. Tests pin executable-as-user behavior.
|
||||
|
||||
Files: scripts/case1-accept.sh (launcher, five call sites, header); scripts/case2-unit.sh (setuid-member header note only); packages/cli/tests/case-sudo-fixture.mjs (fake-mode cases/seams); packages/cli/tests/case1-accept.test.mjs (per-call tests and extraction); docs/plans/DEFERRED.md (bounded closure when acceptance passes). Row276 packet keeps manifest, mutants and test scope. Existing assertions remain.
|
||||
|
||||
- [ ] Extend fake-only tests for all5 calls under exec and hop: healthy stdout/stderr and exit status, blocking descendants, PID/SID ownership, exact inner reason, no leftover; stalled supervisor outer reason and explicit fixture cleanup residual; status/trailer invalid refusal. Talk retains300s argv and separate diagnostics, background scan.
|
||||
- [ ] Implement legacy launcher and five call sites; remove the remaining AS_USER array. Preserve all row253 supervisor bytes and root-only command fences.
|
||||
- [ ] Add root-side capture/tools counterparts for case1 (row253 N1), header setuid-member inner-cleanup refusal (N2), and exact timeout-vs-child137 tests.
|
||||
- [ ] Prepare mutants: drop talk supervision; drop outer bound on one diagnostic; bypass individual call, wrong/unchecked SID, trailer0 turns failed supervision into success, missing stream/display-code preservation; case1 tools/capture mutants. Unique target and byte-identical restore checks.
|
||||
- [ ] Duckworth reads candidate/manifest/mutants. On Sage CLEAR, intended RED/GREEN, each touched test whole, all mutants with passing controls and no timeout counted as kill, ten whole shell suites under recorded3x bounds. Report counts,timing,skips,sweep; stop on unexpected red/survivor/timeout for ruling.
|
||||
|
||||
Review focus: instant command exit retains fail-closed inner-session residual532; hostile same-uid setsid-away or killed/stalled supervisor remains row253 R3, never claimed cleanup; a member changing uid yields inner-cleanup; no stdout is treated as authority; no outer-timeout parsed from caller stderr.
|
||||
@@ -0,0 +1,132 @@
|
||||
{
|
||||
"authority": [
|
||||
"mos-1275",
|
||||
"sage-2659"
|
||||
],
|
||||
"scope": "Own Gander row196 receipt set documented for row278, plus the complete row278 run3 shell phase. Row253 old C4 results do not carry. Receipts choose timeouts only.",
|
||||
"consulted_records": [
|
||||
"ec1e7fad8:agents/gander/work/queue-196/evidence/suite-results-run1.json",
|
||||
"ec1e7fad8:agents/gander/work/queue-196/evidence/suite-results.json",
|
||||
"ec1e7fad8:agents/gander/work/queue-196/r2/mutants/results.json",
|
||||
"ec1e7fad8:agents/gander/work/queue-196/r2/run2/base-results.json",
|
||||
"ec1e7fad8:agents/gander/work/queue-196/r2/run2/killers/results.json",
|
||||
"ec1e7fad8:agents/gander/work/queue-196/r3/gate/base-results.json",
|
||||
"ec1e7fad8:agents/gander/work/queue-196/r3/gate/killers/results.json",
|
||||
"ec1e7fad8:agents/gander/work/queue-196/r3/gate/sso-results.json",
|
||||
"ec1e7fad8:agents/gander/work/queue-196/r4/gate/base-results.json",
|
||||
"ec1e7fad8:agents/gander/work/queue-196/r4/gate/killers/results.json",
|
||||
"ec1e7fad8:agents/gander/work/queue-196/r4/gate/sso-results.json",
|
||||
"ec1e7fad8:agents/gander/work/queue-196/r4/gate/whole-results.json",
|
||||
"ec1e7fad8:agents/gander/work/queue-196/r5/gate/whole-results.json",
|
||||
"ec1e7fad8:agents/gander/work/queue-196/r5/resume/summary.json",
|
||||
"ec1e7fad8:agents/gander/work/queue-196/r5/resume/whole-results.json",
|
||||
"ec1e7fad8:agents/gander/work/queue-196/r6/base-results.json",
|
||||
"ec1e7fad8:agents/gander/work/queue-196/r6/first-attempt/results.json",
|
||||
"ec1e7fad8:agents/gander/work/queue-196/r6/killers/results.json",
|
||||
"ec1e7fad8:agents/gander/work/queue-196/r6/summary.json",
|
||||
"ec1e7fad8:agents/gander/work/queue-196/r6/whole-results.json",
|
||||
"385a7194c:agents/gander/work/queue-278/run3/whole/results.json"
|
||||
],
|
||||
"scripts": {
|
||||
"scripts/test-auth.sh": {
|
||||
"recorded_duration_seconds": 1.9081228229999543,
|
||||
"receipt": "3b91cfdaf:agents/gander/work/queue-253/run5-gate/SUMMARY.json (with earlier maximum retained)",
|
||||
"limit_seconds": 126,
|
||||
"derivation": "ceil(3 * longest successful whole-file receipt) + 120s headroom",
|
||||
"script_sha256": "dd4720cb0cbecd2fc523a318f2222d21d257c8c5889996a3d69e522ee971a707",
|
||||
"previous_maximum_seconds": 1.9081228229999543,
|
||||
"previous_receipt": "ec1e7fad8:agents/gander/work/queue-196/r6/whole-results.json",
|
||||
"row278_run3_seconds": 0.816
|
||||
},
|
||||
"scripts/test-conductor.sh": {
|
||||
"recorded_duration_seconds": 8.057173820000026,
|
||||
"receipt": "3b91cfdaf:agents/gander/work/queue-253/run5-gate/SUMMARY.json (with earlier maximum retained)",
|
||||
"limit_seconds": 145,
|
||||
"derivation": "ceil(3 * longest successful whole-file receipt) + 120s headroom",
|
||||
"script_sha256": "da84150467494a4c979759f400854411bd93a7efce981139757bf526d688a23b",
|
||||
"previous_maximum_seconds": 8.057173820000026,
|
||||
"previous_receipt": "ec1e7fad8:agents/gander/work/queue-196/r6/whole-results.json",
|
||||
"row278_run3_seconds": 6.078
|
||||
},
|
||||
"scripts/test-config.sh": {
|
||||
"recorded_duration_seconds": 3.1678915339999367,
|
||||
"receipt": "3b91cfdaf:agents/gander/work/queue-253/run5-gate/SUMMARY.json (with earlier maximum retained)",
|
||||
"limit_seconds": 130,
|
||||
"derivation": "ceil(3 * longest successful whole-file receipt) + 120s headroom",
|
||||
"script_sha256": "3301c1fabf616bb961d62f6decd0a46dadb0f399b8ba7f3c2ca60023d2b24c78",
|
||||
"previous_maximum_seconds": 3.1678915339999367,
|
||||
"previous_receipt": "ec1e7fad8:agents/gander/work/queue-196/r6/whole-results.json",
|
||||
"row278_run3_seconds": 1.868
|
||||
},
|
||||
"scripts/test-discord.sh": {
|
||||
"recorded_duration_seconds": 8.776727847000002,
|
||||
"receipt": "3b91cfdaf:agents/gander/work/queue-253/run5-gate/SUMMARY.json (with earlier maximum retained)",
|
||||
"limit_seconds": 147,
|
||||
"derivation": "ceil(3 * longest successful whole-file receipt) + 120s headroom",
|
||||
"script_sha256": "a5dcf783c51a5d5d184a357f91ebf9fa1c4e054f0cb84a3d1d7668f6156e025e",
|
||||
"previous_maximum_seconds": 8.776727847000002,
|
||||
"previous_receipt": "ec1e7fad8:agents/gander/work/queue-196/r5/resume/whole-results.json",
|
||||
"row278_run3_seconds": 7.431
|
||||
},
|
||||
"scripts/test-extension-package.sh": {
|
||||
"recorded_duration_seconds": 3.01285702399991,
|
||||
"receipt": "3b91cfdaf:agents/gander/work/queue-253/run5-gate/SUMMARY.json (with earlier maximum retained)",
|
||||
"limit_seconds": 130,
|
||||
"derivation": "ceil(3 * longest successful whole-file receipt) + 120s headroom",
|
||||
"script_sha256": "5d7b687f16fd1a9414b229501177d530ba3af4eadd3cac319b33f38fdba9bf99",
|
||||
"previous_maximum_seconds": 3.01285702399991,
|
||||
"previous_receipt": "ec1e7fad8:agents/gander/work/queue-196/r5/resume/whole-results.json",
|
||||
"row278_run3_seconds": 1.366
|
||||
},
|
||||
"scripts/test-foundation.sh": {
|
||||
"recorded_duration_seconds": 49.367,
|
||||
"receipt": "3b91cfdaf:agents/gander/work/queue-253/run5-gate/SUMMARY.json (with earlier maximum retained)",
|
||||
"limit_seconds": 269,
|
||||
"derivation": "ceil(3 * longest successful whole-file receipt) + 120s headroom",
|
||||
"script_sha256": "2e11038e7f784ec4f539eb768f63bab0fab9c9940334388a1fe2d47ab9e2b008",
|
||||
"previous_maximum_seconds": 39.70872647900006,
|
||||
"previous_receipt": "ec1e7fad8:agents/gander/work/queue-196/r5/resume/whole-results.json",
|
||||
"row278_run3_seconds": 39.901
|
||||
},
|
||||
"scripts/test-land-work.sh": {
|
||||
"recorded_duration_seconds": 16.854,
|
||||
"receipt": "3b91cfdaf:agents/gander/work/queue-253/run5-gate/SUMMARY.json (with earlier maximum retained)",
|
||||
"limit_seconds": 171,
|
||||
"derivation": "ceil(3 * longest successful whole-file receipt) + 120s headroom",
|
||||
"script_sha256": "c9d1d5c9b006d5c12e5f796a9828361059f8e0410846fc55c070c0148b15cef4",
|
||||
"previous_maximum_seconds": 12.623263413000037,
|
||||
"previous_receipt": "ec1e7fad8:agents/gander/work/queue-196/r5/resume/whole-results.json",
|
||||
"row278_run3_seconds": 11.338
|
||||
},
|
||||
"scripts/test-queue.sh": {
|
||||
"recorded_duration_seconds": 35.155,
|
||||
"receipt": "3b91cfdaf:agents/gander/work/queue-253/run5-gate/SUMMARY.json (with earlier maximum retained)",
|
||||
"limit_seconds": 226,
|
||||
"derivation": "ceil(3 * longest successful whole-file receipt) + 120s headroom",
|
||||
"script_sha256": "c270f181d1fa8b6d42ff4131c339beba786a8fd620bb66f8afdcec05e8b10868",
|
||||
"previous_maximum_seconds": 23.443742031000088,
|
||||
"previous_receipt": "ec1e7fad8:agents/gander/work/queue-196/r5/resume/whole-results.json",
|
||||
"row278_run3_seconds": 21.158
|
||||
},
|
||||
"scripts/test-release.sh": {
|
||||
"recorded_duration_seconds": 12.168674453999964,
|
||||
"receipt": "3b91cfdaf:agents/gander/work/queue-253/run5-gate/SUMMARY.json (with earlier maximum retained)",
|
||||
"limit_seconds": 157,
|
||||
"derivation": "ceil(3 * longest successful whole-file receipt) + 120s headroom",
|
||||
"script_sha256": "be706dff832257bd4781abeabd19faa4ac16edf9a4af0c96457e33cf3bb6deb3",
|
||||
"previous_maximum_seconds": 12.168674453999964,
|
||||
"previous_receipt": "ec1e7fad8:agents/gander/work/queue-196/r5/resume/whole-results.json",
|
||||
"row278_run3_seconds": 2.67
|
||||
},
|
||||
"scripts/test-task.sh": {
|
||||
"recorded_duration_seconds": 36.783798266,
|
||||
"receipt": "3b91cfdaf:agents/gander/work/queue-253/run5-gate/SUMMARY.json (with earlier maximum retained)",
|
||||
"limit_seconds": 231,
|
||||
"derivation": "ceil(3 * longest successful whole-file receipt) + 120s headroom",
|
||||
"script_sha256": "10c4abb70bf9b83e617089b06a5436cc7fa6b866a176458b22128c3a15163f94",
|
||||
"previous_maximum_seconds": 36.783798266,
|
||||
"previous_receipt": "ec1e7fad8:agents/gander/work/queue-196/r5/resume/whole-results.json",
|
||||
"row278_run3_seconds": 13.843
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"source": "164e166479378caf0702ab8b1eb3871b54219da3",
|
||||
"base": "48e450ce8fbb10df0beabc8da67ab90ea14587ea",
|
||||
"branch": "gander/row276",
|
||||
"manifest": "b0fdd91183d7e1911ed327e32c03b8de7f0efd4c86bd143fa0ad54390358ee63",
|
||||
"paths": {
|
||||
"docs/plans/DEFERRED.md": "f8af6a2e563caaf99d7e25663370f0ef2842cf5b02d26f3837db8ece3e4fc974",
|
||||
"packages/cli/tests/case-sudo-fixture.mjs": "eac5c83f43056bdeb5781101307e17d4bc01f03e97e02f4f5e930436d7e43535",
|
||||
"packages/cli/tests/case1-accept.test.mjs": "fe4bc897497d83ef247b229cd49acf78b1a607af44e803a8a0a866115ff8cf87",
|
||||
"scripts/case1-accept.sh": "46245e2bc19c8bcab2ac4fbda16cc72f2dfbf656e4bad8db707bb5a254694900",
|
||||
"scripts/case2-unit.sh": "d2709a1da6c9a03f4c8b31bf57ad00df4c0ae62a5cbc0e7588cdceb9dc0e2aa1"
|
||||
},
|
||||
"mutants": 15,
|
||||
"tests": "NOT RUN; all runtime and Node checks await Sage CLEAR",
|
||||
"static_checks": [
|
||||
"bash-n both scripts",
|
||||
"bash-n15variants",
|
||||
"git diff --check",
|
||||
"unique-target checks",
|
||||
"unchanged supervisor bytes",
|
||||
"old assertion-line multiset except mos-1326 authorization"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"base": "48e450ce8",
|
||||
"bytes": 2763,
|
||||
"sha256": "c828087085a1dfdade55c0fb1083f9a2dff8d9b72885708186136497711c9bae",
|
||||
"both_scripts_and_base_byte_identical": true
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
[
|
||||
{
|
||||
"id": "M1",
|
||||
"occurrences": 1,
|
||||
"before": "46245e2bc19c8bcab2ac4fbda16cc72f2dfbf656e4bad8db707bb5a254694900",
|
||||
"after": "f1afa0d8ed98fb96d9d4bbc9e0f4390aeb70f7545375566278d93228b2fe6a9f"
|
||||
},
|
||||
{
|
||||
"id": "M2",
|
||||
"occurrences": 1,
|
||||
"before": "46245e2bc19c8bcab2ac4fbda16cc72f2dfbf656e4bad8db707bb5a254694900",
|
||||
"after": "feba9b8cc38477da84189e1347e370c860313d8c7de9fd9d616e5645694b3119"
|
||||
},
|
||||
{
|
||||
"id": "M3",
|
||||
"occurrences": 1,
|
||||
"before": "46245e2bc19c8bcab2ac4fbda16cc72f2dfbf656e4bad8db707bb5a254694900",
|
||||
"after": "b313e6062f095957086d391dbab0055ef083e8714d1a22e6dd8b99e61a6af162"
|
||||
},
|
||||
{
|
||||
"id": "M4",
|
||||
"occurrences": 1,
|
||||
"before": "46245e2bc19c8bcab2ac4fbda16cc72f2dfbf656e4bad8db707bb5a254694900",
|
||||
"after": "4e9f287d33e7501512d1b58f20587625f67269e1e4302a18b6b36d3d36d53de4"
|
||||
},
|
||||
{
|
||||
"id": "M5",
|
||||
"occurrences": 1,
|
||||
"before": "46245e2bc19c8bcab2ac4fbda16cc72f2dfbf656e4bad8db707bb5a254694900",
|
||||
"after": "b3a7a3ee876316a47b03be8e4e2d8f608d3b1b3f697dd7643089246971c34f68"
|
||||
},
|
||||
{
|
||||
"id": "M6",
|
||||
"occurrences": 1,
|
||||
"before": "46245e2bc19c8bcab2ac4fbda16cc72f2dfbf656e4bad8db707bb5a254694900",
|
||||
"after": "d43e63e0c470695dca61da098ef0260b8cc8d2a76b861fd094459d86b9e9a9f6"
|
||||
},
|
||||
{
|
||||
"id": "M7",
|
||||
"occurrences": 1,
|
||||
"before": "46245e2bc19c8bcab2ac4fbda16cc72f2dfbf656e4bad8db707bb5a254694900",
|
||||
"after": "7cc325a87cd8def5466d6869829a14863af971f7923c90c405a88c21b1c04123"
|
||||
},
|
||||
{
|
||||
"id": "M8",
|
||||
"occurrences": 1,
|
||||
"before": "46245e2bc19c8bcab2ac4fbda16cc72f2dfbf656e4bad8db707bb5a254694900",
|
||||
"after": "b9a33bb8d55296f91dd3b461855c79b53d4b851f24ddd928e6ba6f833baa8521"
|
||||
},
|
||||
{
|
||||
"id": "M9",
|
||||
"occurrences": 1,
|
||||
"before": "46245e2bc19c8bcab2ac4fbda16cc72f2dfbf656e4bad8db707bb5a254694900",
|
||||
"after": "5a95979c618f618ebed2d19f13c6ac1ac1dbc9fcacda7f71a1e5233da51d3c66"
|
||||
},
|
||||
{
|
||||
"id": "M10",
|
||||
"occurrences": 1,
|
||||
"before": "46245e2bc19c8bcab2ac4fbda16cc72f2dfbf656e4bad8db707bb5a254694900",
|
||||
"after": "25c1547df78165f382dcdff3f5a28246ab3cfa480e48aa6892303b59428a1dde"
|
||||
},
|
||||
{
|
||||
"id": "M11",
|
||||
"occurrences": 1,
|
||||
"before": "46245e2bc19c8bcab2ac4fbda16cc72f2dfbf656e4bad8db707bb5a254694900",
|
||||
"after": "2155715c941fb196e9599aa6eb61d3990c650e133c7f6702b64dcc0c35922138"
|
||||
},
|
||||
{
|
||||
"id": "M12",
|
||||
"occurrences": 1,
|
||||
"before": "46245e2bc19c8bcab2ac4fbda16cc72f2dfbf656e4bad8db707bb5a254694900",
|
||||
"after": "1439cf809601ce04af67ff5a6169940ac6a5a562c1c6d563c688002157bf56f3"
|
||||
},
|
||||
{
|
||||
"id": "M13",
|
||||
"occurrences": 1,
|
||||
"before": "46245e2bc19c8bcab2ac4fbda16cc72f2dfbf656e4bad8db707bb5a254694900",
|
||||
"after": "7c9ee12d5dcdd10dda5b50cc379fae31b08a80c3a42c86466b4df1f31a450800"
|
||||
},
|
||||
{
|
||||
"id": "M14",
|
||||
"occurrences": 1,
|
||||
"before": "46245e2bc19c8bcab2ac4fbda16cc72f2dfbf656e4bad8db707bb5a254694900",
|
||||
"after": "ac361aa2f9f01a2f5cc5c4a2392765886540efe9b1c77b178fd81d0432e0bd32"
|
||||
},
|
||||
{
|
||||
"id": "M15",
|
||||
"occurrences": 1,
|
||||
"before": "46245e2bc19c8bcab2ac4fbda16cc72f2dfbf656e4bad8db707bb5a254694900",
|
||||
"after": "39d18b5ea2905bafbe7860db5342768c46b5b9d99b2f187047617593e6b2e007"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": {
|
||||
"packages/cli/tests/case1-accept.test.mjs": 900,
|
||||
"packages/cli/tests/case2-unit.test.mjs": 900
|
||||
},
|
||||
"basis": "No row276 timing yet; initial900s per whole file; Sage may adjust before CLEAR."
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
[
|
||||
{
|
||||
"id": "M1",
|
||||
"file": "scripts/case1-accept.sh",
|
||||
"old": " # WORK is root's private mktemp directory. The dropped uid cannot write it.",
|
||||
"new": " # WORK is root's private mktemp directory. The dropped uid cannot write it.\n [ \"$tag\" != talk ] || TESTER_SUPERVISOR='shift 3; exec \"$@\"'",
|
||||
"killer": "packages/cli/tests/case1-accept.test.mjs",
|
||||
"pattern": "row276 talk legacy inner timeout"
|
||||
},
|
||||
{
|
||||
"id": "M2",
|
||||
"file": "scripts/case1-accept.sh",
|
||||
"old": "LC_ALL=C \"$TS_TIMEOUT\" --verbose --signal=KILL \"${outer}s\" \"$TS_BASH\" -c '\n errors=",
|
||||
"new": "LC_ALL=C \"$TS_BASH\" -c '\n errors=",
|
||||
"killer": "packages/cli/tests/case1-accept.test.mjs",
|
||||
"pattern": "row276 precheck outer bound"
|
||||
},
|
||||
{
|
||||
"id": "M3",
|
||||
"file": "scripts/case1-accept.sh",
|
||||
"old": "\"$setsid_bin\" \"$@\" & child=$!",
|
||||
"new": "\"$@\" & child=$!",
|
||||
"killer": "packages/cli/tests/case1-accept.test.mjs",
|
||||
"pattern": "row276 talk legacy inner timeout"
|
||||
},
|
||||
{
|
||||
"id": "M4",
|
||||
"file": "scripts/case1-accept.sh",
|
||||
"old": "if [ \"$rc\" = 87 ] && [ \"$valid\" = 1 ] && [ \"$display\" -gt 0 ]; then\n printf '%s\\n' \"$display\" > \"$WORK/legacy-$tag.rc\"\n exit 87",
|
||||
"new": "if [ \"$rc\" = 87 ] && [ \"$valid\" = 1 ] && [ \"$display\" -ge 0 ]; then\n printf '%s\\n' \"$display\" > \"$WORK/legacy-$tag.rc\"\n exit \"$display\"",
|
||||
"killer": "packages/cli/tests/case1-accept.test.mjs",
|
||||
"pattern": "row276 earlier forged trailer"
|
||||
},
|
||||
{
|
||||
"id": "M5",
|
||||
"file": "scripts/case1-accept.sh",
|
||||
"old": "process.exitCode=code;",
|
||||
"new": "process.exitCode=0;",
|
||||
"killer": "packages/cli/tests/case1-accept.test.mjs",
|
||||
"pattern": "row276 legacy streams and display status"
|
||||
},
|
||||
{
|
||||
"id": "M6",
|
||||
"file": "scripts/case1-accept.sh",
|
||||
"old": "&& [ \"${BASH_REMATCH[1]}\" -le 255 ]; then",
|
||||
"new": "; then",
|
||||
"killer": "packages/cli/tests/case1-accept.test.mjs",
|
||||
"pattern": "row276 trailer grammar"
|
||||
},
|
||||
{
|
||||
"id": "M7",
|
||||
"file": "scripts/case1-accept.sh",
|
||||
"old": "(0|[1-9][0-9]{0,2})$'\\n'$",
|
||||
"new": "([0-9]{1,3})$'\\n'$",
|
||||
"killer": "packages/cli/tests/case1-accept.test.mjs",
|
||||
"pattern": "row276 trailer grammar"
|
||||
},
|
||||
{
|
||||
"id": "M8",
|
||||
"file": "scripts/case1-accept.sh",
|
||||
"old": "(0|[1-9][0-9]{0,2})$'\\n'$",
|
||||
"new": "(0|[1-9][0-9]{0,2})$'\\n'?$",
|
||||
"killer": "packages/cli/tests/case1-accept.test.mjs",
|
||||
"pattern": "row276 trailer grammar"
|
||||
},
|
||||
{
|
||||
"id": "M9",
|
||||
"file": "scripts/case1-accept.sh",
|
||||
"old": " printf '%s' \"$raw\" >&2",
|
||||
"new": " : # dropped stderr",
|
||||
"killer": "packages/cli/tests/case1-accept.test.mjs",
|
||||
"pattern": "row276 legacy streams and display status"
|
||||
},
|
||||
{
|
||||
"id": "M10",
|
||||
"file": "scripts/case1-accept.sh",
|
||||
"old": "[ \"${st% *}\" = 0 ] && [[ \"${st#* }\" =~ ^[0-7]{3,4}$ ]] && (( (8#${st#* } & 8#022) == 0 )) || return 1",
|
||||
"new": "[ \"${st% *}\" = 0 ] && [[ \"${st#* }\" =~ ^[0-7]{3,4}$ ]] || return 1",
|
||||
"killer": "packages/cli/tests/case1-accept.test.mjs",
|
||||
"pattern": "row276 case1 capture and legacy tools"
|
||||
},
|
||||
{
|
||||
"id": "M11",
|
||||
"file": "scripts/case1-accept.sh",
|
||||
"old": "[ \"$(wc -c < \"$file\")\" -le \"$maximum\" ] || exit 83",
|
||||
"new": ": # dropped capture byte limit",
|
||||
"killer": "packages/cli/tests/case1-accept.test.mjs",
|
||||
"pattern": "row253 C4 acceptance query overflow"
|
||||
},
|
||||
{
|
||||
"id": "M12",
|
||||
"file": "scripts/case1-accept.sh",
|
||||
"old": "if [ \"$rc\" = 0 ] && [ \"$v\" = \"$CLAUDE_PIN (Claude Code)\" ];",
|
||||
"new": "if [ \"$v\" = \"$CLAUDE_PIN (Claude Code)\" ];",
|
||||
"killer": "packages/cli/tests/case1-accept.test.mjs",
|
||||
"pattern": "row276 version and PM checks"
|
||||
},
|
||||
{
|
||||
"id": "M13",
|
||||
"file": "scripts/case1-accept.sh",
|
||||
"old": "if [ \"$rc\" = 0 ] && [[ \"$out\" =~ ^ok\\ ",
|
||||
"new": "if [[ \"$out\" =~ ^ok\\ ",
|
||||
"killer": "packages/cli/tests/case1-accept.test.mjs",
|
||||
"pattern": "row276 version and PM checks"
|
||||
},
|
||||
{
|
||||
"id": "M14",
|
||||
"file": "scripts/case1-accept.sh",
|
||||
"old": "if [ \"$rc\" = 0 ] && [ \"$v\" = \"$NODE_PIN\" ];",
|
||||
"new": "if [ \"$v\" = \"$NODE_PIN\" ];",
|
||||
"killer": "packages/cli/tests/case1-accept.test.mjs",
|
||||
"pattern": "row276 version and PM checks"
|
||||
},
|
||||
{
|
||||
"id": "M15",
|
||||
"file": "scripts/case1-accept.sh",
|
||||
"old": "\"$TS_TAIL\" -c 65536 -- \"$errors\"",
|
||||
"new": "\"$TS_TAIL\" -c 999999999 -- \"$errors\"",
|
||||
"killer": "packages/cli/tests/case1-accept.test.mjs",
|
||||
"pattern": "row276 stderr retention is bounded"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run explicit row276 changes only under Sage's grant. --static runs no tests."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import signal
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
REPO = HERE.parents[3]
|
||||
|
||||
def digest(data):
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
def busy():
|
||||
p = subprocess.run(['bash', '/mnt/storage/scratch/sage-busy-now.sh'], text=True, capture_output=True)
|
||||
if p.returncode or p.stdout.splitlines()[-1:] != ['CLEAR']:
|
||||
raise RuntimeError('busy refusal: ' + p.stdout + p.stderr)
|
||||
|
||||
def run(command, path, seconds=60):
|
||||
if command[:2] == ['node', '--test']:
|
||||
limits = json.loads((HERE/'WHOLE-GATE-LIMITS.json').read_text())['files']
|
||||
limit = limits.get(command[-1], 900)
|
||||
command = ['timeout','-k','10',str(limit),*command]
|
||||
seconds = limit + 30
|
||||
busy()
|
||||
started = time.monotonic()
|
||||
command_file = path.with_suffix('.command.json')
|
||||
command_file.write_text(json.dumps(command))
|
||||
# Keep command text out of wrapper argv: the busy guard scans every argv.
|
||||
# launch.py execs the structured command. bash owns a pipefail/tee pipeline.
|
||||
p = subprocess.Popen(['bash','-o','pipefail','-c',
|
||||
'python3 "$1" "$2" 2>&1 | tee "$3"', '--',str(HERE/'launch.py'),str(command_file),str(path)],
|
||||
cwd=REPO,start_new_session=True)
|
||||
expired = False
|
||||
try:
|
||||
code = p.wait(timeout=seconds)
|
||||
expired = code in (124, 137)
|
||||
except subprocess.TimeoutExpired:
|
||||
expired = True
|
||||
os.killpg(p.pid, signal.SIGKILL)
|
||||
code = p.wait()
|
||||
finally:
|
||||
try:
|
||||
os.killpg(p.pid, signal.SIGKILL)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
return dict(code=code, timeout=expired, seconds=round(time.monotonic()-started, 3), log=str(path))
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument('--static', action='store_true')
|
||||
ap.add_argument('--grant')
|
||||
ap.add_argument('--out', type=Path)
|
||||
args = ap.parse_args()
|
||||
definitions = json.loads((HERE/'changes.json').read_text())
|
||||
checked = []
|
||||
for d in definitions:
|
||||
original = (REPO/d['file']).read_bytes()
|
||||
count = original.count(d['old'].encode())
|
||||
if count != 1:
|
||||
raise RuntimeError(f"{d['id']}: target occurs {count} times")
|
||||
changed = original.replace(d['old'].encode(), d['new'].encode(), 1)
|
||||
checked.append(dict(id=d['id'], file=d['file'], occurrences=count,
|
||||
before=digest(original), after=digest(changed)))
|
||||
if args.static:
|
||||
print(json.dumps(checked, indent=2))
|
||||
return
|
||||
if not args.grant or not args.out:
|
||||
ap.error('execution needs --grant and --out')
|
||||
if not os.environ.get('TMPDIR', '').startswith('/mnt/storage/scratch/tmp/g'):
|
||||
raise RuntimeError('TMPDIR must be the granted scratch directory')
|
||||
status = subprocess.check_output(['git','status','--porcelain','--untracked-files=no'],cwd=REPO)
|
||||
args.out.mkdir(parents=True, exist_ok=True)
|
||||
results = dict(grant=args.grant, static=checked, bases={}, changes=[])
|
||||
failures = 0
|
||||
for d in definitions:
|
||||
key = d['killer']+'|'+d['pattern']
|
||||
command = ['node','--test','--test-reporter=tap','--test-name-pattern',d['pattern'],d['killer']]
|
||||
if key not in results['bases']:
|
||||
base = run(command, args.out/(d['id']+'-base.log'))
|
||||
results['bases'][key] = base
|
||||
(args.out/'results.json').write_text(json.dumps(results,indent=2)+'\n')
|
||||
if base['code'] != 0 or base['seconds'] > 30:
|
||||
raise RuntimeError('base failed or exceeded 30 seconds: '+d['id'])
|
||||
target = REPO/d['file']
|
||||
original = target.read_bytes()
|
||||
try:
|
||||
target.write_bytes(original.replace(d['old'].encode(),d['new'].encode(),1))
|
||||
pre = subprocess.run(['bash','-n',str(target)],capture_output=True)
|
||||
if pre.returncode:
|
||||
raise RuntimeError('invalid syntax: '+d['id'])
|
||||
result = run(command,args.out/(d['id']+'.log'))
|
||||
result.update(id=d['id'], base_seconds=results['bases'][key]['seconds'],
|
||||
outcome='timeout' if result['timeout'] else 'killed' if result['code'] != 0 else 'survived')
|
||||
results['changes'].append(result)
|
||||
if result['timeout']:
|
||||
raise RuntimeError('timeout requires Sage/Mos classification: '+d['id'])
|
||||
if result['outcome'] != 'killed':
|
||||
failures += 1
|
||||
finally:
|
||||
target.write_bytes(original)
|
||||
assert target.read_bytes() == original
|
||||
assert subprocess.check_output(['git','status','--porcelain','--untracked-files=no'],cwd=REPO) == status
|
||||
(args.out/'results.json').write_text(json.dumps(results,indent=2)+'\n')
|
||||
if failures >= 1:
|
||||
raise RuntimeError('first unexpected failure: end the row276 window')
|
||||
print(json.dumps(results,indent=2))
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Exec a structured command without putting its text in wrapper argv."""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
with open(sys.argv[1]) as source:
|
||||
command = json.load(source)
|
||||
os.execvp(command[0], command)
|
||||
@@ -574,3 +574,12 @@ Brief: docs/plans/2026-10-22_case-sudo-legacy-followup.md, “Brief: supervised
|
||||
case1-accept.sh's legacy timeout330 around AS_USER talk and its AS_USER precheck, Claude version, PM launches and Node version diagnostics predate the new socket metadata/probe helpers. sudo's pty monitor can move them to a separate session; the old outer timeout does not prove their descendants are killed. The C4 repair in row253 covers only its new config query and peer probe. These legacy paths need the reviewed supervisor or a header explicitly explaining why each remains unsupervised. The follow-up must preserve the 300-second talk/reply contract, concurrent F5 scan, diagnostics and secret-output refusals, with fake-only session-hop tests and Sage's slot. No host or live unit change is authorized.
|
||||
|
||||
Row 276 / #1781 also owns the case1 root-side capture/tools mutant counterparts (duckworth-0112 N1) and documenting the setuid-member cleanup refusal in the headers (N2). A member that changes uid and cannot be killed causes inner-cleanup refusal, never successful observation. N4 is addressed in row253 by allowing SID confirmation until the existing inner watchdog expires, instead of a separate 200 ms scheduling margin.
|
||||
|
||||
|
||||
## Row276 candidate closure of Row253 F5
|
||||
|
||||
Effective when row276 / #1781 lands after Duckworth review and Sage's gate: all five legacy Case1 calls (F3 talk, F4 precheck, F6 Claude version, F7 PM launches, F10 Node version) use row253's byte-identical after-uid supervisor and their own independent outer bound. No named AS_USER exception remains. Talk retains --wait300 and concurrent F5 observation; stdout and stderr remain separate, with root retaining only stderr's last65536bytes. A trailer can choose only a failure display number, never success. Supervisor0 alone permits pass.
|
||||
|
||||
Row253 N1 closure: row276's M10 checks case1's root-owned/non-writable tool fence through both capture and legacy paths; M11 checks case1's root-side capture byte limit. Each must be killed in the accepted gate. Row253 N2 closure: both case-script headers explicitly state that a uid-changing member which cannot be killed causes inner-cleanup refusal, never successful observation.
|
||||
|
||||
The hostile same-uid setsid-away or killed/stalled-supervisor residual remains as row253 R3. This closes none of that boundary. Instant command exit before SID confirmation remains the fail-closed residual tracked as Vikunja532 (mos-1297); row276 does not change the pinned payload.
|
||||
@@ -10,7 +10,8 @@ case_sudo_entry() {
|
||||
here="$BASHPID"
|
||||
read -r sid < <(/bin/ps -o sid= -p "$here")
|
||||
printf "%s %s\n" "$here" "$sid" >> "$record/session-pids"
|
||||
if { [ -f "$record/stall-config" ] && [[ "$*" == *MOSAIC_TESTER_SOCKET_V1* ]]; } ||
|
||||
if { [ -f "$record/stall-legacy-precheck" ] && [[ "$*" == *MOSAIC_LEGACY_STATUS* ]]; } ||
|
||||
{ [ -f "$record/stall-config" ] && [[ "$*" == *MOSAIC_TESTER_SOCKET_V1* ]]; } ||
|
||||
{ [ -f "$record/stall-probe" ] && [[ "$*" == *SO_PEERCRED* ]]; }; then
|
||||
trap '' TERM HUP
|
||||
[ ! -f "$record/stall-output" ] || /bin/cat "$record/stall-output"
|
||||
@@ -140,3 +141,12 @@ ${kind==='member'?`const c=spawn('/usr/bin/python3',['-I','-S','-c',${JSON.strin
|
||||
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0);
|
||||
}`;
|
||||
}
|
||||
|
||||
// Row276: production budgets include the existing conversation/precheck bounds.
|
||||
export const LEGACY_CASES = Object.freeze([
|
||||
{tag:'talk',inner:320,outer:330,check:'F3'},
|
||||
{tag:'precheck',inner:200,outer:210,check:'F4'},
|
||||
{tag:'claude-version',inner:5,outer:7,check:'F6'},
|
||||
{tag:'pm-list',inner:10,outer:12,check:'F7'},
|
||||
{tag:'node-version',inner:5,outer:7,check:'F10'},
|
||||
]);
|
||||
@@ -5,13 +5,14 @@
|
||||
// sudo runs the stack's user's commands from the tree. Nothing here reads or writes the real /etc,
|
||||
// /proc or /srv, and the stand-in credential is not a credential.
|
||||
import { test } from "node:test";
|
||||
import { createHash } from "node:crypto";
|
||||
import assert from "node:assert/strict";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { tmp } from "./helpers.mjs";
|
||||
import { CASE_SUDO_SESSION, recordedLivePids, killRecordedPids, fixtureGuard, cleanupFixture, pollFixtureGone, blockingQuerySource } from "./case-sudo-fixture.mjs";
|
||||
import { CASE_SUDO_SESSION, recordedLivePids, killRecordedPids, fixtureGuard, cleanupFixture, pollFixtureGone, blockingQuerySource, LEGACY_CASES } from "./case-sudo-fixture.mjs";
|
||||
|
||||
const REPO = fileURLToPath(new URL("../../../", import.meta.url));
|
||||
const SCRIPT = join(REPO, "scripts", "case1-accept.sh");
|
||||
@@ -68,6 +69,7 @@ esac`,
|
||||
// -l -U USER prints the state file; -u jarvis is the bwrap probe; -u mosaic -H env PATH=... CMD runs
|
||||
// CMD from the tree, with the PATH entries and each absolute argument that exists there moved under it.
|
||||
sudo: `${CASE_SUDO_SESSION}
|
||||
/usr/bin/python3 -I -S -c 'import json,os,sys;f=open(os.environ["HC"]+"/sudo-argv.jsonl","a");f.write(json.dumps(sys.argv[1:])+chr(10));f.close()' "$@"
|
||||
case "$1" in
|
||||
-l) [ -f "$HC/sudo-l-$3" ] && cat "$HC/sudo-l-$3"; exit 0 ;;
|
||||
-u) ;;
|
||||
@@ -721,7 +723,26 @@ test("row 195 accept: every in-instance call goes through sudo -u mosaic -H env
|
||||
for(const a of isolated) assert.ok(a.startsWith(`-u mosaic -H env -i HOME=${HOME} PATH=/opt/mosaic-stack/node-${NODE_PIN}/bin:/usr/local/bin:/usr/bin:/bin `),a);
|
||||
for (const a of asMosaic.filter(a=>!isolated.includes(a))) assert.ok(a.startsWith(`${AS_MOSAIC} /`), a);
|
||||
const cmds = asMosaic.filter(a=>!isolated.includes(a)).map((a) => a.slice(AS_MOSAIC.length + 1).split(" ")[0]);
|
||||
assert.deepEqual([...new Set(cmds)].sort(), [CLI, PRECHECK, CLAUDE, NODE].sort());
|
||||
assert.equal(asMosaic.length,7,'two metadata/probe calls and five legacy calls');
|
||||
assert.deepEqual([...new Set(cmds)].sort(), ['/usr/bin/bash']);
|
||||
// mos-1326: assert every argv element, not flattened/substr command detection.
|
||||
const source=read(SCRIPT);
|
||||
const supervisor=source.split("<<'MOSAIC_TESTER_SUPERVISOR'")[1].split('\n').slice(1).join('\n').split('\nMOSAIC_TESTER_SUPERVISOR')[0]+'\n';
|
||||
assert.equal(createHash('sha256').update(supervisor).digest('hex'),'17b881513c35107e26f78c3722bc4a2927d206b197597be0bdbb2c8a8588bb4c');
|
||||
const runner=/LEGACY_COMMAND_RUNNER='([\s\S]*?)'\nlegacy_display_rc/.exec(source)[1];
|
||||
const f7=/F7_JS='([\s\S]*?)';\ncheck_f7/.exec(source)[1];
|
||||
const raw=read(join(h.hc,'sudo-argv.jsonl')).trim().split('\n').map(line=>JSON.parse(line));
|
||||
const legacy=raw.filter(a=>a[0]==='-u'&&a[1]==='mosaic'&&a[4].startsWith('PATH='));
|
||||
const marker=legacy[0][20].split(':')[0];
|
||||
const argv=(tag,inner,command)=>['-u','mosaic','-H','env',`PATH=/opt/mosaic-stack/node-${NODE_PIN}/bin:/usr/local/bin:/usr/bin:/bin`,
|
||||
'/usr/bin/bash','-c',supervisor,'--',String(inner),'/usr/bin/setsid','/usr/bin/sleep',NODE,'-e',runner,'--',tag,...command];
|
||||
assert.deepEqual(legacy,[
|
||||
argv('talk',320,[CLI,'talk','chief',marker+': this is the Case 1 acceptance; reply with one short line.','--business','mosaic-stack','--tester','--wait','300']),
|
||||
argv('precheck',200,[PRECHECK,'--business','mosaic-stack','--profile','factory']),
|
||||
argv('claude-version',5,[CLAUDE,'--version']),
|
||||
argv('pm-list',10,[NODE,'-e',f7,'--',`${HOME}/.config/mosaic-dev/businesses/mosaic-stack.json`,CLI,'mosaic-stack']),
|
||||
argv('node-version',5,[NODE,'--version']),
|
||||
]);
|
||||
assert.deepEqual(sudo.filter((a) => !a.startsWith("-u mosaic")).sort(),
|
||||
["-l -U localadmin", "-l -U mosaic", "-u jarvis /usr/bin/bwrap --version"]);
|
||||
const talk = calls(h, "mosaic").filter((a) => a.startsWith("talk "));
|
||||
@@ -735,3 +756,107 @@ test("row 195 accept: the host check runs with --check and never --enforce", (t)
|
||||
run(h);
|
||||
assert.deepEqual(calls(h, "hostcheck"), ["--check"]);
|
||||
});
|
||||
|
||||
|
||||
// Row276 exercises the real legacy launcher with fixture-only commands, never sudo.
|
||||
function legacyBody() {
|
||||
const source=read(SCRIPT),start=source.indexOf('# row253-supervisor-begin'),end=source.indexOf('# row276-legacy-end');
|
||||
assert.ok(start>=0&&end>start,'legacy supervisor block exists');
|
||||
return source.slice(start,end);
|
||||
}
|
||||
function legacyCall(h,tag,command,{inner='0.5',outer='2',body=legacyBody()}={}) {
|
||||
return spawnSync('/bin/bash',['-c',body+'\nlegacy_run "$@"','--',tag,String(inner),String(outer),...command],{
|
||||
encoding:'utf8',env:{...process.env,PATH:`${h.bin}:${process.env.PATH}`,HC:h.hc,ROOT_DIR:h.root,REAL_NODE:process.execPath,TMPDIR:h.dir,
|
||||
S_USER:'mosaic',S_HOME:'/srv/mosaic',NODE,WORK:h.dir},
|
||||
});
|
||||
}
|
||||
test('row276 legacy call inventory pins every production bound and the conversation contract',()=>{
|
||||
const s=read(SCRIPT);
|
||||
assert.equal(s.includes('${AS_USER[@]}'),false);
|
||||
for(const x of LEGACY_CASES)assert.ok(s.includes(`legacy_run ${x.tag} ${x.inner} ${x.outer} `),x.tag);
|
||||
assert.match(s,/legacy_run talk 320 330 .*--tester --wait 300/);
|
||||
assert.match(s,/legacy_display_rc talk/);assert.match(s,/legacy_display_rc precheck/);
|
||||
});
|
||||
for(const {tag} of LEGACY_CASES)test(`row276 ${tag} legacy inner timeout cleans both sudo models`,async t=>{
|
||||
for(const mode of ['exec','hop']){
|
||||
const h=host(t),records=join(h.hc,'legacy-pids'),files=[join(h.hc,'session-pids'),records];
|
||||
put(join(h.hc,'session-mode'),mode);
|
||||
const file=join(h.dir,'blocked.mjs');put(file,blockingQuerySource(records,'signals')+'\nloadSystem();');
|
||||
const capture=cleanupFixture(t,files),stop=fixtureGuard(t,files,4500);
|
||||
const start=performance.now(),r=legacyCall(h,tag,[NODE,file]);capture();
|
||||
assert.equal(r.status,81,r.stdout+r.stderr);
|
||||
assert.equal(r.stderr,`case1-accept: ${tag} inner-timeout\n`);
|
||||
assert.ok((performance.now()-start)/1000<2,mode);
|
||||
assert.deepEqual(await pollFixtureGone(files),[],mode);await stop();
|
||||
}
|
||||
});
|
||||
test('row276 legacy streams and display status preserve failures without granting success',t=>{
|
||||
for(const mode of ['exec','hop'])for(const code of [0,1,7,81,137]){
|
||||
const h=host(t);put(join(h.hc,'session-mode'),mode);
|
||||
const r=legacyCall(h,'talk',[NODE,'-e',`process.stdout.write('sent retained\\nreply retained\\n');process.stderr.write('original diagnostic\\n');process.exitCode=${code}`],{inner:3,outer:5});
|
||||
assert.equal(r.status,code===0?0:87,r.stderr);
|
||||
assert.equal(r.stdout,'sent retained\nreply retained\n');assert.equal(r.stderr,'original diagnostic\n');
|
||||
assert.equal(read(join(h.dir,'legacy-talk.rc')),String(code===0?0:code)+'\n');
|
||||
}
|
||||
});
|
||||
test('row276 trailer grammar cannot make supervisor failure pass',t=>{
|
||||
for(const trailer of ['0','256','999','01','+1','-1','1-no-newline','missing']){
|
||||
const h=host(t),body=legacyBody().replace('process.stderr.write("\\nMOSAIC_LEGACY_STATUS="+code+"\\n");',
|
||||
trailer==='missing'?'':trailer==='1-no-newline'?'process.stderr.write("\\nMOSAIC_LEGACY_STATUS=1");':`process.stderr.write("\\nMOSAIC_LEGACY_STATUS=${trailer}\\n");`);
|
||||
assert.notEqual(body,legacyBody(),'runner trailer seam matched');
|
||||
const r=legacyCall(h,'talk',[NODE,'-e','process.exitCode=7'],{body,inner:3,outer:5});
|
||||
assert.equal(r.status,87,r.stderr);assert.equal(read(join(h.dir,'legacy-talk.rc')),'87\n');
|
||||
assert.ok(r.stderr.endsWith('case1-accept: talk inner-command\n'),r.stderr);
|
||||
}
|
||||
});
|
||||
test('row276 earlier forged trailer and appended zero remain failures',t=>{
|
||||
for(const late of [false,true]){
|
||||
const h=host(t);let body=legacyBody();
|
||||
if(late)body=body.replace('process.stderr.write("\\nMOSAIC_LEGACY_STATUS="+code+"\\n");','process.stderr.write("\\nMOSAIC_LEGACY_STATUS="+code+"\\nMOSAIC_LEGACY_STATUS=0\\n");');
|
||||
const r=legacyCall(h,'talk',[NODE,'-e','process.stderr.write("\\nMOSAIC_LEGACY_STATUS=0\\n");process.exitCode=7'],{body,inner:3,outer:5});
|
||||
assert.equal(r.status,87,r.stderr);assert.equal(read(join(h.dir,'legacy-talk.rc')),late?'87\n':'7\n');
|
||||
}
|
||||
});
|
||||
test('row276 precheck outer bound discards success status and fixture cleans residual',async t=>{
|
||||
for(const mode of ['exec','hop']){
|
||||
const h=host(t),files=[join(h.hc,'session-pids')];put(join(h.hc,'session-mode'),mode);put(join(h.hc,'stall-legacy-precheck'),'');
|
||||
const capture=cleanupFixture(t,files),stop=fixtureGuard(t,files,4500);
|
||||
const start=performance.now(),r=legacyCall(h,'precheck',[NODE,'-e',''],{inner:0.5,outer:1});capture();const seconds=(performance.now()-start)/1000;
|
||||
assert.equal(r.status,82,r.stderr);assert.equal(r.stderr,'case1-accept: precheck outer-timeout\n');assert.equal(r.stdout,'');
|
||||
assert.ok(seconds>=1&&seconds<3,`${mode}:${seconds}s`);assert.equal(read(join(h.dir,'legacy-precheck.rc')),'82\n');
|
||||
killRecordedPids(files[0]);assert.deepEqual(await pollFixtureGone(files),[],'explicit same-uid stalled-supervisor residual cleanup');await stop();
|
||||
}
|
||||
});
|
||||
test('row276 case1 capture and legacy tools refuse unsafe system tools',t=>{
|
||||
for(const legacy of [false,true]){
|
||||
const h=host(t),statFile=join(h.bin,'stat');edit(statFile,s=>s.replace('case "${@: -1}" in',`case "\${@: -1}" in\n /usr/bin/timeout) echo '0 777';exit 0 ;;`));
|
||||
const body=legacyBody();
|
||||
const r=legacy?legacyCall(h,'node-version',[NODE,'--version'],{body}):spawnSync('/bin/bash',['-c',body+'\ntester_capture 0.5 2 128 mosaic /srv/mosaic /usr/bin /usr/bin/true'],{encoding:'utf8',env:{...process.env,PATH:`${h.bin}:${process.env.PATH}`,HC:h.hc,ROOT_DIR:h.root,REAL_NODE:process.execPath,TMPDIR:h.dir}});
|
||||
assert.equal(r.status,84,r.stdout+r.stderr);if(legacy)assert.equal(r.stderr,'case1-accept: node-version supervisor-tools\n');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
test('row276 stderr retention is bounded and keeps the final diagnostic',t=>{
|
||||
const h=host(t),r=legacyCall(h,'talk',[NODE,'-e',"process.stderr.write('X'.repeat(70000)+'end diagnostic\\n')"],{inner:3,outer:5});
|
||||
assert.equal(r.status,0,r.stderr);assert.ok(Buffer.byteLength(r.stderr)<=65536);assert.ok(r.stderr.endsWith('end diagnostic\n'));
|
||||
});
|
||||
test('row276 version and PM checks refuse successful-looking stdout on supervisor failure',t=>{
|
||||
const source=read(SCRIPT),h=host(t);
|
||||
for(const [name,next,output] of [['check_f6','// The PM check',`${CLAUDE_PIN} (Claude Code)`],['check_f7','check_f8()', 'ok pm run r-0002'],['check_f10','check_f11()',NODE_PIN]]){
|
||||
const at=source.indexOf(name+'()'),end=source.indexOf(next,at+1);assert.ok(at>=0&&end>at);
|
||||
const code=source.slice(at,end)+'\nfpass(){ echo PASS; }; ffail(){ echo FAIL; }; tester_capture_reason(){ echo inner-command; }; legacy_run(){ printf "%s\n" "$OUTPUT"; return 87; }; '+name;
|
||||
const r=spawnSync('/bin/bash',['-c',code],{encoding:'utf8',env:{...process.env,CLAUDE,CLAUDE_PIN,NODE,NODE_PIN,WORK:h.dir,OUTPUT:output,BUSINESS_FILE:'/fake',COPY:'/fake',BUSINESS:'mosaic-stack'}});
|
||||
assert.equal(r.status,0,r.stderr);assert.equal(r.stdout,'FAIL\n',name);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
test('row276 full acceptance keeps all five healthy calls in both sudo models',async t=>{
|
||||
for(const mode of ['exec','hop']){
|
||||
const h=host(t),files=[join(h.hc,'session-pids')];put(join(h.hc,'session-mode'),mode);
|
||||
const capture=cleanupFixture(t,files),r=run(h);capture();
|
||||
assert.equal(r.status,0,r.out+r.err);assert.deepEqual(failed(r),[],mode);
|
||||
assert.deepEqual(await pollFixtureGone(files),[],mode);
|
||||
}
|
||||
});
|
||||
+94
-21
@@ -18,8 +18,12 @@
|
||||
# fixed system Python, also as the stack user, for a read-only SO_PEERCRED observation.
|
||||
# The config/probe supervisor is fixed root-validated system Bash plus setsid
|
||||
# and sleep, after the uid drop: query inner/outer 5/7 s, probe 2/4 s. Root
|
||||
# independently bounds its sudo wait. Legacy AS_USER talk and diagnostics below
|
||||
# retain their old behavior and are NOT covered by this supervisor (F5 follow-up).
|
||||
# independently bounds its sudo wait. All legacy calls use the same payload:
|
||||
# talk320/330 s (conversation --wait300), precheck200/210 s (its Claude probe180),
|
||||
# Claude/Node versions5/7 s, PM launches10/12 s. Stderr retains its last65536bytes,
|
||||
# stdout streams to the original caller. Supervisor0 alone permits success.
|
||||
# A member that changes uid and cannot be killed refuses inner-cleanup; hostile
|
||||
# same-uid setsid-away or a killed/stalled supervisor remains the R3 residual.
|
||||
#
|
||||
# One line per check, `<id> <path> <mark> <evidence>`, then `count <mark> N` for each mark and a
|
||||
# result line. F checks are `pass` or `FAIL`. P checks are `closed`, `accepted <ruling>` or
|
||||
@@ -107,8 +111,8 @@ BUS_UNIT="mosaic-bus@$BUSINESS.service" CON_UNIT=mosaic-webui.service
|
||||
HOSTCHECK=/usr/local/lib/mosaic-stack/hostcheck.sh
|
||||
SETTINGS=/etc/claude-code/managed-settings.json
|
||||
LABEL_OUTER='bwrap (enforce)' LABEL_CHILD='bwrap//&unpriv_bwrap (enforce)'
|
||||
AS_USER=(sudo -u "$S_USER" -H env "PATH=$(dirname "$NODE"):/usr/local/bin:/usr/bin:/bin")
|
||||
WORK="$(mktemp -d)" || refuse "cannot make a temp dir"
|
||||
chmod 700 "$WORK" || refuse "cannot protect temp dir"
|
||||
trap 'rm -rf -- "$WORK"' EXIT
|
||||
|
||||
# Row253: bounded unprivileged metadata, parsed by root without running copy code.
|
||||
@@ -196,8 +200,8 @@ MOSAIC_TESTER_SUPERVISOR
|
||||
|
||||
tester_tools() {
|
||||
local input target p st magic
|
||||
TS_BASH="" TS_SETSID="" TS_SLEEP="" TS_TIMEOUT="" TS_HEAD=""
|
||||
for input in /bin/bash /usr/bin/setsid /usr/bin/sleep /usr/bin/timeout /usr/bin/head; do
|
||||
TS_BASH="" TS_SETSID="" TS_SLEEP="" TS_TIMEOUT="" TS_HEAD="" TS_TAIL=""
|
||||
for input in /bin/bash /usr/bin/setsid /usr/bin/sleep /usr/bin/timeout /usr/bin/head "$@"; do
|
||||
target="$(realpath -e -- "$input" 2>/dev/null)" || return 1
|
||||
case "$target" in /home/*|/root/*|/run/user/*) return 1 ;; esac
|
||||
[ -f "$target" ] && [ -x "$target" ] && [ ! -L "$target" ] || return 1
|
||||
@@ -213,7 +217,7 @@ tester_tools() {
|
||||
case "$input" in
|
||||
/bin/bash) TS_BASH="$target" ;; /usr/bin/setsid) TS_SETSID="$target" ;;
|
||||
/usr/bin/sleep) TS_SLEEP="$target" ;; /usr/bin/timeout) TS_TIMEOUT="$target" ;;
|
||||
/usr/bin/head) TS_HEAD="$target" ;;
|
||||
/usr/bin/head) TS_HEAD="$target" ;; /usr/bin/tail) TS_TAIL="$target" ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
@@ -246,6 +250,68 @@ tester_capture() (
|
||||
cat -- "$file"
|
||||
)
|
||||
|
||||
|
||||
# row276-legacy-begin
|
||||
# The runner preserves a failed command's display code. Its exit status, not
|
||||
# this dropped-uid trailer, controls supervisor success. Root reads only a tail.
|
||||
LEGACY_COMMAND_RUNNER='
|
||||
const {spawnSync}=require("node:child_process");
|
||||
const {constants}=require("node:os");
|
||||
const [tag,command,...args]=process.argv.slice(1);
|
||||
const r=spawnSync(command,args,{stdio:"inherit"});
|
||||
const code=Number.isInteger(r.status)?r.status:r.signal?128+(constants.signals[r.signal]??0):127;
|
||||
if(r.error)process.stderr.write("legacy command "+(r.error.code??"failed")+"\n");
|
||||
process.stderr.write("\nMOSAIC_LEGACY_STATUS="+code+"\n");
|
||||
process.exitCode=code;
|
||||
'
|
||||
legacy_display_rc() {
|
||||
local tag="$1" fallback="$2" value=""
|
||||
[ ! -f "$WORK/legacy-$tag.rc" ] || value="$(<"$WORK/legacy-$tag.rc")"
|
||||
if [[ "$value" =~ ^[1-9][0-9]{0,2}$ ]] && [ "$value" -le 255 ]; then printf '%s' "$value"
|
||||
else printf '%s' "$fallback"; fi
|
||||
}
|
||||
legacy_run() (
|
||||
local tag="$1" inner="$2" outer="$3" notice errors child="" rc=0 raw suffix display=87 valid=0; shift 3
|
||||
# WORK is root's private mktemp directory. The dropped uid cannot write it.
|
||||
printf '%s\n' 84 > "$WORK/legacy-$tag.rc"
|
||||
tester_tools /usr/bin/tail || { printf 'case1-accept: %s supervisor-tools\n' "$tag" >&2; exit 84; }
|
||||
notice="$(mktemp "$WORK/legacy-notice.XXXXXX")" || exit 88
|
||||
errors="$(mktemp "$WORK/legacy-stderr.XXXXXX")" || { rm -f -- "$notice"; exit 88; }
|
||||
trap 'if [ -n "$child" ]; then kill -KILL -- "-$child" 2>/dev/null || :; wait "$child" 2>/dev/null || :; fi; rm -f -- "$notice" "$errors"' EXIT
|
||||
trap 'exit 88' HUP INT TERM
|
||||
LC_ALL=C "$TS_TIMEOUT" --verbose --signal=KILL "${outer}s" "$TS_BASH" -c '
|
||||
errors="$1"; shift
|
||||
exec "$@" 2> "$errors"
|
||||
' -- "$errors" sudo -u "$S_USER" -H env "PATH=$(dirname "$NODE"):/usr/local/bin:/usr/bin:/bin" "$TS_BASH" -c "$TESTER_SUPERVISOR" -- "$inner" "$TS_SETSID" "$TS_SLEEP" "$NODE" -e "$LEGACY_COMMAND_RUNNER" -- "$tag" "$@" 2> "$notice" &
|
||||
child=$!
|
||||
wait "$child" 2>/dev/null || rc=$?
|
||||
kill -KILL -- "-$child" 2>/dev/null || :
|
||||
if [ "$rc" = 137 ] && [[ "$(<"$notice")" == *': sending signal KILL to command '* ]]; then rc=82; fi
|
||||
# Sentinel retains the terminal newline through command substitution.
|
||||
raw="$("$TS_TAIL" -c 65536 -- "$errors" && printf .)" || { raw=""; rc=88; }
|
||||
raw="${raw%.}"
|
||||
suffix=$'\nMOSAIC_LEGACY_STATUS='
|
||||
if [[ "$raw" =~ $'\nMOSAIC_LEGACY_STATUS='(0|[1-9][0-9]{0,2})$'\n'$ ]] && [ "${BASH_REMATCH[1]}" -le 255 ]; then
|
||||
display="${BASH_REMATCH[1]}"; valid=1
|
||||
raw="${raw%"$suffix"*}"
|
||||
fi
|
||||
printf '%s' "$raw" >&2
|
||||
if [ "$rc" = 0 ]; then
|
||||
printf '%s\n' 0 > "$WORK/legacy-$tag.rc"
|
||||
exit 0
|
||||
fi
|
||||
if [ "$rc" = 87 ] && [ "$valid" = 1 ] && [ "$display" -gt 0 ]; then
|
||||
printf '%s\n' "$display" > "$WORK/legacy-$tag.rc"
|
||||
exit 87
|
||||
fi
|
||||
case "$rc" in 81|82|84|85|86|88) ;; *) rc=87 ;; esac
|
||||
printf '%s\n' "$rc" > "$WORK/legacy-$tag.rc"
|
||||
if [ -n "$raw" ] && [[ "$raw" != *$'\n' ]]; then printf '\n' >&2; fi
|
||||
printf 'case1-accept: %s %s\n' "$tag" "$(tester_capture_reason "$rc")" >&2
|
||||
exit "$rc"
|
||||
)
|
||||
# row276-legacy-end
|
||||
|
||||
TESTER_QUERY='import {readFileSync} from "node:fs";import {pathToFileURL} from "node:url";const m=await import(pathToFileURL(process.argv[1]).href);const s=m.loadSystem();console.log(JSON.stringify({dataRoot:s.dataRoot,socket:readFileSync(process.argv[2],"utf8").includes("MOSAIC_TESTER_SOCKET_V1")}));'
|
||||
tester_facts() {
|
||||
local LC_ALL=C pattern='^\{"dataRoot":"([A-Za-z0-9/._@+-]+)","socket":(true|false)\}$'
|
||||
@@ -425,10 +491,10 @@ scan_f5() {
|
||||
|
||||
# F3 and F5 together: the message goes out in the background and the scan runs until it is answered.
|
||||
check_f3_f5() {
|
||||
local marker text rc=0 out err n tpid
|
||||
local marker text rc=0 shown out err n tpid
|
||||
marker="case1-accept-$(od -An -N6 -tx1 /dev/urandom | tr -d ' \n')"
|
||||
text="$marker: this is the Case 1 acceptance; reply with one short line."
|
||||
timeout 330 "${AS_USER[@]}" "$COPY/scripts/mosaic" talk chief "$text" --business "$BUSINESS" --tester --wait 300 > "$WORK/talk.out" 2> "$WORK/talk.err" &
|
||||
legacy_run talk 320 330 "$COPY/scripts/mosaic" talk chief "$text" --business "$BUSINESS" --tester --wait 300 > "$WORK/talk.out" 2> "$WORK/talk.err" &
|
||||
tpid=$!
|
||||
while :; do
|
||||
scan_f5 && break
|
||||
@@ -442,24 +508,27 @@ check_f3_f5() {
|
||||
if [ "$rc" = 0 ] && grep -q "^sent [^ ]* to chief" <<< "$out" && [ "$n" -ge 2 ]; then
|
||||
fpass F3 tester-reply "chief replied to $marker"
|
||||
else
|
||||
ffail F3 tester-reply "talk exited $rc with $n lines on stdout; last: $(clean "$(tail -n 1 <<< "${err:-$out}")")"
|
||||
shown="$(legacy_display_rc talk "$rc")"
|
||||
ffail F3 tester-reply "talk exited $shown with $n lines on stdout; last: $(clean "$(tail -n 1 <<< "${err:-$out}")")"
|
||||
fi
|
||||
if [ -n "$F5_OK" ]; then F5_LINE=(fpass "$F5_OK"); else F5_LINE=(ffail "$F5_WHY"); fi
|
||||
}
|
||||
|
||||
check_f4() {
|
||||
local rc=0 out
|
||||
out="$("${AS_USER[@]}" "$COPY/scripts/case2-precheck.sh" --business "$BUSINESS" --profile factory 2>&1)" || rc=$?
|
||||
local rc=0 shown out
|
||||
out="$(legacy_run precheck 200 210 "$COPY/scripts/case2-precheck.sh" --business "$BUSINESS" --profile factory 2>&1)" || rc=$?
|
||||
if [ "$rc" = 0 ]; then fpass F4 precheck "case2-precheck.sh --profile factory exits 0"
|
||||
else ffail F4 precheck "case2-precheck.sh --profile factory exited $rc; last: $(clean "$(tail -n 1 <<< "$out")")"; fi
|
||||
else shown="$(legacy_display_rc precheck "$rc")"; ffail F4 precheck "case2-precheck.sh --profile factory exited $shown; last: $(clean "$(tail -n 1 <<< "$out")")"; fi
|
||||
}
|
||||
|
||||
check_f6() {
|
||||
local want="/opt/mosaic-stack/claude-$CLAUDE_PIN/" v
|
||||
local want="/opt/mosaic-stack/claude-$CLAUDE_PIN/" v rc=0
|
||||
[[ "$CLAUDE" == "$want"* ]] || { ffail F6 claude-pin "--claude $CLAUDE is not under $want"; return; }
|
||||
[[ "$CLAUDE/" != */../* ]] || { ffail F6 claude-pin "--claude $CLAUDE has a .. segment"; return; }
|
||||
v="$("${AS_USER[@]}" "$CLAUDE" --version 2>/dev/null | sed -n 1p)"
|
||||
if [ "$v" = "$CLAUDE_PIN (Claude Code)" ]; then fpass F6 claude-pin "$CLAUDE prints '$v'"
|
||||
v="$(legacy_run claude-version 5 7 "$CLAUDE" --version 2> "$WORK/claude-version.err")" || rc=$?
|
||||
v="$(sed -n 1p <<< "$v")"
|
||||
if [ "$rc" = 0 ] && [ "$v" = "$CLAUDE_PIN (Claude Code)" ]; then fpass F6 claude-pin "$CLAUDE prints '$v'"
|
||||
elif [ "$rc" != 0 ]; then ffail F6 claude-pin "claude-version $(tester_capture_reason "$rc"); last: $(clean "$(tail -c 200 "$WORK/claude-version.err")")"
|
||||
else ffail F6 claude-pin "$CLAUDE --version prints '$(clean "$v")', not '$CLAUDE_PIN (Claude Code)'"; fi
|
||||
}
|
||||
|
||||
@@ -477,9 +546,11 @@ let l;try{l=JSON.parse(r.stdout)}catch{console.log("fail launches list printed n
|
||||
const live=Array.isArray(l)?l.filter((x)=>x&&x.instance===by&&x.live===true):[];
|
||||
console.log(live.length!==1?`fail ${live.length} live launches of ${by}`:live[0].business!==b?`fail the live launch of ${by} is for ${live[0].business}`:`ok ${by} run ${live[0].run}`);'
|
||||
check_f7() {
|
||||
local out
|
||||
out="$("${AS_USER[@]}" "$NODE" -e "$F7_JS" -- "$BUSINESS_FILE" "$COPY/scripts/mosaic" "$BUSINESS" 2>/dev/null | sed -n 1p)"
|
||||
if [[ "$out" =~ ^ok\ [a-z0-9][a-z0-9-]*\ run\ [A-Za-z0-9._-]+$ ]]; then fpass F7 pm-live "one live launch: ${out#ok }"
|
||||
local out rc=0
|
||||
out="$(legacy_run pm-list 10 12 "$NODE" -e "$F7_JS" -- "$BUSINESS_FILE" "$COPY/scripts/mosaic" "$BUSINESS" 2> "$WORK/pm-list.err")" || rc=$?
|
||||
out="$(sed -n 1p <<< "$out")"
|
||||
if [ "$rc" = 0 ] && [[ "$out" =~ ^ok\ [a-z0-9][a-z0-9-]*\ run\ [A-Za-z0-9._-]+$ ]]; then fpass F7 pm-live "one live launch: ${out#ok }"
|
||||
elif [ "$rc" != 0 ]; then ffail F7 pm-live "pm-list $(tester_capture_reason "$rc"); last: $(clean "$(tail -c 200 "$WORK/pm-list.err")")"
|
||||
else ffail F7 pm-live "$(clean "${out#fail }")"; fi
|
||||
}
|
||||
|
||||
@@ -517,10 +588,12 @@ check_f9() {
|
||||
}
|
||||
|
||||
check_f10() {
|
||||
local want="/opt/mosaic-stack/node-$NODE_PIN/bin/node" v
|
||||
local want="/opt/mosaic-stack/node-$NODE_PIN/bin/node" v rc=0
|
||||
[ "$NODE" = "$want" ] || { ffail F10 node-pin "--node $NODE is not $want"; return; }
|
||||
v="$("${AS_USER[@]}" "$NODE" --version 2>/dev/null | sed -n 1p)"
|
||||
if [ "$v" = "$NODE_PIN" ]; then fpass F10 node-pin "$NODE prints $v"
|
||||
v="$(legacy_run node-version 5 7 "$NODE" --version 2> "$WORK/node-version.err")" || rc=$?
|
||||
v="$(sed -n 1p <<< "$v")"
|
||||
if [ "$rc" = 0 ] && [ "$v" = "$NODE_PIN" ]; then fpass F10 node-pin "$NODE prints $v"
|
||||
elif [ "$rc" != 0 ]; then ffail F10 node-pin "node-version $(tester_capture_reason "$rc"); last: $(clean "$(tail -c 200 "$WORK/node-version.err")")"
|
||||
else ffail F10 node-pin "$NODE --version prints '$(clean "$v")', not $NODE_PIN"; fi
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#
|
||||
# The library profile takes exactly these serve flags, each once:
|
||||
# --port N (1..1023) --business ID (the unit's) --public-host NAME
|
||||
# A member that changes uid and cannot be killed refuses inner-cleanup, never success.
|
||||
# --auth-header HEADER --allow-user ID --send
|
||||
# --proof-header NAME --proof-file PATH
|
||||
# The factory profile takes --port N (1..1023) and --business ID, and an
|
||||
|
||||
Reference in new issue
Block a user