206 lines
18 KiB
Diff
206 lines
18 KiB
Diff
diff --git a/docs/TOOLS.md b/docs/TOOLS.md
|
|
index 8fecefb40..20be532e9 100644
|
|
--- a/docs/TOOLS.md
|
|
+++ b/docs/TOOLS.md
|
|
@@ -291,7 +291,8 @@ Failure publishes nothing and removes the temporary tree.
|
|
`wrapper` is a live switching step, reserved for Jason or Mos. It accepts
|
|
only an existing copy and a regular non-symlink mosaic wrapper with the
|
|
Q81 marker on line 2 and its exact lease line to mosaic-old. It preserves
|
|
-both lines and atomically replaces the wrapper's final exec with the
|
|
+both lines, adds comments naming the copy root and Q81's mosaic-old lease
|
|
+probe, and atomically replaces the wrapper's final exec with the
|
|
copy's scripts/mosaic. --bin is a fixture override; the default is
|
|
~/.npm-global/bin. Neither the Q81 worktree nor mosaic-old is removed.
|
|
`check` changes nothing: it reads effective systemd WorkingDirectory and
|
|
@@ -305,7 +306,8 @@ Bus render/install accept --copy ID, --copies DIR, --pm and the unchanged
|
|
--pm adds the PM startup; a refusal exits 3, and RestartPreventExitStatus
|
|
prevents a retry. Install refuses any busy bus instance, even with
|
|
--no-reload; a failed state/drop-in read also refuses. Any template or
|
|
-instance drop-in assigning ExecStart or WorkingDirectory refuses,
|
|
+instance drop-in, or inherited mosaic-.service.d/ or service.d/ drop-in,
|
|
+assigning ExecStart or WorkingDirectory refuses,
|
|
naming the file. Status reads effective roots, --pm and DropInPaths,
|
|
reports checkout or copy identity and refactor drift, and writes nothing.
|
|
|
|
diff --git a/packages/cli/README.md b/packages/cli/README.md
|
|
index 4c05b4213..fb4532c84 100644
|
|
--- a/packages/cli/README.md
|
|
+++ b/packages/cli/README.md
|
|
@@ -470,11 +470,25 @@ mosaic-stack/copies/<full id>. bus-service.sh render/install take --copy
|
|
ID and --pm alongside the existing --claude pin. Without --copy render
|
|
keeps today's checkout unit. A PM refusal exits 3 and the unit does not
|
|
retry it. Install refuses every busy instance (also --no-reload), failed
|
|
-state reads and drop-ins overriding ExecStart or WorkingDirectory.
|
|
+state reads and drop-ins overriding ExecStart or WorkingDirectory, including
|
|
+inherited mosaic-.service.d/ and service.d/ in every unit lookup directory.
|
|
Status reads effective systemd properties, reports the tree/copy, PM flag,
|
|
refactor drift and drop-in files; it changes nothing. The temporary PM
|
|
ExecStart drop-in must be removed by Mos before installing the pinned unit.
|
|
|
|
+The wrapper keeps the Q81 marker on line 2 and the final copy exec on the
|
|
+last line. Comments name the copy root and explain that the Q81 lease probe
|
|
+still answers at mosaic-old.
|
|
+
|
|
+Copy npm ci may warn that @google/genai's preinstall, esbuild's postinstall
|
|
+and protobufjs's postinstall are blocked. This is expected for the current
|
|
+copy paths: Mosaic launches Pi's published bundled CLI, whose runtime does
|
|
+not load those installed package entrypoints. @google/genai's preinstall
|
|
+only echoes; esbuild's script installs/checks its build binary; protobufjs's
|
|
+script only warns about dependency version schemes. The copy runs the
|
|
+already published bundle and does not build it. No allowScripts change is
|
|
+needed for these paths; adding a runtime loader requires a fresh audit.
|
|
+
|
|
The one update and rollback plan is [TOOLS.md, Pinned stack update](../../docs/TOOLS.md#pinned-stack-update).
|
|
Each live switch/unit change/stop/start requires Mos's go; agents build
|
|
and test fixtures only. Restarting the bus ends its sessions, so no PM
|
|
diff --git a/packages/cli/tests/host.test.mjs b/packages/cli/tests/host.test.mjs
|
|
index 6a1170357..63dab06c3 100644
|
|
--- a/packages/cli/tests/host.test.mjs
|
|
+++ b/packages/cli/tests/host.test.mjs
|
|
@@ -828,7 +828,7 @@ test("bus-service.sh renders the unit and installs it into a given directory", (
|
|
// A user unit cannot order on a system target (Darkwing F3).
|
|
assert.doesNotMatch(render.stdout, /network-online/);
|
|
const fakeBin=join(dir,"bin");mkdirSync(fakeBin);
|
|
- writeFileSync(join(fakeBin,"systemctl"),"#!/bin/sh\ncase \"$*\" in *show*) echo DropInPaths=;; esac\nexit 0\n");chmodSync(join(fakeBin,"systemctl"),0o755);
|
|
+ writeFileSync(join(fakeBin,"systemctl"),"#!/bin/sh\ncase \"$*\" in *list-units*) exit 0;; *)exit 99;; esac\n");chmodSync(join(fakeBin,"systemctl"),0o755);
|
|
writeFileSync(join(fakeBin,"systemd-analyze"),`#!/bin/sh\nprintf '%s\\n' '${dir}/unit-search'\n`);chmodSync(join(fakeBin,"systemd-analyze"),0o755);
|
|
const unitEnv={...process.env,PATH:`${fakeBin}:${process.env.PATH}`};
|
|
const first = spawnSync(script, ["install", "--dir", dir, "--no-reload"], { encoding: "utf8",env:unitEnv });
|
|
@@ -852,7 +852,7 @@ test("bus-service.sh pins the claude binary, refuses one the unit can't carry, a
|
|
return p;
|
|
};
|
|
mkdirSync(join(dir, "bin"));
|
|
- writeFileSync(join(dir,"bin","systemctl"),'#!/bin/sh\ncase "$*" in *show*) echo DropInPaths=;; esac\nexit 0\n');chmodSync(join(dir,"bin","systemctl"),0o755);
|
|
+ writeFileSync(join(dir,"bin","systemctl"),'#!/bin/sh\ncase "$*" in *list-units*) exit 0;; *)exit 99;; esac\n');chmodSync(join(dir,"bin","systemctl"),0o755);
|
|
writeFileSync(join(dir,"bin","systemd-analyze"),`#!/bin/sh\nprintf '%s\\n' '${dir}/unit-search'\n`);chmodSync(join(dir,"bin","systemd-analyze"),0o755);
|
|
const onPath = stand(join(dir, "bin", "claude"));
|
|
const named = stand(join(dir, "claude-named"));
|
|
diff --git a/packages/cli/tests/stack-copy.test.mjs b/packages/cli/tests/stack-copy.test.mjs
|
|
index 8460b9198..d98e771ac 100644
|
|
--- a/packages/cli/tests/stack-copy.test.mjs
|
|
+++ b/packages/cli/tests/stack-copy.test.mjs
|
|
@@ -46,6 +46,11 @@ test('wrapper preserves Q81 lease and marker, rejects foreign/symlink; check use
|
|
const lease=`[ "\${1:-}" = __lease-capability ] && exec "${s.bin}/mosaic-old" "$@"`;
|
|
writeFileSync(wrapper,`#!/usr/bin/env bash\n${marker}\n${lease}\nexec "/old/scripts/mosaic" "$@"\n`);
|
|
assert.equal(s.run('stack-copy.sh',['wrapper',s.id,'--copies',s.copies,'--bin',s.bin]).status,0);
|
|
+ const lines=readFileSync(wrapper,'utf8').trimEnd().split('\n');
|
|
+ assert.equal(lines[1],marker);
|
|
+ assert.equal(lines[2],`# CLI runs from pinned copy: ${join(s.copies,s.id)}`);
|
|
+ assert.equal(lines[3],'# The Q81 lease probe still answers at mosaic-old.');
|
|
+ assert.equal(lines.at(-1),`exec "${join(s.copies,s.id)}/scripts/mosaic" "$@"`);
|
|
assert.match(readFileSync(wrapper,'utf8'),new RegExp(s.id));assert.ok(readFileSync(wrapper,'utf8').includes(lease));
|
|
const old=join(s.bin,'mosaic-old');writeFileSync(old,'#!/bin/sh\nprintf \"legacy:%s\\n\" \"$1\"\n');chmodSync(old,0o755);
|
|
assert.equal(spawnSync(wrapper,['__lease-capability'],{env:s.env,encoding:'utf8'}).stdout,'legacy:__lease-capability\n');
|
|
@@ -54,7 +59,11 @@ test('wrapper preserves Q81 lease and marker, rejects foreign/symlink; check use
|
|
s.fake(`echo 'LoadState=loaded'; echo 'WorkingDirectory=${copy}';case \"$*\" in *mosaic-bus*)echo 'ExecStart={ path=/other/scripts/mosaic ; argv[]=/other/scripts/mosaic bus start demo ; }';; *)echo 'ExecStart={ path=/usr/bin/node ; argv[]=/usr/bin/node ${copy}/packages/webui/src/cli.mjs serve ; }';;esac\n`);
|
|
assert.equal(s.run('stack-copy.sh',['check','--copies',s.copies,'--bin',s.bin]).status,1,'effective ExecStart mismatch');
|
|
s.fake(`echo 'LoadState=loaded'; echo 'WorkingDirectory=/other';\n`);assert.equal(s.run('stack-copy.sh',['check','--copies',s.copies,'--bin',s.bin]).status,1);
|
|
- const foreign=`#!/usr/bin/env bash\n# foreign wrapper\n${lease}\nexec \"/old/scripts/mosaic\" \"$@\"\n`;writeFileSync(wrapper,foreign);assert.notEqual(s.run('stack-copy.sh',['wrapper',s.id,'--copies',s.copies,'--bin',s.bin]).status,0);assert.equal(readFileSync(wrapper,'utf8'),foreign);
|
|
+ const foreign=`#!/usr/bin/env bash\n# foreign wrapper\n${lease}\nexec \"${copy}/scripts/mosaic\" \"$@\"\n`;writeFileSync(wrapper,foreign);
|
|
+ // Restore matching effective roots: only the foreign marker refuses check.
|
|
+ s.fake(`echo 'LoadState=loaded';echo 'WorkingDirectory=${copy}';case "$*" in *mosaic-bus*) echo 'ExecStart={ path=${copy}/scripts/mosaic ; argv[]=${copy}/scripts/mosaic bus start demo ; }';; *)echo 'ExecStart={ path=/usr/bin/node ; argv[]=/usr/bin/node ${copy}/packages/webui/src/cli.mjs serve ; }';;esac\n`);
|
|
+ assert.equal(s.run('stack-copy.sh',['check','--copies',s.copies,'--bin',s.bin]).status,1);
|
|
+ assert.notEqual(s.run('stack-copy.sh',['wrapper',s.id,'--copies',s.copies,'--bin',s.bin]).status,0);assert.equal(readFileSync(wrapper,'utf8'),foreign);
|
|
const backing=join(s.bin,'backing');writeFileSync(backing,`#!/usr/bin/env bash\n${marker}\n${lease}\nexec \"/old/scripts/mosaic\" \"$@\"\n`);unlinkSync(wrapper);symlinkSync(backing,wrapper);assert.notEqual(s.run('stack-copy.sh',['wrapper',s.id,'--copies',s.copies,'--bin',s.bin]).status,0);
|
|
});
|
|
test('bus unit copy/pm pins, install refuses all busy states and failed reads, drop-in refusal',t=>{
|
|
@@ -65,7 +74,7 @@ test('bus unit copy/pm pins, install refuses all busy states and failed reads, d
|
|
for(const state of ['active','activating','deactivating','reloading']){s.fake(`echo '[email protected] loaded ${state} running fixture';\n`);const r=s.run('bus-service.sh',['install',...args,'--dir',units,'--no-reload']);assert.notEqual(r.status,0,state);assert.ok(!existsSync(units));}
|
|
s.fake('exit 1\n');assert.notEqual(s.run('bus-service.sh',['install',...args,'--dir',units,'--no-reload']).status,0);assert.ok(!existsSync(units));
|
|
const dropdir=join(s.search,'[email protected]');mkdirSync(dropdir);const drop=join(dropdir,'pm.conf');writeFileSync(drop,'[Service]\nExecStart=elsewhere\n');
|
|
- s.fake(`case "$*" in *list-units*) echo '[email protected] loaded inactive dead fixture';; *show*) echo 'LoadState=loaded';echo 'DropInPaths=${drop}';; esac\n`);
|
|
+ s.fake(`case "$*" in *list-units*) echo '[email protected] loaded inactive dead fixture';; *)exit 99;; esac\n`);
|
|
const refused=s.run('bus-service.sh',['install',...args,'--dir',units,'--no-reload']);assert.notEqual(refused.status,0);assert.ok(refused.stderr.includes(drop));assert.ok(!existsSync(units));
|
|
writeFileSync(drop,'[Service]\nEnvironment=SAFE=1\n');assert.equal(s.run('bus-service.sh',['install',...args,'--dir',units,'--no-reload']).status,0);
|
|
});
|
|
@@ -151,7 +160,7 @@ test('drop-ins in either override field and malformed metadata refuse; uninstall
|
|
const s=setup(t),units=join(s.root,'units'),dropdir=join(s.search,'[email protected]');mkdirSync(dropdir);const drop=join(dropdir,'override.conf');
|
|
for(const field of ['ExecStart','WorkingDirectory']){
|
|
writeFileSync(drop,`[Service]\n${field}=override\n`);
|
|
- s.fake(`case "$*" in *list-units*) echo '[email protected] loaded inactive dead fixture';; *[email protected]*) echo 'DropInPaths=${drop}';; *)echo DropInPaths=;;esac\n`);
|
|
+ s.fake(`case "$*" in *list-units*) echo '[email protected] loaded inactive dead fixture';; *)exit 99;;esac\n`);
|
|
const r=s.run('bus-service.sh',['install','--dir',units,'--no-reload']);assert.notEqual(r.status,0);assert.ok(r.stderr.includes(drop));assert.ok(!existsSync(units));
|
|
}
|
|
s.fake('exit 0\n');s.analyze('exit 0\n');const unknown=s.run('bus-service.sh',['install','--dir',units,'--no-reload']);assert.notEqual(unknown.status,0);assert.match(unknown.stderr,/unknown/);
|
|
@@ -162,9 +171,11 @@ test('drop-ins in either override field and malformed metadata refuse; uninstall
|
|
|
|
test('install never shows a template name on systemd 261; template drop-ins still refuse',t=>{
|
|
const s=setup(t),units=join(s.root,'units'),calls=join(s.root,'systemctl-calls');
|
|
- s.fake(`echo "$*" >> '${calls}'\ncase "$*" in *show*[email protected]*) echo 'neither a valid invocation ID nor unit name' >&2; exit 1;; *list-units*) echo '[email protected] loaded inactive dead fixture';; *show*)echo DropInPaths=;; esac\n`);
|
|
+ s.fake(`echo "$*" >> '${calls}'\ncase "$*" in *show*[email protected]*) echo 'neither a valid invocation ID nor unit name' >&2; exit 1;; *list-units*) echo '[email protected] loaded inactive dead fixture';; *)exit 99;; esac\n`);
|
|
const result=s.run('bus-service.sh',['install','--dir',units,'--no-reload']);assert.equal(result.status,0,result.stderr);
|
|
assert.doesNotMatch(readFileSync(calls,'utf8'),/show mosaic-bus@\.service/);
|
|
+ // This rejection arm is deliberately exercised; it is not a dead show reply.
|
|
+ assert.equal(spawnSync(join(s.bin,'systemctl'),['--user','show','[email protected]'],{env:s.env}).status,1);
|
|
const dropdir=join(s.search,'[email protected]');mkdirSync(dropdir);const file=join(dropdir,'pm.conf');writeFileSync(file,'[Service]\nExecStart=override\n');
|
|
const before=readFileSync(join(units,'[email protected]'),'utf8');
|
|
const denied=s.run('bus-service.sh',['install','--dir',units,'--no-reload']);assert.notEqual(denied.status,0);assert.ok(denied.stderr.includes(file));assert.equal(readFileSync(join(units,'[email protected]'),'utf8'),before);
|
|
@@ -173,7 +184,29 @@ test('unlisted instance drop-in is found in another unit lookup directory; path
|
|
const s=setup(t),units=join(s.root,'units'),second=join(s.root,'second-search');mkdirSync(second);
|
|
const dropdir=join(second,'[email protected]');mkdirSync(dropdir);const file=join(dropdir,'cwd.conf');writeFileSync(file,'[Service]\nWorkingDirectory=/other\n');
|
|
s.analyze(`printf '%s\\n' '${s.search}' '${second}'\n`);
|
|
- s.fake("case \"$*\" in *list-units*) echo '[email protected] loaded inactive dead fixture';; *show*)echo DropInPaths=;; esac\n");
|
|
+ s.fake("case \"$*\" in *list-units*) echo '[email protected] loaded inactive dead fixture';; *)exit 99;; esac\n");
|
|
const denied=s.run('bus-service.sh',['install','--dir',units,'--no-reload']);assert.notEqual(denied.status,0);assert.ok(denied.stderr.includes(file));assert.ok(!existsSync(units));
|
|
s.analyze('exit 1\n');const failed=s.run('bus-service.sh',['install','--dir',units,'--no-reload']);assert.notEqual(failed.status,0);assert.match(failed.stderr,/unit-paths failed/);assert.ok(!existsSync(units));
|
|
});
|
|
+
|
|
+// Row 108: inherited drop-ins apply even without a loaded bus instance.
|
|
+for(const name of ['mosaic-.service.d','service.d'])test(`inherited ${name} override refuses without changing the unit`,t=>{
|
|
+ const s=setup(t),units=join(s.root,'units');mkdirSync(units);
|
|
+ const target=join(units,'[email protected]');writeFileSync(target,'previous unit bytes');
|
|
+ s.fake('exit 0\n');
|
|
+ const dropdir=join(s.search,name);mkdirSync(dropdir);const file=join(dropdir,'override.conf');
|
|
+ for(const field of ['WorkingDirectory','ExecStart']){
|
|
+ writeFileSync(file,`[Service]\n${field}=/other\n`);
|
|
+ const denied=s.run('bus-service.sh',['install','--dir',units,'--no-reload']);
|
|
+ assert.equal(denied.status,1,denied.stderr);assert.ok(denied.stderr.includes(file));assert.equal(readFileSync(target,'utf8'),'previous unit bytes');
|
|
+ }
|
|
+ writeFileSync(file,'[Service]\nEnvironment=SAFE=1\n');
|
|
+ const allowed=s.run('bus-service.sh',['install','--dir',units,'--no-reload']);assert.equal(allowed.status,0,allowed.stderr);
|
|
+});
|
|
+test('inherited drop-in unreadable directory or file refuses before writing',t=>{
|
|
+ const s=setup(t),units=join(s.root,'units');mkdirSync(units);const target=join(units,'[email protected]');writeFileSync(target,'previous unit bytes');s.fake('exit 0\n');
|
|
+ const dropdir=join(s.search,'service.d');symlinkSync(join(s.root,'absent-dir'),dropdir);
|
|
+ const dir=s.run('bus-service.sh',['install','--dir',units,'--no-reload']);assert.equal(dir.status,1);assert.ok(dir.stderr.includes(dropdir));assert.equal(readFileSync(target,'utf8'),'previous unit bytes');
|
|
+ unlinkSync(dropdir);mkdirSync(dropdir);const file=join(dropdir,'broken.conf');symlinkSync(join(s.root,'absent-file'),file);
|
|
+ const broken=s.run('bus-service.sh',['install','--dir',units,'--no-reload']);assert.equal(broken.status,1);assert.ok(broken.stderr.includes(file));assert.equal(readFileSync(target,'utf8'),'previous unit bytes');
|
|
+});
|
|
diff --git a/scripts/bus-service.sh b/scripts/bus-service.sh
|
|
index 297c633b2..f71250204 100755
|
|
--- a/scripts/bus-service.sh
|
|
+++ b/scripts/bus-service.sh
|
|
@@ -7,8 +7,8 @@
|
|
# business (mosaic-bus@<business>). Nothing here reads a capability, the
|
|
# binding or the token. See packages/cli/README.md, "Bus host".
|
|
#
|
|
-# render [--claude PATH] print the rendered unit on stdout
|
|
-# install [--dir DIR] [--no-reload] [--claude PATH]
|
|
+# render [--copy ID [--copies DIR]] [--pm] [--claude PATH] print the rendered unit on stdout
|
|
+# install [--copy ID [--copies DIR]] [--pm] [--dir DIR] [--no-reload] [--claude PATH]
|
|
# write the unit (write to a temp file, then
|
|
# rename) and run `systemctl --user daemon-reload`
|
|
# uninstall [--dir DIR] [--no-reload]
|
|
@@ -115,7 +115,7 @@ install_guard() {
|
|
[ -e "$directory" ] || [ -L "$directory" ] || continue
|
|
[ -d "$directory" ] && [ -r "$directory" ] && [ -x "$directory" ] || die "refusing: cannot inspect unit path $directory; nothing was changed"
|
|
# * includes the empty instance, i.e. [email protected].
|
|
- for dropdir in "$directory"/mosaic-bus@*.service.d; do
|
|
+ for dropdir in "$directory"/mosaic-bus@*.service.d "$directory"/mosaic-.service.d "$directory"/service.d; do
|
|
[ -e "$dropdir" ] || [ -L "$dropdir" ] || continue
|
|
[ -d "$dropdir" ] && [ -r "$dropdir" ] && [ -x "$dropdir" ] || die "refusing: cannot inspect drop-ins $dropdir; nothing was changed"
|
|
for file in "$dropdir"/*.conf; do
|
|
diff --git a/scripts/stack-copy.sh b/scripts/stack-copy.sh
|
|
index a3b6f9143..f384a983d 100755
|
|
--- a/scripts/stack-copy.sh
|
|
+++ b/scripts/stack-copy.sh
|
|
@@ -66,7 +66,7 @@ wrapper() {
|
|
[ "$(grep -Fxc "$lease" "$file" || true)" = 1 ] || die 'refusing: Q81 lease line missing or ambiguous'
|
|
tmp="$(mktemp "$BIN/.mosaic.XXXXXX")"
|
|
trap 'rm -f "$tmp"' EXIT
|
|
- printf '#!/usr/bin/env bash\n%s\n%s\nexec "%s/scripts/mosaic" "$@"\n' "$MARKER" "$lease" "$root" > "$tmp"
|
|
+ printf '#!/usr/bin/env bash\n%s\n# CLI runs from pinned copy: %s\n# The Q81 lease probe still answers at mosaic-old.\n%s\nexec "%s/scripts/mosaic" "$@"\n' "$MARKER" "$root" "$lease" "$root" > "$tmp"
|
|
chmod 0755 "$tmp";mv -f "$tmp" "$file";trap - EXIT
|
|
echo "wrapper: $root"
|
|
}
|