row 177 round 2 (#1675): a tester-thread run's decision.raise refuses tester-thread-human when it routes to the human; F2 dispatch comment, F3 tester-writes header and name
This commit is contained in:
1 parent
1f994257a5
commit
9d9d95485a
4 files changed
+50
-7
No files matched your search
@@ -611,7 +611,7 @@ or not, and reads its `levels()`. Tests: `packages/bus/tests/routes-verb.test.mj
|
||||
|
||||
A tester (row 110, #1604) is a PM test's sender, never the human. `src/tester-cli.mjs <socket>` is human-cli.mjs with its own nonce variable, `MOSAIC_BUS_TESTER_NONCE`, so neither shim's nonce proves the other. `verifyTester` (`src/tester.mjs`) checks the same process proof but walks the ancestry for a managed run only: a launch record, `MOSAIC_RUN_ID`, `MOSAIC_BUS_CAP` or `MOSAIC_AGENT_RUN` refuses `tester-required`. Claude and Codex markers pass, since a tester runs from inside Mos or a T3 seat. A role session can't bind: its shim reports a pid from the sandbox's PID namespace, and the seat sandbox test runs that case under bwrap. `bindTester` then binds only when the business var `bus.tester` is exactly `true`, read at bus start, else `tester-off`. A request carries exactly one of `cap`, `human` or `tester`. The session may use `message.send`, `message.receive` and `message.read`, plus the reader's views of `inbox`, `agents`, `tasks`, `trail`, `decisions`, `messages` and `replies`. Row 175 (#1673, Q173, mos-0870 N2) adds the reads `usage`, `names`, `limits`, `plugins`, `instances`, `project.list` and `thread.list`, each answering what the human's answers (`thread.list` lists the tester's own threads since row 177), and the writes `name.set` (`by` `tester`), `project.create` (`created_by` `tester`), `plugins.set`, `instances.set` and `routes.set`, each with every check the human's write runs and the organization receipt's actor `tester`. `TESTER_VERBS` and `TESTER_ORGANIZATION_VERBS` in `src/broker.mjs` list them; every other verb refuses `tester-refused`, among them `limits.set`, `organization.set`, `organization.revision`, `routes`, `decision.resolve`, `launch.revoke`, `launch.restore`, the credential verbs and `project.file.save`. Tests: `tests/tester-writes.test.mjs`. It sends only to a role, with any body and no `corrects` or `decision`. Its message is stored `from_role 'tester'`, `from_run 'tester-cli'`, with no request and no event, so no `human.input` backs it and `task_create` can't cite it. A role answers at the `tester` address, and only with `in_reply_to` naming a tester's message (`tester-reply-required`); the human can't send there. Those replies never reach the human's `message.receive` or `messages`.
|
||||
|
||||
Row 177 (#1675, Q173) gives the tester threads of its own. `thread.create`, `thread.open`, `thread.send`, `thread.messages` and `assistant.stop` join `TESTER_VERBS`. A tester's thread has owner `tester` and its opens `by` `tester`; the human and the tester each reach only their own threads (`thread-not-owned`), and each `thread.list` lists only its own, so the human's list never shows a tester thread. A business whose `human` is `tester` refuses `invalid-business` at start, since the two would share threads. A tester's `thread.send` is stored `from_role 'tester'`, `from_run 'tester-cli'`, and returns `request: null` with no `human.input`, as its `message.send` does. Row 120's lists can't name the tester (`invalid-messaging`), so in a thread owned by `tester` the broker reads the tester as `human`: the tester's `thread.send` passes when the instance would hear the human, and the thread's run answers `to: 'tester'` (with `in_reply_to`, as above) when it could answer the human. The run's own `message.send` to `human` refuses `tester-thread-human`, so its replies stay in the thread and never reach the human's inbox; the human sees them in the trail as `tester` traffic. That covers the run's own sends only: the assistant's lists still let it message the chief of staff, and the chief can message the human. That is no new reach, since the chief has no lists and a tester's `message.send` already reaches it. The tester's `assistant.stop` revokes the runs of its own threads only, recorded `by` `tester`; the human's stop never touches them. Thread names are unique per project, not per owner, so a tester's `thread.create` with a name the human already used in that project answers `thread-exists`; that tells the tester the name exists and nothing more. A tester reads a reply with `message.receive` (what `talk --tester` and `follow --tester` use) or, in its own thread, `thread.messages`. `inbox --tester --json` prints decisions only, and the `messages` view reads the human's address, so neither shows a reply to the tester (sage-1148, #1673 comment 29334). Tests: `tests/tester-threads.test.mjs`.
|
||||
Row 177 (#1675, Q173) gives the tester threads of its own. `thread.create`, `thread.open`, `thread.send`, `thread.messages` and `assistant.stop` join `TESTER_VERBS`. A tester's thread has owner `tester` and its opens `by` `tester`; the human and the tester each reach only their own threads (`thread-not-owned`), and each `thread.list` lists only its own, so the human's list never shows a tester thread. A business whose `human` is `tester` refuses `invalid-business` at start, since the two would share threads. A tester's `thread.send` is stored `from_role 'tester'`, `from_run 'tester-cli'`, and returns `request: null` with no `human.input`, as its `message.send` does. Row 120's lists can't name the tester (`invalid-messaging`), so in a thread owned by `tester` the broker reads the tester as `human`: the tester's `thread.send` passes when the instance would hear the human, and the thread's run answers `to: 'tester'` (with `in_reply_to`, as above) when it could answer the human. The run's own `message.send` to `human` refuses `tester-thread-human`, and so does its `decision.raise` when the route comes out `human` (an uncited gated raise or a hard wait), before anything is stored. So neither its replies nor its decisions reach the human's inbox; the human sees its messages in the trail as `tester` traffic. That covers the run's own sends and raises only: the assistant's lists still let it message the chief of staff, a raise routed to the chief as arbiter stays allowed, and the chief can message the human. That is no new reach, since the chief has no lists and a tester's `message.send` already reaches it. The tester's `assistant.stop` revokes the runs of its own threads only, recorded `by` `tester`; the human's stop never touches them. Thread names are unique per project, not per owner, so a tester's `thread.create` with a name the human already used in that project answers `thread-exists`; that tells the tester the name exists and nothing more. A tester reads a reply with `message.receive` (what `talk --tester` and `follow --tester` use) or, in its own thread, `thread.messages`. `inbox --tester --json` prints decisions only, and the `messages` view reads the human's address, so neither shows a reply to the tester (sage-1148, #1673 comment 29334). Tests: `tests/tester-threads.test.mjs`.
|
||||
|
||||
## Shared reads and adapter boundary
|
||||
|
||||
|
||||
@@ -620,6 +620,11 @@ export class Broker {
|
||||
#sender(s) {
|
||||
return s.human ? 'human' : s.tester ? 'tester' : s.role;
|
||||
}
|
||||
// Row 177 (#1675): a run of a thread the tester owns. It answers the tester, never the human.
|
||||
#testerThread(s) {
|
||||
return !s.human && !s.tester && !!s.thread
|
||||
&& this.#store.get('SELECT owner FROM threads WHERE id=?', s.thread)?.owner === 'tester';
|
||||
}
|
||||
// Row 141 (#1635, REQ-MSG-2): the role names a name may not equal, which are the definitions the
|
||||
// loaded business knows (sage-0590). The broker doesn't read roles/.
|
||||
#definitions(b) {
|
||||
@@ -1292,8 +1297,9 @@ export class Broker {
|
||||
fail('read-only');
|
||||
if (s.tester && !TESTER_VERBS.includes(request.verb)) fail('tester-refused');
|
||||
// A tester reads the human's views, as a reader does, and its message verbs are its own.
|
||||
// Since row 175 the view also carries the writes name.set and project.create. Neither
|
||||
// branch checks `reader`: each passes a tester by `s.tester` and records `tester`.
|
||||
// Since row 175 the view also carries the writes name.set and project.create, and since row 177
|
||||
// thread.create, thread.open, thread.send, thread.messages and assistant.stop. None of their
|
||||
// branches checks `reader`: each passes a tester by `s.tester`, and each write records `tester`.
|
||||
const view = s.tester && !request.verb.startsWith('message.') ? { ...s, reader: true } : s;
|
||||
const result = this.#dispatch(view, request.verb, args);
|
||||
this.#secretCheck(result);
|
||||
@@ -1665,6 +1671,9 @@ export class Broker {
|
||||
cls === 'cross-role' || (cls === 'gated' && spec !== null && !this.#hardWait(s, a.action, a.target));
|
||||
const proposedRoute = arbitrated ? b.arbiters[domain] : cls === 'gated' ? 'human' : s.role;
|
||||
const route = arbitrated && proposedRoute === s.role ? 'human' : proposedRoute;
|
||||
// Row 177 r2 (#1675, F1): nor does it put a decision in the human's inbox. A raise routed to an
|
||||
// arbiter stays allowed; the chief's reach to the human is no new reach (README, N1).
|
||||
if (route === 'human' && this.#testerThread(s)) fail('tester-thread-human');
|
||||
// Row 133 (#1627): whoever closes the decision messages the raiser, so the route must be
|
||||
// allowed to message the raising role before anything is stored. A within-role decision is
|
||||
// routed to the raiser's own role and closes in this call.
|
||||
@@ -1846,8 +1855,7 @@ export class Broker {
|
||||
if (to !== 'human' && to !== 'tester' && !Object.hasOwn(b.roles, to)) fail('unknown-role');
|
||||
// Row 177 (#1675): a run of a tester's thread answers the tester, never the human, and the
|
||||
// lists read the tester there as the human, so the run reaches it as far as it reaches the human.
|
||||
const testerThread = !s.human && !s.tester && s.thread
|
||||
&& this.#store.get('SELECT owner FROM threads WHERE id=?', s.thread)?.owner === 'tester';
|
||||
const testerThread = this.#testerThread(s);
|
||||
if (testerThread && to === 'human') fail('tester-thread-human');
|
||||
this.#checkMessaging(s.business, this.#sender(s), testerThread && to === 'tester' ? 'human' : to);
|
||||
string(a.body, 32768);
|
||||
|
||||
@@ -192,3 +192,38 @@ test('row 177 T7: the human trail shows a tester thread message as the tester',
|
||||
assert.equal(rows[0].from_role, 'tester');
|
||||
assert.equal(rows[0].from_run, 'tester-cli');
|
||||
});
|
||||
|
||||
// Row 177 r2 (#1675, F1): an action outside the assistant's grants is gated, and an uncited gated
|
||||
// raise routes to the human, so the tester-thread run's raise refuses before anything is stored.
|
||||
const raise = (action = 'task.create') => ({
|
||||
action,
|
||||
question: `${action}?`,
|
||||
options: [
|
||||
{ key: 'yes', text: 'Yes' },
|
||||
{ key: 'no', text: 'No' },
|
||||
],
|
||||
recommendation: 'no',
|
||||
blocking: false,
|
||||
});
|
||||
|
||||
test('row 177 T8: a tester-thread run never raises a decision to the human; a human-thread run still does', (t) => {
|
||||
const { store, call, code, launch, H, T } = both(t);
|
||||
const decisions = () => store.get('SELECT count(*) n FROM decisions').n;
|
||||
launch('a', T);
|
||||
launch('h', H);
|
||||
assert.equal(code('a', 'decision.raise', raise()), 'tester-thread-human');
|
||||
assert.deepEqual(call('human', 'inbox'), []);
|
||||
assert.equal(decisions(), 0);
|
||||
const raised = call('h', 'decision.raise', raise());
|
||||
assert.equal(store.get('SELECT route_to FROM decisions WHERE id=?', raised.id).route_to, 'human');
|
||||
assert.deepEqual(call('human', 'inbox').map((d) => d.id), [raised.id]);
|
||||
});
|
||||
|
||||
test('row 177 T8: a tester-thread raise routed to the arbiter stays allowed', (t) => {
|
||||
const crossRole = { definition: 'assistant', authority: { withinRole: ['message.send'], crossRole: ['task.create'] } };
|
||||
const { store, call, launch, T } = both(t, demo({ roles: { ...roles, assistant: crossRole } }));
|
||||
launch('a', T);
|
||||
const raised = call('a', 'decision.raise', raise());
|
||||
assert.equal(store.get('SELECT route_to FROM decisions WHERE id=?', raised.id).route_to, 'chief');
|
||||
assert.deepEqual(call('human', 'inbox'), []);
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
// Row 175 (#1673, Q173, mos-0870 N2, mos-0872): the test system's tester. It reads what the human
|
||||
// reads, makes seven writes recorded with actor `tester`, and every other human action still
|
||||
// refuses it tester-refused.
|
||||
// refuses it tester-refused. Row 177 (#1675) adds the thread verbs, tested in tester-threads.test.mjs.
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync } from 'node:fs';
|
||||
@@ -57,7 +57,7 @@ async function runtime(t) {
|
||||
return { root, file, bus, human, tester, call, code, log, bytes };
|
||||
}
|
||||
|
||||
test('row 175: each of the seven reads as the tester deep-equals the human read', async (t) => {
|
||||
test('row 175: each of the six reads as the tester deep-equals the human read', async (t) => {
|
||||
const r = await runtime(t);
|
||||
r.call(r.human, 'name.set', { instance: 'coder', name: 'rex' });
|
||||
const project = r.call(r.human, 'project.create', { name: 'alpha' }).id;
|
||||
|
||||
Reference in new issue
Block a user