diff --git a/BUILD-LOG.md b/BUILD-LOG.md index e1b4b02be..1969abc64 100644 --- a/BUILD-LOG.md +++ b/BUILD-LOG.md @@ -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 `oauth4webapi@3.8.8` 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 `oauth4webapi@3.8.8` 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. diff --git a/docs/SESSIONS.md b/docs/SESSIONS.md index 574a8794b..64bbd8376 100644 --- a/docs/SESSIONS.md +++ b/docs/SESSIONS.md @@ -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-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-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. diff --git a/docs/TOOLS.md b/docs/TOOLS.md index 90fa89d72..1a79fe297 100644 --- a/docs/TOOLS.md +++ b/docs/TOOLS.md @@ -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. --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 -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 diff --git a/docs/guides/host-run-sheet.md b/docs/guides/host-run-sheet.md index bc2afd190..14412bf05 100644 --- a/docs/guides/host-run-sheet.md +++ b/docs/guides/host-run-sheet.md @@ -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` 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 diff --git a/packages/cli/tests/stack-release.test.mjs b/packages/cli/tests/stack-release.test.mjs index 067bca7a2..370fbe6da 100644 --- a/packages/cli/tests/stack-release.test.mjs +++ b/packages/cli/tests/stack-release.test.mjs @@ -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('has_flag() {','\n\n# Row 200'), exact('send_for_target() {','\n\n# Row 187:'), + exact('sso_for_target() {','\n\n# Row 200 item 2'), ].join('\n'); const runCase=(setup)=>{ 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)`),{ 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)=>{ diff --git a/packages/cli/tests/stack-upgrade.test.mjs b/packages/cli/tests/stack-upgrade.test.mjs index 2370467fe..66d7ff5c5 100644 --- a/packages/cli/tests/stack-upgrade.test.mjs +++ b/packages/cli/tests/stack-upgrade.test.mjs @@ -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]); }); +// 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)=>{ const e=await useed(t); fault(e,'hostcheck-rc','1'); diff --git a/scripts/stack-upgrade.sh b/scripts/stack-upgrade.sh index baafde210..f1705697c 100755 --- a/scripts/stack-upgrade.sh +++ b/scripts/stack-upgrade.sh @@ -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 # 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() { - 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" 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#* })" @@ -137,12 +141,19 @@ read_args() { case "${ARGS[i]}" in --user) S_USER="$v" ;; --home) S_HOME="$v" ;; --business) BUSINESS="$v" ;; --node) NODE="$v" ;; --copy) ARGS_COPY="$v"; copies=$((copies + 1)) ;; + --profile) profile="$v" ;; esac i=$((i + 1)) ;; *) refuse args-unreadable "$f has ${ARGS[i]} before --, which case2-unit.sh does not take" ;; esac done [ "$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-]*$ ]] && [[ "$NODE" =~ ^/[A-Za-z0-9/._+-]+$ ]] && [[ "$ARGS_COPY" =~ ^[0-9a-f]{40}$ ]] || refuse args-unreadable "$f lacks a plain --user, --home, --business, --node or --copy"