fix(case2-unit): factory Console takes the paired --sso --send; .53 launches through the proxy (Vikunja #415, #416)

The factory profile of scripts/case2-unit.sh now takes the bare pair
--sso --send, ported from the lane's serve_flags rule (b00019548). Either
flag alone refuses, and the proxy preview set and the proof pair still
refuse. Before any unit write, a bounded query run as the stack user,
through the existing config-query supervisor, checks that the stack
user's system config has console.sso; the Console refuses --sso without
it at start (packages/webui/src/cli.mjs:95). The server needs no change.

The library profile is unchanged: still read-only, still refusing --send
and --sso. Taking library --send with forward-auth and proof would need
the #385 refusal reverted in cli.mjs:63, serve.mjs:681 and the service,
release and upgrade scripts, so it is not done here.

docs/guides/case1-business.mosaic-stack.json now launches
claude-opus-5-5 with provider proxy, route anthropic, under account
factory with credential proxy.
This commit is contained in:
jason.woltje committed 2026-11-02 22:09:01 -06:00
1 parent 1ff45c4ad6
commit bd79daffdd
4 files changed
+119 -14

No files matched your search

+7 -2
View File
@@ -38,10 +38,15 @@
"launch": {
"by": "chief",
"instances": ["assistant"],
"account": {
"name": "factory",
"credential": "proxy"
},
"models": {
"claude-opus-5-5": {
"provider": "anthropic",
"model": "claude-opus-5-5"
"provider": "proxy",
"model": "claude-opus-5-5",
"route": "anthropic"
}
}
}
@@ -73,7 +73,9 @@ test("row 323: committed Case 1 document loads unchanged through the business lo
assert.equal(b.launch.by, "chief");
assert.deepEqual(b.launch.instances, ["assistant"]);
assert.deepEqual(b.arbiters, { delivery: "chief", technical: "chief" });
assert.deepEqual(b.launch.models["claude-opus-5-5"], { provider: "anthropic", model: "claude-opus-5-5" });
// #416: the .53 factory reaches Anthropic through the proxy, under the factory account.
assert.deepEqual(b.launch.models["claude-opus-5-5"], { provider: "proxy", model: "claude-opus-5-5", route: "anthropic" });
assert.deepEqual(b.launch.account, { name: "factory", credential: "proxy" });
for (const role of Object.values(b.roles)) {
assert.equal(role.vars.harness, "claude-code");
assert.equal(role.vars.model, "claude-opus-5-5");
@@ -83,6 +85,17 @@ test("row 323: committed Case 1 document loads unchanged through the business lo
}
});
// #416 changed the shipped route; the direct-provider input it replaced still loads.
test("row 323: a direct Anthropic launch without an account still loads", (t) => {
const x = world(t);
x.doc.launch.models["claude-opus-5-5"] = { provider: "anthropic", model: "claude-opus-5-5" };
delete x.doc.launch.account;
writeJson(x.business, x.doc);
const b = loadBusiness("mosaic-stack", { dir: x.configDir, rolesDir: REPO_ROLES });
assert.deepEqual(b.launch.models["claude-opus-5-5"], { provider: "anthropic", model: "claude-opus-5-5" });
assert.equal(b.launch.account, undefined);
});
test("row 323: real business validate accepts the Case 1 fixture without tracker credentials", (t) => {
const x = world(t);
configure(x);
+74 -2
View File
@@ -568,14 +568,86 @@ test("factory refuses duplicate socket, a missing --port or --business, and each
refuses(unit(h, { profile: "factory", flags: [...factoryFlags(), '--port', '443'] }), /given twice/);
refuses(unit(h, { profile: "factory", flags: ["--business", "acme"] }), /serve flag --port is required/);
refuses(unit(h, { profile: "factory", flags: ["--port", "443"] }), /serve flag --business is required/);
for (const [flag, value] of [["--public-host", "x.example"], ["--auth-header", "X-A"], ["--allow-user", "u"], ["--proof-header", "X-P"], ["--proof-file", h.proof], ["--send", null]]) {
for (const [flag, value] of [["--public-host", "x.example"], ["--auth-header", "X-A"], ["--allow-user", "u"], ["--proof-header", "X-P"], ["--proof-file", h.proof]]) {
refuses(unit(h, { profile: "factory", flags: [...factoryFlags(), flag, ...(value === null ? [] : [value])] }), new RegExp(`the factory profile refuses ${flag}`));
}
for (const [flag, value, pattern] of [["--checkout", "/srv/x", /refusing --checkout/], ["--board", "http://127.0.0.1:7331", /refusing --board/], ["--sso", null, /refusing --sso/], ["--nonsense", "1", /unknown serve flag/]]) {
for (const [flag, value, pattern] of [["--checkout", "/srv/x", /refusing --checkout/], ["--board", "http://127.0.0.1:7331", /refusing --board/], ["--nonsense", "1", /unknown serve flag/]]) {
refuses(unit(h, { profile: "factory", flags: [...factoryFlags(), flag, ...(value === null ? [] : [value])] }), pattern);
}
});
// #415/#416: the factory Console signs its owner in itself (console.sso) and takes writes only as the
// paired --sso --send. The pair never unlocks the proxy preview set or the proof pair.
const ssoConfig = (h) => writeJson(h.query, { dataRoot: h.dataRoot, console: { sso: { providers: [] } } });
const noUnits = (h) => {
assert.equal(existsSync(busFile(h)), false);
assert.equal(existsSync(webuiFile(h)), false);
assert.equal(existsSync(h.log), false);
};
test("#415 factory with --sso --send renders and the Console unit carries both flags", (t) => {
const h = hub(t);
ssoConfig(h);
const r = unit(h, { profile: "factory", flags: [...factoryFlags(), "--sso", "--send"] });
assert.equal(r.code, 0, r.out + r.err);
const webui = read(webuiFile(h));
assert.deepEqual(lines(webui, "ExecStart"), [`${USR_NODE} ${copyDir(h)}/packages/webui/src/cli.mjs serve --port 443 --business acme --tester-socket --sso --send`]);
assert.deepEqual(lines(webui, "EnvironmentFile"), []);
assert.deepEqual(webui.split("\n").filter((l) => /^(ProtectSystem|ReadWritePaths|ProtectHome|PrivateTmp)=/.test(l)), HARDENING(h));
// The console.sso check ran as the stack user, through the same sudo as the tester query.
const sudo = read(h.sudoLog);
assert.match(sudo, new RegExp(`^-u mosaic -H env -i HOME=${h.home} PATH=`, "m"));
assert.match(sudo, /s\.console&&s\.console\.sso/);
// Without the tester marker the pair renders too, in the order given.
const g = hub(t);
ssoConfig(g);
assert.equal(unit(g, { profile: "factory", flags: ["--port", "443", "--send", "--business", "acme", "--sso"] }).code, 0);
assert.deepEqual(lines(read(webuiFile(g)), "ExecStart"), [`${USR_NODE} ${copyDir(g)}/packages/webui/src/cli.mjs serve --port 443 --send --business acme --sso`]);
});
test("#415 factory refuses --sso alone and --send alone before writing a unit", (t) => {
const h = hub(t);
ssoConfig(h);
refuses(unit(h, { profile: "factory", flags: [...factoryFlags(), "--sso"] }), /refusing --sso without --send in the factory profile/);
refuses(unit(h, { profile: "factory", flags: [...factoryFlags(), "--send"] }), /the factory profile refuses --send without --sso/);
refuses(unit(h, { profile: "factory", flags: [...factoryFlags(), "--sso", "--sso", "--send"] }), /serve flag --sso given twice/);
refuses(unit(h, { profile: "factory", flags: [...factoryFlags(), "--sso", "--send", "--send"] }), /serve flag --send given twice/);
noUnits(h);
});
test("#415 factory with --sso --send still refuses each proxy preview flag and the proof flags", (t) => {
const h = hub(t);
ssoConfig(h);
for (const [flag, value] of [["--public-host", "x.example"], ["--auth-header", "X-A"], ["--allow-user", "u"], ["--proof-header", "X-P"], ["--proof-file", h.proof]]) {
refuses(unit(h, { profile: "factory", flags: [...factoryFlags(), "--sso", "--send", flag, value] }), new RegExp(`the factory profile refuses ${flag}: its host has no proxy`));
}
noUnits(h);
});
test("#415 factory --sso refuses before any unit write when the stack user's config has no console.sso", (t) => {
const h = hub(t);
const pair = ["--port", "443", "--business", "acme", "--sso", "--send"];
refuses(unit(h, { profile: "factory", flags: pair }), /--sso needs console\.sso in mosaic's system config/);
writeJson(h.query, { dataRoot: h.dataRoot, console: {} });
refuses(unit(h, { profile: "factory", flags: pair }), /--sso needs console\.sso in mosaic's system config/);
noUnits(h);
// Only the exact one-line answer counts; anything else refuses, and so does a failed query.
for (const [output, status, pattern] of [['{"sso":true}\nforged', "0", /sso config query has invalid facts/], ['{"sso":"true"}', "0", /sso config query has invalid facts/], ['{"sso":true}', "1", /sso config query inner-command/]]) {
writeFileSync(join(h.root, "query-output"), output);
writeFileSync(join(h.root, "query-status"), status);
refuses(unit(h, { profile: "factory", flags: pair }), pattern);
}
noUnits(h);
});
test("#416 library still refuses --sso, alone and with --send, before writing a unit", (t) => {
const h = hub(t);
ssoConfig(h);
refuses(unit(h, { flags: [...libraryFlags(h), "--sso"] }), /refusing --sso: the Console signs people in through the proxy/);
refuses(unit(h, { flags: [...libraryFlags(h), "--sso", "--send"] }), /refusing --sso: the Console signs people in through the proxy/);
noUnits(h);
});
test("row253 factory refuses retired TCP and a value after the socket marker", (t) => {
const h = hub(t);
refuses(unit(h, { profile: "factory", flags: ['--port','443','--business','acme','--tester-port','7333'] }), /case2-unit.sh retires tester TCP; use --tester-socket/);
+24 -9
View File
@@ -20,7 +20,7 @@
# never imports copy code. This adds sudo and GNU timeout to the installer
# dependencies. The inner query supervisor runs after the uid drop: 5 s inside,
# 7 s on root's wait. Fixed system Bash, setsid, sleep, timeout and head are
# resolved and root-validated. Only the config query uses this supervisor.
# resolved and root-validated. Only the config queries use this supervisor.
# It renders the default instance only.
#
# The library profile takes exactly these serve flags, each once:
@@ -29,10 +29,13 @@
# --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.
# and --sso are 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.
# optional bare --tester-socket marker. It also takes the paired bare
# --sso --send (#415): the Console signs its owner in from console.sso, which
# the stack user's config must have; either one alone refuses, and the
# preview and proof flags still refuse. Retired tester TCP refuses. Only
# factory takes --pm and --claude.
#
# On a re-install the installed units must equal the render in everything
# but the copy id, and all selected units must name one copy, so a re-pin or rollback
@@ -298,6 +301,7 @@ tester_capture() (
cat -- "$file"
)
SSO_QUERY='import {pathToFileURL} from "node:url";const m=await import(pathToFileURL(process.argv[1]).href);const s=m.loadSystem();console.log(JSON.stringify({sso:Boolean(s.console&&s.console.sso)}));'
TESTER_QUERY='import {readFileSync} from "node:fs";import {pathToFileURL} from "node:url";const m=await import(pathToFileURL(process.argv[1]).href);const s=m.loadSystem();console.log(JSON.stringify({dataRoot:s.dataRoot,socket:readFileSync(process.argv[2],"utf8").includes("MOSAIC_TESTER_SOCKET_V1")}));'
tester_facts() {
local LC_ALL=C pattern='^\{"dataRoot":"([A-Za-z0-9/._@+-]+)","socket":(true|false)\}$'
@@ -342,7 +346,7 @@ tester_location() {
serve_flags() {
local -A seen=()
local flags='' name value
local port='' public_host='' auth_header='' allow_user='' proof_header='' proof_file='' tester_socket=0
local port='' public_host='' auth_header='' allow_user='' proof_header='' proof_file='' tester_socket=0 sso=0
local i=0 n=${#SERVE[@]}
while [ "$i" -lt "$n" ]; do
name="${SERVE[$i]}"
@@ -351,10 +355,8 @@ serve_flags() {
--tester-socket) [ "$PROFILE" = factory ] || die "the library profile refuses --tester-socket" ;;
--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)" ;;
--sso) [ "$PROFILE" = factory ] || 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" = factory ] || 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)
[ "$PROFILE" = library ] || die "the factory profile refuses $name: its host has no proxy" ;;
@@ -363,6 +365,8 @@ serve_flags() {
[ -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" = --sso ]; then sso=1; flags="$flags --sso"; 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
@@ -391,6 +395,17 @@ serve_flags() {
[ -n "${seen[$name]:-}" ] || die "serve flag $name is required"
done
if [ "$PROFILE" = factory ]; then
# #415: only the paired owner sign-in and writes. No preview header or
# proof file is inferred. The Console refuses --sso without console.sso
# at start; this asks the stack user's config first, before any write.
[ "$sso" = 0 ] || [ -n "${seen[--send]:-}" ] || die "refusing --sso without --send in the factory profile"
[ -z "${seen[--send]:-}" ] || [ "$sso" = 1 ] || die "the factory profile refuses --send without --sso"
if [ "$sso" = 1 ]; then
local answer
answer="$(tester_capture 5 7 4096 "$USER_NAME" "$HOME_DIR" "$UNIT_PATH" "$NODE" --input-type=module -e "$SSO_QUERY" "$COPY/packages/cli/src/config.mjs")" || die "sso config query $(tester_capture_reason "$?")"
[ "$answer" != '{"sso":false}' ] || die "--sso needs console.sso in $USER_NAME's system config"
[ "$answer" = '{"sso":true}' ] || die "sso config query has invalid facts"
fi
if [ "$tester_socket" = 1 ]; then
local query
query="$(tester_capture 5 7 4096 "$USER_NAME" "$HOME_DIR" "$UNIT_PATH" "$NODE" --input-type=module -e "$TESTER_QUERY" "$COPY/packages/cli/src/config.mjs" "$COPY/packages/webui/src/cli.mjs")" || die "tester config query $(tester_capture_reason "$?")"