Files
stack/agents/sage/work/queue-145/diffs/306-landed.diff
jason.woltjeandClaude Opus 5.5 f57c80b4cf agents: Sage row 144 and 145 packets (vikunja-19 landings, #1638 #1639) and Q167 issue receipts
Row 145 BUILD.md records both review rounds, the gate on 9357baac9
(GATE_PASSED, 24 steps) and the landed commits 92938ab43..fba879f6a.
q167/ holds the issue receipts for rows 135-143 and 146-147.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-15 17:52:12 -05:00

253 lines
14 KiB
Diff

commit 077f3a0fc8248562f7b7e610027775649f86ccb9
Author: Jason Woltje <[email protected]>
Date: Thu Oct 15 17:25:23 2026 -0500
roles: C-suite messaging limited to human, chief-of-staff, pm and cto (vikunja:19/306, row 145)
Jason-approved merge, landed unchanged: git diff bbf61d6dd..3c80fea74
from work/vikunja-19-306. Business reviewer verdict 8ee5be8b approve on that
commit; Jason resolved merge decision 0eebf83f via the CLI. Applied cleanly
on the parent landing; no conflict. Filbert reviews the rebased diff.
Integration with row 121, found in Filbert's round 1 (#1639, comment 28623):
packages/bus/tests/csuite-messaging.test.mjs setup() binds the assistant to a
thread, since row 121 made assistant a thread definition and bindLaunch refuses
an assistant run without one. The human creates a project and thread first.
No assertion or code changes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
packages/bus/tests/csuite-messaging.test.mjs | 74 +++++++++++++++++++++++++++
packages/business/tests/csuite.test.mjs | 3 ++
roles/chief-executive-officer.json | 6 ++-
roles/chief-executive-officer.md | 1 +
roles/chief-financial-officer.json | 6 ++-
roles/chief-financial-officer.md | 1 +
roles/chief-information-officer.json | 6 ++-
roles/chief-information-officer.md | 1 +
roles/chief-information-security-officer.json | 6 ++-
roles/chief-information-security-officer.md | 1 +
roles/chief-marketing-officer.json | 6 ++-
roles/chief-marketing-officer.md | 1 +
12 files changed, 107 insertions(+), 5 deletions(-)
diff --git a/packages/bus/tests/csuite-messaging.test.mjs b/packages/bus/tests/csuite-messaging.test.mjs
new file mode 100644
index 000000000..c40a75b8b
--- /dev/null
+++ b/packages/bus/tests/csuite-messaging.test.mjs
@@ -0,0 +1,74 @@
+// vikunja:19/306 (REQ-ROLE-4, REQ-ASSIST-2): the five C-suite roles talk to the user, the chief of
+// staff, the pm and the cto, and to no one else, so an advisory role can't send work to the coder or
+// reviewer around the pm. The lists are read from the shipped role files, so the test pins the files
+// the stack runs. A message outside the lists is refused and logged, as the assistant's are.
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
+import { tmpdir } from 'node:os';
+import { join } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { Store } from '../src/store.mjs';
+import { Broker } from '../src/broker.mjs';
+
+const shipped = (name) => JSON.parse(readFileSync(fileURLToPath(new URL(`../../../roles/${name}.json`, import.meta.url)), 'utf8'));
+const CSUITE = ['chief-executive-officer', 'chief-financial-officer', 'chief-information-officer', 'chief-information-security-officer', 'chief-marketing-officer'];
+const within = { authority: { withinRole: ['message.send'], crossRole: [] } };
+const OTHERS = { chief: 'chief-of-staff', pm: 'pm', cto: 'cto', coder: 'coder', reviewer: 'reviewer', assistant: 'assistant' };
+const roles = Object.fromEntries([...CSUITE.map((c) => [c, { definition: c, ...within }]), ...Object.entries(OTHERS).map(([i, d]) => [i, { definition: d, ...within }])]);
+const definitions = Object.fromEntries([
+ ...CSUITE.map((c) => [c, { messaging: shipped(c).messaging }]),
+ ...Object.values(OTHERS).map((d) => [d, d === 'assistant' ? { messaging: shipped('assistant').messaging } : {}]),
+]);
+const RECIPIENT = 'message-recipient-refused';
+const SENDER = 'message-sender-refused';
+
+function setup(t) {
+ const root = mkdtempSync(join(tmpdir(), 'bus-csuite-msg-'));
+ const store = new Store(root);
+ t.after(() => { store.close(); rmSync(root, { recursive: true, force: true }); });
+ const b = new Broker({ store, businesses: { demo: { id: 'demo', human: 'jason', arbiters: { technical: 'cto', delivery: 'pm' }, roles, definitions, projects: { stack: { root: '/nonexistent' } } } } });
+ const caps = { human: b.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true }) };
+ // Row 121: the assistant is a thread definition, so its run binds to a thread.
+ const project = b.request(caps.human, { verb: 'project.create', args: { name: 'demo' } }).id;
+ const thread = b.request(caps.human, { verb: 'thread.create', args: { project, name: 'main' } }).id;
+ for (const role of Object.keys(roles)) {
+ caps[role] = b.bindLaunch({ business: 'demo', role, run: `${role}-run`, harness: 'pi', ...(role === 'assistant' ? { thread } : {}) });
+ b.request(caps[role], { verb: 'role.claim' });
+ }
+ const send = (from, to) => b.request(caps[from], { verb: 'message.send', args: { to, body: 'hello' } });
+ const messages = () => store.get('SELECT count(*) n FROM messages').n;
+ const refusals = () => store.all("SELECT actor_role, body FROM events WHERE kind='action.refused' ORDER BY seq").map((e) => [e.actor_role, JSON.parse(e.body).code]);
+ return { send, messages, refusals };
+}
+
+test('each C-suite role file carries the same two lists: human, chief-of-staff, pm, cto', () => {
+ for (const c of CSUITE) assert.deepEqual(shipped(c).messaging, { sendTo: ['human', 'chief-of-staff', 'pm', 'cto'], receiveFrom: ['human', 'chief-of-staff', 'pm', 'cto'] }, c);
+});
+
+for (const c of CSUITE) {
+ test(`${c}: messages to and from the user, chief of staff, pm and cto pass`, (t) => {
+ const { send, messages } = setup(t);
+ for (const to of ['human', 'chief', 'pm', 'cto']) send(c, to);
+ for (const from of ['human', 'chief', 'pm', 'cto']) send(from, c);
+ assert.equal(messages(), 8);
+ });
+
+ test(`${c}: a message to or from the coder, reviewer or assistant is refused and logged, and nothing is stored`, (t) => {
+ const { send, messages, refusals } = setup(t);
+ const out = ['coder', 'reviewer', 'assistant'];
+ for (const to of out) assert.throws(() => send(c, to), { code: RECIPIENT }, `to ${to}`);
+ for (const from of out) assert.throws(() => send(from, c), from === 'assistant' ? { code: RECIPIENT } : { code: SENDER }, `from ${from}`);
+ assert.equal(messages(), 0);
+ assert.deepEqual(refusals(), [
+ ...out.map(() => [c, RECIPIENT]),
+ ['coder', SENDER], ['reviewer', SENDER], ['assistant', RECIPIENT],
+ ]);
+ });
+
+ test(`${c}: another C-suite role isn't on the lists either`, (t) => {
+ const { send } = setup(t);
+ const other = CSUITE.find((x) => x !== c);
+ assert.throws(() => send(c, other), { code: RECIPIENT });
+ });
+}
diff --git a/packages/business/tests/csuite.test.mjs b/packages/business/tests/csuite.test.mjs
index aad662481..cc8f434c2 100644
--- a/packages/business/tests/csuite.test.mjs
+++ b/packages/business/tests/csuite.test.mjs
@@ -15,6 +15,9 @@ const APPROVED = Object.freeze({
network: "api-only",
authority: { withinRole: ["message.send"], crossRole: [] },
credentials: [],
+ // Least privilege (vikunja:19/306): an advisory role talks to the user, the chief of staff, the pm
+ // and the cto, so it can't send work to the coder or reviewer around the pm.
+ messaging: { sendTo: ["human", "chief-of-staff", "pm", "cto"], receiveFrom: ["human", "chief-of-staff", "pm", "cto"] },
});
// Jason's business file has the same shape: the five instances by definition name, no credentials
diff --git a/roles/chief-executive-officer.json b/roles/chief-executive-officer.json
index d601ce95f..95d6679e0 100644
--- a/roles/chief-executive-officer.json
+++ b/roles/chief-executive-officer.json
@@ -9,5 +9,9 @@
"withinRole": ["message.send"],
"crossRole": []
},
- "credentials": []
+ "credentials": [],
+ "messaging": {
+ "sendTo": ["human", "chief-of-staff", "pm", "cto"],
+ "receiveFrom": ["human", "chief-of-staff", "pm", "cto"]
+ }
}
diff --git a/roles/chief-executive-officer.md b/roles/chief-executive-officer.md
index 5b470df03..3cbec50bd 100644
--- a/roles/chief-executive-officer.md
+++ b/roles/chief-executive-officer.md
@@ -37,4 +37,5 @@ instance as target, naming the run and saying why.
started. It is the truth for launch state and task status. A message
or a note of yours that disagrees with it is out of date.
- Send messages through the bus, addressed to a role, not a person.
+- You may send messages to the user (`human`) and to the `chief-of-staff`, `pm` and `cto` roles, and receive messages from them, and from no other role. Work for the coder or reviewer goes through the pm's tasks. A message outside that list is refused and logged.
- Report a blocker as soon as you find it, with what you tried.
diff --git a/roles/chief-financial-officer.json b/roles/chief-financial-officer.json
index 801427a4a..0384fac2c 100644
--- a/roles/chief-financial-officer.json
+++ b/roles/chief-financial-officer.json
@@ -9,5 +9,9 @@
"withinRole": ["message.send"],
"crossRole": []
},
- "credentials": []
+ "credentials": [],
+ "messaging": {
+ "sendTo": ["human", "chief-of-staff", "pm", "cto"],
+ "receiveFrom": ["human", "chief-of-staff", "pm", "cto"]
+ }
}
diff --git a/roles/chief-financial-officer.md b/roles/chief-financial-officer.md
index 17d56f2da..6fb381142 100644
--- a/roles/chief-financial-officer.md
+++ b/roles/chief-financial-officer.md
@@ -37,4 +37,5 @@ instance as target, naming the run and saying why.
started. It is the truth for launch state and task status. A message
or a note of yours that disagrees with it is out of date.
- Send messages through the bus, addressed to a role, not a person.
+- You may send messages to the user (`human`) and to the `chief-of-staff`, `pm` and `cto` roles, and receive messages from them, and from no other role. Work for the coder or reviewer goes through the pm's tasks. A message outside that list is refused and logged.
- Report a blocker as soon as you find it, with what you tried.
diff --git a/roles/chief-information-officer.json b/roles/chief-information-officer.json
index 1daf8fa2b..4a415d7c7 100644
--- a/roles/chief-information-officer.json
+++ b/roles/chief-information-officer.json
@@ -9,5 +9,9 @@
"withinRole": ["message.send"],
"crossRole": []
},
- "credentials": []
+ "credentials": [],
+ "messaging": {
+ "sendTo": ["human", "chief-of-staff", "pm", "cto"],
+ "receiveFrom": ["human", "chief-of-staff", "pm", "cto"]
+ }
}
diff --git a/roles/chief-information-officer.md b/roles/chief-information-officer.md
index b04a719fc..89ce6b687 100644
--- a/roles/chief-information-officer.md
+++ b/roles/chief-information-officer.md
@@ -37,4 +37,5 @@ instance as target, naming the run and saying why.
started. It is the truth for launch state and task status. A message
or a note of yours that disagrees with it is out of date.
- Send messages through the bus, addressed to a role, not a person.
+- You may send messages to the user (`human`) and to the `chief-of-staff`, `pm` and `cto` roles, and receive messages from them, and from no other role. Work for the coder or reviewer goes through the pm's tasks. A message outside that list is refused and logged.
- Report a blocker as soon as you find it, with what you tried.
diff --git a/roles/chief-information-security-officer.json b/roles/chief-information-security-officer.json
index 008990000..2db30f8fd 100644
--- a/roles/chief-information-security-officer.json
+++ b/roles/chief-information-security-officer.json
@@ -9,5 +9,9 @@
"withinRole": ["message.send"],
"crossRole": []
},
- "credentials": []
+ "credentials": [],
+ "messaging": {
+ "sendTo": ["human", "chief-of-staff", "pm", "cto"],
+ "receiveFrom": ["human", "chief-of-staff", "pm", "cto"]
+ }
}
diff --git a/roles/chief-information-security-officer.md b/roles/chief-information-security-officer.md
index 61af0eee7..239d2798d 100644
--- a/roles/chief-information-security-officer.md
+++ b/roles/chief-information-security-officer.md
@@ -37,4 +37,5 @@ instance as target, naming the run and saying why.
started. It is the truth for launch state and task status. A message
or a note of yours that disagrees with it is out of date.
- Send messages through the bus, addressed to a role, not a person.
+- You may send messages to the user (`human`) and to the `chief-of-staff`, `pm` and `cto` roles, and receive messages from them, and from no other role. Work for the coder or reviewer goes through the pm's tasks. A message outside that list is refused and logged.
- Report a blocker as soon as you find it, with what you tried.
diff --git a/roles/chief-marketing-officer.json b/roles/chief-marketing-officer.json
index 91300eb2a..76312f93d 100644
--- a/roles/chief-marketing-officer.json
+++ b/roles/chief-marketing-officer.json
@@ -9,5 +9,9 @@
"withinRole": ["message.send"],
"crossRole": []
},
- "credentials": []
+ "credentials": [],
+ "messaging": {
+ "sendTo": ["human", "chief-of-staff", "pm", "cto"],
+ "receiveFrom": ["human", "chief-of-staff", "pm", "cto"]
+ }
}
diff --git a/roles/chief-marketing-officer.md b/roles/chief-marketing-officer.md
index 57056cc68..422210653 100644
--- a/roles/chief-marketing-officer.md
+++ b/roles/chief-marketing-officer.md
@@ -37,4 +37,5 @@ instance as target, naming the run and saying why.
started. It is the truth for launch state and task status. A message
or a note of yours that disagrees with it is out of date.
- Send messages through the bus, addressed to a role, not a person.
+- You may send messages to the user (`human`) and to the `chief-of-staff`, `pm` and `cto` roles, and receive messages from them, and from no other role. Work for the coder or reviewer goes through the pm's tasks. A message outside that list is refused and logged.
- Report a blocker as soon as you find it, with what you tried.