Unclaimed files left in the canonical checkout. Inventory: canonical-cleanup-inventory.md (commit-as-evidence groups). detect-secrets scan clean on 2026-11-02: hits are sha256 pins, git object ids, test sentinels already in tracked tests and unit-file lines.
267 lines
17 KiB
Diff
267 lines
17 KiB
Diff
diff --git a/packages/cli/tests/stack-release.test.mjs b/packages/cli/tests/stack-release.test.mjs
|
|
index e5610bb0d..d8546dc30 100644
|
|
--- a/packages/cli/tests/stack-release.test.mjs
|
|
+++ b/packages/cli/tests/stack-release.test.mjs
|
|
@@ -90,7 +90,13 @@ function systemctl(){
|
|
}
|
|
process.exit(0);
|
|
}
|
|
- if(verb==='is-active'){const q=rest[0]==='--quiet',u=q?rest[1]:rest[0],s=(load()[u]||{}).state||'inactive';if(!q)console.log(s);process.exit(s==='active'?0:3)}
|
|
+ if(verb==='is-active'){
|
|
+ const q=rest[0]==='--quiet',u=q?rest[1]:rest[0];let s=(load()[u]||{}).state||'inactive';
|
|
+ // bus-stops-after N (T29): the next N bus checks answer the real state, every later one answers
|
|
+ // inactive, and units.json stays as it is, as if the bus stopped between two checks.
|
|
+ if(isBus(u)&&fault('bus-stops-after')){const n=Number(fs.readFileSync(faultFile('bus-stops-after'),'utf8'));if(n>0)fs.writeFileSync(faultFile('bus-stops-after'),String(n-1));else s='inactive'}
|
|
+ if(!q)console.log(s);process.exit(s==='active'?0:3);
|
|
+ }
|
|
if(verb==='is-enabled'){console.log('disabled');process.exit(1)}
|
|
if(verb==='daemon-reload')process.exit(0);
|
|
if(verb==='list-units'){
|
|
@@ -150,9 +156,12 @@ function journalctl(){
|
|
}
|
|
|
|
function systemdAnalyze(){
|
|
- const path=process.getBuiltinModule('node:path');
|
|
+ const fs=process.getBuiltinModule('node:fs'),path=process.getBuiltinModule('node:path');
|
|
if(process.argv[2]!=='--user'||process.argv[3]!=='unit-paths')process.exit(1);
|
|
console.log(path.join(process.env.XDG_CONFIG_HOME,'systemd','user'));
|
|
+ // The unit-path fault adds its text as one more path, as T28's relative path needs.
|
|
+ const extra=path.join(process.env.SD_STATE,'faults','unit-path');
|
|
+ if(fs.existsSync(extra))console.log(fs.readFileSync(extra,'utf8'));
|
|
}
|
|
|
|
// The bus host: holds the store open in WAL mode until SIGTERM, as the broker does.
|
|
@@ -303,7 +312,7 @@ async function mk(t){
|
|
|
|
async function run(e,...args){
|
|
const before=existsSync(e.data)?kept(e):[];
|
|
- const ch=spawn('bash',[SR,...args,'--copies',COPIES,'--bin',e.bin],{env:e.env,stdio:['ignore','pipe','pipe']});
|
|
+ const ch=spawn('bash',[e.sr??SR,...args,'--copies',COPIES,'--bin',e.bin],{env:e.env,stdio:['ignore','pipe','pipe']});
|
|
let out='',err='';ch.stdout.on('data',(d)=>{out+=d});ch.stderr.on('data',(d)=>{err+=d});
|
|
const timer=setTimeout(()=>ch.kill('SIGKILL'),120000);
|
|
const [code]=await once(ch,'close');clearTimeout(timer);
|
|
@@ -730,3 +739,91 @@ test('T22 the stopped store read: a -wal or -shm refuses store-unclean and is le
|
|
assert.deepEqual(Object.keys(stop.files),['bus.sqlite']);assert.deepEqual(start.files,stop.files);
|
|
});
|
|
});
|
|
+
|
|
+// --- Row 166 (#1663): docs/plans/2026-10-18_stack-release-hardening.md items 1 to 4 ---
|
|
+
|
|
+test('T28 a unit path, drop-in directory or drop-in file that cannot be read, or a relative unit path, refuses drop-in-override before any stop',async(t)=>{
|
|
+ const cases=[
|
|
+ ['unit path',(e)=>[e.units,`cannot inspect unit path ${e.units}`]],
|
|
+ ['drop-in directory',(e)=>{const d=join(e.units,`${BUS}.d`);mkdirSync(d);writeFileSync(join(d,'note.conf'),'[Service]\n');return [d,`cannot inspect drop-ins ${d}`]}],
|
|
+ ['drop-in file',(e)=>{const d=join(e.units,`${BUS}.d`),f=join(d,'claude.conf');mkdirSync(d);writeFileSync(f,'[Service]\n');return [f,`cannot read drop-in ${f}`]}],
|
|
+ ];
|
|
+ for(const [name,make] of cases)await t.test(name,async(t)=>{
|
|
+ const e=await mk(t);await seed(e);
|
|
+ const [p,msg]=make(e),before=fp(e),n=calls(e).length,logs=logLines(e).length,mode=statSync(p).mode&0o7777;
|
|
+ chmodSync(p,0);
|
|
+ let r;try{r=await run(e,'activate',C1,'acme')}finally{chmodSync(p,mode)}
|
|
+ assert.equal(r.code,3,r.out+r.err);assert.equal(r.err,`stack-release: refusing drop-in-override: ${msg}\n`);
|
|
+ assert.deepEqual(fp(e),before);assert.deepEqual(startsStops(e,n),[]);
|
|
+ assert.deepEqual([units(e)[BUS].state,units(e)[CON].state],['active','active']);
|
|
+ assert.equal(logLines(e).length,logs+1);assert.equal(logLines(e).at(-1).code,'drop-in-override');
|
|
+ });
|
|
+ // install_guard refuses a unit path that isn't absolute, so the precheck does too.
|
|
+ await t.test('relative unit path',async(t)=>{
|
|
+ const e=await mk(t);await seed(e);fault(e,'unit-path','systemd/user');
|
|
+ const before=fp(e),n=calls(e).length,logs=logLines(e).length;
|
|
+ const r=await run(e,'activate',C1,'acme');assert.equal(r.code,3,r.out+r.err);
|
|
+ assert.equal(r.err,'stack-release: refusing drop-in-override: invalid systemd unit path systemd/user\n');
|
|
+ assert.deepEqual(fp(e),before);assert.deepEqual(startsStops(e,n),[]);
|
|
+ assert.deepEqual([units(e)[BUS].state,units(e)[CON].state],['active','active']);
|
|
+ assert.equal(logLines(e).length,logs+1);assert.equal(logLines(e).at(-1).code,'drop-in-override');
|
|
+ });
|
|
+});
|
|
+
|
|
+test('T29 a bus that stops during the held-store read: --plan says so, rollback --restore refuses restore-mismatch',async(t)=>{
|
|
+ await t.test('--plan',async(t)=>{
|
|
+ const e=await mk(t);await seed(e);fault(e,'bus-stops-after','1');
|
|
+ const before=fp(e),logs=logLines(e).length;
|
|
+ const r=await run(e,'activate',C,'acme','--plan');assert.equal(r.code,0,r.err);assert.equal(r.err,'');
|
|
+ assert.equal(r.out,['plan: activate for acme',`from: ${B}`,`to: ${C}`,
|
|
+ 'store schema: unknown (the bus stopped during the read, so the store state changed; run --plan again)','target schema: 3h',
|
|
+ 'commits: the run adds 2 (newest first, at most 200):',...commitLines(`${B}..${C}`),...planTail(e,C),''].join('\n'));
|
|
+ assert.deepEqual(fp(e),before);assert.equal(logLines(e).length,logs);
|
|
+ });
|
|
+ await t.test('rollback --restore',async(t)=>{
|
|
+ const e=await mk(t);await seed(e);
|
|
+ assert.equal((await run(e,'activate',C,'acme','--migrate')).code,0);const act=pointer(e).activation;
|
|
+ const stamp=readFileSync(join(e.data,'backups','stack',act,'STAMP'),'utf8').replace(/\n+$/,'');
|
|
+ fault(e,'bus-stops-after','1');const before=fp(e),n=calls(e).length;
|
|
+ const r=await run(e,'rollback','acme','--restore',act);assert.equal(r.code,3,r.out+r.err);
|
|
+ assert.equal(r.err,`stack-release: refusing restore-mismatch: the bus stopped while the events after ${stamp} were counted, so the store state changed; nothing was stopped\n`);
|
|
+ assert.deepEqual(fp(e),before);assert.deepEqual(startsStops(e,n),[]);assert.equal(posts(e).length,0);
|
|
+ assert.equal(logLines(e).at(-1).code,'restore-mismatch');
|
|
+ });
|
|
+});
|
|
+
|
|
+test('T30 a copy check that agrees on another copy fails the gate with copy-check and rolls back',async(t)=>{
|
|
+ const e=await mk(t);await seed(e);
|
|
+ // A repo of its own, so the stand-in check replaces stack-copy.sh for this test only. Sourced, the
|
|
+ // stand-in is the real script; run with `check`, it reports all three on the pointer's copy.
|
|
+ const own=join(e.root,'repo');cpSync(repo,own,{recursive:true});
|
|
+ const sc=join(own,'scripts','stack-copy.sh'),at=join(COPIES,B);cpSync(sc,join(own,'scripts','stack-copy.real.sh'));
|
|
+ writeFileSync(sc,`#!/usr/bin/env bash\nreal="$(dirname "\${BASH_SOURCE[0]}")/stack-copy.real.sh"\nif [ "\${BASH_SOURCE[0]}" != "$0" ]; then source "$real"; return 0; fi\nif [ "\${1:-}" = check ]; then printf 'bus: %s\\nConsole: %s\\nCLI: %s\\n' '${at}' '${at}' '${at}'; exit 0; fi\nexec bash "$real" "$@"\n`);
|
|
+ e.sr=join(own,'scripts','stack-release.sh');
|
|
+ const r=await run(e,'activate',C1,'acme','--timeout','3');assert.equal(r.code,1,r.out+r.err);
|
|
+ assert.equal(r.err,rolledBack('copy-check'));
|
|
+ assert.ok(r.out.includes(`stack-release: gate failed on ${C1}: copy-check (the bus, Console and CLI agree on ${at}, not ${join(COPIES,C1)})\n`),r.out);
|
|
+ const l=logLines(e).at(-1);assert.deepEqual([l.op,l.result,l.code,l.rollback],['activate','gate-failed','copy-check','ok']);
|
|
+ assert.equal(pointer(e).copy,B);assert.deepEqual([units(e)[BUS].state,units(e)[BUS].wd],['active',at]);
|
|
+});
|
|
+
|
|
+test('T31 a quoted or escaped Environment= or ExecStart= is unreadable: refusals by name, nothing read from /a',async(t)=>{
|
|
+ const forms=[
|
|
+ ['Environment double quotes',(e)=>edit(join(e.units,'[email protected]'),`Environment=MOSAIC_CLAUDE_BIN=${e.claude}\n`,'Environment="MOSAIC_CLAUDE_BIN=/a b/claude"\n')],
|
|
+ ['Environment single quotes',(e)=>edit(join(e.units,'[email protected]'),`Environment=MOSAIC_CLAUDE_BIN=${e.claude}\n`,'Environment=\'MOSAIC_CLAUDE_BIN=/a b/claude\'\n')],
|
|
+ ['Environment backslash',(e)=>edit(join(e.units,'[email protected]'),`Environment=MOSAIC_CLAUDE_BIN=${e.claude}\n`,'Environment=MOSAIC_CLAUDE_BIN=/a\\ b/claude\n')],
|
|
+ ['ExecStart quotes',(e)=>edit(join(e.units,'[email protected]'),' bus start %i\n',' bus start %i "--pm"\n')],
|
|
+ ['Console ExecStart quotes',(e)=>edit(join(e.units,CON),' --business acme\n',' --business "acme"\n')],
|
|
+ ];
|
|
+ for(const [name,change] of forms)await t.test(name,async(t)=>{
|
|
+ const e=await mk(t);await seed(e);change(e);const before=fp(e),n=calls(e).length;
|
|
+ const r=await run(e,'activate',C1,'acme');assert.equal(r.code,3,r.out+r.err);
|
|
+ assert.equal(r.err,`stack-release: refusing unit-flags-drift: cannot read ${BUS} or ${CON}\n`);
|
|
+ assert.deepEqual(fp(e),before);assert.deepEqual(startsStops(e,n),[]);
|
|
+ const s=await run(e,'status','acme');assert.equal(s.code,0,s.err);assert.ok(s.out.includes('unit flags: cannot read the units\n'),s.out);
|
|
+ const a=await mk(t);await seed(a,{pointer:'none'});change(a);
|
|
+ const d=await run(a,'adopt','acme');assert.equal(d.code,3,d.out+d.err);
|
|
+ assert.equal(d.err,`stack-release: refusing check-failed: cannot read ${BUS} or ${CON}\n`);assert.equal(pointer(a),null);
|
|
+ for(const x of [r,d])assert.ok(!x.err.includes('/a'),x.err);
|
|
+ });
|
|
+});
|
|
diff --git a/scripts/stack-release.sh b/scripts/stack-release.sh
|
|
index 4b16e848a..ed5b41d46 100755
|
|
--- a/scripts/stack-release.sh
|
|
+++ b/scripts/stack-release.sh
|
|
@@ -143,20 +143,26 @@ process.exit(1);' -- "$LOG" "$1" "$P_COPY"
|
|
|
|
show() { systemctl --user show "$1" -p LoadState -p WorkingDirectory -p ExecStart -p Environment -p MainPID -p DropInPaths 2>/dev/null; }
|
|
argv_of() { sed -n 's/^ExecStart=.*argv\[\]=\([^;]*\) ;.*/\1/p' <<< "$1"; }
|
|
+# systemctl show quotes a value that holds a space and escapes with a backslash, and the read -a
|
|
+# split below can't undo either, so such a line is unreadable rather than read wrong (row 166 item 4).
|
|
+unquoted() { [[ "$1" != *[\"\'\\]* ]]; }
|
|
|
|
# Sets U_PM U_CLAUDE U_CONSOLE (array) from the effective units; returns 1 if a unit can't be read.
|
|
read_units() {
|
|
- local bus console tok
|
|
+ local bus console tok bus_argv console_argv env
|
|
bus="$(show "$BUS_UNIT")" && console="$(show "$CONSOLE_UNIT")" || return 1
|
|
[ "$(sed -n 's/^LoadState=//p' <<< "$bus")" = loaded ] && [ "$(sed -n 's/^LoadState=//p' <<< "$console")" = loaded ] || return 1
|
|
+ bus_argv="$(argv_of "$bus")" console_argv="$(argv_of "$console")" env="$(sed -n 's/^Environment=//p' <<< "$bus")"
|
|
+ unquoted "$bus_argv" && unquoted "$console_argv" || return 1
|
|
+ unquoted "$env" || return 1
|
|
local -a argv
|
|
- read -r -a argv <<< "$(argv_of "$bus")"
|
|
+ read -r -a argv <<< "$bus_argv"
|
|
U_PM=0
|
|
for tok in "${argv[@]:1}"; do [ "$tok" != --pm ] || U_PM=1; done
|
|
U_CLAUDE=""
|
|
- read -r -a argv <<< "$(sed -n 's/^Environment=//p' <<< "$bus")"
|
|
+ read -r -a argv <<< "$env"
|
|
for tok in "${argv[@]}"; do case "$tok" in MOSAIC_CLAUDE_BIN=*) U_CLAUDE="${tok#MOSAIC_CLAUDE_BIN=}" ;; esac; done
|
|
- read -r -a argv <<< "$(argv_of "$console")"
|
|
+ read -r -a argv <<< "$console_argv"
|
|
U_CONSOLE=()
|
|
local i serve=0
|
|
for ((i = 0; i < ${#argv[@]}; i++)); do
|
|
@@ -371,7 +377,10 @@ process.stdout.write(why);' -- "$out" "$BUSINESS" "$main" "$START_MS" 2>&1)" ||
|
|
node -e '
|
|
fetch(`http://127.0.0.1:${process.argv[1]}/healthz`,{signal:AbortSignal.timeout(5000)}).then((r)=>process.exit(r.status===200?0:1),()=>process.exit(1));' -- "$(console_port)" || return 1
|
|
GATE=copy-check
|
|
- scripts/stack-copy.sh check --copies "$COPIES" --bin "$BIN" --business "$BUSINESS" > /dev/null 2>&1 || return 1
|
|
+ out="$(scripts/stack-copy.sh check --copies "$COPIES" --bin "$BIN" --business "$BUSINESS" 2>/dev/null)" || return 1
|
|
+ # check passes when the three agree on one copy; this run needs it to be the target (row 166 item 3).
|
|
+ out="$(sed -n 's/^bus: //p' <<< "$out")"
|
|
+ [ "$out" = "$copy" ] || { GATE_WHY="the bus, Console and CLI agree on $out, not $copy"; return 1; }
|
|
if [ "$pm" = 1 ]; then
|
|
GATE=pm-launch-unconfirmed
|
|
pm_launched "$copy" || return 1
|
|
@@ -499,17 +508,23 @@ instance_busy() {
|
|
done <<< "$out"
|
|
}
|
|
|
|
-# install_guard's drop-in scan (bus-service.sh), done before anything stops.
|
|
+# install_guard's drop-in scan (bus-service.sh), done before anything stops. A path it can't read
|
|
+# refuses here, as install_guard would refuse it after the stop (row 166 item 1).
|
|
drop_in_override() {
|
|
- local paths directory dropdir file
|
|
+ local paths directory dropdir file content
|
|
paths="$(systemd-analyze --user unit-paths 2>/dev/null)" && [ -n "$paths" ] || refuse drop-in-override "systemd unit paths are unknown"
|
|
while IFS= read -r directory; do
|
|
- [ -n "$directory" ] && [ -d "$directory" ] || continue
|
|
+ [ -n "$directory" ] || continue
|
|
+ case "$directory" in /*) ;; *) refuse drop-in-override "invalid systemd unit path $directory" ;; esac
|
|
+ [ -e "$directory" ] || [ -L "$directory" ] || continue
|
|
+ [ -d "$directory" ] && [ -r "$directory" ] && [ -x "$directory" ] || refuse drop-in-override "cannot inspect unit path $directory"
|
|
for dropdir in "$directory"/mosaic-bus@*.service.d "$directory"/mosaic-.service.d "$directory"/service.d; do
|
|
- [ -d "$dropdir" ] || continue
|
|
+ [ -e "$dropdir" ] || [ -L "$dropdir" ] || continue
|
|
+ [ -d "$dropdir" ] && [ -r "$dropdir" ] && [ -x "$dropdir" ] || refuse drop-in-override "cannot inspect drop-ins $dropdir"
|
|
for file in "$dropdir"/*.conf; do
|
|
- [ -e "$file" ] || continue
|
|
- if grep -Eq '^[[:space:]]*(ExecStart|WorkingDirectory)[[:space:]]*=' "$file" 2>/dev/null; then
|
|
+ [ -e "$file" ] || [ -L "$file" ] || continue
|
|
+ content="$(cat "$file" 2>/dev/null)" || refuse drop-in-override "cannot read drop-in $file"
|
|
+ if grep -Eq '^[[:space:]]*(ExecStart|WorkingDirectory)[[:space:]]*=' <<< "$content"; then
|
|
refuse drop-in-override "$file overrides ExecStart or WorkingDirectory; the operator removes it as in TOOLS.md step 4"
|
|
fi
|
|
done
|
|
@@ -572,17 +587,21 @@ do_adopt() {
|
|
# pointer's schema, and says so, when there is no store file or it can't be read.
|
|
plan_store() {
|
|
local src="" why=""
|
|
- S_SCHEMA="" S_FROM=""
|
|
+ S_SCHEMA="" S_FROM="" S_CHANGED=""
|
|
if [ ! -e "$STORE" ]; then why="there is no store file"
|
|
elif systemctl --user is-active --quiet "$BUS_UNIT"; then
|
|
src="the store the running bus holds"
|
|
S_SCHEMA="$(node -e "$STORE_JS" -- heldversion "$STORE" 2>/dev/null)" || S_SCHEMA=""
|
|
+ # A bus that stops during the readOnly open can leave -wal or -shm beside the stopped
|
|
+ # store, so the version read can't be planned on (row 166 item 2).
|
|
+ systemctl --user is-active --quiet "$BUS_UNIT" || S_CHANGED=1
|
|
elif store_unclean; then why="the store was not read: $UNCLEAN is present"
|
|
else
|
|
src="the stopped store"
|
|
S_SCHEMA="$(store_read version 2>/dev/null)" || S_SCHEMA=""
|
|
fi
|
|
- if [ -n "$S_SCHEMA" ]; then S_FROM="read from $src"
|
|
+ if [ -n "$S_CHANGED" ]; then S_SCHEMA=unknown S_FROM="the bus stopped during the read, so the store state changed; run --plan again"
|
|
+ elif [ -n "$S_SCHEMA" ]; then S_FROM="read from $src"
|
|
else S_SCHEMA="$P_SCHEMA" S_FROM="from the pointer; ${why:-$src could not be read}"
|
|
fi
|
|
}
|
|
@@ -631,8 +650,10 @@ plan() {
|
|
echo "to: $TO"
|
|
echo "store schema: $S_SCHEMA ($S_FROM)"
|
|
echo "target schema: $T_SCHEMA"
|
|
- [ "$S_SCHEMA" = "$P_SCHEMA" ] || echo "note: the pointer says $P_SCHEMA; the run reads the stopped store and goes by that"
|
|
- if [ "$S_SCHEMA" = "$T_SCHEMA" ]; then
|
|
+ [ "$S_SCHEMA" = "$P_SCHEMA" ] || [ -n "$S_CHANGED" ] || echo "note: the pointer says $P_SCHEMA; the run reads the stopped store and goes by that"
|
|
+ if [ -n "$S_CHANGED" ]; then
|
|
+ : # no migration or restore line from a read the bus stopped under
|
|
+ elif [ "$S_SCHEMA" = "$T_SCHEMA" ]; then
|
|
echo "migration: none; the store is at the target's version"
|
|
elif [ "$OP" = activate ] && in_chain "$S_SCHEMA"; then
|
|
echo "migration: $S_SCHEMA to $T_SCHEMA, after a backup of the stopped store"
|
|
@@ -715,6 +736,8 @@ do_rollback() {
|
|
# no file (the probe); a stopped one falls under the store_unclean rule.
|
|
if systemctl --user is-active --quiet "$BUS_UNIT"; then
|
|
COUNT="$(node -e "$STORE_JS" -- heldcount "$STORE" "$(cat "$o/STAMP")")" || rc=1
|
|
+ # plan_store's recheck (row 166 item 2), before anything stops.
|
|
+ systemctl --user is-active --quiet "$BUS_UNIT" || refuse restore-mismatch "the bus stopped while the events after $(cat "$o/STAMP") were counted, so the store state changed; nothing was stopped"
|
|
else
|
|
! store_unclean || refuse store-unclean "$UNCLEAN is present; the bus did not close the store cleanly"
|
|
COUNT="$(store_read count "$(cat "$o/STAMP")")" || rc=1
|
|
@@ -778,7 +801,7 @@ LOG="$STATE/stack-activation-log.jsonl"
|
|
STORE="$DATA/bus/bus.sqlite"
|
|
BUS_UNIT="mosaic-bus@$BUSINESS.service"
|
|
FROM="" TO="" ACT="" RS="" POST="" BACKUP="" TAKEN="" RESTORED="" STEPS="" COUNT="" UNCLEAN="" FAIL=""
|
|
-STORE_SCHEMA="" START_EPOCH="" START_MS=0 GATE="" GATE_WHY=""
|
|
+STORE_SCHEMA="" START_EPOCH="" START_MS=0 GATE="" GATE_WHY="" S_CHANGED=""
|
|
P_COPY="" P_PM=0 P_SCHEMA="" P_CLAUDE="" P_CONSOLE=()
|
|
case "$OP" in
|
|
adopt) do_adopt ;;
|