fix(cli): build review fixes for the parity verbs (#1873, P1-3)

- autonomy set: an empty --authority-resolver clears the resolver as the
  panel does; --reverse-window-hours takes 1 to 168, as the page and bus do.
- The default instance gets a one-line refusal (exit 3) naming the named
  instance and --dev-local-owner, not a stack trace.
- tasks create|edit|archive: a value starting with -- is a usage error;
  --business is checked before the agent guard, and the guard also reads
  process.env.
- Drop the unused consoleRefusal export.
- Docs: --business is required, resolver clearing, window range, and the
  named-instance requirement.
This commit was merged in pull request #1946.
This commit is contained in:
jason.woltje committed 2026-11-11 20:47:35 -06:00
1 parent 226e28d1e6
commit 666f4d6864
7 files changed
+61 -20

No files matched your search

+5 -5
View File
@@ -317,11 +317,11 @@ scripts/mosaic org role-templates <org> [--json]
scripts/mosaic org hire|change <org> --name <name> --template <template> --harness <harness> --model <model> [--thinking <level>] [--level org|project|root] [--project <project>] --reason <text> [--json] scripts/mosaic org hire|change <org> --name <name> --template <template> --harness <harness> --model <model> [--thinking <level>] [--level org|project|root] [--project <project>] --reason <text> [--json]
scripts/mosaic org fire <org> --name <name> [--level org|project|root] [--project <project>] --reason <text> [--json] scripts/mosaic org fire <org> --name <name> [--level org|project|root] [--project <project>] --reason <text> [--json]
scripts/mosaic org autonomy <org> [--json] scripts/mosaic org autonomy <org> [--json]
scripts/mosaic org autonomy <org> set --revision <rev> [--cross-role|--merge|--policy|--authority in|near|out] [--notices on|off] [--reverse-window-hours <n>] [--authority-resolver <name>] [--json] scripts/mosaic org autonomy <org> set --revision <rev> [--cross-role|--merge|--policy|--authority in|near|out] [--notices on|off] [--reverse-window-hours 1-168] [--authority-resolver <name>|""] [--json]
scripts/mosaic org autonomy <org> review cross-role|merge|policy|authority [--json] scripts/mosaic org autonomy <org> review cross-role|merge|policy|authority [--json]
scripts/mosaic tasks create --title <text> --requirement <REQ-X-N> [--project <name>] [--state <state>] [--assignees <role,...>] [--priority 0-5] [--due <iso>|none] [--business <id>] [--json] scripts/mosaic tasks create --title <text> --requirement <REQ-X-N> [--project <name>] [--state <state>] [--assignees <role,...>] [--priority 0-5] [--due <iso>|none] --business <id> [--json]
scripts/mosaic tasks edit task:<n> --expect <digest> [--title ...] [--state ...] [--assignees ...] [--priority ...] [--due ...] [--business <id>] [--json] scripts/mosaic tasks edit task:<n> --expect <digest> [--title ...] [--state ...] [--assignees ...] [--priority ...] [--due ...] --business <id> [--json]
scripts/mosaic tasks archive task:<n> --expect <digest> [--business <id>] [--json] scripts/mosaic tasks archive task:<n> --expect <digest> --business <id> [--json]
scripts/mosaic talk <role> <text> [--wait SECONDS] [--business <id>] scripts/mosaic talk <role> <text> [--wait SECONDS] [--business <id>]
scripts/mosaic project create <name> [--repo <repo>] | list [--json] scripts/mosaic project create <name> [--repo <repo>] | list [--json]
scripts/mosaic threads [--project <project>] [--json] [--tester] scripts/mosaic threads [--project <project>] [--json] [--tester]
@@ -344,7 +344,7 @@ For local development on a named instance only, `mosaic bus start BUSINESS --dev
`mosaic console restart` and `mosaic org reload ORG` are the Console's Settings controls (F3) from a terminal (U1, #1873). Each sends one request to the running Console named in the instance's console state, as the page sends it (the Console's own Host and Origin, `Sec-Fetch-Site: same-origin`, a JSON `{}` body), so the Console applies the same owner, `--send` and origin checks and writes the same action trail, `<dataRoot>/console/actions.jsonl`. Both refuse inside an agent run (exit 3) before reading anything, and refuse with exit 3 when no Console runs. `console restart` works only for a Console started in the foreground by `mosaic console start`, or by `mosaic console start --dev-local-owner --detach`, which leaves a detached supervisor (`packages/cli/src/console-supervisor.mjs`, its own session) running the Console as the foreground start does; its receipt adds the supervisor's `supervisor` pid, and its later warnings go to `console/supervisor-*.log` under the data root. `mosaic console stop` stops that Console, and its supervisor then exits. `mosaic console start` refuses inside an agent run (exit 3), with or without `--dev-local-owner` or `--detach`, checking the starting process's own environment as well as the one given for the Console (agent-guard, #1873). A detached start whose supervisor does not reply within 60 seconds signals the supervisor's process group, then kills it, and clears `console/start.lock` only when the lock names that supervisor (pid and start time) and it is gone. Any other Console answers `not-supervised`. `console restart` waits up to 30 seconds for the relaunched Console to answer and prints `{"outcome":"restarted","from":OLD,"boot":NEW}`. `org reload ORG` restarts a running bus or starts a stopped one and prints the Console's done receipt. A request that left but got no answer is reported as outcome unknown (exit 1) and is never retried; check `mosaic console status`, `mosaic bus status ORG` and the trail. `mosaic bus restart ORG` remains the trail-less host verb. `mosaic console restart` and `mosaic org reload ORG` are the Console's Settings controls (F3) from a terminal (U1, #1873). Each sends one request to the running Console named in the instance's console state, as the page sends it (the Console's own Host and Origin, `Sec-Fetch-Site: same-origin`, a JSON `{}` body), so the Console applies the same owner, `--send` and origin checks and writes the same action trail, `<dataRoot>/console/actions.jsonl`. Both refuse inside an agent run (exit 3) before reading anything, and refuse with exit 3 when no Console runs. `console restart` works only for a Console started in the foreground by `mosaic console start`, or by `mosaic console start --dev-local-owner --detach`, which leaves a detached supervisor (`packages/cli/src/console-supervisor.mjs`, its own session) running the Console as the foreground start does; its receipt adds the supervisor's `supervisor` pid, and its later warnings go to `console/supervisor-*.log` under the data root. `mosaic console stop` stops that Console, and its supervisor then exits. `mosaic console start` refuses inside an agent run (exit 3), with or without `--dev-local-owner` or `--detach`, checking the starting process's own environment as well as the one given for the Console (agent-guard, #1873). A detached start whose supervisor does not reply within 60 seconds signals the supervisor's process group, then kills it, and clears `console/start.lock` only when the lock names that supervisor (pid and start time) and it is gone. Any other Console answers `not-supervised`. `console restart` waits up to 30 seconds for the relaunched Console to answer and prints `{"outcome":"restarted","from":OLD,"boot":NEW}`. `org reload ORG` restarts a running bus or starts a stopped one and prints the Console's done receipt. A request that left but got no answer is reported as outcome unknown (exit 1) and is never retried; check `mosaic console status`, `mosaic bus status ORG` and the trail. `mosaic bus restart ORG` remains the trail-less host verb.
The hire form and the autonomy panel from a terminal (E2, #1873, P1-3). `mosaic org role-templates|hire|fire|change ORG` and `mosaic org autonomy ORG [set|review]` send the page's own request to the running Console's org route (`/api/orgs/ORG/decisions/hire/templates|start`, `/api/orgs/ORG/autonomy[/review]`), as `org reload` does, so the Console applies the same owner, `--send`, origin, body and live-session checks and the same bus verbs run (`hire.templates.console`, `hire.start.console`, `autonomy.get.console`, `autonomy.set.console`, `autonomy.review.console`). `hire` and `change` read the templates first and send the chosen template's name and sha256, as the form does. `autonomy set` needs the 64-hex `--revision` from `mosaic org autonomy ORG` and sends every setting the panel sends, with only the named ones changed; a stale revision is refused `revision-conflict`. A Console refusal exits 3 with the Console's code and message; `outcome-unknown` exits 1; an unknown template exits 2. Every one of these verbs, reads included, refuses inside an agent run. `mosaic tasks create|edit|archive` are the task board's three writes: the arguments pass the board's own check (`workspace-tasks.mjs parseTaskWrite`) and go to the bus as the person (`tasks.create|edit|archive`, human transport), so the bus runs its human, field, digest and archive checks. A stale `--expect` is refused `task-conflict`; read the digest with `mosaic tasks --json`. The hire form and the autonomy panel from a terminal (E2, #1873, P1-3). `mosaic org role-templates|hire|fire|change ORG` and `mosaic org autonomy ORG [set|review]` send the page's own request to the running Console's org route (`/api/orgs/ORG/decisions/hire/templates|start`, `/api/orgs/ORG/autonomy[/review]`), as `org reload` does, so the Console applies the same owner, `--send`, origin, body and live-session checks and the same bus verbs run (`hire.templates.console`, `hire.start.console`, `autonomy.get.console`, `autonomy.set.console`, `autonomy.review.console`). `hire` and `change` read the templates first and send the chosen template's name and sha256, as the form does. `autonomy set` needs the 64-hex `--revision` from `mosaic org autonomy ORG` and sends every setting the panel sends, with only the named ones changed; a stale revision is refused `revision-conflict`; an empty `--authority-resolver ""` clears the resolver, as emptying the panel's field does. These verbs need a named instance (`MOSAIC_CONFIG` set to `~/.config/mosaic-NAME/config.json`) whose Console runs with `--dev-local-owner`; the default instance is refused with exit 3. A Console refusal exits 3 with the Console's code and message; `outcome-unknown` exits 1; an unknown template exits 2. Every one of these verbs, reads included, refuses inside an agent run. `mosaic tasks create|edit|archive` are the task board's three writes: the arguments pass the board's own check (`workspace-tasks.mjs parseTaskWrite`) and go to the bus as the person (`tasks.create|edit|archive`, human transport), so the bus runs its human, field, digest and archive checks. A stale `--expect` is refused `task-conflict`; read the digest with `mosaic tasks --json`.
The bus and Console lifecycle verbs refuse inside an agent run (agent-guard, #1873): `mosaic bus start`, `bus stop`, `bus restart` and `bus recover`, and `mosaic console start`, `console stop` and `console restart`, and `mosaic org archive` and `org delete`, which stop the organization's bus, and `mosaic org restore`, which starts it (`org restore --no-bus` starts nothing and is not refused), and `mosaic org role-templates`, `hire`, `fire`, `change` and `autonomy` (E2). Each exits 3 with `refused: this runs only from a human shell, outside any agent run (found MARKER in the environment)` before it reads anything. It checks the environment it is given and its own process environment, so a sanitized environment does not hide a marker. A usage error (a missing or extra argument, `--pm` with `--no-launch`, or a bad `--dev-insecure-bind`/`--dev-allow-host`) still comes first (exit 4). `mosaic bus status` and `mosaic console status` stay open to agents. The Console's Restart bus and Restart Console controls keep working: the Console starts its bus host with its own environment, which carries no marker, and Restart Console relaunches the Console under its supervisor without going through the CLI. The markers are the ones `packages/cli/src/transport.mjs` lists. The check is cooperative: a process that drops the markers from its environment is not stopped by it. The bus and Console lifecycle verbs refuse inside an agent run (agent-guard, #1873): `mosaic bus start`, `bus stop`, `bus restart` and `bus recover`, and `mosaic console start`, `console stop` and `console restart`, and `mosaic org archive` and `org delete`, which stop the organization's bus, and `mosaic org restore`, which starts it (`org restore --no-bus` starts nothing and is not refused), and `mosaic org role-templates`, `hire`, `fire`, `change` and `autonomy` (E2). Each exits 3 with `refused: this runs only from a human shell, outside any agent run (found MARKER in the environment)` before it reads anything. It checks the environment it is given and its own process environment, so a sanitized environment does not hide a marker. A usage error (a missing or extra argument, `--pm` with `--no-launch`, or a bad `--dev-insecure-bind`/`--dev-allow-host`) still comes first (exit 4). `mosaic bus status` and `mosaic console status` stay open to agents. The Console's Restart bus and Restart Console controls keep working: the Console starts its bus host with its own environment, which carries no marker, and Restart Console relaunches the Console under its supervisor without going through the CLI. The markers are the ones `packages/cli/src/transport.mjs` lists. The check is cooperative: a process that drops the markers from its environment is not stopped by it.
+8 -5
View File
@@ -28,9 +28,9 @@ mosaic inbox [--business <id>] [--json]
mosaic inbox --read <id>... | --read-all [--business <id>] mosaic inbox --read <id>... | --read-all [--business <id>]
mosaic decide <decision> <option> [--note <text>] [--yes] [--business <id>] mosaic decide <decision> <option> [--note <text>] [--yes] [--business <id>]
mosaic tasks [--business <id>] [--json] mosaic tasks [--business <id>] [--json]
mosaic tasks create --title <text> --requirement <REQ-X-N> [--project <name>] [--state <state>] [--assignees <role,...>] [--priority 0-5] [--due <iso>|none] [--business <id>] [--json] mosaic tasks create --title <text> --requirement <REQ-X-N> [--project <name>] [--state <state>] [--assignees <role,...>] [--priority 0-5] [--due <iso>|none] --business <id> [--json]
mosaic tasks edit task:<n> --expect <digest> [--title <text>] [--state <state>] [--assignees <role,...>] [--priority 0-5] [--due <iso>|none] [--business <id>] [--json] mosaic tasks edit task:<n> --expect <digest> [--title <text>] [--state <state>] [--assignees <role,...>] [--priority 0-5] [--due <iso>|none] --business <id> [--json]
mosaic tasks archive task:<n> --expect <digest> [--business <id>] [--json] mosaic tasks archive task:<n> --expect <digest> --business <id> [--json]
mosaic agents [--business <id>] [--json] [--tester] mosaic agents [--business <id>] [--json] [--tester]
mosaic trail <task|decision> [--business <id>] [--json] mosaic trail <task|decision> [--business <id>] [--json]
mosaic talk <role> <text> [--wait <seconds> | --follow] [--business <id>] mosaic talk <role> <text> [--wait <seconds> | --follow] [--business <id>]
@@ -1323,7 +1323,7 @@ mosaic org role-templates ORG [--json]
mosaic org hire|change ORG --name NAME --template TEMPLATE --harness H --model M [--thinking T] [--level org|project|root] [--project P] --reason TEXT [--json] mosaic org hire|change ORG --name NAME --template TEMPLATE --harness H --model M [--thinking T] [--level org|project|root] [--project P] --reason TEXT [--json]
mosaic org fire ORG --name NAME [--level org|project|root] [--project P] --reason TEXT [--json] mosaic org fire ORG --name NAME [--level org|project|root] [--project P] --reason TEXT [--json]
mosaic org autonomy ORG [--json] mosaic org autonomy ORG [--json]
mosaic org autonomy ORG set --revision REV [--cross-role|--merge|--policy|--authority in|near|out] [--notices on|off] [--reverse-window-hours N] [--authority-resolver NAME] [--json] mosaic org autonomy ORG set --revision REV [--cross-role|--merge|--policy|--authority in|near|out] [--notices on|off] [--reverse-window-hours 1-168] [--authority-resolver NAME|""] [--json]
mosaic org autonomy ORG review cross-role|merge|policy|authority [--json] mosaic org autonomy ORG review cross-role|merge|policy|authority [--json]
``` ```
@@ -1333,7 +1333,10 @@ and session checks and the same bus verbs. `hire` and `change` read the role
templates first and send the chosen template's name and sha256. `autonomy set` templates first and send the chosen template's name and sha256. `autonomy set`
needs the 64-hex `--revision` that `mosaic org autonomy ORG` prints and sends every needs the 64-hex `--revision` that `mosaic org autonomy ORG` prints and sends every
setting the panel sends, changing only the named ones; a stale revision is setting the panel sends, changing only the named ones; a stale revision is
refused `revision-conflict`. A Console refusal exits 3 with its code and refused `revision-conflict`. `--authority-resolver ""` clears the resolver, as
emptying the panel's field does, and `--reverse-window-hours` takes 1 to 168.
These verbs need a named instance whose Console runs with `--dev-local-owner`;
the default instance is refused with exit 3. A Console refusal exits 3 with its code and
message, an answer that never came exits 1, an unknown template exits 2. All of message, an answer that never came exits 1, an unknown template exits 2. All of
these verbs, reads included, refuse inside an agent run (exit 3) after their these verbs, reads included, refuse inside an agent run (exit 3) after their
usage checks (exit 4) and before any config is read. usage checks (exit 4) and before any config is read.
-1
View File
@@ -391,4 +391,3 @@ export async function consoleOrgRequest(ctx, id, { method = 'GET', path, body, t
throw new CliError('the Console did not answer; see mosaic console status', 1); throw new CliError('the Console did not answer; see mosaic console status', 1);
} }
} }
export { refusal as consoleRefusal };
+9 -4
View File
@@ -19,7 +19,7 @@ import { consoleOrgRequest } from './org-console.mjs';
import { refuseInsideAgent } from './transport.mjs'; import { refuseInsideAgent } from './transport.mjs';
export const ORG_OWNER_VERBS = Object.freeze(['role-templates', 'hire', 'fire', 'change', 'autonomy']); export const ORG_OWNER_VERBS = Object.freeze(['role-templates', 'hire', 'fire', 'change', 'autonomy']);
export const ORG_OWNER_USAGE = 'mosaic org role-templates ID [--json] | hire|change ID --name NAME --template TEMPLATE --harness HARNESS --model MODEL [--thinking LEVEL] [--level org|project|root] [--project PROJECT] --reason TEXT [--json] | fire ID --name NAME [--level org|project|root] [--project PROJECT] --reason TEXT [--json] | autonomy ID [--json] | autonomy ID set --revision REV [--cross-role|--merge|--policy|--authority in|near|out] [--notices on|off] [--reverse-window-hours N] [--authority-resolver NAME] [--json] | autonomy ID review cross-role|merge|policy|authority [--json]'; export const ORG_OWNER_USAGE = 'mosaic org role-templates ID [--json] | hire|change ID --name NAME --template TEMPLATE --harness HARNESS --model MODEL [--thinking LEVEL] [--level org|project|root] [--project PROJECT] --reason TEXT [--json] | fire ID --name NAME [--level org|project|root] [--project PROJECT] --reason TEXT [--json] | autonomy ID [--json] | autonomy ID set --revision REV [--cross-role|--merge|--policy|--authority in|near|out] [--notices on|off] [--reverse-window-hours N] [--authority-resolver NAME|""] [--json] | autonomy ID review cross-role|merge|policy|authority [--json]';
const usage = why => new CliError(`${why}\nusage: ${ORG_OWNER_USAGE}`, 4); const usage = why => new CliError(`${why}\nusage: ${ORG_OWNER_USAGE}`, 4);
const ORG_ID = /^[a-z0-9][a-z0-9._-]{0,63}$/; const ORG_ID = /^[a-z0-9][a-z0-9._-]{0,63}$/;
@@ -77,7 +77,7 @@ function autonomyChanges(v) {
changes.notices = v['--notices'] === 'on'; changes.notices = v['--notices'] === 'on';
} }
if (v['--reverse-window-hours'] !== undefined) { if (v['--reverse-window-hours'] !== undefined) {
if (!/^[1-9][0-9]{0,2}$/.test(v['--reverse-window-hours'])) throw usage('--reverse-window-hours takes a whole number of hours'); if (!/^[1-9][0-9]{0,2}$/.test(v['--reverse-window-hours']) || Number(v['--reverse-window-hours']) > 168) throw usage('--reverse-window-hours takes a whole number of hours from 1 to 168');
changes.reverseWindowHours = Number(v['--reverse-window-hours']); changes.reverseWindowHours = Number(v['--reverse-window-hours']);
} }
if (v['--authority-resolver'] !== undefined) changes.authorityResolver = v['--authority-resolver']; if (v['--authority-resolver'] !== undefined) changes.authorityResolver = v['--authority-resolver'];
@@ -94,7 +94,10 @@ export function autonomyDocument(current, changes) {
doc.notices = shown.notices === true; doc.notices = shown.notices === true;
doc.reverseWindowHours = shown.reverseWindowHours ?? 24; doc.reverseWindowHours = shown.reverseWindowHours ?? 24;
if (shown.authorityResolver) doc.authorityResolver = shown.authorityResolver; if (shown.authorityResolver) doc.authorityResolver = shown.authorityResolver;
return { ...doc, ...changes }; const out = { ...doc, ...changes };
// An empty --authority-resolver clears it: the panel leaves an emptied field out of its save.
if (out.authorityResolver === '') delete out.authorityResolver;
return out;
} }
// Autonomy codes carry no message from the route; these are the panel's own words (autonomy.js). // Autonomy codes carry no message from the route; these are the panel's own words (autonomy.js).
@@ -189,7 +192,9 @@ export async function orgOwnerCommand(args, io, deps = {}) {
// environment as well as io.env, as org archive|delete check. // environment as well as io.env, as org archive|delete check.
refuseInsideAgent(process.env); refuseInsideAgent(process.env);
refuseInsideAgent(io.env); refuseInsideAgent(io.env);
const instance = devOwnerInstance(io.env); let instance;
try { instance = devOwnerInstance(io.env); }
catch (e) { throw new CliError(`${e.message}; these verbs need a named instance whose Console runs with --dev-local-owner`, 3); }
const system = deps.system ?? loadSystem({ env: io.env, instance }); const system = deps.system ?? loadSystem({ env: io.env, instance });
const ctx = { configDir: configDir(io.env), dataRoot: system.dataRoot, instance, principal: DEV_OWNER }; const ctx = { configDir: configDir(io.env), dataRoot: system.dataRoot, instance, principal: DEV_OWNER };
const { value, text } = await run(ctx); const { value, text } = await run(ctx);
+11 -4
View File
@@ -9,11 +9,12 @@
import { parseTaskWrite } from '../../webui/src/workspace-tasks.mjs'; import { parseTaskWrite } from '../../webui/src/workspace-tasks.mjs';
import { CliError } from './errors.mjs'; import { CliError } from './errors.mjs';
import { orgSelector } from './org-selector.mjs'; import { orgSelector } from './org-selector.mjs';
import { refuseInsideAgent } from './transport.mjs';
export const TASKS_WRITE_OPS = Object.freeze(['create', 'edit', 'archive']); export const TASKS_WRITE_OPS = Object.freeze(['create', 'edit', 'archive']);
export const TASKS_WRITE_USAGE = 'mosaic tasks create --title TEXT --requirement REQ-X-N [--project NAME] [--state STATE] [--assignees ROLE,...] [--priority 0-5] [--due ISO|none] [--business <id>] [--json]\n' + export const TASKS_WRITE_USAGE = 'mosaic tasks create --title TEXT --requirement REQ-X-N [--project NAME] [--state STATE] [--assignees ROLE,...] [--priority 0-5] [--due ISO|none] --business <id> [--json]\n' +
' mosaic tasks edit task:N --expect DIGEST [--title TEXT] [--state STATE] [--assignees ROLE,...] [--priority 0-5] [--due ISO|none] [--business <id>] [--json]\n' + ' mosaic tasks edit task:N --expect DIGEST [--title TEXT] [--state STATE] [--assignees ROLE,...] [--priority 0-5] [--due ISO|none] --business <id> [--json]\n' +
' mosaic tasks archive task:N --expect DIGEST [--business <id>] [--json]'; ' mosaic tasks archive task:N --expect DIGEST --business <id> [--json]';
const usage = why => new CliError(`${why}\nusage: ${TASKS_WRITE_USAGE}`, 4); const usage = why => new CliError(`${why}\nusage: ${TASKS_WRITE_USAGE}`, 4);
// Each op's options, as the board's op takes its keys (workspace-tasks.mjs OPS). // Each op's options, as the board's op takes its keys (workspace-tasks.mjs OPS).
@@ -51,11 +52,14 @@ export function parseTasksWrite(args) {
else if (a === '--json') { if (json) throw usage('duplicate --json'); json = true; } else if (a === '--json') { if (json) throw usage('duplicate --json'); json = true; }
else if (OPTIONS[op].includes(a)) { else if (OPTIONS[op].includes(a)) {
if (Object.hasOwn(values, a)) throw usage(`duplicate ${a}`); if (Object.hasOwn(values, a)) throw usage(`duplicate ${a}`);
if (i + 1 >= args.length) throw usage(`${a} needs one value`); if (i + 1 >= args.length || args[i + 1].startsWith('--')) throw usage(`${a} needs one value`);
values[a] = args[++i]; values[a] = args[++i];
} else if (a.startsWith('--')) throw usage(`tasks ${op} does not take ${a}`); } else if (a.startsWith('--')) throw usage(`tasks ${op} does not take ${a}`);
else positional.push(a); else positional.push(a);
} }
// The organization is named before anything else is checked, so a missing --business is a usage
// error (exit 4) inside an agent run too, as for the org owner verbs.
if (!selector.given) throw usage('name the organization with --business');
if (positional.length !== (op === 'create' ? 0 : 1)) throw usage(op === 'create' ? 'tasks create takes no task' : `tasks ${op} takes one task, task:N`); if (positional.length !== (op === 'create' ? 0 : 1)) throw usage(op === 'create' ? 'tasks create takes no task' : `tasks ${op} takes one task, task:N`);
const body = op === 'create' ? {} : { task_ref: positional[0] }; const body = op === 'create' ? {} : { task_ref: positional[0] };
for (const [flag, raw] of Object.entries(values)) { for (const [flag, raw] of Object.entries(values)) {
@@ -75,6 +79,9 @@ export function parseTasksWrite(args) {
export async function tasksWriteCommand(args, io, callFor) { export async function tasksWriteCommand(args, io, callFor) {
const write = parseTasksWrite(args); const write = parseTasksWrite(args);
// agent-guard (#1873): this process's own environment as well as io.env (callFor checks io.env),
// as the org owner verbs check.
refuseInsideAgent(process.env);
const parsed = { positional: [], flags: new Set(), values: write.business === undefined ? {} : { '--business': write.business } }; const parsed = { positional: [], flags: new Set(), values: write.business === undefined ? {} : { '--business': write.business } };
const call = callFor(parsed); const call = callFor(parsed);
let result; let result;
+27
View File
@@ -133,6 +133,7 @@ test('hire and autonomy verbs: usage errors first (exit 4), then refused inside
['org', 'autonomy', 'alpha', 'set', '--merge', 'near'], ['org', 'autonomy', 'alpha', 'set', '--merge', 'near'],
['org', 'autonomy', 'alpha', 'set', '--revision', REV(3), '--merge', 'sideways'], ['org', 'autonomy', 'alpha', 'set', '--revision', REV(3), '--merge', 'sideways'],
['org', 'autonomy', 'alpha', 'set', '--revision', '3', '--merge', 'near'], ['org', 'autonomy', 'alpha', 'set', '--revision', '3', '--merge', 'near'],
['org', 'autonomy', 'alpha', 'set', '--revision', REV(3), '--reverse-window-hours', '200'],
['org', 'autonomy', 'alpha', 'review', 'everything'], ['org', 'autonomy', 'alpha', 'review', 'everything'],
['org', 'role-templates', 'Not An Id'], ['org', 'role-templates', 'Not An Id'],
]) { ]) {
@@ -151,6 +152,10 @@ test('hire and autonomy verbs: usage errors first (exit 4), then refused inside
await assert.rejects(orgCommand(args.slice(1), x, {}), e => e instanceof CliError && e.exitCode === 3 && /outside any agent run/.test(e.message), args.join(' ')); await assert.rejects(orgCommand(args.slice(1), x, {}), e => e instanceof CliError && e.exitCode === 3 && /outside any agent run/.test(e.message), args.join(' '));
} }
} finally { if (saved === undefined) delete process.env.CLAUDECODE; else process.env.CLAUDECODE = saved; } } finally { if (saved === undefined) delete process.env.CLAUDECODE; else process.env.CLAUDECODE = saved; }
// The default instance is a one-line refusal (exit 3), not an uncaught error.
const d = io(humanEnv());
delete d.env.MOSAIC_CONFIG;
await assert.rejects(orgCommand(['autonomy', 'alpha'], d, {}), e => e instanceof CliError && e.exitCode === 3 && /requires a named instance.*--dev-local-owner/.test(e.message));
}); });
test('role-templates, hire, change and fire send the hire form\'s request and get its answer, refusals included', { timeout: 120000 }, async t => { test('role-templates, hire, change and fire send the hire form\'s request and get its answer, refusals included', { timeout: 120000 }, async t => {
@@ -233,6 +238,9 @@ test('autonomy reads, saves with the panel\'s revision check and records a revie
// Imported here so that the other tests still run, and fail on their own, on a base without it. // Imported here so that the other tests still run, and fail on their own, on a base without it.
const { autonomyDocument } = await import('../src/org-owner.mjs'); const { autonomyDocument } = await import('../src/org-owner.mjs');
assert.deepEqual(autonomyDocument({ revision: REV(3), levels: LOOP, autonomy: { notices: false } }, { merge: 'near the loop' }), panel); assert.deepEqual(autonomyDocument({ revision: REV(3), levels: LOOP, autonomy: { notices: false } }, { merge: 'near the loop' }), panel);
// An emptied resolver field: the panel leaves it out of the save, and so does the terminal.
assert.deepEqual(autonomyDocument({ revision: REV(3), levels: LOOP, autonomy: { notices: false, authorityResolver: 'ops' } }, { authorityResolver: '' }), { ...LOOP, notices: false, reverseWindowHours: 24 });
assert.equal(autonomyDocument({ revision: REV(3), levels: LOOP, autonomy: { notices: false, authorityResolver: 'ops' } }, {}).authorityResolver, 'ops');
r = await mosaic(env, ['org', 'autonomy', 'alpha', 'set', '--revision', REV(3), '--merge', 'near', '--json']); r = await mosaic(env, ['org', 'autonomy', 'alpha', 'set', '--revision', REV(3), '--merge', 'near', '--json']);
assert.equal(r.status, 0, r.stderr); assert.equal(r.status, 0, r.stderr);
assert.deepEqual(JSON.parse(r.stdout), { revision: REV(4), transaction: 'tx-4', writable: true }); assert.deepEqual(JSON.parse(r.stdout), { revision: REV(4), transaction: 'tx-4', writable: true });
@@ -332,6 +340,25 @@ test('tasks create, edit and archive send the board\'s writes to the bus as the
assert.ok(r.err.includes(boardError.message), `${r.err} / ${boardError.message}`); assert.ok(r.err.includes(boardError.message), `${r.err} / ${boardError.message}`);
} }
assert.deepEqual(s.calls, []); assert.deepEqual(s.calls, []);
// Terminal-only slips the board cannot make are usage too: an option-looking word is not a value,
// and the organization is named (exit 4 even inside an agent run).
for (const [argv, env] of [
[['tasks', 'create', '--title', '--json', '--requirement', 'REQ-TASK-1', '--business', 'demo'], humanEnv()],
[['tasks', 'create', '--title', 'x', '--requirement', 'REQ-TASK-1'], { ...humanEnv(), CLAUDECODE: '1' }],
]) {
const y = io(env);
await assert.rejects(main(argv, y, { system: { dataRoot: s.dataRoot }, transport: s.transport }), e => e instanceof CliError && e.exitCode === 4, argv.join(' '));
}
assert.deepEqual(s.calls, []);
// A marker only in this process's own environment counts too.
const savedMarker = process.env.CLAUDECODE;
process.env.CLAUDECODE = '1';
try {
const y = io(humanEnv());
delete y.env.CLAUDECODE;
await assert.rejects(main(['tasks', 'create', '--title', 'x', '--requirement', 'REQ-TASK-1', '--business', 'demo'], y, { system: { dataRoot: s.dataRoot }, transport: s.transport }), e => e instanceof CliError && e.exitCode === 3 && /outside any agent run/.test(e.message));
} finally { if (savedMarker === undefined) delete process.env.CLAUDECODE; else process.env.CLAUDECODE = savedMarker; }
assert.deepEqual(s.calls, []);
// Inside an agent run the write is refused before anything is sent. // Inside an agent run the write is refused before anything is sent.
const x = io({ ...humanEnv(), CLAUDECODE: '1' }); const x = io({ ...humanEnv(), CLAUDECODE: '1' });
await assert.rejects(main(['tasks', 'create', '--title', 'x', '--requirement', 'REQ-TASK-1', '--business', 'demo'], x, { system: { dataRoot: s.dataRoot }, transport: s.transport }), e => e instanceof CliError && e.exitCode === 3); await assert.rejects(main(['tasks', 'create', '--title', 'x', '--requirement', 'REQ-TASK-1', '--business', 'demo'], x, { system: { dataRoot: s.dataRoot }, transport: s.transport }), e => e instanceof CliError && e.exitCode === 3);
+1 -1
View File
@@ -22,7 +22,7 @@ baseline CLI.
| Start, stop or restart a bus | `mosaic bus start <id>` / `mosaic bus stop <id>` / `mosaic bus restart <id>` | S6 integration hook | | Start, stop or restart a bus | `mosaic bus start <id>` / `mosaic bus stop <id>` / `mosaic bus restart <id>` | S6 integration hook |
| Restart (or first start) an organization's bus | `mosaic bus restart <id>`, or `mosaic bus start <id>` for a bus never started | Enabled (F3): the owner's **Restart bus** control, offered where a page says the bus is down or a change needs a restart; trailed | | Restart (or first start) an organization's bus | `mosaic bus restart <id>`, or `mosaic bus start <id>` for a bus never started | Enabled (F3): the owner's **Restart bus** control, offered where a page says the bus is down or a change needs a restart; trailed |
| Restart the Console | Stop and run `mosaic console start` again | Enabled (F3) when `mosaic console start` runs it in the foreground, or with `--dev-local-owner --detach` under a detached supervisor (D1): Settings > Console; the owner only; trailed. Other Consoles answer `not-supervised` | | Restart the Console | Stop and run `mosaic console start` again | Enabled (F3) when `mosaic console start` runs it in the foreground, or with `--dev-local-owner --detach` under a detached supervisor (D1): Settings > Console; the owner only; trailed. Other Consoles answer `not-supervised` |
| Save, review, confirm or reverse an organization's autonomy | Read, save and review: `mosaic org autonomy ORG`, `mosaic org autonomy ORG set --revision REV [--cross-role|--merge|--policy|--authority in|near|out] [--notices on|off] [--reverse-window-hours N] [--authority-resolver NAME]`, `mosaic org autonomy ORG review CLASS` (E2, through this route and its revision check); confirm and reverse act on inbox decisions and have no verb yet | Enabled (D1) on a `--dev-local-owner` Console: the owner only, on the selected organization's bus, behind a fence that passes only the four console verbs `autonomy.set.console`, `autonomy.review.console`, `decision.confirm.console` and `decision.reverse.console` | | Save, review, confirm or reverse an organization's autonomy | Read, save and review: `mosaic org autonomy ORG`, `mosaic org autonomy ORG set --revision REV [--cross-role|--merge|--policy|--authority in|near|out] [--notices on|off] [--reverse-window-hours 1-168] [--authority-resolver NAME|""]`, `mosaic org autonomy ORG review CLASS` (E2, through this route and its revision check); confirm and reverse act on inbox decisions and have no verb yet | Enabled (D1) on a `--dev-local-owner` Console: the owner only, on the selected organization's bus, behind a fence that passes only the four console verbs `autonomy.set.console`, `autonomy.review.console`, `decision.confirm.console` and `decision.reverse.console` |
| Hire, fire or change an agent; read role templates | `mosaic org role-templates ORG`; `mosaic org hire|change ORG --name NAME --template TEMPLATE --harness H --model M [--thinking T] [--level org|project|root] [--project P] --reason TEXT`; `mosaic org fire ORG --name NAME [--level ...] --reason TEXT` (E2, through this route) | Enabled (D4) for the owner on a selected organization's bus: one decision raised and approved through `hire.start.console`, with the bus's hire checks | | Hire, fire or change an agent; read role templates | `mosaic org role-templates ORG`; `mosaic org hire|change ORG --name NAME --template TEMPLATE --harness H --model M [--thinking T] [--level org|project|root] [--project P] --reason TEXT`; `mosaic org fire ORG --name NAME [--level ...] --reason TEXT` (E2, through this route) | Enabled (D4) for the owner on a selected organization's bus: one decision raised and approved through `hire.start.console`, with the bus's hire checks |
| Create, edit or archive a board task | `mosaic tasks create --title T --requirement REQ [--project P] [--state S] [--assignees R,...] [--priority 0-5] [--due ISO|none]`; `mosaic tasks edit task:N --expect DIGEST ...`; `mosaic tasks archive task:N --expect DIGEST` (E2: the board's own check, then the person's `tasks.create|edit|archive` on the bus) | Enabled for the owner on an organization with the built-in task store | | Create, edit or archive a board task | `mosaic tasks create --title T --requirement REQ [--project P] [--state S] [--assignees R,...] [--priority 0-5] [--due ISO|none]`; `mosaic tasks edit task:N --expect DIGEST ...`; `mosaic tasks archive task:N --expect DIGEST` (E2: the board's own check, then the person's `tasks.create|edit|archive` on the bus) | Enabled for the owner on an organization with the built-in task store |
| Create chat project linked to a configured repository | `mosaic project create <name> --repo <project> --org <id>` | Enabled | | Create chat project linked to a configured repository | `mosaic project create <name> --repo <project> --org <id>` | Enabled |