fix(console): keep preview deployment profiles read-only (#385)

This commit is contained in:
jason.woltje committed 2026-10-29 00:54:30 -05:00
1 parent 9936a1be1a
commit ab2bc23c7f
10 files changed
+132 -44

No files matched your search

+1
View File
@@ -5818,3 +5818,4 @@ Before: agents can read the library; supporting-path and push-credential depende
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 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.
+1
View File
@@ -1880,3 +1880,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.
+7 -2
View File
@@ -493,6 +493,8 @@ every table in COUNTED.
--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.
--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.
```
This is an operator plan, not an action agents run. Both reviews and the
@@ -602,8 +604,11 @@ and only the Console flags change.
`activate ... --no-send` (row 189, mos-0972) drops `--send` from the pointer's
Console flags and the unit, so the Console serves reads and no human sends. It
may name the live copy while the pointer holds `--send`; once `--send` is gone a
same-copy `--no-send` refuses `already-active`. Later activates, `pm-on` and
rollbacks carry the pointer, so send stays off. No flag puts `--send` back.
same-copy `--no-send` refuses `already-active`. Independently, an activate or
rollback drops an inherited legacy `--send` when the run has no `--sso`, before
the plan reaches `webui-service.sh`; verified `--sso --send` remains carried.
Later activates, `pm-on` and rollbacks keep send off. Only an explicit `--send`
beside given or carried `--sso` puts it back.
`--plan` prints one `send:` line: `dropped`, `off` with or without the
`--no-send` note, or `carried`.
+5
View File
@@ -380,6 +380,11 @@ or other write (`chmod 0644` or tighter). Before `--` it takes only `--user`, `-
`--copy` (exactly once), `--node`, `--profile`, `--claude` and `--pm`. `stack-upgrade.sh` changes only the
`--copy` line, and only after confirm passes.
For the `library` profile, the serve argv after `--` keeps the proxy-preview trio and required proof pair
but omits `--send`. That Console is read-only until Case 2 gains real OIDC in #412; `case2-unit.sh`
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.
## 8. Enrollment, once per host
+13 -3
View File
@@ -103,7 +103,7 @@ case_sudo_run '${root}' "$@"`);
}
const libraryFlags = (h) => ["--port", "443", "--business", "acme", "--public-host", "books.example", "--auth-header", "X-Auth-User",
"--allow-user", "melanie", "--send", "--proof-header", "X-Proxy-Proof", "--proof-file", h.proof];
"--allow-user", "melanie", "--proof-header", "X-Proxy-Proof", "--proof-file", h.proof];
const factoryFlags = () => ["--port", "443", "--business", "acme", "--tester-socket"];
function unit(h, { profile = "library", node = USR_NODE, copy = COPY_A, extra = [], flags, env = {} } = {}) {
@@ -300,7 +300,7 @@ function template(h, from, name, edit) {
return file;
}
test("a clean library render writes both units 0644 with the fixed lines, and enables both then starts the bus then the Console", (t) => {
test("a clean read-only library render writes both units 0644 with the fixed lines, and enables both then starts the bus then the Console", (t) => {
const h = hub(t);
const r = unit(h);
assert.equal(r.code, 0, r.err);
@@ -311,6 +311,7 @@ test("a clean library render writes both units 0644 with the fixed lines, and en
assert.deepEqual(lines(bus, "ExecStart"), [`${copyDir(h)}/scripts/mosaic bus start %i`]);
assert.deepEqual(lines(bus, "EnvironmentFile"), [`-${h.home}/.config/mosaic-dev/secrets/claude-session.env`]);
assert.deepEqual(lines(webui, "ExecStart"), [`${USR_NODE} ${copyDir(h)}/packages/webui/src/cli.mjs serve ${libraryFlags(h).join(" ")}`]);
assert.doesNotMatch(lines(webui, "ExecStart")[0], / --(?:send|sso)(?: |$)/);
assert.deepEqual(lines(webui, "EnvironmentFile"), []);
for (const text of [bus, webui]) {
assert.deepEqual(lines(text, "WantedBy"), ["multi-user.target"]);
@@ -336,6 +337,15 @@ test("a clean library render writes both units 0644 with the fixed lines, and en
]);
});
test("the read-only library profile explicitly refuses --send before writing a unit", (t) => {
const h = hub(t);
const r = unit(h, { flags: [...libraryFlags(h), "--send"] });
refuses(r, /the library profile is read-only; refusing --send until Case 2 supports --sso \(#412\)/);
assert.equal(existsSync(busFile(h)), false);
assert.equal(existsSync(webuiFile(h)), false);
assert.equal(existsSync(h.log), false);
});
test("PATH starts with the --node directory, for a node outside the default list and for /usr/bin/node", (t) => {
const h = hub(t);
const node = outsideNode();
@@ -504,7 +514,7 @@ test("library refuses a privileged-floor miss, a bad proof pair and every flag o
const without = (...names) => {
const out = [];
for (let i = 0; i < base.length; i++) {
if (names.includes(base[i])) { if (base[i] !== "--send") i++; continue; }
if (names.includes(base[i])) { i++; continue; }
out.push(base[i]);
}
return out;
+76 -29
View File
@@ -1128,19 +1128,22 @@ const CARRY_MSG=(to)=>`${to}'s Console does not support --proof-header, which th
const HEADER_MSG=(h)=>`--proof-header ${h} must be a header name of a-z, 0-9 and - (64 at most, lowercased) other than host, origin, x-mosaic-tester and the --auth-header value`;
test('T53 row 187: activate sets, carries and drops the proof pair',async(t)=>{
await t.test('tests 1 and 9: the pair goes after the preview flags, before --send and the kept --tester-port, as the unit renders it; a new pair replaces it',async(t)=>{
await t.test('tests 1 and 9: initial activation drops legacy preview send; the pair goes before the kept --tester-port and a new pair replaces it',async(t)=>{
const e=await mk(t),base=await seedPreview(e,undefined,['--send']),f=await proofFile(e),g=await proofFile(e,'proof2.187',0o400);
const pre=base.slice(0,-1);
let r=await prun(e,'activate',C1,'acme','--tester-port','7333');assert.equal(r.code,0,r.out+r.err);
assert.deepEqual(pointer(e).console,[...pre,'--tester-port','7333']);
assert.match(stepText(e),/Z drop --send; header preview is read-only \(#385\)\n/);
const out=await planOnly(e,0,'activate',C1,'acme',...pair(f));
assert.deepEqual(proofLines(out),[`proof: on, header x-console-proof, file ${f}`]);
assert.ok(out.find((l)=>l.startsWith('Console: ')).endsWith(` ${PREVIEW.join(' ')} ${pair(f).join(' ')} --send --tester-port 7333`));
assert.deepEqual(sendLines(out),['send: off']);
assert.ok(out.find((l)=>l.startsWith('Console: ')).endsWith(` ${PREVIEW.join(' ')} ${pair(f).join(' ')} --tester-port 7333`));
r=await prun(e,'activate',C1,'acme',...pair(f));assert.equal(r.code,0,r.out+r.err);
assert.deepEqual(pointer(e).console,[...pre,...pair(f),'--send','--tester-port','7333']);
assert.ok(conExec(e).endsWith(` --checkout ${repo} ${[...pre,...pair(f),'--send','--tester-port','7333'].join(' ')}`),conExec(e));
assert.deepEqual(pointer(e).console,[...pre,...pair(f),'--tester-port','7333']);
assert.ok(conExec(e).endsWith(` --checkout ${repo} ${[...pre,...pair(f),'--tester-port','7333'].join(' ')}`),conExec(e));
r=await prun(e,'status','acme');assert.equal(r.code,0,r.err);assert.match(r.out,/unit flags: match the pointer/);
r=await prun(e,'activate',C1,'acme',...pair(g,'X-Proof-Two'));assert.equal(r.code,0,r.out+r.err);
assert.deepEqual(pointer(e).console,[...pre,...pair(g,'X-Proof-Two'),'--send','--tester-port','7333']);
assert.deepEqual(pointer(e).console,[...pre,...pair(g,'X-Proof-Two'),'--tester-port','7333']);
r=await prun(e,'status','acme');assert.equal(r.code,0,r.err);assert.match(r.out,/unit flags: match the pointer/);
assert.equal(conExec(e).match(/--proof-header/g).length,1);assert.ok(!conExec(e).includes(f));
await noLeak(e);
@@ -1348,6 +1351,51 @@ test('T54 row 188: a carried pair gets the proof file checks, and a proof file i
const sendLines=(out)=>out.filter((l)=>l.startsWith('send:'));
const noSend=(c)=>c.filter((x)=>x!=='--send');
const LIVE_NOTE=`note: ${B} is the pointer's copy; the run re-pins it and only the Console flags change`;
test('Mos2255 source component: legacy preview send drops while explicit and SSO paths keep their guards',()=>{
const source=readFileSync(SR,'utf8');
const exact=(start,end)=>{
const a=source.indexOf(start),b=source.indexOf(end,a);
assert.ok(a>=0&&b>a,`missing source component ${start}`);
return source.slice(a,b);
};
const component=[
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:'),
].join('\n');
const runCase=(setup)=>{
const r=spawnSync('/bin/bash',['-c',`${component}
PLAN=1; PLAN_REFUSE=(); SEND=0; NO_SEND=0; NO_SSO=0
${setup}
send_for_target
printf 'console=%s\\nplan=%s\\nstep=%s\\nrefuses=%s\\n' "\${RUN_CONSOLE[*]}" "$SEND_PLAN" "$SEND_STEP" "\${PLAN_REFUSE[*]}"
`],{encoding:'utf8'});
assert.equal(r.status,0,r.stderr);
return Object.fromEntries(r.stdout.trimEnd().split('\n').map((line)=>line.split(/=(.*)/s).slice(0,2)));
};
const preview='--port 7332 --business acme --public-host console.example --auth-header x-user --allow-user jason';
assert.deepEqual(runCase(`P_CONSOLE=(${preview} --send); RUN_CONSOLE=("\${P_CONSOLE[@]}")`),{
console:preview,
plan:'send: dropped; header preview is read-only (#385)',
step:'drop --send; header preview is read-only (#385)',
refuses:'',
});
assert.match(runCase(`SEND=1; P_CONSOLE=(${preview} --send); RUN_CONSOLE=("\${P_CONSOLE[@]}")`).refuses,/^send-needs-sso: --send needs --sso/);
assert.deepEqual(runCase('P_CONSOLE=(--port 7332 --business acme --sso --send --tester-port 7333); RUN_CONSOLE=("${P_CONSOLE[@]}")'),{
console:'--port 7332 --business acme --sso --send --tester-port 7333',plan:'send: carried',step:'',refuses:'',
});
assert.deepEqual(runCase(`NO_SEND=1; P_CONSOLE=(${preview} --send); RUN_CONSOLE=("\${P_CONSOLE[@]}")`),{
console:preview,plan:'send: dropped; --no-send was given',step:'drop --send; --no-send was given',refuses:'',
});
assert.deepEqual(runCase(`NO_SSO=1; P_CONSOLE=(${preview} --sso --send); RUN_CONSOLE=(${preview} --send)`),{
console:preview,plan:'send: dropped; --no-sso drops --send (F39)',step:'drop --send; --no-sso was given',refuses:'',
});
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:'',
});
});
test('T55 row 189: activate --no-send drops --send, and rollback and pm-on carry it off',async(t)=>{
await t.test('test 1: a live-copy --no-send drops --send from the pointer and the unit, keeps the rest in order, logs the step and backs up the stopped store',async(t)=>{
const e=await mk(t),base=await seedPreview(e,undefined,['--send','--tester-port','7333']),want=noSend(base);
@@ -1381,8 +1429,8 @@ test('T55 row 189: activate --no-send drops --send, and rollback and pm-on carry
assert.deepEqual(out.filter((l)=>l.startsWith('note: ')),[LIVE_NOTE]);
assert.equal(out[out.indexOf('send: dropped; --no-send was given')+1],`Console: scripts/webui-service.sh install --copy ${B} --copies ${COPIES} ${noSend(base).join(' ')}`);
out=await planOnly(e,0,'activate',C1,'acme');
assert.deepEqual(sendLines(out),['send: carried']);assert.deepEqual(out.filter((l)=>l.startsWith('note: ')),[]);
assert.ok(out.find((l)=>l.startsWith('Console: ')).endsWith(' --send'));
assert.deepEqual(sendLines(out),['send: dropped; header preview is read-only (#385)']);assert.deepEqual(out.filter((l)=>l.startsWith('note: ')),[]);
assert.ok(out.find((l)=>l.startsWith('Console: ')).endsWith(` ${noSend(base).join(' ')}`));
assert.deepEqual(sendLines(await planOnly(e,0,'activate',C1,'acme','--no-send')),['send: dropped; --no-send was given']);
let r=await prun(e,'activate',C1,'acme','--no-send');assert.equal(r.code,0,r.out+r.err);
assert.deepEqual(pointer(e).console,noSend(base));assert.match(stepText(e),/Z drop --send; --no-send was given\n/);
@@ -1423,29 +1471,24 @@ test('T55 row 189: activate --no-send drops --send, and rollback and pm-on carry
assert.deepEqual(fp(e),before);assert.deepEqual(startsStops(e,n),[]);assert.equal(logLines(e).length,logs);
await noLeak(e);
});
await t.test('test 6: with both a pair and --send in the pointer, --no-send and --no-proof each drop what they name and keep the rest',async(t)=>{
const e=await mk(t),base=await seedPreview(e,undefined,['--send']),pre=noSend(base),f=await proofFile(e),g=await proofFile(e,'proof2.189',0o400);
let r=await prun(e,'activate',B,'acme',...pair(f));assert.equal(r.code,0,r.out+r.err);
assert.deepEqual(pointer(e).console,[...pre,...pair(f),'--send']);
await t.test('test 6: a legacy pointer with both a pair and --send drops send implicitly or explicitly while preserving proof choices',async(t)=>{
const e=await mk(t),f=await proofFile(e),g=await proofFile(e,'proof2.189',0o400),base=await seedPreview(e,undefined,[...pair(f),'--send']),pre=noSend(base),plain=base.slice(0,-5);
let out=await planOnly(e,0,'activate',C1,'acme','--no-proof');
assert.deepEqual([...proofLines(out),...sendLines(out)],['proof: dropped; --no-proof drops header x-console-proof','send: carried']);
assert.deepEqual([...proofLines(out),...sendLines(out)],['proof: dropped; --no-proof drops header x-console-proof','send: dropped; header preview is read-only (#385)']);
out=await planOnly(e,0,'activate',C1,'acme','--no-send');
assert.deepEqual([...proofLines(out),...sendLines(out)],['proof: carried, header x-console-proof','send: dropped; --no-send was given']);
out=await planOnly(e,0,'activate',B,'acme','--no-send','--no-proof');
assert.deepEqual([...proofLines(out),...sendLines(out)],['proof: dropped; --no-proof drops header x-console-proof','send: dropped; --no-send was given']);
r=await prun(e,'activate',C1,'acme','--no-send');assert.equal(r.code,0,r.out+r.err);
assert.deepEqual(pointer(e).console,[...pre,...pair(f)]);
let r=await prun(e,'activate',C1,'acme','--no-send');assert.equal(r.code,0,r.out+r.err);
assert.deepEqual(pointer(e).console,pre);
r=await prun(e,'activate',C1,'acme','--no-send',...pair(g,'X-Proof-Two'));assert.equal(r.code,0,r.out+r.err);
assert.deepEqual(pointer(e).console,[...pre,...pair(g,'X-Proof-Two')]);
assert.deepEqual(pointer(e).console,[...plain,...pair(g,'X-Proof-Two')]);
r=await prun(e,'status','acme');assert.equal(r.code,0,r.err);assert.match(r.out,/unit flags: match the pointer/);
const h=await mk(t),hbase=await seedPreview(h,undefined,['--send']),hpre=noSend(hbase),k=await proofFile(h);
r=await prun(h,'activate',B,'acme',...pair(k));assert.equal(r.code,0,r.out+r.err);
r=await prun(h,'activate',C1,'acme','--no-proof');assert.equal(r.code,0,r.out+r.err);
assert.deepEqual(pointer(h).console,[...hpre,'--send']);
const h=await mk(t),k=await proofFile(h),hbase=await seedPreview(h,undefined,[...pair(k),'--send']),hpre=hbase.slice(0,-5);
r=await prun(h,'activate',C1,'acme','--no-send','--no-proof');assert.equal(r.code,0,r.out+r.err);
assert.deepEqual(pointer(h).console,hpre);assert.doesNotMatch(conExec(h),/--proof-| --send( |$)/);
const steps=stepText(h);
assert.match(steps,/Z --no-proof was given and the pointer holds no proof pair, so there is nothing to drop\n/);assert.match(steps,/Z drop --send; --no-send was given\n/);
assert.match(steps,new RegExp(`Z drop --proof-header x-console-proof --proof-file ${k}; --no-proof was given\\n`));assert.match(steps,/Z drop --send; --no-send was given\n/);
r=await prun(h,'status','acme');assert.equal(r.code,0,r.err);assert.match(r.out,/unit flags: match the pointer/);
for(const x of [e,h])await noLeak(x);
});
@@ -1477,15 +1520,15 @@ function setEdit(set,name,from,to,sums=true){
}
test('T57 row 200: activate --sso, its refusals before any stop, and the rollback to a Console without --sso',async(t)=>{
await t.test('item 1: --sso drops the preview set and the pair, puts --sso before --send and the tester port, and the plan names what it drops and carries',async(t)=>{
await t.test('item 1: a pre-SSO activation drops legacy send; --sso then drops preview and proof before the tester port',async(t)=>{
const e=await mk(t),base=await seedPreview(e,undefined,['--send']),f=await proofFile(e),pre=noSend(base);
let r=await prun(e,'activate',B,'acme','--tester-port','7333',...pair(f));assert.equal(r.code,0,r.out+r.err);
assert.deepEqual(pointer(e).console,[...pre,...pair(f),'--send','--tester-port','7333']);
assert.deepEqual(pointer(e).console,[...pre,...pair(f),'--tester-port','7333']);
await ssoConfig(e);
const want=['--port',e.port,'--business','acme','--sso','--send','--tester-port','7333'];
const want=['--port',e.port,'--business','acme','--sso','--tester-port','7333'];
const out=await planOnly(e,0,'activate',C1,'acme','--sso');
assert.deepEqual([...proofLines(out),...sendLines(out),...ssoLines(out)],['proof: dropped; --sso drops header x-console-proof','send: carried',
`sso: on; drops ${DROPS}; carries --port ${e.port} --business acme --send --tester-port 7333`]);
assert.deepEqual([...proofLines(out),...sendLines(out),...ssoLines(out)],['proof: dropped; --sso drops header x-console-proof','send: off',
`sso: on; drops ${DROPS}; carries --port ${e.port} --business acme --tester-port 7333`]);
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','--sso');assert.equal(r.code,0,r.out+r.err);
assert.equal(pointer(e).copy,C1);assert.deepEqual(pointer(e).console,want);
@@ -1500,13 +1543,15 @@ test('T57 row 200: activate --sso, its refusals before any stop, and the rollbac
const e=await mk(t),base=await seedPreview(e,undefined,['--send']);await ssoConfig(e);
let r=await prun(e,'activate',C1,'acme');assert.equal(r.code,0,r.out+r.err);
let out=await planOnly(e,0,'activate',C1,'acme','--sso');
assert.deepEqual([...proofLines(out),...ssoLines(out)],['proof: off',`sso: on; drops ${DROPS}; carries --port ${e.port} --business acme --send`]);
assert.deepEqual([...proofLines(out),...sendLines(out),...ssoLines(out)],['proof: off','send: off',`sso: on; drops ${DROPS}; carries --port ${e.port} --business acme`]);
r=await prun(e,'activate',C1,'acme','--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.deepEqual(pointer(e).console,['--port',e.port,'--business','acme','--sso','--send']);
assert.deepEqual(pointer(e).console,['--port',e.port,'--business','acme','--sso']);
assert.doesNotMatch(stepText(e),/proof/);
await refused(e,['activate',C1,'acme','--sso'],'already-active',`${C1} is the pointer's copy`);
await refused(e,['activate',C1,'acme'],'already-active',`${C1} is the pointer's copy`);
r=await prun(e,'activate',C1,'acme','--send');assert.equal(r.code,0,r.out+r.err);
assert.deepEqual(pointer(e).console,['--port',e.port,'--business','acme','--sso','--send']);
out=await planOnly(e,0,'activate',C,'acme');assert.deepEqual(ssoLines(out),['sso: carried']);
r=await prun(e,'activate',C,'acme','--migrate');assert.equal(r.code,0,r.out+r.err);
// Carried, not turned on: no stamped step names sso (the install output's serve line still shows --sso).
@@ -1600,6 +1645,7 @@ test('T57 row 200: activate --sso, its refusals before any stop, and the rollbac
let r=await prun(e,'activate',C1,'acme');assert.equal(r.code,0,r.out+r.err);
const set=arrivalSet(e,C1);
r=await prun(e,'activate',C1,'acme','--sso');assert.equal(r.code,0,r.out+r.err);
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);
// The latest set holds the --sso Console; the arrival set holds B's.
assert.match(readFileSync(join(setOf(e,pointer(e).activation),SHOW),'utf8'),/ --sso /);
@@ -1801,9 +1847,10 @@ test('T58 row 273: activate --send and --no-sso',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);
assert.ok(pre.endsWith(` ${want.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);
r=await prun(e,'activate',C1,'acme','--send');assert.equal(r.code,0,r.out+r.err);
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(' '))]);
@@ -1812,7 +1859,7 @@ test('T58 row 273: activate --send and --no-sso',async(t)=>{
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'));
assert.equal(conExec(e),pre);
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);
+10 -2
View File
@@ -413,6 +413,14 @@ test('render --sso --send ends ExecStart with --send and changes nothing else',
}
});
test('render refuses duplicate --send under --sso', async (t) => {
const dir = tmp(t);
const r = await run(dir, ['render', '--sso', '--send', '--send']);
assert.equal(r.status, 1, r.stderr);
assert.equal(r.stdout, '');
assert.match(r.stderr, /^webui-service: --send given twice$/m);
});
test('--send without --sso, including every header-preview shape, is refused and writes nothing', async (t) => {
const dir = tmp(t);
const pairs = [PREVIEW.slice(0, 2), PREVIEW.slice(2, 4), PREVIEW.slice(4, 6)];
@@ -475,8 +483,8 @@ test('install --send into --dir with --no-reload writes the sending unit, and st
assert.deepEqual(otherCalls(join(dir, 'calls.txt')).filter((c) => !/^--user (is-enabled|is-active|show) mosaic-webui\.service/.test(c)), []);
const none = await run(dir, ['status', '--dir', join(dir, 'empty')], bin);
assert.doesNotMatch(none.stdout, /^send:/m, 'no unit, no send line');
// serve would take --send as an allowed user; N5 refuses it, so no unit
// ends with `--allow-user --send` without sending.
// This malformed identity makes --send the --allow-user value and is not a
// duplicate-flag guard; the dedicated --sso --send --send case above is.
const before = readFileSync(target, 'utf8');
for (const args of [['--public-host', 'console.example.com', '--auth-header', 'X-authentik-username', '--allow-user', '--send']]) {
const r = await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--port', String(port), ...args]);
+8 -4
View File
@@ -24,8 +24,10 @@
# The library profile takes exactly these serve flags, each once:
# --port N (1..1023) --business ID (the unit's) --public-host NAME
# A member that changes uid and cannot be killed refuses inner-cleanup, never success.
# --auth-header HEADER --allow-user ID --send
# --auth-header HEADER --allow-user ID
# --proof-header NAME --proof-file PATH
# It is proxy-preview read-only until Case 2 supports --sso (#412); --send
# is explicitly refused. The proof still protects proxy preview reads.
# The factory profile takes --port N (1..1023) and --business ID, and an
# optional bare --tester-socket marker. Retired tester TCP refuses. Only factory takes
# --pm and --claude.
@@ -343,15 +345,17 @@ serve_flags() {
--checkout) die "refusing --checkout: Case 2 has no queue panel" ;;
--board) die "refusing --board: Case 2 runs no control board" ;;
--sso) die "refusing --sso: the Console signs people in through the proxy, with the preview set and the proof pair (Q175 phase 2 is a follow-up)" ;;
--send)
[ "$PROFILE" = library ] || die "the factory profile refuses --send: its host has no proxy"
die "the library profile is read-only; refusing --send until Case 2 supports --sso (#412)" ;;
--port|--business) ;;
--public-host|--auth-header|--allow-user|--proof-header|--proof-file|--send)
--public-host|--auth-header|--allow-user|--proof-header|--proof-file)
[ "$PROFILE" = library ] || die "the factory profile refuses $name: its host has no proxy" ;;
*) die "unknown serve flag: $name" ;;
esac
[ -z "${seen[$name]:-}" ] || die "serve flag $name given twice"
seen[$name]=1
if [ "$name" = --tester-socket ]; then tester_socket=1; flags="$flags --tester-socket"; i=$((i + 1)); continue; fi
if [ "$name" = --send ]; then flags="$flags --send"; i=$((i + 1)); continue; fi
[ $((i + 1)) -lt "$n" ] || die "serve flag $name needs a value"
value="${SERVE[$((i + 1))]}"
case "$name" in
@@ -375,7 +379,7 @@ serve_flags() {
i=$((i + 2))
done
local required=(--port --business)
[ "$PROFILE" = factory ] || required+=(--public-host --auth-header --allow-user --send --proof-header --proof-file)
[ "$PROFILE" = factory ] || required+=(--public-host --auth-header --allow-user --proof-header --proof-file)
for name in "${required[@]}"; do
[ -n "${seen[$name]:-}" ] || die "serve flag $name is required"
done
+10 -3
View File
@@ -1107,8 +1107,9 @@ const k=Object.hasOwn(d,"console")?"console":Object.hasOwn(d,"federation")?"fede
}
# 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.
# it. A legacy pointer's implicit preview --send also drops before the plan reaches the installer,
# because header preview is read-only (#385). Verified --sso send is still carried. 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
@@ -1165,7 +1166,13 @@ send_for_target() {
SEND_STEP="--no-send was given and the pointer holds no --send, so there is nothing to drop"
fi
elif [ "$had" = 1 ]; then
SEND_PLAN="send: carried"
if has_flag --sso "${RUN_CONSOLE[@]}"; then
SEND_PLAN="send: carried"
else
RUN_CONSOLE=("${without_send[@]}")
SEND_PLAN="send: dropped; header preview is read-only (#385)"
SEND_STEP="drop --send; header preview is read-only (#385)"
fi
else
SEND_PLAN="send: off"
fi
+1 -1
View File
@@ -142,7 +142,7 @@ parse_flags() {
--allow-user) [ $# -ge 2 ] || usage; ALLOW_USER="$2"; SERVE_SET=1; shift 2 ;;
--proof-header) [ $# -ge 2 ] || usage; PROOF_HEADER="$2"; PROOF_SET=1; SERVE_SET=1; shift 2 ;;
--proof-file) [ $# -ge 2 ] || usage; PROOF_FILE="$2"; PROOF_SET=1; SERVE_SET=1; shift 2 ;;
--send) SEND=1; SERVE_SET=1; shift ;;
--send) [ "$SEND" = 0 ] || die "--send given twice"; SEND=1; SERVE_SET=1; shift ;;
--sso) SSO=1; SERVE_SET=1; shift ;;
*) usage ;;
esac