Compare commits

...
Author SHA1 Message Date
jason.woltje cac7d0f67a row 273: T53 test 6 pins the usage line with --no-sso
The usage line gained --no-sso in the proof and sso bracket. T53 test 6
still matched the old bracket and failed the whole run on 978143bf4.
2026-10-21 21:32:11 -05:00
jason.woltje 978143bf42 row 273 notes: name the changed T57 item 3 assertion for review (sage-2321) 2026-10-21 20:34:59 -05:00
jason.woltje 6273b65e9d 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.
2026-10-21 20:34:11 -05:00
jason.woltje 5b5fefc8bc stack-release tests: T58 adds sage-2305 cases a and b (row 273)
(a) --sso on, then two re-pins that carry it, then --no-sso gives back the
arrival's ExecStart byte for byte. (b) every set in the log holds --sso, so
--no-sso refuses sso-off-flags before any unit stops.
2026-10-21 20:24:57 -05:00
jason.woltje 3730567146 stack-release: --no-sso drops --send in one place, send_for_target (row 273) 2026-10-21 20:20:41 -05:00
jason.woltje 7dc538d90a stack-release: activate --send and --no-sso (row 273, #1778)
activate --send turns send on only when the run's Console flags hold
--sso, given or carried (F39 b), and puts it after --sso and before the
tester flags. activate --no-sso turns --sso off with the Console flags
from the set of the activation that turned it on, drops --send (F39 a)
and keeps the pointer's tester. Both are usage errors under --units
external. T58 covers the brief's T1-T4; the external test covers T5.
Design and the set-walk choice: agents/darkwing/work/queue-273/BUILD.md.
2026-10-21 20:19:48 -05:00
4 changed files with 456 additions and 30 deletions
+113
View File
@@ -0,0 +1,113 @@
# Row 273 (#1778): activate --send and --no-sso
Darkwing, 2026-10-22. Brief: `docs/plans/2026-10-22_activate-send-nosso.md` (blob in f0d0b31e9), approved by
Mos in mos-1246 (F39). Base: origin/refactor 0042f713d. Reviewer: Filbert. Gate owner: Sage.
## What changed
Only `scripts/stack-release.sh` and its two node test files.
- `activate --send` puts `--send` into the run's Console flags after `--sso` and before the tester
flags, where webui-service.sh renders it. It refuses `send-needs-sso` unless those flags hold `--sso`,
given or carried. The plan line is `send: on; --send was given`, or `send: carried` when the pointer
already holds it. `check_target` lets it name the pointer's copy only when the pointer holds no `--send`,
so a second `--send` on the same copy refuses `already-active`.
- `activate --no-sso` refuses `sso-not-on` when the pointer holds no `--sso`. Otherwise it takes the
Console flags from a rollback set (below) and keeps the pointer's tester flags. `send_for_target`
drops `--send`, whether it came from the set or the pointer. It is the only place that does, so one
mutant there (M3) shows the drop is tested. The plan prints `sso: off; Console flags from <set>/<file>; restores <flags>`
and a send line, `send: dropped; --no-sso drops --send (F39)` or `send: off; --no-sso was given and the
pointer holds no --send`. The steps log gets `drop --send; --no-sso was given` and
`turn off --sso; the Console flags come from <set>/<file>`.
- Usage errors: `--send` with `--no-send`; `--no-sso` with `--sso`, `--send`, a proof pair or
`--no-proof`; either flag on any verb but activate; either flag under `--units external`.
`--no-sso --no-send` is allowed, since the brief doesn't forbid it and both drop the same flag.
- The set read that `sso_base` did inline for a rollback to a Console without `--sso` moved into
`set_flags`, which both paths call. Its three messages are unchanged, and T57 still pins them.
- `from_log` has a new mode, `sets`: the activation of each ok activate and rollback line, newest first,
stopping at the adopt line, which took no rollback set.
## The consequential choice: which set --no-sso reads
The brief says the flags come from "the rollback set of the activation that turned `--sso` on", and that
"a set whose Console already ran with `--sso`" refuses. Each set holds the Console unit from before its
run. So the activation that turned `--sso` on is the newest one whose set holds a Console without
`--sso`: every activation after it ran with `--sso`, and so did every set it left.
`no_sso_base` walks the log's sets newest first and stops at the first set whose Console has no `--sso`.
It refuses `sso-off-flags` at the first set it can't read (missing, failing its SHA256SUMS, or with no
argv the script can parse), since it can't tell whether that set was the one. If every set ran with
`--sso`, it refuses with `the Console in <set>/<file> already ran with --sso, and the log names no
earlier activation`. A log with no set at all refuses with `the log names no activation with a rollback
set`.
The tradeoff: a later same-copy re-pin that carried `--sso` (a `--no-send`, or a re-pin onto a newer
copy) doesn't break `--no-sso`, which the phase 2 window needs if a re-pin lands between step 5 and a
rollback. The cost is that "a set that ran with `--sso` refuses" now applies only when no older set
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.
- Changed existing assertion, for the review to approve by name: T57 item 3 (sage-1389), "the
plain case", ends with `assert.deepEqual(pointer(g).console,[...noSend(gbase),...pair(f)])`. It
used to expect `'--send'` at the end. The case seeds a legacy `--send` pointer, turns `--sso` on
(which carries `--send`), then rolls back to B, whose Console has no `--sso`. A carried `--send`
that ends without `--sso` breaks F39 b whatever its source, so the old pin asserted the wrong
outcome (Sage accepted this in sage-2321). Row 200's two sso plan strings also lose "--send and"
(`ROLLBACK_PLAN` and the round 3 named-instance case), since `--send` no longer follows the pointer
on that path.
- 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
whole. test-release.sh is release.sh's Docker selftest and doesn't call stack-release.sh, so the cases
live in the node tests and test-release.sh runs unchanged.
## Tests (T58 in stack-release.test.mjs)
- T1: `--sso --send` from B onto C1 with a tester port: plan, pointer, unit and steps. `--send` beside a
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
`--sso` and keeps the pointer's tester port (7334), which differs from the set's (7333). A third
(sage-2305 a) turns `--sso` on, then re-pins twice with `--send` and `--no-send`, so the two newest
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`; 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.
## Mutants
| # | Change | Expected killer |
|---|---|---|
| M1 | `set_flags` skips the SHA256SUMS comparison when `--no-sso` is given | T4 tampered set |
| M2 | `no_sso_base` skips the `sso-not-on` refusal | T4 `sso-not-on` |
| 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.
@@ -66,13 +66,13 @@ async function closed(e,x,a){
const storeSha=(e)=>sha(e.store);
const backupStore=(e,a)=>join(e.data,'backups','stack',a,'bus','bus.sqlite');
test('row 232: confirm, revert, gate, --to and --expect-from need --units external; the user-only flags are usage under it',async(t)=>{
test('row 232: confirm, revert, gate, --to and --expect-from need --units external; the user-only flags are usage under it, and so are row 273\'s --send and --no-sso',async(t)=>{
const {e,x}=await xseed(t);
const logs=logLines(e).length;
for(const a of [['confirm',C1,'acme'],['revert',C1,'acme'],['gate',B,'acme','--since','1'],['rollback','acme','--to',B],['activate',C1,'acme','--plan','--expect-from',B]]){
const r=await run(e,...a);assert.equal(r.code,4,a.join(' '));assert.match(r.err,/^usage: stack-release.sh adopt BUSINESS\n/);
}
for(const a of [['activate',C1,'acme','--pm'],['activate',C1,'acme','--tester-port','7444'],['activate',C1,'acme','--no-proof'],['activate',C1,'acme','--no-send'],['activate',C1,'acme','--sso'],['activate',C1,'acme','--proof-header','X','--proof-file','/p'],['rollback','acme'],['gate',B,'acme'],['activate',C1,'acme','--expect-from',B],['confirm',C1,'acme','--rollback',B],['confirm',C1,'acme','--cause','A5']]){
for(const a of [['activate',C1,'acme','--pm'],['activate',C1,'acme','--tester-port','7444'],['activate',C1,'acme','--no-proof'],['activate',C1,'acme','--no-send'],['activate',C1,'acme','--sso'],['activate',C1,'acme','--send'],['activate',C1,'acme','--no-sso'],['activate',C1,'acme','--proof-header','X','--proof-file','/p'],['rollback','acme'],['gate',B,'acme'],['activate',C1,'acme','--expect-from',B],['confirm',C1,'acme','--rollback',B],['confirm',C1,'acme','--cause','A5']]){
const r=await run(x,...a);assert.equal(r.code,4,a.join(' '));
}
assert.equal(logLines(e).length,logs);
+199 -7
View File
@@ -1258,7 +1258,7 @@ test('T53 row 187: activate sets, carries and drops the proof pair',async(t)=>{
['activate',C1,'acme','--plan','--proof-file',f]]){
const r=await prun(e,...args);assert.equal(r.code,4,args.join(' '));assert.equal(r.out,'');assert.equal(r.err,usageText);
}
assert.match(usageText,/\[--proof-header NAME --proof-file PATH \| --no-proof \| --sso\]/);
assert.match(usageText,/\[--proof-header NAME --proof-file PATH \| --no-proof \| --sso \| --no-sso\]/);
assert.deepEqual(fp(e),before);assert.deepEqual(startsStops(e,n),[]);assert.equal(logLines(e).length,logs);
await noLeak(e);
});
@@ -1417,7 +1417,7 @@ test('T55 row 189: activate --no-send drops --send, and rollback and pm-on carry
['rehearse','acme','--backup','/tmp/x','--port','7400','--no-send']]){
const r=await prun(e,...args);assert.equal(r.code,4,args.join(' '));assert.equal(r.out,'');assert.equal(r.err,usageText);
}
assert.match(usageText,/\| --no-proof \| --sso\] \[--no-send\]/);
assert.match(usageText,/\| --no-proof \| --sso \| --no-sso\] \[--send \| --no-send\]/);
assert.deepEqual(fp(e),before);assert.deepEqual(startsStops(e,n),[]);assert.equal(logLines(e).length,logs);
await noLeak(e);
});
@@ -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.
@@ -1526,7 +1526,7 @@ test('T57 row 200: activate --sso, its refusals before any stop, and the rollbac
['rollback','acme','--sso'],['pm-on','acme','--sso'],['status','acme','--sso'],['adopt','acme','--sso']]){
const r=await prun(e,...args);assert.equal(r.code,4,args.join(' '));assert.equal(r.out,'');assert.equal(r.err,usageText);
}
assert.match(usageText,/\[--proof-header NAME --proof-file PATH \| --no-proof \| --sso\] \[--no-send\]/);
assert.match(usageText,/\[--proof-header NAME --proof-file PATH \| --no-proof \| --sso \| --no-sso\] \[--send \| --no-send\]/);
assert.deepEqual(fp(e),before);assert.deepEqual(startsStops(e,n),[]);assert.equal(logLines(e).length,logs);
await noLeak(e);
});
@@ -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`));
@@ -1695,6 +1695,198 @@ test('T57 row 200: activate --sso, its refusals before any stop, and the rollbac
});
});
// Row 273 (#1778, mos-1246 F39): activate --send turns send on only beside --sso, given or carried, and
// activate --no-sso turns --sso off with the Console flags from the set of the activation that turned
// it on, dropping --send. The tester follows the pointer.
const SEND_MSG="--send needs --sso in the run's Console flags, given or carried; under forward auth any same-uid process could send (F39)";
const NOT_ON_MSG="the pointer's Console flags hold no --sso, so --no-sso has nothing to turn off";
const OFF_MSG=(why)=>`the Console flags from before --sso cannot be read: ${why}`;
const OFF_PLAN=(set,restores)=>`sso: off; Console flags from ${set}/${SHOW}; restores ${restores}`;
const NOTE=(to)=>`note: ${to} is the pointer's copy; the run re-pins it and only the Console flags change`;
test('T58 row 273: activate --send and --no-sso',async(t)=>{
await t.test('T1: --sso --send turns send on after --sso and before the tester port, and the plan says so',async(t)=>{
const e=await mk(t);await seedPreview(e,undefined,['--tester-port','7333']);await ssoConfig(e);
const want=['--port',e.port,'--business','acme','--sso','--send','--tester-port','7333'];
const out=await planOnly(e,0,'activate',C1,'acme','--sso','--send');
assert.deepEqual([...sendLines(out),...ssoLines(out)],['send: on; --send was given',`sso: on; drops ${DROPS}; carries --port ${e.port} --business acme --send --tester-port 7333`]);
assert.equal(out.find((l)=>l.startsWith('Console: ')),`Console: scripts/webui-service.sh install --copy ${C1} --copies ${COPIES} ${want.join(' ')}`);
let r=await prun(e,'activate',C1,'acme','--sso','--send');assert.equal(r.code,0,r.out+r.err);
assert.equal(pointer(e).copy,C1);assert.deepEqual(pointer(e).console,want);
assert.ok(conExec(e).endsWith(` --checkout ${repo} ${want.join(' ')}`),conExec(e));
const steps=stepText(e);
assert.ok(steps.includes('Z turn on --send; --send was given\n'),steps);assert.ok(steps.includes(`Z turn on --sso; drop ${DROPS}\n`),steps);
r=await prun(e,'status','acme');assert.equal(r.code,0,r.err);assert.match(r.out,/unit flags: match the pointer/);
// The same copy with send on again is already active.
await refused(e,['activate',C1,'acme','--send'],'already-active',`${C1} is the pointer's copy`);
await noLeak(e);
});
await t.test('T1: --send beside a carried --sso re-pins the live copy once, and the plan names the carried --sso',async(t)=>{
const e=await mk(t);await seedPreview(e);await ssoConfig(e);
let r=await prun(e,'activate',C1,'acme','--sso');assert.equal(r.code,0,r.out+r.err);
assert.deepEqual(pointer(e).console,['--port',e.port,'--business','acme','--sso']);
const out=await planOnly(e,0,'activate',C1,'acme','--send');
assert.deepEqual([...sendLines(out),...ssoLines(out)],['send: on; --send was given','sso: carried']);
assert.ok(out.includes(NOTE(C1)),out.join('\n'));
r=await prun(e,'activate',C1,'acme','--send');assert.equal(r.code,0,r.out+r.err);
const l=logLines(e).at(-1);assert.deepEqual([l.op,l.from,l.to,l.result],['activate',C1,C1,'ok']);
const want=['--port',e.port,'--business','acme','--sso','--send'];
assert.deepEqual(pointer(e).console,want);assert.ok(conExec(e).endsWith(` --checkout ${repo} ${want.join(' ')}`),conExec(e));
assert.ok(stepText(e).includes('Z turn on --send; --send was given\n'));assert.doesNotMatch(stepText(e),/^\S+Z .*sso/m);
await refused(e,['activate',C1,'acme','--send'],'already-active',`${C1} is the pointer's copy`);
// Another copy carries both.
assert.deepEqual(sendLines(await planOnly(e,0,'activate',C,'acme','--send')),['send: carried']);
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: --send without --sso, given or carried, refuses before any stop, and --plan lists it',async(t)=>{
const e=await mk(t);await seedPreview(e);
await refused(e,['activate',C1,'acme','--send'],'send-needs-sso',SEND_MSG);
await refused(e,['activate',B,'acme','--send'],'send-needs-sso',SEND_MSG);
const out=await planOnly(e,3,'activate',C1,'acme','--send');
assert.deepEqual(sendLines(out),['send: off']);
assert.deepEqual(out.filter((l)=>l.startsWith('refuses ')),[`refuses send-needs-sso: ${SEND_MSG}`]);
// A pointer that already holds --send without --sso still refuses a --send.
const g=await mk(t);await seedPreview(g,undefined,['--send']);
await refused(g,['activate',C1,'acme','--send'],'send-needs-sso',SEND_MSG);
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);
const usageText=(await prun(e,'--help')).err,n=calls(e).length,logs=logLines(e).length,before=fp(e);
for(const args of [['activate',C1,'acme','--send','--no-send'],['activate',C1,'acme','--no-send','--send','--plan'],['activate',C1,'acme','--sso','--send','--no-send'],
['activate',C1,'acme','--no-sso','--sso'],['activate',C1,'acme','--sso','--no-sso','--plan'],['activate',C1,'acme','--no-sso','--send'],
['activate',C1,'acme','--no-sso',...pair(f)],['activate',C1,'acme','--no-sso','--no-proof'],
['rollback','acme','--send'],['rollback','acme','--no-sso'],['pm-on','acme','--send'],['pm-on','acme','--no-sso'],['status','acme','--no-sso'],['adopt','acme','--send']]){
r=await prun(e,...args);assert.equal(r.code,4,args.join(' '));assert.equal(r.out,'');assert.equal(r.err,usageText);
}
assert.match(usageText,/\[--proof-header NAME --proof-file PATH \| --no-proof \| --sso \| --no-sso\] \[--send \| --no-send\]/);
assert.match(usageText,/\n--send needs --sso in the run's Console flags, given or carried\. --no-sso needs --sso in the\n/);
assert.deepEqual(fp(e),before);assert.deepEqual(startsStops(e,n),[]);assert.equal(logLines(e).length,logs);
await noLeak(e);
});
await t.test('T3: --no-sso on the same copy returns the unit and the pointer to the flags from before --sso, without --send',async(t)=>{
const e=await mk(t),base=await seedPreview(e,undefined,['--send','--tester-port','7333']);await ssoConfig(e);
let r=await prun(e,'activate',C1,'acme');assert.equal(r.code,0,r.out+r.err);
const pre=conExec(e),want=noSend(base);
assert.ok(pre.endsWith(` ${base.join(' ')}`),pre);
r=await prun(e,'activate',C1,'acme','--sso');assert.equal(r.code,0,r.out+r.err);
const set=setOf(e,pointer(e).activation);
assert.deepEqual(pointer(e).console,['--port',e.port,'--business','acme','--sso','--send','--tester-port','7333']);
const out=await planOnly(e,0,'activate',C1,'acme','--no-sso');
assert.deepEqual([...proofLines(out),...sendLines(out),...ssoLines(out)],['proof: off','send: dropped; --no-sso drops --send (F39)',OFF_PLAN(set,want.slice(0,-2).join(' '))]);
assert.ok(out.includes(NOTE(C1)),out.join('\n'));
assert.equal(out[out.findIndex((l)=>l.startsWith('sso:'))+1],`Console: scripts/webui-service.sh install --copy ${C1} --copies ${COPIES} ${want.join(' ')}`);
r=await prun(e,'activate',C1,'acme','--no-sso');assert.equal(r.code,0,r.out+r.err);
const l=logLines(e).at(-1);assert.deepEqual([l.op,l.from,l.to,l.result],['activate',C1,C1,'ok']);
assert.equal(pointer(e).copy,C1);assert.deepEqual(pointer(e).console,want);
assert.equal(conExec(e),pre.replace(' --send --tester-port 7333',' --tester-port 7333'));
const steps=stepText(e);
assert.ok(steps.includes('Z drop --send; --no-sso was given\n'),steps);
assert.ok(steps.includes(`Z turn off --sso; the Console flags come from ${set}/${SHOW}\n`),steps);
r=await prun(e,'status','acme');assert.equal(r.code,0,r.err);assert.match(r.out,/unit flags: match the pointer/);
// --sso is off now, so a second --no-sso has nothing to turn off.
await refused(e,['activate',C1,'acme','--no-sso'],'sso-not-on',NOT_ON_MSG);
await noLeak(e);
});
await t.test('T3: the set walk passes later re-pins that ran with --sso, and the tester follows the pointer',async(t)=>{
const e=await mk(t),base=await seedPreview(e,undefined,['--send','--tester-port','7333']);await ssoConfig(e);
// The activation that turns --sso on also moves the tester to 7334, so the set and the pointer differ.
let r=await prun(e,'activate',C1,'acme','--sso','--tester-port','7334');assert.equal(r.code,0,r.out+r.err);
const set=setOf(e,pointer(e).activation);
r=await prun(e,'activate',C1,'acme','--no-send');assert.equal(r.code,0,r.out+r.err);
assert.match(readFileSync(join(setOf(e,pointer(e).activation),SHOW),'utf8'),/ --sso /);
assert.deepEqual(pointer(e).console,['--port',e.port,'--business','acme','--sso','--tester-port','7334']);
const restores=base.slice(0,-3),want=[...restores,'--tester-port','7334'];
const out=await planOnly(e,0,'activate',C1,'acme','--no-sso');
assert.deepEqual([...sendLines(out),...ssoLines(out)],['send: off; --no-sso was given and the pointer holds no --send',OFF_PLAN(set,restores.join(' '))]);
r=await prun(e,'activate',C1,'acme','--no-sso');assert.equal(r.code,0,r.out+r.err);
assert.deepEqual(pointer(e).console,want);assert.ok(conExec(e).endsWith(` --checkout ${repo} ${want.join(' ')}`),conExec(e));
assert.doesNotMatch(stepText(e),/drop --send/);
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('T3 (sage-2305 a): after --sso, two later re-pins that carry it, then --no-sso restores the flags from before --sso byte for byte',async(t)=>{
const e=await mk(t),base=await seedPreview(e,undefined,['--tester-port','7333']);await ssoConfig(e);
let r=await prun(e,'activate',C1,'acme');assert.equal(r.code,0,r.out+r.err);
const pre=conExec(e);assert.ok(pre.endsWith(` ${base.join(' ')}`),pre);
r=await prun(e,'activate',C1,'acme','--sso');assert.equal(r.code,0,r.out+r.err);
const set=setOf(e,pointer(e).activation);
// Two same-copy re-pins carry --sso, so the two newest sets both hold a Console with it.
r=await prun(e,'activate',C1,'acme','--send');assert.equal(r.code,0,r.out+r.err);
r=await prun(e,'activate',C1,'acme','--no-send');assert.equal(r.code,0,r.out+r.err);
const acts=logLines(e).filter((l)=>l.result==='ok'&&l.op==='activate').map((l)=>l.activation).slice(-2);
for(const a of acts)assert.match(readFileSync(join(setOf(e,a),SHOW),'utf8'),/ --sso /);
const out=await planOnly(e,0,'activate',C1,'acme','--no-sso');
assert.deepEqual(ssoLines(out),[OFF_PLAN(set,base.slice(0,-2).join(' '))]);
r=await prun(e,'activate',C1,'acme','--no-sso');assert.equal(r.code,0,r.out+r.err);
assert.deepEqual(pointer(e).console,base);assert.equal(conExec(e),pre);
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('T4 (sage-2305 b): a log in which every set holds a Console with --sso refuses before any unit stops',async(t)=>{
const e=await mk(t);await seedPreview(e,undefined,['--send']);await ssoConfig(e);
let r=await prun(e,'activate',C1,'acme','--sso');assert.equal(r.code,0,r.out+r.err);
const first=setOf(e,pointer(e).activation);
r=await prun(e,'activate',C1,'acme','--no-send');assert.equal(r.code,0,r.out+r.err);
assert.match(readFileSync(join(setOf(e,pointer(e).activation),SHOW),'utf8'),/ --sso /);
// The set of the activation that turned --sso on now claims its Console ran with it too, sums intact.
setEdit(first,SHOW,' --public-host ',' --sso --public-host ');
const why=OFF_MSG(`the Console in ${join(first,SHOW)} already ran with --sso, and the log names no earlier activation`);
await refused(e,['activate',C1,'acme','--no-sso'],'sso-off-flags',why);
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, 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);
assert.deepEqual((await planOnly(e,3,'activate',C1,'acme','--no-sso')).filter((l)=>l.startsWith('refuses ')),[`refuses sso-not-on: ${NOT_ON_MSG}`]);
// One set, from the activation that turned --sso on.
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`)}`]);
setEdit(set,SHOW,' --allow-user mallory',' --sso --allow-user jason');
await refused(e,['activate',C1,'acme','--no-sso'],'sso-off-flags',OFF_MSG(`the Console in ${show} already ran with --sso, and the log names no earlier activation`));
setEdit(set,SHOW,' serve ',' ');
await refused(e,['activate',C1,'acme','--no-sso'],'sso-off-flags',OFF_MSG(`${show} holds no Console argv this script can read`));
rmSync(show);
await refused(e,['activate',C1,'acme','--no-sso'],'sso-off-flags',OFF_MSG(`${set} has no ${SHOW} and SHA256SUMS`));
writeFileSync(show,text);setEdit(set,SHOW,'\n','\n');
r=await prun(e,'activate',C1,'acme','--no-sso');assert.equal(r.code,0,r.out+r.err);
assert.doesNotMatch(conExec(e),/ --sso( |$)| --send( |$)/);
// A pointer with --sso and only the adopt line: no activation took a set.
const g=await mk(t);await seedPreview(g,undefined,['--sso']);
await refused(g,['activate',C1,'acme','--no-sso'],'sso-off-flags',OFF_MSG('the log names no activation with a rollback set'));
for(const x of [e,g])await noLeak(x);
});
});
// --- rehearse (D2, row 163; the brief's "Stack rehearsal on a scratch root") ---
// Copies of their own in RCOPIES: the real scripts/mosaic, config loader, business loader, roles and
// mosaic-config.mjs, with stand-ins for the bus host (cli.mjs), the Console, the Store and
+142 -21
View File
@@ -5,6 +5,8 @@
# Console tester listener flags follow target support (row 176, #1674); so does the row 178 proof
# pair, which activate sets or drops (row 187, #1685). activate --no-send drops --send (row 189, #1687).
# activate --sso swaps the preview set and the pair for the Console's own sign-in (row 200, #1698).
# activate --send turns send on, only beside --sso, and activate --no-sso turns --sso off with the
# Console flags from before it, dropping --send (row 273, #1778, mos-1246 F39).
# Design and tests: agents/darkwing/work/queue-162/BUILD.md.
#
# Who runs it. Decision 96 (Mos, mos-0716) lets this script stop and start the live units under
@@ -38,14 +40,14 @@ COPIES="${XDG_DATA_HOME:-$HOME/.local/share}/mosaic-stack/copies"
BIN="$HOME/.npm-global/bin"
UNIT_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
TIMEOUT=60 WANT_PM=0 AFTER_HARNESS=0 MIGRATE=0 PLAN=0 RESTORE="" EVENTS="" TESTER_PORT="" TESTER_SOCKET=0 EXPECTED_INTERPRETER=""
PROOF_HEADER="" PROOF_FILE="" NO_PROOF=0 NO_SEND=0 SSO=0 R_BACKUP="" R_PORT=""
PROOF_HEADER="" PROOF_FILE="" NO_PROOF=0 NO_SEND=0 SEND=0 SSO=0 NO_SSO=0 R_BACKUP="" R_PORT=""
UNITS=user TO_FLAG="" EXPECT_FROM="" SINCE="" ROLLBACK_OLD="" CAUSE=""
usage() {
cat >&2 <<'USAGE'
usage: stack-release.sh adopt BUSINESS
stack-release.sh activate ID BUSINESS [--pm | --pm-after-harness] [--migrate] [--tester-socket | --tester-port N (legacy copies)]
[--proof-header NAME --proof-file PATH | --no-proof | --sso] [--no-send] [--timeout S] [--plan]
[--proof-header NAME --proof-file PATH | --no-proof | --sso | --no-sso] [--send | --no-send] [--timeout S] [--plan]
stack-release.sh pm-on BUSINESS [--timeout S]
stack-release.sh rollback BUSINESS [--restore ACTIVATION [--events N]] [--timeout S] [--plan]
stack-release.sh status BUSINESS
@@ -61,7 +63,9 @@ every table in COUNTED.
--units external (row 232) reads root's system units and starts or stops none. adopt, activate,
rollback, status, confirm, revert and gate take it; rollback then needs --to ID, the previous copy.
activate and rollback --plan take --expect-from OLD. --pm, --pm-after-harness, --tester-port, the
proof flags, --no-proof, --no-send, --sso and pm-on are not for it.
proof flags, --no-proof, --send, --no-send, --sso, --no-sso and pm-on are not for it.
--send needs --sso in the run's Console flags, given or carried. --no-sso needs --sso in the
pointer's, and takes the Console flags from the rollback set of the activation that turned it on.
USAGE
exit "${1:-4}"
}
@@ -128,7 +132,9 @@ parse_opts() {
--proof-file) [ "$OP" = activate ] && [ $# -ge 2 ] && [ -n "$2" ] || usage; PROOF_FILE="$2"; shift 2 ;;
--no-proof) [ "$OP" = activate ] || usage; NO_PROOF=1; shift ;;
--no-send) [ "$OP" = activate ] || usage; NO_SEND=1; shift ;;
--send) [ "$OP" = activate ] || usage; SEND=1; shift ;;
--sso) [ "$OP" = activate ] || usage; SSO=1; shift ;;
--no-sso) [ "$OP" = activate ] || usage; NO_SSO=1; shift ;;
--migrate) [ "$OP" = activate ] || usage; MIGRATE=1; shift ;;
--restore) [ "$OP" = rollback ] && [ $# -ge 2 ] && [[ "$2" =~ ^[0-9]{8}T[0-9]{6}Z-[0-9a-f]{12}(-[1-9])?$ ]] || usage; RESTORE="$2"; shift 2 ;;
--backup) [ "$OP" = rehearse ] && [ $# -ge 2 ] && { [ "$2" = last ] || [[ "$2" =~ ^[0-9]{8}T[0-9]{6}Z-[0-9a-f]{12}(-[1-9])?$ ]] || [[ "$2" =~ ^/[A-Za-z0-9/._+-]*$ ]]; } || usage; R_BACKUP="$2"; shift 2 ;;
@@ -150,7 +156,8 @@ parse_opts() {
[ "$OP" != confirm ] && [ "$OP" != revert ] && [ "$OP" != gate ] && [ -z "$TO_FLAG" ] && [ -z "$EXPECT_FROM" ] || usage
else
[ "$OP" != rollback ] || [ -n "$TO_FLAG" ] || usage
[ "$TESTER_SOCKET" = 0 ] && [ "$WANT_PM" = 0 ] && [ -z "$TESTER_PORT" ] && [ -z "$PROOF_HEADER" ] && [ -z "$PROOF_FILE" ] && [ "$NO_PROOF" = 0 ] && [ "$NO_SEND" = 0 ] && [ "$SSO" = 0 ] || usage
[ "$TESTER_SOCKET" = 0 ] && [ "$WANT_PM" = 0 ] && [ -z "$TESTER_PORT" ] && [ -z "$PROOF_HEADER" ] && [ -z "$PROOF_FILE" ] && [ "$NO_PROOF" = 0 ] && [ "$NO_SEND" = 0 ] && [ "$SSO" = 0 ] &&
[ "$SEND" = 0 ] && [ "$NO_SSO" = 0 ] || usage
fi
[ "$OP" != gate ] || [ -n "$SINCE" ] || usage
[ -z "$EXPECT_FROM" ] || [ "$PLAN" = 1 ] || usage
@@ -167,6 +174,10 @@ parse_opts() {
[ "$NO_PROOF" = 0 ] || [ -z "$PROOF_HEADER" ] || usage
# --sso drops the pair itself, so it takes neither a pair nor --no-proof (row 200).
[ "$SSO" = 0 ] || { [ -z "$PROOF_HEADER" ] && [ "$NO_PROOF" = 0 ]; } || usage
# Row 273: --send and --no-send contradict. --no-sso takes its Console flags from a rollback set
# and drops --send, so it goes with no --sso, --send, pair or --no-proof.
[ "$SEND" = 0 ] || [ "$NO_SEND" = 0 ] || usage
[ "$NO_SSO" = 0 ] || { [ "$SSO" = 0 ] && [ "$SEND" = 0 ] && [ -z "$PROOF_HEADER" ] && [ "$NO_PROOF" = 0 ]; } || usage
local d
for d in "$COPIES" "$BIN"; do
case "$d" in /*) ;; *) usage ;; esac
@@ -225,7 +236,8 @@ for(const k of open.keys())process.stdout.write(k+"\n");' -- "$LOG"
# line that made the pointer's copy live, the backup a rollback would restore. `newest` prints the
# backup field of the newest line that has one, rehearse's --backup last (row 233). `arrival` prints the
# activation and `from` of the line that moved the stack onto the pointer's copy from another one,
# past any same-copy re-pin after it (row 200, sage-1389).
# past any same-copy re-pin after it (row 200, sage-1389). `sets` prints the activation of each
# activate and rollback, newest first, back to the adopt line, whose run took no rollback set (row 273).
from_log() {
[ -f "$LOG" ] || return 1
node -e '
@@ -234,12 +246,13 @@ const lines=require("fs").readFileSync(file,"utf8").split("\n").filter((l)=>l.tr
for(const l of lines.reverse()){
let o;try{o=JSON.parse(l)}catch{continue}
if(o.result!=="ok"||!["adopt","activate","rollback"].includes(o.op)||!o.to)continue;
if(mode==="sets"){if(o.op==="adopt")process.exit(0);process.stdout.write(`${o.activation??""}\n`);continue}
if(mode==="previous"&&o.to!==current){process.stdout.write(o.to+"\n");process.exit(0)}
if(mode==="backup"&&o.to===current){if(o.backup)process.stdout.write(o.backup+"\n");process.exit(o.backup?0:1)}
if(mode==="newest"&&o.backup){process.stdout.write(o.backup+"\n");process.exit(0)}
if(mode==="arrival"&&o.to===current&&o.from!==current){process.stdout.write(`${o.activation??""} ${o.from??""}\n`);process.exit(0)}
}
process.exit(1);' -- "$LOG" "$1" "$P_COPY"
process.exit(mode==="sets"?0:1);' -- "$LOG" "$1" "$P_COPY"
}
# --- Reading the units ---
@@ -323,15 +336,18 @@ in_chain() { local v; for v in "${T_CHAIN[@]}"; do [ "$v" != "$1" ] || return 0;
# The item 3 checks on the target copy, in order: copy, schema, already-active. An activate that
# sets a proof pair, drops the pointer's pair with --no-proof, drops the pointer's --send with
# --no-send, or turns on --sso, may name the pointer's copy: it re-pins the live copy with new
# Console flags (row 187, Part B's B3 and B-R1; row 189, mos-0972; row 200, the phase 2 window).
# --no-send, turns on --sso, turns on --send, or turns off --sso may name the pointer's copy: it
# re-pins the live copy with new Console flags (row 187, Part B's B3 and B-R1; row 189, mos-0972;
# row 200, the phase 2 window; row 273). --no-sso on a pointer without --sso gets past here and
# refuses sso-not-on in no_sso_base, which names the reason.
check_target() {
copy_exists "$TO" 2>/dev/null || refuse copy-missing "no copy $TO in $COPIES"
copy_ok "$TO" 2>/dev/null || refuse copy-unreachable "$TO is not reachable from refactor and origin/refactor"
target_schema "$TO" || refuse copy-schema-unknown "cannot read SCHEMA_VERSION and its chain from $COPIES/$TO/packages/bus/src/store.mjs"
[ "$TO" != "$P_COPY" ] || [ -n "$PROOF_HEADER" ] ||
[ "$TO" != "$P_COPY" ] || [ -n "$PROOF_HEADER" ] || [ "$NO_SSO" = 1 ] ||
{ [ "$NO_PROOF" = 1 ] && [[ " ${P_CONSOLE[*]} " == *" --proof-header "* ]]; } ||
{ [ "$NO_SEND" = 1 ] && [[ " ${P_CONSOLE[*]} " == *" --send "* ]]; } ||
{ [ "$SEND" = 1 ] && [[ " ${P_CONSOLE[*]} " != *" --send "* ]]; } ||
{ [ "$SSO" = 1 ] && [[ " ${P_CONSOLE[*]} " != *" --sso "* ]]; } ||
refuse already-active "$TO is the pointer's copy"
}
@@ -854,21 +870,17 @@ has_flag() { local f="$1"; shift; [[ " $* " == *" $f "* ]]; }
# skips sso_checks after a refusal here, so --plan lists that refusal alone.
sso_base() {
BASE_CONSOLE=("${P_CONSOLE[@]}") SSO_PLAN="" SSO_STEP="" SSO_NO_TARGET=""
if [ "$NO_SSO" = 1 ]; then no_sso_base; return 0; fi
has_flag --sso "${P_CONSOLE[@]}" || return 0
[ "$SSO" = 1 ] || SSO_PLAN="sso: carried"
[ "$OP" = rollback ] && ! sso_supported || return 0
local f="show-$CONSOLE_UNIT.txt" argv="" why="" act="" from="" set
local f="show-$CONSOLE_UNIT.txt" why="" act="" from="" set
read -r act from <<< "$(from_log arrival || true)"
set="$STATE/stack-rollback/$act"
if [ "$from" != "$TO" ]; then why="the log names no activation that moved the stack from $TO onto $P_COPY"
elif ! [[ "$act" =~ ^[0-9]{8}T[0-9]{6}Z-[0-9a-f]{12}(-[1-9])?$ ]]; then why="the activation $act that moved the stack onto $P_COPY names no rollback set"
elif [ ! -f "$set/$f" ] || [ ! -f "$set/SHA256SUMS" ]; then why="$set has no $f and SHA256SUMS"
elif [ "$(sum_of "$set" "$f")" != "$(sha256sum < "$set/$f" | cut -d' ' -f1)" ]; then why="$set/$f does not match the set's SHA256SUMS"
else
argv="$(argv_of "$(cat "$set/$f")")"
if ! unquoted "$argv" || ! serve_flags "$argv"; then why="$set/$f holds no Console argv this script can read"
elif has_flag --sso "${SERVE_FLAGS[@]}"; then why="the Console in $set/$f already ran with --sso"
fi
elif ! set_flags "$set"; then why="$SET_WHY"
elif has_flag --sso "${SERVE_FLAGS[@]}"; then why="the Console in $set/$f already ran with --sso"
fi
if [ -n "$why" ]; then
SSO_NO_TARGET=1
@@ -876,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
@@ -894,10 +907,79 @@ 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"
}
# Sets SERVE_FLAGS to the Console flags in rollback set $1's show-$CONSOLE_UNIT.txt, the Console
# unit as it was before that set's run, checked against the set's SHA256SUMS. Returns 1 with
# SET_WHY when the file is missing, fails its sum or holds no argv this script can read.
set_flags() {
local set="$1" f="show-$CONSOLE_UNIT.txt" argv
SET_WHY=""
if [ ! -f "$set/$f" ] || [ ! -f "$set/SHA256SUMS" ]; then SET_WHY="$set has no $f and SHA256SUMS"
elif [ "$(sum_of "$set" "$f")" != "$(sha256sum < "$set/$f" | cut -d' ' -f1)" ]; then SET_WHY="$set/$f does not match the set's SHA256SUMS"
else
argv="$(argv_of "$(cat "$set/$f")")"
unquoted "$argv" && serve_flags "$argv" && return 0
SET_WHY="$set/$f holds no Console argv this script can read"
fi
return 1
}
# Row 273 (#1778, mos-1246 F39 a): activate --no-sso turns --sso off. The Console flags come from the
# rollback set of the activation that turned --sso on. Each set holds the Console from before its
# run, so that activation is the newest one whose set holds a Console without --sso: every newer
# set ran with it. The walk reads the log's activations newest first, back to the adopt line, and
# refuses at the first set it can't read, since it can't tell whether that set is the one. --send
# 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
# 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
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"
fi
fi
if [ -n "$why" ]; then
SSO_NO_TARGET=1
check_refuse sso-off-flags "the Console flags from before --sso cannot be read: $why"
return 0
fi
# A --send in the set stays here: send_for_target is the one place that drops it, so the
# restores list leaves it out.
BASE_CONSOLE=()
for ((i = 0; i < ${#SERVE_FLAGS[@]}; i++)); do
case "${SERVE_FLAGS[i]}" in
--tester-port) i=$((i + 1)) ;;
--tester-socket) ;;
*) BASE_CONSOLE+=("${SERVE_FLAGS[i]}")
[ "${SERVE_FLAGS[i]}" = --send ] || restored+="${restored:+ }${SERVE_FLAGS[i]}" ;;
esac
done
for ((i = 0; i < ${#P_CONSOLE[@]}; i++)); do
case "${P_CONSOLE[i]}" in
--tester-port) i=$((i + 1)); BASE_CONSOLE+=(--tester-port "${P_CONSOLE[i]:-}") ;;
--tester-socket) BASE_CONSOLE+=(--tester-socket) ;;
esac
done
SSO_PLAN="sso: off; Console flags from $set/$f; restores ${restored:-nothing}"
SSO_STEP="turn off --sso; the Console flags come from $set/$f"
}
# Row 200 (#1698): activate --sso drops the preview set and the proof pair, which serve refuses beside
# it, and puts --sso where webui-service.sh renders it: after the other flags, before --send and
# --tester-port. Sets SSO_DROP (the plan's dropped preview flags), PROOF_PLAN and PROOF_STEP.
@@ -1017,14 +1099,53 @@ process.exit(d!==null&&typeof d==="object"&&Object.hasOwn(d,"console")?0:1)' --
# Row 189 (mos-0972): --no-send drops --send from RUN_CONSOLE, so the unit and the pointer both lose
# it; without the flag the pointer's --send goes on unchanged. Rollback and pm-on never take it, so
# 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. --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=()
local -a without_send=() with_send=()
for ((i = 0; i < ${#RUN_CONSOLE[@]}; i++)); do
if [ "${RUN_CONSOLE[i]}" = --send ]; then had=1; else without_send+=("${RUN_CONSOLE[i]}"); fi
done
SEND_STEP=""
if [ "$NO_SEND" = 1 ]; then
if [ "$NO_SSO" = 1 ]; then
RUN_CONSOLE=("${without_send[@]}")
if has_flag --send "${P_CONSOLE[@]}"; then
SEND_PLAN="send: dropped; --no-sso drops --send (F39)"
SEND_STEP="drop --send; --no-sso was given"
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"
check_refuse send-needs-sso "--send needs --sso in the run's Console flags, given or carried; under forward auth any same-uid process could send (F39)"
return 0
fi
if [ "$had" = 1 ]; then
SEND_PLAN="send: carried"
return 0
fi
# webui-service.sh renders --send after --sso, before the tester flags.
for ((i = 0; i < ${#RUN_CONSOLE[@]}; i++)); do
case "${RUN_CONSOLE[i]}" in --tester-port | --tester-socket) break ;; esac
with_send+=("${RUN_CONSOLE[i]}")
done
RUN_CONSOLE=("${with_send[@]}" --send "${RUN_CONSOLE[@]:i}")
SEND_PLAN="send: on; --send was given"
SEND_STEP="turn on --send; --send was given"
elif [ "$NO_SEND" = 1 ]; then
RUN_CONSOLE=("${without_send[@]}")
if [ "$had" = 1 ]; then
SEND_PLAN="send: dropped; --no-send was given"