bus, harness: a role holding organization.change reads the organization revision itself with organization.revision and the organization_revision tool (row 134, #1669)

Researcher's candidate 8977d907 (patch 843fdee9), approved by Filbert in #1669 comment 29193. Built on d58c9c5cd and applied on 04155deff without fuzz; the patch's +/- lines hash the same on both bases. Row 171's one broker.mjs line (project-name-is-repo, at :2256) is the only change on a manifest path between them.
This commit is contained in:
2026-10-19 14:35:31 -05:00
parent 04155deff6
commit 3a600cf5b2
11 changed files with 181 additions and 35 deletions
+2 -2
View File
@@ -685,8 +685,8 @@ closed). Without `--json` the read prints JSON without `tokens` followed by
The Console's Assistant section (signed-in send mode) also sets an instance's
seat name, as `scripts/mosaic name` does, and stops the assistant's runs in the
human's threads. The chief of staff makes the same changes with its
`change_organization`, `set_name` and `end_run` tools. Until row 134 gives it a
revision read, the revision comes from the human.
`organization_revision`, `change_organization`, `set_name` and `end_run` tools;
`organization_revision` reads the revision that `change_organization` sends.
Convert an old organization file with `scripts/mosaic limits migrate --file
ORGANIZATION --models MAPPING --roles-dir roles > CONVERTED`. It needs no bus,
+2 -1
View File
@@ -192,7 +192,8 @@ an operator inspect content and delivery evidence after an uncertain result.
| `role.claim` | No args; claims capability's role/run. One current holder per business/instance; a revoked run cannot reclaim that role, including after restart. |
| `role.release` | No args; only its current holder releases. |
| `role.revoke` | `{role,decision}`; needs an approved decision for this exact target and holder incarnation, routed as REQ-DEC-2 says: the human's for an uncited gated raise, the arbiter's for one citing the current spec. A role whose file lists `role.end` (the chief of staff, row 123, REQ-COS-1) sends `{role,reason}` instead: no decision, a reason of 1 to 500 characters, logged in the `action.allowed` body. Sending both is `invalid-request`; the caller's own run is `role-end-refused`; any other role sending `reason` gets `decision-required`. An instance whose definition is in `END_PROTECTED` (the assistant) is ended or revoked only by the chief of staff or with a decision the human resolved through the CLI (`via` `cli`); anyone else's reason, or a decision an arbiter resolved, is `role-end-refused` (Jason's ruling Q169, row 139). A target whose definition is `THREAD_DEFINITION` (the assistant) has no role claim: the end revokes the current claim of each of its threads, refuses `not-held` when none is held, and returns `{revoked:true,threads:N}` (row 155, #1649). |
| `organization.set` | `{revision,limits?,models?,instances?,assistants?}`; the keys `limits.set` takes, through the same writer and receipt. Needs `organization.change` in the caller's role file (the chief of staff); the receipt's actor is `<definition>.<instance>`, e.g. `chief-of-staff.chief`. Every other key is `invalid-request`. `limits` and `limits.set` stay human-only. `assistants` (row 155, Q169) is described in the business README; a refusal from it passes through as its `organization-*` code. No verb gives an agent the revision until row 134, so the chief of staff sends one the human gave it. |
| `organization.set` | `{revision,limits?,models?,instances?,assistants?}`; the keys `limits.set` takes, through the same writer and receipt. Needs `organization.change` in the caller's role file (the chief of staff); the receipt's actor is `<definition>.<instance>`, e.g. `chief-of-staff.chief`. Every other key is `invalid-request`. `limits` and `limits.set` stay human-only. `assistants` (row 155, Q169) is described in the business README; a refusal from it passes through as its `organization-*` code. The chief of staff learns the revision from `organization.revision` (below); a stale one is `revision-conflict`. |
| `organization.revision` | No args (row 134, #1669). Answers `{revision}`, the organization file's current revision, and nothing else the human's `limits` read returns: no limits, models, instances or tokens. Needs `organization.change` in the caller's role file, with the same refusals as `organization.set` (`not-holder`, `read-only`, `tester-refused`, `agent-required` for the human, `decision-required` for a role whose only classification is cross-role; no shipped role is). A read never spends the grant and writes no receipt. Any argument is `invalid-request`. `limits` stays human-only. |
| `decision.raise` | `{action,question,options:[{key,text}],recommendation,blocking,domain?,target?,project?,task_ref?,requirement_ref?,supersedes?,choice?,approvalChoice?}`. Returns decision with decoded options and authorization context. A blocking decision holds a task: `blocking` true with no `task_ref` refuses `task-required`. A non-blocking one needs no task, and an uncited gated action such as `prd.approve` or `role.revoke` still routes to the human's inbox. |
| `decision.resolve` | `{id,choice,note?}`; option must exist, resolver must be routed role or outside-agent human CLI. Sends the raising role a DECISION message (Decision close messages, below). |
| `decision.seen` | `{id,note?}`; acknowledgement by resolver, not a resolution. |
+11 -5
View File
@@ -1221,14 +1221,18 @@ export class Broker {
return this.#requestProject(s, { verb: request.verb, args: structuredClone(args) });
if (PROJECT_FILE_VERBS.includes(request.verb))
return this.#requestProjectFile(s, { verb: request.verb, args: structuredClone(args) });
if (["limits", "limits.set", "organization.set", "plugins", "plugins.set", "instances", "instances.set", "routes", "routes.set"].includes(request.verb)) {
if (["limits", "limits.set", "organization.set", "organization.revision", "plugins", "plugins.set", "instances", "instances.set", "routes", "routes.set"].includes(request.verb)) {
// Row 123 (#1617, REQ-COS-1, REQ-LIMIT-1): organization.set is limits.set for a role whose
// file lists organization.change. It takes the same keys, runs the same write path and
// logs the same receipt; only the actor differs: the role definition and instance.
const role = request.verb === "organization.set";
// Row 134 (#1669): organization.revision is the read that goes with it. It answers the
// organization file's current revision, and nothing else the human's limits read returns,
// to a role whose file lists the same grant. A read never spends the grant.
const revisionRead = request.verb === "organization.revision";
const role = request.verb === "organization.set" || revisionRead;
if (!role && !s.human) fail("human-required");
if (!this.#organization) fail("not-configured");
if (["limits", "plugins", "instances", "routes"].includes(request.verb)) keys(args, []);
if (["limits", "plugins", "instances", "routes", "organization.revision"].includes(request.verb)) keys(args, []);
else if (request.verb === "plugins.set") keys(args, ["revision", "plugins"], ["revision", "plugins"]);
// Row 165 (#1662): instances.set is human-only and outside ORGANIZATION_KEYS, so
// organization.set and the chief of staff's tool never reach it (sage-0918).
@@ -1249,7 +1253,9 @@ export class Broker {
}
let result;
try {
result = this.#organization(s.business, role ? "limits.set" : request.verb, args, actor);
result = revisionRead
? { revision: this.#organization(s.business, "limits", {}, actor).revision }
: this.#organization(s.business, role ? "limits.set" : request.verb, args, actor);
} catch (e) {
if (e.code === "revision-conflict") fail("revision-conflict");
if (e.code?.startsWith("organization-")) fail(e.code);
@@ -1263,7 +1269,7 @@ export class Broker {
}
throw new BusError("outcome-unknown");
}
if (role) this.#store.transaction(() => this.#consumeAuthority(s, "organization.change"));
if (role && !revisionRead) this.#store.transaction(() => this.#consumeAuthority(s, "organization.change"));
this.#secretCheck(result);
return result;
}
+121 -2
View File
@@ -1,6 +1,6 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { chmodSync, copyFileSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { chmodSync, copyFileSync, existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { createHash } from 'node:crypto';
@@ -58,7 +58,10 @@ function setup(t) {
organization: (...args) => {
organization.push(args);
if (writer.error) throw writer.error;
return { revision: 'b'.repeat(64) };
// The human's limits read answers more than the revision; organization.revision must not.
return args[1] === 'limits'
? { revision: 'b'.repeat(64), limits: { total: 7 }, models: { m: {} }, tokens: { used: 1 } }
: { revision: 'b'.repeat(64) };
},
});
const caps = {};
@@ -286,6 +289,60 @@ test('REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set
assert.equal(organization.length, before);
});
// Row 134 (#1669, REQ-COS-1, REQ-LIMIT-1): the revision organization.set needs, for the role that may send it.
test('REQ-COS-1: organization.revision answers the revision alone to the chief of staff and spends nothing', (t) => {
const { call, organization, store, allowed, writer } = setup(t);
assert.deepEqual(call('chief', 'organization.revision'), { revision: 'b'.repeat(64) });
assert.deepEqual(call('chief', 'organization.revision', {}), { revision: 'b'.repeat(64) });
// It runs the human's limits read for the business and keeps its revision: no limits, models or tokens.
assert.deepEqual(organization, [
['demo', 'limits', {}, 'chief-of-staff.chief'],
['demo', 'limits', {}, 'chief-of-staff.chief'],
]);
// The human's own read is unchanged and still answers the whole file.
assert.deepEqual(Object.keys(call('human', 'limits')).sort(), ['limits', 'models', 'revision', 'tokens']);
// A read leaves no trace of the grant: no allowed event of any action, so nothing is spent.
assert.equal(allowed('organization.change').length, 0);
assert.equal(store.all("SELECT 1 FROM events WHERE kind='action.allowed'").length, 0);
// The write that follows still has the grant; the read before it, and after it, change nothing.
call('chief', 'organization.set', PATCH);
assert.equal(allowed('organization.change').length, 1);
call('chief', 'organization.revision');
assert.equal(allowed('organization.change').length, 1);
call('chief', 'organization.set', PATCH);
assert.equal(allowed('organization.change').length, 2);
// The read takes no arguments; a key is refused before the file is read.
const before = organization.length;
for (const args of [{ revision: 'a'.repeat(64) }, { limits: {} }, { anything: 1 }])
assert.throws(() => call('chief', 'organization.revision', args), /invalid-request/, JSON.stringify(args));
assert.equal(organization.length, before);
// A failing read passes the broker's refusal on and leaves the same trace as a refused write.
writer.error = new Error('disk');
assert.throws(() => call('chief', 'organization.revision'), /outcome-unknown/);
assert.equal(allowed('organization.change').length, 2);
});
test('REQ-COS-1, REQ-LIMIT-1: organization.revision is refused for every caller whose role file lacks organization.change', (t) => {
const { call, organization, allowed, refused } = setup(t);
for (const who of ['pm', 'coder', 'assistant'])
assert.throws(() => call(who, 'organization.revision'), /decision-required/, who);
assert.throws(() => call('human', 'organization.revision'), /agent-required/);
assert.throws(() => call('reader', 'organization.revision'), /read-only/);
assert.throws(() => call('tester', 'organization.revision'), /tester-refused/);
// Nothing read the file, and nothing was allowed; each role's refusal is logged.
assert.equal(organization.length, 0);
assert.equal(allowed('organization.change').length, 0);
assert.deepEqual(
refused().filter((e) => ['pm', 'coder', 'assistant'].includes(e.actor_role)).map((e) => [e.actor_role, e.body.code]),
['pm', 'coder', 'assistant'].map((who) => [who, 'decision-required']),
);
// The older human reads stay the human's, and the chief of staff has no way around them.
for (const verb of ['limits', 'plugins', 'instances', 'routes'])
assert.throws(() => call('chief', verb), /human-required/, verb);
assert.equal(organization.length, 0);
});
test('REQ-ROLE-3, REQ-LIMIT-1: a chief-of-staff change is validated, written and logged like a human\'s; an invalid one is refused', async (t) => {
const root = mkdtempSync(join(tmpdir(), 'bus-chief-runtime-'));
const rolesDir = rolesCopy(root);
@@ -354,6 +411,68 @@ test('REQ-ROLE-3, REQ-LIMIT-1: a chief-of-staff change is validated, written and
assert.equal(receipts().length, count);
});
test('REQ-COS-1: the chief of staff reads the revision, changes the file with it, and reads again after a stale one is refused', async (t) => {
const root = mkdtempSync(join(tmpdir(), 'bus-chief-revision-'));
const rolesDir = rolesCopy(root);
const file = join(root, 'businesses/demo.json');
writeJson(file, { ...businessDoc(root, 'demo'), plugins: { harness: ['pi'] } });
mkdirSync(join(root, 'data'), { mode: 0o700 });
const bus = await startBroker({
dataRoot: join(root, 'data'),
businesses: demo(),
organization: { dir: root, rolesDir, system: systemFor(root) },
});
t.after(async () => {
await bus.close();
rmSync(root, { recursive: true, force: true });
});
const { broker } = bus;
const human = broker.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true });
const chief = broker.bindLaunch({ business: 'demo', role: 'chief', run: 'chief-run', harness: 'pi' });
broker.request(chief, { verb: 'role.claim' });
const coder = broker.bindLaunch({ business: 'demo', role: 'coder', run: 'coder-run', harness: 'pi' });
broker.request(coder, { verb: 'role.claim' });
const log = join(root, 'businesses/.history/demo/changes.jsonl');
// No write yet means no log: the read must not start one.
const receipts = () => (existsSync(log) ? readFileSync(log, 'utf8').trim().split('\n').map(JSON.parse) : []);
const document = () => JSON.parse(readFileSync(file, 'utf8'));
const digest = () => createHash('sha256').update(readFileSync(file)).digest('hex');
// The read answers the file's own revision, as the human's read does, and only that. The file and
// the receipts are as they were.
const bytes = readFileSync(file, 'utf8');
const count = receipts().length;
const read = broker.request(chief, { verb: 'organization.revision' });
assert.deepEqual(Object.keys(read), ['revision']);
assert.match(read.revision, /^[a-f0-9]{64}$/);
assert.equal(read.revision, digest());
assert.equal(read.revision, broker.request(human, { verb: 'limits' }).revision);
assert.equal(readFileSync(file, 'utf8'), bytes);
assert.equal(receipts().length, count);
assert.throws(() => broker.request(coder, { verb: 'organization.revision' }), /decision-required/);
// That revision is the one organization.set takes.
const first = broker.request(chief, { verb: 'organization.set', args: { revision: read.revision, limits: { total: 9 } } });
assert.equal(document().launch.limits.total, 9);
assert.equal(first.revision, digest());
assert.equal(receipts().filter((r) => r.state === 'committed').at(-1).actor, 'chief-of-staff.chief');
// The human changes the file; the chief's revision is stale and its change is refused.
const byHuman = broker.request(human, { verb: 'limits.set', args: { revision: first.revision, limits: { total: 4 } } });
assert.throws(
() => broker.request(chief, { verb: 'organization.set', args: { revision: first.revision, limits: { total: 8 } } }),
/revision-conflict/,
);
assert.equal(document().launch.limits.total, 4);
// Reading again gives the new revision, and the change then lands.
const again = broker.request(chief, { verb: 'organization.revision' });
assert.equal(again.revision, byHuman.revision);
assert.notEqual(again.revision, read.revision);
broker.request(chief, { verb: 'organization.set', args: { revision: again.revision, limits: { total: 8 } } });
assert.equal(document().launch.limits.total, 8);
});
test('Q169: the chief of staff\'s change of the assistant is validated, written and logged; a stale or protection-stripping one is refused', async (t) => {
const root = mkdtempSync(join(tmpdir(), 'bus-chief-assistant-'));
const rolesDir = rolesCopy(root);
+3 -2
View File
@@ -416,8 +416,9 @@ assistant. The update key is `assistants`, next to `limits`, `models` and
`read` returns `assistants` as well: each assistant instance's `definition`,
`vars` (`{}` when absent) and `tracker` (`null` when absent). The human sends
the patch with `mosaic limits set --file` or the Console's Assistant section;
the chief of staff sends it with `change_organization`. Until row 134 gives
the chief of staff a revision read, the revision comes from the human.
the chief of staff sends it with `change_organization`. The chief of staff
reads the revision with `organization.revision` (row 134), which returns the
revision alone.
`changes.jsonl` logs the actor: the human's id, or `chief-of-staff.<instance>`.
For inherited selections, print-only migration reads the existing system config
+6 -3
View File
@@ -116,14 +116,17 @@ empty. The tools:
the tool's text and the prompt line say so. A
`role.end` holder's prompt names the tool in place of the "only the human
stops a run" line; every other role keeps that line.
- `organization_revision` (organization.change holders, row 134) sends
`organization.revision` with no arguments and returns `{revision}` alone,
the value `change_organization` must send. A read never spends the grant.
- `change_organization` (organization.change holders) sends
`organization.set` with `revision` and the keys that verb takes, `limits`,
`models`, `instances` and `assistants` (row 155: an assistant instance's
`vars`, `tracker` or `null`, and `definition`, whose schema enum is
`THREAD_DEFINITION`). `ORGANIZATION_KEYS` in `tools.mjs` is the
broker's list; a test fails when the two differ. No tool reads the
revision until row 134, so the description tells the chief of staff to use
the one the human gave it.
broker's list; a test fails when the two differ. The description points
the chief of staff at `organization_revision` for the revision, and the
broker refuses a stale one with `revision-conflict`.
- `set_name` (instance.name holders) sends `name.set` with `instance` and
`name`. The broker checks the grant, the name pattern and the refusals
`name-is-instance`, `name-is-role` and `name-taken`.
+4 -3
View File
@@ -171,9 +171,10 @@ export function typedTools({ resolved, arbiter = false, tracker = false, project
}, ["role", "reason"]));
}
if (held.has("organization.change")) {
// Row 155 (#1649, Q169): assistants configures the assistant. Until row 134 gives the chief a
// revision read, the revision is one the human gave it.
add("change_organization", "organization.set", "organization.change", "Change the organization file: the launch limits, the model table, the model each instance runs, and the assistant's vars, tracker and definition, or reset token limits so they count from now. revision is the file's current revision, as the human gave it to you; the broker refuses a stale one with revision-conflict. Send only the keys you change; each replaces what the file holds for it.", obj({
// Row 134 (#1669): organization_revision is the read change_organization's revision comes from.
// Row 155 (#1649, Q169): assistants configures the assistant.
add("organization_revision", "organization.revision", "organization.change", "Read the organization file's current revision, 64 hex characters, and nothing else from the file. Send it as change_organization's revision. If the file changes between your read and your change, the broker refuses with revision-conflict; read again. Reading spends no grant.", obj({}));
add("change_organization", "organization.set", "organization.change", "Change the organization file: the launch limits, the model table, the model each instance runs, and the assistant's vars, tracker and definition, or reset token limits so they count from now. revision is the file's current revision, from organization_revision; the broker refuses a stale one with revision-conflict. Send only the keys you change; each replaces what the file holds for it.", obj({
revision: str("The organization file's current revision", { pattern: "^[a-f0-9]{64}$" }),
limits: { type: "object", description: "The launch limits, replacing the file's launch limits" },
models: { type: "object", description: "The model table, replacing the file's model table" },
+3 -3
View File
@@ -91,7 +91,7 @@ test("a chief-of-staff bundle lists launch among the bus tools and the manifest'
const b = build(t, r);
const prompt = readFileSync(b.files.prompt, "utf8");
assert.match(prompt, /Role instance: chief \(definition chief-of-staff\)\. Run: run-1\./);
assert.match(prompt, /Bus tools: send_message, launch, end_run, change_organization, set_name, read_spec, write_record, raise_decision, /);
assert.match(prompt, /Bus tools: send_message, launch, end_run, organization_revision, change_organization, set_name, read_spec, write_record, raise_decision, /);
assert.ok(b.manifest.typed.includes("launch"));
assert.deepEqual(JSON.parse(readFileSync(b.files.policy, "utf8")).needs, []);
});
@@ -104,9 +104,9 @@ test("a chief-of-staff bundle lists end_run, change_organization and set_name as
const r = resolvedFor(t, "chief", (d) => (d.roles.chief.vars = { harness, model: "claude-opus-5-5" }));
const b = build(t, r);
const prefix = harness === "pi" ? "" : "mcp__mosaic__";
assert.deepEqual(b.manifest.typed, ["send_message", "launch", "end_run", "change_organization", "set_name", "read_spec", "write_record", "raise_decision", "list_agents", "inbox", "trail", "list_tasks", "get_task"].map((n) => prefix + n), harness);
assert.deepEqual(b.manifest.typed, ["send_message", "launch", "end_run", "organization_revision", "change_organization", "set_name", "read_spec", "write_record", "raise_decision", "list_agents", "inbox", "trail", "list_tasks", "get_task"].map((n) => prefix + n), harness);
assert.deepEqual(b.manifest.untyped, [], harness);
assert.match(readFileSync(b.files.prompt, "utf8"), /Bus tools: send_message, launch, end_run, change_organization, set_name, read_spec, write_record, raise_decision, /);
assert.match(readFileSync(b.files.prompt, "utf8"), /Bus tools: send_message, launch, end_run, organization_revision, change_organization, set_name, read_spec, write_record, raise_decision, /);
assert.doesNotMatch(readFileSync(b.files.prompt, "utf8"), /Actions you hold that have no bus tool yet/);
}
});
+11 -5
View File
@@ -381,15 +381,16 @@ test("typed tools carry the runner's capability; launch goes to the host's launc
// Row 132 (#1628, REQ-ROLE-2, REQ-COS-1, REQ-LIMIT-1): the chief's two tools reach the broker by their verbs
// with the arguments the session gave, and a refusal comes back with the broker's code.
test("end_run and change_organization reach the broker with the tool's arguments, and refusals keep their codes", async (t) => {
// Row 134 (#1669): organization_revision reaches the broker's organization.revision and answers the revision alone.
test("end_run, organization_revision and change_organization reach the broker with the tool's arguments, and refusals keep their codes", async (t) => {
const written = [];
const organization = (...args) => {
written.push(args);
if (args[2].revision === "e".repeat(64)) throw Object.assign(new Error("stale"), { code: "revision-conflict" });
return { revision: "b".repeat(64) };
return args[1] === "limits" ? { revision: "c".repeat(64), limits: { total: 7 }, tokens: {} } : { revision: "b".repeat(64) };
};
const tools = typedTools({ resolved: resolvedFor(t, "chief") }).filter((x) => ["end_run", "change_organization"].includes(x.name));
assert.deepEqual(tools.map((x) => x.name), ["end_run", "change_organization"]);
const tools = typedTools({ resolved: resolvedFor(t, "chief") }).filter((x) => ["end_run", "organization_revision", "change_organization"].includes(x.name));
assert.deepEqual(tools.map((x) => x.name), ["end_run", "organization_revision", "change_organization"]);
const ctx = await setup(t, { as: "chief", organization, tools, policy: { typed: tools.map((x) => x.name) } });
ctx.inbox = [];
ctx.call(ctx.broker.bindLaunch({ business: "demo", role: "coder", run: "coder-run", harness: "pi" }), "role.claim");
@@ -400,14 +401,18 @@ test("end_run and change_organization reach the broker with the tool's arguments
send("end_run", { role: "coder", reason: "stuck on the same test for an hour" });
send("end_run", { role: "chief", reason: "my own run" });
send("end_run", { role: "assistant", reason: "the user's seat" });
send("organization_revision", {});
send("organization_revision", { surprise: 1 });
send("change_organization", change);
send("change_organization", { ...change, surprise: 1 });
send("change_organization", { ...change, revision: "e".repeat(64) });
await until(() => replies(ctx)() && ctx.inbox.length >= 6);
await until(() => replies(ctx)() && ctx.inbox.length >= 8);
assert.deepEqual(ctx.inbox.map((m) => m.body), [
'{"revoked":true}',
"refused: role-end-refused",
"refused: not-held",
'{"revision":"' + "c".repeat(64) + '"}',
"refused: invalid-request",
'{"revision":"' + "b".repeat(64) + '"}',
"refused: invalid-request",
"refused: revision-conflict",
@@ -415,6 +420,7 @@ test("end_run and change_organization reach the broker with the tool's arguments
assert.deepEqual({ ...ctx.store.get("SELECT op, reason, by FROM role_claims WHERE role='coder' ORDER BY rowid DESC LIMIT 1") }, { op: "revoke", reason: "stuck on the same test for an hour", by: "chief-run" });
// The writer saw the tool's arguments and the chief's name; the unknown key never reached it.
assert.deepEqual(written.map((w) => [w[0], w[1], w[2], w[3]]), [
["demo", "limits", {}, "chief-of-staff.chief"],
["demo", "limits.set", change, "chief-of-staff.chief"],
["demo", "limits.set", { ...change, revision: "e".repeat(64) }, "chief-of-staff.chief"],
]);
+16 -7
View File
@@ -40,7 +40,7 @@ test("send_message offers the BLOCK class, beside the classes it already had", (
test("the chief of staff gets launch, send_message, end_run, change_organization and set_name, and no task verbs", (t) => {
const chief = resolvedFor(t, "chief");
const tools = typedTools({ resolved: chief, tracker: true });
assert.deepEqual(names(tools), ["send_message", "launch", "end_run", "change_organization", "set_name", "read_spec", "write_record", "raise_decision", "list_agents", "inbox", "trail", "list_tasks", "get_task"]);
assert.deepEqual(names(tools), ["send_message", "launch", "end_run", "organization_revision", "change_organization", "set_name", "read_spec", "write_record", "raise_decision", "list_agents", "inbox", "trail", "list_tasks", "get_task"]);
const launch = tools.find((x) => x.name === "launch");
assert.equal(launch.verb, "launch");
assert.equal(launch.action, "role.launch");
@@ -104,7 +104,15 @@ test("end_run sends role and reason, no decision; change_organization sends revi
assert.equal(assistant.additionalProperties, false);
assert.deepEqual(assistant.properties.definition.enum, ["assistant"]);
assert.deepEqual(assistant.properties.tracker.type, ["object", "null"]);
assert.match(change.description, /as the human gave it to you/);
assert.match(change.description, /from organization_revision;/);
assert.doesNotMatch(change.description, /human gave it/);
// Row 134 (#1669): the revision read takes no arguments and is held by the same grant.
const read = tools.find((x) => x.name === "organization_revision");
assert.deepEqual([read.verb, read.action], ["organization.revision", "organization.change"]);
assert.deepEqual(read.parameters.properties, {});
assert.deepEqual(read.parameters.required, []);
assert.equal(read.parameters.additionalProperties, false);
assert.ok(tools.indexOf(read) < tools.indexOf(change));
});
// Row 155 (#1649, REQ-MSG-2): set_name is row 141's name.set under the instance.name grant.
@@ -131,16 +139,17 @@ test("the organization keys change_organization takes are the broker's ORGANIZAT
});
// Row 132 (#1628): the grants are the offer; a role without them gets neither tool.
test("a role without role.end or organization.change gets neither tool, and each tool follows its own grant", (t) => {
test("a role without role.end or organization.change gets none of the three tools, and each follows its own grant", (t) => {
const ORGANIZATION_TOOLS = (n) => ["end_run", "organization_revision", "change_organization"].includes(n);
for (const instance of ["pm", "coder", "reviewer"]) {
const tools = typedTools({ resolved: resolvedFor(t, instance), arbiter: true, tracker: true });
assert.deepEqual(names(tools).filter((n) => n === "end_run" || n === "change_organization"), [], instance);
assert.deepEqual(names(tools).filter(ORGANIZATION_TOOLS), [], instance);
}
const assistant = resolvedFor(t, "assistant", (d) => (d.roles.assistant = { definition: "assistant", holder: "mos" }));
assert.deepEqual(names(typedTools({ resolved: assistant, tracker: true })).filter((n) => n === "end_run" || n === "change_organization"), []);
assert.deepEqual(names(typedTools({ resolved: assistant, tracker: true })).filter(ORGANIZATION_TOOLS), []);
const only = (grant) => resolvedFor(t, "chief", (d) => (d.roles.chief.vars = { ...d.roles.chief.vars, "limits.authority": ["message.send", grant] }));
assert.deepEqual(names(typedTools({ resolved: only("role.end") })).filter((n) => n === "end_run" || n === "change_organization"), ["end_run"]);
assert.deepEqual(names(typedTools({ resolved: only("organization.change") })).filter((n) => n === "end_run" || n === "change_organization"), ["change_organization"]);
assert.deepEqual(names(typedTools({ resolved: only("role.end") })).filter(ORGANIZATION_TOOLS), ["end_run"]);
assert.deepEqual(names(typedTools({ resolved: only("organization.change") })).filter(ORGANIZATION_TOOLS), ["organization_revision", "change_organization"]);
});
// Row 165 (#1662, sage-0918): instances and instances.set are the human's. No role's tools send
+2 -2
View File
@@ -36,8 +36,8 @@ product's code, you don't review it and you don't create tasks yourself.
- You and the human are the ones who configure and stop the assistant.
Change its vars, its tracker or its definition with
`change_organization` and `assistants`; its holder and credentials
stay hand edits. The revision comes from the human until
you have a way to read it. End its runs with `end_run`, which ends one
stay hand edits. Read the revision with `organization_revision` before
each change. End its runs with `end_run`, which ends one
run per thread; it starts again when the human writes to it. Set an
instance's seat name with `set_name`.
- Report to the assistant what the roles answer, and every refusal with