From 6273b65e9db591201034884875ff6c77db441057 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Wed, 21 Oct 2026 20:34:11 -0500 Subject: [PATCH] stack-release: a rollback that drops --sso drops --send; the --no-sso walk refuses an ok line with no activation (row 273, sage-2315) B1: row 200's rollback to a Console without --sso kept the pointer's --send, which F39 b forbids once --send can sit beside --sso. send_for_target drops it whenever the run drops the pointer's --sso, and the plan and steps say so. N1: an ok line with no activation refuses sso-off-flags instead of being skipped. --- agents/darkwing/work/queue-273/BUILD.md | 25 ++++++++++++++++-- packages/cli/tests/stack-release.test.mjs | 31 +++++++++++++++++++---- scripts/stack-release.sh | 26 ++++++++++++++----- 3 files changed, 69 insertions(+), 13 deletions(-) diff --git a/agents/darkwing/work/queue-273/BUILD.md b/agents/darkwing/work/queue-273/BUILD.md index 417c27b55..6dfd1ea11 100644 --- a/agents/darkwing/work/queue-273/BUILD.md +++ b/agents/darkwing/work/queue-273/BUILD.md @@ -47,6 +47,23 @@ rollback. The cost is that "a set that ran with `--sso` refuses" now applies onl without `--sso` exists. A tampered set on the walk still refuses, even one newer than the set the walk would have used. Filbert, please rule on this reading. +## Filbert's early look: B1 and N1 (sage-2315) + +Filbert accepted the walk. Sage ruled both findings into this row. + +- B1: row 200's rollback to a copy whose Console has no `--sso` took the flags from the set and kept + the pointer's `--send`. From an `--sso --send` pointer, which this row's `--send` can now make, that + gave a Console with `--send` and no `--sso`, which F39 b forbids. `send_for_target` now drops `--send` + whenever the run drops the pointer's `--sso`, so it is still the one place that drops it. The plan + line reads `send: dropped; the run drops --sso, and --send needs it (F39)`, and the steps log gets + `drop --send; the run drops --sso`. Row 200's sso line now names only the tester as following the + pointer. One T57 case pinned the old carry (a legacy `--send` pointer, `--sso`, then a rollback to + B). It now expects no `--send`. +- N1: the walk skipped an ok line with no activation. Skipping it could land on an older set that + isn't the right one, so it now refuses `sso-off-flags` with `an ok line in the log names no + activation`. The walk reads the log through `mapfile`. That gives no entry for an empty log, so a + log with no set still gets its own message. + ## test-release.sh The window plan says test-release.sh "gets cases for both". The brief lists it only as a file to run @@ -59,6 +76,8 @@ live in the node tests and test-release.sh runs unchanged. carried `--sso` re-pins C1 once; a second `--send` refuses `already-active`; C carries it. - T2: `--send` without `--sso` refuses on another copy and on the pointer's copy, also when the pointer carries `--send` alone; `--plan` lists the refusal. +- T2 (sage-2315 B1): `--sso --send` onto C1, then a rollback to B, whose Console has no `--sso`. The + pointer and the unit end with neither flag, and the plan and steps name the drop. - T2 and T4 usage: every forbidden pairing and verb, checked against the full usage text. - T3: from an `--sso --send` pointer, `--no-sso` on the same copy gives back the pre-`--sso` ExecStart line byte for byte, without `--send`. A second case walks past a `--no-send` re-pin that ran with @@ -67,8 +86,8 @@ live in the node tests and test-release.sh runs unchanged. sets both hold `--sso`. `--no-sso` then gives back the arrival's ExecStart line byte for byte. - T4 (sage-2305 b): two sets, both edited or written to hold `--sso` with sums intact, refuse `sso-off-flags` through `refused()`. -- T4: `sso-not-on`; a set that fails its sums, ran with `--sso`, has no argv or is missing; a log with no - set. Each runs through `refused()`, so nothing stops and nothing else changes. +- T4: `sso-not-on`; an ok line with no activation; a set that fails its sums, ran with `--sso`, has no + argv or is missing; a log with no set. Each runs through `refused()`, so nothing stops and nothing else changes. - T5: stack-release-external.test.mjs adds `--send` and `--no-sso` to the row 232 usage list. - T55 test 5 and the T57 usage test match the new usage line. @@ -81,5 +100,7 @@ live in the node tests and test-release.sh runs unchanged. | M3 | the `--no-sso` branch of `send_for_target` keeps `--send` | T3 | | M4 | `--send` skips the `send-needs-sso` refusal | T2 | | M5 | parse_opts drops the `--send`/`--no-send` rule | T2 and T4 usage | +| M6 | a rollback to a Console without `--sso` keeps `--send` (B1) | T2 (sage-2315 B1) | +| M7 | the walk skips an ok line with no activation (N1) | T4 no-activation line | Runs and results go in the round's notes once Sage clears the slot. diff --git a/packages/cli/tests/stack-release.test.mjs b/packages/cli/tests/stack-release.test.mjs index 83421b793..afbba0deb 100644 --- a/packages/cli/tests/stack-release.test.mjs +++ b/packages/cli/tests/stack-release.test.mjs @@ -1465,7 +1465,7 @@ const DROPS='--public-host console.example --auth-header x-user --allow-user jas const SHOW=`show-${CON}.txt`; const setOf=(e,act)=>join(e.state,'stack-rollback',act); const arrivalSet=(e,to)=>setOf(e,logLines(e).findLast((l)=>l.result==='ok'&&l.to===to&&l.from!==to).activation); -const ROLLBACK_PLAN=(set)=>`sso: dropped; ${B}'s Console does not support --sso, so the rollback takes the Console flags from ${set}/${SHOW}, with --send and --tester-port as the pointer has them`; +const ROLLBACK_PLAN=(set)=>`sso: dropped; ${B}'s Console does not support --sso, so the rollback takes the Console flags from ${set}/${SHOW}, with --tester-port as the pointer has it`; const KEY_MSG=(e,to,op,why)=>`${e.cfg} holds a console key, and ${to}'s loader refuses it (${why}); remove the key first (restore the config.json backup from the window's step 1), then run the ${op} again`; const RB_MSG=(why)=>`${B}'s Console does not support --sso, and the Console flags it ran with cannot be read: ${why}`; // Rewrites a file in a rollback set (0700, files kept by cp -p) and, unless sums is false, its SHA256SUMS line. @@ -1600,14 +1600,14 @@ test('T57 row 200: activate --sso, its refusals before any stop, and the rollbac assert.ok(conExec(e).endsWith(` --checkout ${repo} ${want.join(' ')}`),conExec(e)); assert.ok(stepText(e).includes(`Z drop --sso; ${B}'s Console does not support it, so the Console flags come from ${set}/${SHOW}\n`)); r=await prun(e,'status','acme');assert.equal(r.code,0,r.err);assert.match(r.out,/unit flags: match the pointer/); - // The plain case: --send and the preview set and pair come back as they were. + // The plain case: the preview set and pair come back as they were. --send lost its --sso, so it goes (row 273, F39 b). const g=await mk(t),gbase=await seedPreview(g,undefined,['--send']),f=await proofFile(g); r=await prun(g,'activate',B,'acme',...pair(f));assert.equal(r.code,0,r.out+r.err); await ssoConfig(g); r=await prun(g,'activate',C1,'acme','--sso');assert.equal(r.code,0,r.out+r.err); setKey(g,undefined); r=await prun(g,'rollback','acme');assert.equal(r.code,0,r.out+r.err); - assert.deepEqual(pointer(g).console,[...noSend(gbase),...pair(f),'--send']); + assert.deepEqual(pointer(g).console,[...noSend(gbase),...pair(f)]); for(const x of [e,g])await noLeak(x); }); await t.test('item 3: a rollback set that is missing, fails its sums, holds no argv or already ran --sso refuses sso-rollback-flags',async(t)=>{ @@ -1679,7 +1679,7 @@ test('T57 row 200: activate --sso, its refusals before any stop, and the rollbac r=await prun(a,'status','acme');assert.equal(r.code,0,r.err);assert.match(r.out,/unit flags: match the pointer/); setKey(a,undefined); const set=arrivalSet(a,C1);assert.ok(existsSync(join(set,show)),show); - assert.deepEqual(ssoLines(await planOnly(a,0,'rollback','acme')),[`sso: dropped; ${B}'s Console does not support --sso, so the rollback takes the Console flags from ${set}/${show}, with --send and --tester-port as the pointer has them`]); + assert.deepEqual(ssoLines(await planOnly(a,0,'rollback','acme')),[`sso: dropped; ${B}'s Console does not support --sso, so the rollback takes the Console flags from ${set}/${show}, with --tester-port as the pointer has it`]); r=await prun(a,'rollback','acme');assert.equal(r.code,0,r.out+r.err); assert.equal(pointer(a).copy,B);assert.deepEqual(pointer(a).console,want.slice(0,-1)); assert.ok(stepText(a).includes(`Z drop --sso; ${B}'s Console does not support it, so the Console flags come from ${set}/${show}\n`)); @@ -1751,6 +1751,22 @@ test('T58 row 273: activate --send and --no-sso',async(t)=>{ await refused(g,['activate',B,'acme','--send'],'already-active',`${B} is the pointer's copy`); for(const x of [e,g])await noLeak(x); }); + await t.test('T2 (sage-2315 B1): a rollback from an --sso --send pointer to a Console without --sso ends with neither flag',async(t)=>{ + const e=await mk(t),base=await seedPreview(e);await ssoConfig(e); + let r=await prun(e,'activate',C1,'acme','--sso','--send');assert.equal(r.code,0,r.out+r.err); + assert.deepEqual(pointer(e).console,['--port',e.port,'--business','acme','--sso','--send']); + const set=arrivalSet(e,C1); + setKey(e,undefined); + const out=await planOnly(e,0,'rollback','acme'); + assert.deepEqual([...sendLines(out),...ssoLines(out)],['send: dropped; the run drops --sso, and --send needs it (F39)',ROLLBACK_PLAN(set)]); + assert.equal(out.find((l)=>l.startsWith('Console: ')),`Console: scripts/webui-service.sh install --copy ${B} --copies ${COPIES} ${base.join(' ')}`); + r=await prun(e,'rollback','acme');assert.equal(r.code,0,r.out+r.err); + assert.equal(pointer(e).copy,B);assert.deepEqual(pointer(e).console,base); + assert.doesNotMatch(conExec(e),/ --sso( |$)| --send( |$)/);assert.ok(conExec(e).endsWith(` --checkout ${repo} ${base.join(' ')}`),conExec(e)); + assert.ok(stepText(e).includes('Z drop --send; the run drops --sso\n'),stepText(e)); + r=await prun(e,'status','acme');assert.equal(r.code,0,r.err);assert.match(r.out,/unit flags: match the pointer/); + await noLeak(e); + }); await t.test('T2 and T4: --send with --no-send, --no-sso with --sso, --send, a pair or --no-proof, and either flag on another verb, is a usage error',async(t)=>{ const e=await mk(t);await seedPreview(e);await ssoConfig(e);const f=await proofFile(e); let r=await prun(e,'activate',C1,'acme','--sso','--send');assert.equal(r.code,0,r.out+r.err); @@ -1838,7 +1854,7 @@ test('T58 row 273: activate --send and --no-sso',async(t)=>{ assert.deepEqual((await planOnly(e,3,'activate',C1,'acme','--no-sso')).filter((l)=>l.startsWith('refuses ')),[`refuses sso-off-flags: ${why}`]); await noLeak(e); }); - await t.test('T4: --no-sso refuses before any stop on a pointer without --sso, a tampered or missing set, a set that ran with --sso, and a log with no set',async(t)=>{ + await t.test('T4: --no-sso refuses before any stop on a pointer without --sso, a tampered or missing set, a set that ran with --sso, an ok line with no activation, and a log with no set',async(t)=>{ const e=await mk(t);await seedPreview(e,undefined,['--send']); await refused(e,['activate',B,'acme','--no-sso'],'sso-not-on',NOT_ON_MSG); await refused(e,['activate',C1,'acme','--no-sso'],'sso-not-on',NOT_ON_MSG); @@ -1847,6 +1863,11 @@ test('T58 row 273: activate --send and --no-sso',async(t)=>{ await ssoConfig(e); let r=await prun(e,'activate',C1,'acme','--sso');assert.equal(r.code,0,r.out+r.err); const set=setOf(e,pointer(e).activation),show=join(set,SHOW),text=readFileSync(show,'utf8'); + // An ok line with no activation stops the walk (sage-2315 N1); skipping it would reach the set above. + const logText=readFileSync(e.log,'utf8'); + appendFileSync(e.log,JSON.stringify({at:'2026-10-22T00:00:00Z',op:'activate',from:C1,to:C1,backup:null,activation:null,result:'ok'})+'\n'); + await refused(e,['activate',C1,'acme','--no-sso'],'sso-off-flags',OFF_MSG('an ok line in the log names no activation')); + writeFileSync(e.log,logText); setEdit(set,SHOW,' --allow-user jason',' --allow-user mallory',false); await refused(e,['activate',C1,'acme','--no-sso'],'sso-off-flags',OFF_MSG(`${show} does not match the set's SHA256SUMS`)); assert.deepEqual((await planOnly(e,3,'activate',C1,'acme','--no-sso')).filter((l)=>l.startsWith('refuses ')),[`refuses sso-off-flags: ${OFF_MSG(`${show} does not match the set's SHA256SUMS`)}`]); diff --git a/scripts/stack-release.sh b/scripts/stack-release.sh index e9657a894..7c7676c71 100755 --- a/scripts/stack-release.sh +++ b/scripts/stack-release.sh @@ -888,7 +888,8 @@ sso_base() { return 0 fi # --send and --tester-port follow the pointer, so a --no-send or a tester port set after that - # activation stays (row 189's rule that rollback carries --send off). + # activation stays (row 189's rule that rollback carries --send off). A --send carried here has + # lost its --sso, so send_for_target drops it (row 273, F39 b). local i BASE_CONSOLE=() for ((i = 0; i < ${#SERVE_FLAGS[@]}; i++)); do @@ -906,7 +907,7 @@ sso_base() { --tester-socket) BASE_CONSOLE+=(--tester-socket) ;; esac done - SSO_PLAN="sso: dropped; $TO's Console does not support --sso, so the rollback takes the Console flags from $set/$f, with --send and --tester-port as the pointer has them" + SSO_PLAN="sso: dropped; $TO's Console does not support --sso, so the rollback takes the Console flags from $set/$f, with --tester-port as the pointer has it" SSO_STEP="drop --sso; $TO's Console does not support it, so the Console flags come from $set/$f" } @@ -934,17 +935,20 @@ set_flags() { # is dropped (F39 a) and the tester follows the pointer. Sets BASE_CONSOLE, SSO_PLAN and SSO_STEP. no_sso_base() { local f="show-$CONSOLE_UNIT.txt" why="" act set="" found=0 i restored="" + local -a acts=() if ! has_flag --sso "${P_CONSOLE[@]}"; then check_refuse sso-not-on "the pointer's Console flags hold no --sso, so --no-sso has nothing to turn off" return 0 fi - while read -r act; do - [ -n "$act" ] || continue + # An empty entry is an ok line with no activation; skipping it could land on an older set. + mapfile -t acts < <(from_log sets || true) + for act in "${acts[@]}"; do set="$STATE/stack-rollback/$act" + if [ -z "$act" ]; then why="an ok line in the log names no activation"; break; fi if ! [[ "$act" =~ ^[0-9]{8}T[0-9]{6}Z-[0-9a-f]{12}(-[1-9])?$ ]]; then why="the activation $act names no rollback set"; break; fi set_flags "$set" || { why="$SET_WHY"; break; } has_flag --sso "${SERVE_FLAGS[@]}" || { found=1; break; } - done <<< "$(from_log sets || true)" + done if [ -z "$why" ] && [ "$found" = 0 ]; then if [ -z "$set" ]; then why="the log names no activation with a rollback set" else why="the Console in $set/$f already ran with --sso, and the log names no earlier activation" @@ -1097,7 +1101,8 @@ process.exit(d!==null&&typeof d==="object"&&Object.hasOwn(d,"console")?0:1)' -- # they carry the pointer as it is. Sets SEND_PLAN (the plan's send: line) and SEND_STEP. # Row 273 (mos-1246 F39): under forward auth any same-uid process can send as the signed-in user # through the Console's port, so --send turns send on only when the run's Console flags hold --sso, -# given or carried, and --no-sso drops the pointer's --send. +# given or carried. --no-sso drops the pointer's --send, and so does a rollback to a Console without +# --sso. send_for_target() { local had=0 i local -a without_send=() with_send=() @@ -1113,6 +1118,15 @@ send_for_target() { else SEND_PLAN="send: off; --no-sso was given and the pointer holds no --send" fi + elif has_flag --sso "${P_CONSOLE[@]}" && ! has_flag --sso "${RUN_CONSOLE[@]}"; then + # Row 273 (F39 b): a rollback to a Console without --sso drops it, and --send goes with it. + RUN_CONSOLE=("${without_send[@]}") + if [ "$had" = 1 ]; then + SEND_PLAN="send: dropped; the run drops --sso, and --send needs it (F39)" + SEND_STEP="drop --send; the run drops --sso" + else + SEND_PLAN="send: off" + fi elif [ "$SEND" = 1 ]; then if ! has_flag --sso "${RUN_CONSOLE[@]}"; then SEND_PLAN="send: off"