cli: hint at the pinned mosaic when the bus refuses another tree's proof (row 130, #1625)
When the broker answers human-required or tester-required, the CLI
now prints a second line saying the bus accepts these commands only
from its own tree and to run `mosaic` on PATH on a pinned host. The
entry block becomes an exported run(); exit codes are unchanged.
Lands Darkwing's row 130 round 1 candidate, approved by Filbert in
comment 28520 (rev 1228). Candidate manifest 9a5de54d, all four files
match. Landing gate on 9e837ce5f: 23 suites, GATE_PASSED 00:44:07Z.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -351,6 +351,11 @@ check is expected to refuse this restored mixed setup; #225 remains open.
|
||||
Later rollback repeats steps 2-6 with the prior shared copy's full id, each
|
||||
live step again approved by Mos. Nothing prunes copies or removes mosaic-old.
|
||||
|
||||
After a switch, the human commands and `--tester` work only through `mosaic`
|
||||
on PATH; the checkout's `scripts/mosaic` exits 3 with `human-required` or
|
||||
`tester-required`, and queue writes stay on it ([packages/cli/README.md, One
|
||||
pinned copy](../packages/cli/README.md#one-pinned-copy-row-85-1579)).
|
||||
|
||||
## Console service (`scripts/webui-service.sh`)
|
||||
|
||||
```bash
|
||||
|
||||
+12
-1
@@ -86,7 +86,9 @@ mosaic launches list [--json]
|
||||
exception to the agent check above: Mos or a T3 seat may run it for a PM
|
||||
test, and the broker still refuses it under a managed run
|
||||
(`tester-required`) or while the business var `bus.tester` isn't `true`
|
||||
(`tester-off`), each exit 3. `talk --tester` text must start with
|
||||
(`tester-off`), each exit 3. Like the human commands, `--tester` works
|
||||
only from the tree the bus runs from ("One pinned copy" below).
|
||||
`talk --tester` text must start with
|
||||
`[mos test]`, else exit 2 before any call. The role's reply comes back to
|
||||
the tester, not to the human's inbox. `decide`, `stop` and `launches`
|
||||
take no `--tester` (exit 4).
|
||||
@@ -494,6 +496,15 @@ 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
|
||||
turn may be in flight. Queue writes stay on the canonical scripts/mosaic.
|
||||
|
||||
The human commands and `--tester` run only from the tree the bus runs from
|
||||
(row 130, #1625). The broker checks the path of the calling human-cli.mjs or
|
||||
tester-cli.mjs against its own. On a host with a pinned copy that tree is
|
||||
the copy, and `mosaic` on PATH is the wrapper that runs it. The checkout's
|
||||
`scripts/mosaic` exits 3 with `human-required` or `tester-required` and
|
||||
prints one more line: "the bus accepts these commands only from the tree it
|
||||
runs from; on a pinned host run `mosaic` on PATH". Queue writes still go
|
||||
through the checkout's `scripts/mosaic`.
|
||||
|
||||
## Notifier
|
||||
|
||||
The notifier is a child of the host. Each poll (every 30 s) it reads the
|
||||
|
||||
@@ -388,12 +388,27 @@ export async function main(argv, io = { env: process.env, stdin: process.stdin,
|
||||
throw usage();
|
||||
}
|
||||
|
||||
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
||||
// The broker's human and tester proofs accept only the human-cli.mjs and
|
||||
// tester-cli.mjs of the tree the bus runs from. Since row 85 that is a
|
||||
// pinned copy, so the checkout's own scripts/mosaic is refused with these
|
||||
// codes (row 130). The message stays the bare code: app.mjs reads it.
|
||||
export const OTHER_TREE = "the bus accepts these commands only from the tree it runs from; on a pinned host run `mosaic` on PATH";
|
||||
const OTHER_TREE_CODES = new Set(["human-required", "tester-required"]);
|
||||
|
||||
// main, with a CliError printed to stderr. Returns the exit code.
|
||||
export async function run(argv, io = { env: process.env, stdin: process.stdin, stdout: process.stdout, stderr: process.stderr }, deps = {}) {
|
||||
try {
|
||||
await main(process.argv.slice(2));
|
||||
await main(argv, io, deps);
|
||||
return 0;
|
||||
} catch (error) {
|
||||
if (!(error instanceof CliError)) throw error;
|
||||
process.stderr.write(`mosaic: ${error.message}\n`);
|
||||
process.exitCode = error.exitCode;
|
||||
io.stderr.write(`mosaic: ${error.message}\n`);
|
||||
if (OTHER_TREE_CODES.has(error.code)) io.stderr.write(`mosaic: ${OTHER_TREE}\n`);
|
||||
return error.exitCode;
|
||||
}
|
||||
}
|
||||
|
||||
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
||||
const code = await run(process.argv.slice(2));
|
||||
if (code !== 0) process.exitCode = code;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { OTHER_TREE, run } from "../src/cli.mjs";
|
||||
import { humanTransport } from "../src/transport.mjs";
|
||||
import { humanEnv, io, tmp } from "./helpers.mjs";
|
||||
|
||||
// Row 130 (#1625): the broker accepts the human and tester proofs only from
|
||||
// the tree the bus runs from, so a refusal with human-required or
|
||||
// tester-required gets one more stderr line naming the command that works.
|
||||
// The spawn stands in for human-cli.mjs and tester-cli.mjs: it exits 1 with
|
||||
// the code on stderr, and the real transport parses it.
|
||||
const refusing = (code) => () => ({ status: 1, stdout: "", stderr: `${code}\n` });
|
||||
const HINT = `mosaic: ${OTHER_TREE}\n`;
|
||||
|
||||
test("a human command refused with human-required prints the other-tree line and exits 3", async (t) => {
|
||||
const x = io();
|
||||
const transport = (business) => humanTransport({ socket: "/nonexistent/bus.sock", business, env: {}, spawn: refusing("human-required") });
|
||||
const code = await run(["inbox", "--business", "demo"], x, { system: { dataRoot: tmp(t) }, transport });
|
||||
assert.equal(code, 3);
|
||||
assert.equal(x.err.text, `mosaic: human-required\n${HINT}`);
|
||||
assert.equal(x.out.text, "");
|
||||
});
|
||||
|
||||
test("a --tester command refused with tester-required prints the other-tree line and exits 3", async (t) => {
|
||||
const x = io({ ...humanEnv(), CLAUDECODE: "1" });
|
||||
const code = await run(["agents", "--tester", "--business", "demo"], x, { system: { dataRoot: tmp(t) }, testerSpawn: refusing("tester-required") });
|
||||
assert.equal(code, 3);
|
||||
assert.equal(x.err.text, `mosaic: tester-required\n${HINT}`);
|
||||
});
|
||||
|
||||
test("other exit-3 refusals print no other-tree line", async (t) => {
|
||||
const off = io({ ...humanEnv(), CLAUDECODE: "1" });
|
||||
assert.equal(await run(["agents", "--tester", "--business", "demo"], off, { system: { dataRoot: tmp(t) }, testerSpawn: refusing("tester-off") }), 3);
|
||||
assert.equal(off.err.text, "mosaic: tester-off\n");
|
||||
|
||||
const agent = io({ ...humanEnv(), CLAUDECODE: "1" });
|
||||
assert.equal(await run(["inbox", "--business", "demo"], agent, { system: { dataRoot: tmp(t) } }), 3);
|
||||
assert.match(agent.err.text, /^mosaic: refused: this runs only from a human shell/);
|
||||
assert.doesNotMatch(agent.err.text, /tree it runs from/);
|
||||
});
|
||||
|
||||
test("the line names no path", () => {
|
||||
assert.doesNotMatch(OTHER_TREE, /\//);
|
||||
});
|
||||
Reference in New Issue
Block a user