fix(upgrade): refuse legacy library --send before units stop (#385)

This commit is contained in:
jason.woltje committed 2026-11-02 12:03:59 -06:00
1 parent ab2bc23c7f
commit d0ddfa8de2
7 files changed
+84 -3

No files matched your search

+1
View File
@@ -5819,3 +5819,4 @@ Before: agents can read the library; supporting-path and push-credential depende
2026-10-29T03:27:31Z | Codex implementation sub-agent | #385 OIDC-only writer preparation on exact base 09a847edaa25b609bac7728ff83f224708a29399 | Added fail-closed CLI, programmatic and service-render refusals for `--send` without `--sso`; retained read-only header preview/proof; migrated inventoried writer tests to real in-process OIDC login/callback sessions and updated docs. Focused regression 5/5 and selected service cases 4/4 pass; syntax and diff checks pass. The broader writer suite is blocked before execution because the clean checkout has no `node_modules` and exact pinned `[email protected]` is unavailable; no install, commit, push, PR, queue move or green/completion claim. 2026-10-29T03:27:31Z | Codex implementation sub-agent | #385 OIDC-only writer preparation on exact base 09a847edaa25b609bac7728ff83f224708a29399 | Added fail-closed CLI, programmatic and service-render refusals for `--send` without `--sso`; retained read-only header preview/proof; migrated inventoried writer tests to real in-process OIDC login/callback sessions and updated docs. Focused regression 5/5 and selected service cases 4/4 pass; syntax and diff checks pass. The broader writer suite is blocked before execution because the clean checkout has no `node_modules` and exact pinned `[email protected]` is unavailable; no install, commit, push, PR, queue move or green/completion claim.
2026-10-29T05:49:53Z | Codex Daybreak Blue xhigh implementation sub-agent | CORRECTION and Mos2255 #385 review repair R2 | The 03:27:31Z dependency/no-commit statements are historical: exact pinned `[email protected]` is now materialized through the ignored `node_modules` fixture with no install, and original preparation is committed at `9936a1be1a37fdb30ba53cf2f14b7897225c3e4a`. On that unchanged HEAD, uncommitted R2 source/tests/docs make Case 2 library preview read-only until #412, drop inherited non-SSO `--send` during release planning, and refuse duplicate service `--send`. Focused RED was 0/2, 0/1, 0/1; focused GREEN and selected T53/T55/T57/T58 fixture blocks pass. No root/native/live/full-suite/CI action, commit, push, PR/API, merge or frozen/live-pin change. 2026-10-29T05:49:53Z | Codex Daybreak Blue xhigh implementation sub-agent | CORRECTION and Mos2255 #385 review repair R2 | The 03:27:31Z dependency/no-commit statements are historical: exact pinned `[email protected]` is now materialized through the ignored `node_modules` fixture with no install, and original preparation is committed at `9936a1be1a37fdb30ba53cf2f14b7897225c3e4a`. On that unchanged HEAD, uncommitted R2 source/tests/docs make Case 2 library preview read-only until #412, drop inherited non-SSO `--send` during release planning, and refuse duplicate service `--send`. Focused RED was 0/2, 0/1, 0/1; focused GREEN and selected T53/T55/T57/T58 fixture blocks pass. No root/native/live/full-suite/CI action, commit, push, PR/API, merge or frozen/live-pin change.
2026-11-02T18:03:53Z | Claude Opus 5.5 implementation sub-agent (lead T3 c5c1e8ba) | #385 PR 1836 repair R3 on ab2bc23c7 | R2-F1: `scripts/stack-upgrade.sh` `read_args` now scans the serve argv after `--` and refuses `args-unreadable` before anything stops when the profile is library (no `--profile`, or the last one says library, as `case2-unit.sh` reads it) and `--send` is present; the message names the remediation, and the args file is never rewritten. `do_upgrade` and `do_rollback` both call `read_args` first, so rollback is covered; factory and tester paths are unchanged. RED on ab2bc23c7: new stack-upgrade case 0/1 (exit 2 rollback-failed, A4 and render-old failed, both units stopped), controls 1/1; GREEN 2/2. R2-I1: one component case runs the real `sso_for_target` then `send_for_target` on a legacy preview `--send` pointer, carried after `--sso` and before `--tester-socket`. R2-I2: TOOLS.md says a same-copy plain activate on a legacy pointer refuses already-active and needs `--no-send`. R2-I3: no code change; `uninstall`/`status` with `--send --send` exit 1, not 4, fail-closed either way. host-run-sheet section 7 gains the migration step. Whole files, one at a time: stack-upgrade 23/23 (69 s), stack-release 225/225 (608 s; a first run was cut by my 600 s background limit at 221 passing, its orphan fixture holder killed), case2-unit 48/48 (73 s). No root, systemd, container, network, push, PR or live action.
+1
View File
@@ -1881,3 +1881,4 @@ are never rewritten or removed; corrections are new entries.
2026-10-26 | Sage | Mos1909/1915/1917 real postlanding reviews | Boyd/Dewey/Filbert/Gladstone real authored candidate52ba verdictfiles independently byte/hash-verified, allAPPROVE scoped; Dewey656broaderPARTIAL retained. Formal330R1/331R4 requests failedpre-send/no comments or approvals; no borrowed/copied/mintedkeys. LegacyhelperGETname jason.woltje is NOT ownJarvis, ownslotabsent; rows tracking-open/#436, files cited via normalqueue. NewFilbertNEW-330-01 filedtask665 exactreadback, notlandingreopen/gate. Repair646-648/652-656 remainOPEN untilactualcomments;1917 conditionalbacklog routing retained. #662scope1338posted;660PR405REVISE/notready. Product1968inputs unchanged; no source/helper/test/gate edits/runs; peerjournal prefixes preserved. 2026-10-26 | Sage | Mos1909/1915/1917 real postlanding reviews | Boyd/Dewey/Filbert/Gladstone real authored candidate52ba verdictfiles independently byte/hash-verified, allAPPROVE scoped; Dewey656broaderPARTIAL retained. Formal330R1/331R4 requests failedpre-send/no comments or approvals; no borrowed/copied/mintedkeys. LegacyhelperGETname jason.woltje is NOT ownJarvis, ownslotabsent; rows tracking-open/#436, files cited via normalqueue. NewFilbertNEW-330-01 filedtask665 exactreadback, notlandingreopen/gate. Repair646-648/652-656 remainOPEN untilactualcomments;1917 conditionalbacklog routing retained. #662scope1338posted;660PR405REVISE/notready. Product1968inputs unchanged; no source/helper/test/gate edits/runs; peerjournal prefixes preserved.
2026-10-29T03:27:31Z | Codex Daybreak Blue xhigh implementation sub-agent | #385 OIDC-only Console writes, branch work/vikunja-19-385-q284-complete at exact base 09a847edaa25b609bac7728ff83f224708a29399 | Source/test/docs preparation complete for review; focused refusal 5/5 and selected service 4/4 pass, broader real-OIDC writer tests blocked by absent pinned oauth4webapi@3.8.8; no install/commit/push/PR/queue move or overall-green claim. 2026-10-29T03:27:31Z | Codex Daybreak Blue xhigh implementation sub-agent | #385 OIDC-only Console writes, branch work/vikunja-19-385-q284-complete at exact base 09a847edaa25b609bac7728ff83f224708a29399 | Source/test/docs preparation complete for review; focused refusal 5/5 and selected service 4/4 pass, broader real-OIDC writer tests blocked by absent pinned oauth4webapi@3.8.8; no install/commit/push/PR/queue move or overall-green claim.
2026-10-29T05:49:53Z | Codex Daybreak Blue xhigh implementation sub-agent | Mos2255 #385 review repair R2 on work/vikunja-19-385-q284-complete at unchanged HEAD 9936a1be1a37fdb30ba53cf2f14b7897225c3e4a/base 09a847edaa25b609bac7728ff83f224708a29399 | Corrects the 03:27:31Z historical blocker: pinned oauth4webapi3.8.8 fixture is now present without install and original prep is committed. F1/F2/F3 repaired in uncommitted source/tests/docs; F4/F7 disclosed only, F6 append conflicts preserved. Three focused test files only: intentional RED captured, GREEN controls plus T53 12/12, T55 8/8, T57 13/13 and T58 11/11 pass. No root/native/live/full-suite/CI, commit/push/PR/API/merge, frozen packet or live-pin action. 2026-10-29T05:49:53Z | Codex Daybreak Blue xhigh implementation sub-agent | Mos2255 #385 review repair R2 on work/vikunja-19-385-q284-complete at unchanged HEAD 9936a1be1a37fdb30ba53cf2f14b7897225c3e4a/base 09a847edaa25b609bac7728ff83f224708a29399 | Corrects the 03:27:31Z historical blocker: pinned oauth4webapi3.8.8 fixture is now present without install and original prep is committed. F1/F2/F3 repaired in uncommitted source/tests/docs; F4/F7 disclosed only, F6 append conflicts preserved. Three focused test files only: intentional RED captured, GREEN controls plus T53 12/12, T55 8/8, T57 13/13 and T58 11/11 pass. No root/native/live/full-suite/CI, commit/push/PR/API/merge, frozen packet or live-pin action.
2026-11-02T18:03:53Z | Claude Opus 5.5 implementation sub-agent (lead T3 c5c1e8ba) | #385 PR 1836 repair R3 on work/vikunja-19-385-q284-complete at ab2bc23c7 | R2-F1 fixed: stack-upgrade refuses a library args file holding `--send` after `--` before any unit stops, for upgrade and rollback; R2-I1 component case and R2-I2 TOOLS.md precision added, R2-I3 noted. RED 0/1 then GREEN 2/2; stack-upgrade 23/23, stack-release 225/225, case2-unit 48/48. One local commit; no push, PR, Vikunja, Gitea or live action.
+3 -1
View File
@@ -494,7 +494,9 @@ every table in COUNTED.
pointer's, and takes the Console flags from the rollback set of the activation that turned it on. pointer's, and takes the Console flags from the rollback set of the activation that turned it on.
--no-sso drops the pointer's --send, and so does a rollback to a Console without --sso. --no-sso drops the pointer's --send, and so does a rollback to a Console without --sso.
An inherited legacy --send beside header-preview flags is dropped during planning because preview is An inherited legacy --send beside header-preview flags is dropped during planning because preview is
read-only; it never reaches the service installer. read-only; it never reaches the service installer. The drop alone is not a change: an activate of the
pointer's own copy refuses already-active unless another flag changes the Console (check_target's
list), so on a legacy pointer pass --no-send there.
``` ```
This is an operator plan, not an action agents run. Both reviews and the This is an operator plan, not an action agents run. Both reviews and the
+15 -1
View File
@@ -385,7 +385,21 @@ but omits `--send`. That Console is read-only until Case 2 gains real OIDC in #4
explicitly refuses `--send`. Proxy preview and proof-protected reads remain available. The `factory` explicitly refuses `--send`. Proxy preview and proof-protected reads remain available. The `factory`
profile and its private tester socket are unchanged. profile and its private tester socket are unchanged.
Precheck line: none. `stack-upgrade.sh` refuses `args-unreadable` on any of these rules. A library host installed before #385 has a `--send` line after `--`, because the earlier
`case2-unit.sh` required it. `stack-upgrade.sh upgrade` and `rollback` read the args file before
anything stops, and when the profile is `library` (no `--profile` line, or the last one says
`library`) and `--send` appears after `--`, they refuse `args-unreadable` with both units still
running. The script never rewrites the file. To migrate, as root:
1. Open `/etc/mosaic-stack/dev.args` and delete the one line that reads `--send`. Change nothing else.
2. Check it: `stat -c '%U %a' /etc/mosaic-stack/dev.args` still gives `root` and a mode with no group or
other write, and `grep -cx -- --send /etc/mosaic-stack/dev.args` prints `0`.
3. Rerun the same `stack-upgrade.sh` command.
The Console stays read-only after the upgrade, until #412 (#385).
Precheck line: none. `stack-upgrade.sh` refuses `args-unreadable` on any of these rules, before it stops
a unit.
## 8. Enrollment, once per host ## 8. Enrollment, once per host
@@ -1363,6 +1363,7 @@ test('Mos2255 source component: legacy preview send drops while explicit and SSO
exact('check_refuse() {','\n\n# Item 1 and item 4'), exact('check_refuse() {','\n\n# Item 1 and item 4'),
exact('has_flag() {','\n\n# Row 200'), exact('has_flag() {','\n\n# Row 200'),
exact('send_for_target() {','\n\n# Row 187:'), exact('send_for_target() {','\n\n# Row 187:'),
exact('sso_for_target() {','\n\n# Row 200 item 2'),
].join('\n'); ].join('\n');
const runCase=(setup)=>{ const runCase=(setup)=>{
const r=spawnSync('/bin/bash',['-c',`${component} const r=spawnSync('/bin/bash',['-c',`${component}
@@ -1394,6 +1395,11 @@ printf 'console=%s\\nplan=%s\\nstep=%s\\nrefuses=%s\\n' "\${RUN_CONSOLE[*]}" "$S
assert.deepEqual(runCase(`P_CONSOLE=(${preview} --sso --send); RUN_CONSOLE=(${preview} --send)`),{ assert.deepEqual(runCase(`P_CONSOLE=(${preview} --sso --send); RUN_CONSOLE=(${preview} --send)`),{
console:preview,plan:'send: dropped; the run drops --sso, and --send needs it (F39)',step:'drop --send; the run drops --sso',refuses:'', console:preview,plan:'send: dropped; the run drops --sso, and --send needs it (F39)',step:'drop --send; the run drops --sso',refuses:'',
}); });
// R2-I1 (#385): activate --sso on a legacy preview --send pointer. The real sso_for_target builds the
// run's flags from the pointer's, and send_for_target carries --send after --sso, before the tester flag.
assert.deepEqual(runCase(`SSO=1; P_CONSOLE=(${preview} --send --tester-socket); RUN_CONSOLE=("\${P_CONSOLE[@]}"); sso_for_target`),{
console:'--port 7332 --business acme --sso --send --tester-socket',plan:'send: carried',step:'',refuses:'',
});
}); });
test('T55 row 189: activate --no-send drops --send, and rollback and pm-on carry it off',async(t)=>{ test('T55 row 189: activate --no-send drops --send, and rollback and pm-on carry it off',async(t)=>{
+46
View File
@@ -433,6 +433,52 @@ test('row 233: the args file must be the running user\'s, with no group or other
assert.ok(!existsSync(join(e.sd,'root-systemctl.log')));assert.deepEqual(live(e),['active','active',B,B]); assert.ok(!existsSync(join(e.sd,'root-systemctl.log')));assert.deepEqual(live(e),['active','active',B,B]);
}); });
// R2-F1 (#385, PR 1836): base case2-unit.sh required --send for the library profile, and the current one
// refuses it, so a legacy args file would stop both units at A2 and fail the A4 render and render-old.
// read_args refuses it first, for an upgrade and a rollback alike, and never rewrites the file.
const legacySend=(f)=>`${f} has --send after --, which case2-unit.sh refuses for the library profile: remove the --send line, then rerun; the library Console is read-only until #412 (#385) (nothing was stopped)`;
test('#385 R2-F1: a library args file holding --send after -- refuses args-unreadable before anything stops, for upgrade and rollback',async(t)=>{
const e=await useed(t);
const legacy=e.argsText+'--send\n';
const cases=[
['the default profile',legacy,()=>upgrade(e,C1)],
['an explicit --profile library',legacy.replace('--\n','--profile\nlibrary\n--\n'),()=>upgrade(e,C1)],
['the last --profile is library',legacy.replace('--\n','--profile\nfactory\n--profile\nlibrary\n--\n'),()=>upgrade(e,C1)],
['a rollback',legacy,()=>drive(e,B,'rollback','--instance','dev','--to',B,...TO)],
];
for(const [why,text,run] of cases){
writeFileSync(e.args,text);chmodSync(e.args,0o640);
const n=existsSync(e.slog)?slog(e).length:0;
const r=await run();assert.equal(r.code,3,`${why}: ${r.all}`);
assert.equal(r.err.split('\n').at(-2),`stack-upgrade: args-unreadable: ${legacySend(e.args)}`,why);
assert.deepEqual(slog(e).slice(n).map((x)=>x.split(' ')[0]),['start','result'],why);
assert.equal(slog(e).at(-1),`result exit=3 args-unreadable ${legacySend(e.args)}`,why);
// The file is left as the operator wrote it.
assert.equal(readFileSync(e.args,'utf8'),text,why);assert.deepEqual(readdirSync(e.etc),['dev.args'],why);
}
assert.ok(!existsSync(join(e.sd,'sudo.log')));assert.ok(!existsSync(join(e.sd,'render.log')));
assert.ok(!existsSync(join(e.sd,'root-systemctl.log')));assert.deepEqual(live(e),['active','active',B,B]);
});
test('#385 R2-F1 controls: library args without --send and a factory args file pass read_args as before',async(t)=>{
const e=await useed(t);
// A bundle that does not exist stops the run at its first step after read_args, with nothing stopped.
const gone=join(e.root,'no.bundle');
for(const [why,text] of [
['library, no --send',e.argsText.replace('--\n','--profile\nlibrary\n--\n')],
['default profile, no --send',e.argsText],
['factory with --send, which case2-unit.sh refuses at the render as before',e.argsText.replace('--\n','--profile\nfactory\n--\n')+'--send\n'],
['factory, no --send',e.argsText.replace('--\n','--profile\nfactory\n--\n')],
]){
writeFileSync(e.args,text);chmodSync(e.args,0o640);
const n=existsSync(e.slog)?slog(e).length:0;
const r=await drive(e,C1,'upgrade','--instance','dev','--bundle',gone,'--sha',C1,...TO);assert.equal(r.code,3,`${why}: ${r.all}`);
assert.deepEqual(slog(e).slice(n).map((x)=>x.split(' ')[0]),['start','fetch','result'],why);
assert.match(slog(e).at(-1),/^result exit=3 fetch-failed /,why);
}
assert.ok(!existsSync(join(e.sd,'root-systemctl.log')));assert.deepEqual(live(e),['active','active',B,B]);
});
test('row 233: hostcheck (row 240): a nonzero --check or a missing file refuses profile-drift before the plan',async(t)=>{ test('row 233: hostcheck (row 240): a nonzero --check or a missing file refuses profile-drift before the plan',async(t)=>{
const e=await useed(t); const e=await useed(t);
fault(e,'hostcheck-rc','1'); fault(e,'hostcheck-rc','1');
+12 -1
View File
@@ -121,8 +121,12 @@ logl "start rc=0 argv:$(printf ' %q' "$0" "${ARGV[@]}")"
# I2's file: one case2-unit.sh argument per line, owned by root with no group or other write. Sets # I2's file: one case2-unit.sh argument per line, owned by root with no group or other write. Sets
# ARGS (the lines), S_USER, S_HOME, BUSINESS, NODE and ARGS_COPY, and the paths derived from them. # ARGS (the lines), S_USER, S_HOME, BUSINESS, NODE and ARGS_COPY, and the paths derived from them.
# #385 R2-F1: base case2-unit.sh required --send for the library profile (its default, and the last
# --profile wins, as there), and the current one refuses it. A2 would stop both units, and A4 and
# render-old would both fail on the same args, so a library file holding --send after -- refuses
# here, before anything stops. The file is never rewritten; the operator removes the line.
read_args() { read_args() {
local f="$ETC/$INSTANCE.args" st i copies=0 v local f="$ETC/$INSTANCE.args" st i copies=0 v profile=library
[ -f "$f" ] && [ ! -L "$f" ] || refuse args-unreadable "no regular file $f" [ -f "$f" ] && [ ! -L "$f" ] || refuse args-unreadable "no regular file $f"
st="$(stat -c '%u %a' "$f")" || refuse args-unreadable "cannot stat $f" st="$(stat -c '%u %a' "$f")" || refuse args-unreadable "cannot stat $f"
[ "${st% *}" = "$(id -u)" ] && (( (8#${st#* } & 8#022) == 0 )) || refuse args-unreadable "$f must be $(id -un)'s with no group or other write (it is uid ${st% *}, mode ${st#* })" [ "${st% *}" = "$(id -u)" ] && (( (8#${st#* } & 8#022) == 0 )) || refuse args-unreadable "$f must be $(id -un)'s with no group or other write (it is uid ${st% *}, mode ${st#* })"
@@ -137,12 +141,19 @@ read_args() {
case "${ARGS[i]}" in case "${ARGS[i]}" in
--user) S_USER="$v" ;; --home) S_HOME="$v" ;; --business) BUSINESS="$v" ;; --node) NODE="$v" ;; --user) S_USER="$v" ;; --home) S_HOME="$v" ;; --business) BUSINESS="$v" ;; --node) NODE="$v" ;;
--copy) ARGS_COPY="$v"; copies=$((copies + 1)) ;; --copy) ARGS_COPY="$v"; copies=$((copies + 1)) ;;
--profile) profile="$v" ;;
esac esac
i=$((i + 1)) ;; i=$((i + 1)) ;;
*) refuse args-unreadable "$f has ${ARGS[i]} before --, which case2-unit.sh does not take" ;; *) refuse args-unreadable "$f has ${ARGS[i]} before --, which case2-unit.sh does not take" ;;
esac esac
done done
[ "$copies" = 1 ] || refuse args-unreadable "$f needs exactly one --copy before --, not $copies" [ "$copies" = 1 ] || refuse args-unreadable "$f needs exactly one --copy before --, not $copies"
if [ "$profile" = library ]; then
for ((i = i + 1; i < ${#ARGS[@]}; i++)); do
[ "${ARGS[i]}" != --send ] ||
refuse args-unreadable "$f has --send after --, which case2-unit.sh refuses for the library profile: remove the --send line, then rerun; the library Console is read-only until #412 (#385)"
done
fi
[[ "$S_USER" =~ ^[a-z_][a-z0-9_-]*$ ]] && [[ "$S_HOME" =~ ^/[A-Za-z0-9/._+-]+$ ]] && [[ "$BUSINESS" =~ ^[a-z0-9][a-z0-9-]*$ ]] && [[ "$S_USER" =~ ^[a-z_][a-z0-9_-]*$ ]] && [[ "$S_HOME" =~ ^/[A-Za-z0-9/._+-]+$ ]] && [[ "$BUSINESS" =~ ^[a-z0-9][a-z0-9-]*$ ]] &&
[[ "$NODE" =~ ^/[A-Za-z0-9/._+-]+$ ]] && [[ "$ARGS_COPY" =~ ^[0-9a-f]{40}$ ]] || [[ "$NODE" =~ ^/[A-Za-z0-9/._+-]+$ ]] && [[ "$ARGS_COPY" =~ ^[0-9a-f]{40}$ ]] ||
refuse args-unreadable "$f lacks a plain --user, --home, --business, --node or --copy" refuse args-unreadable "$f lacks a plain --user, --home, --business, --node or --copy"