row 206 (193b2, #1704): sage-2818 rulings, only a human principal matches a share
#readable returns false for an agent session, so a `*` share no longer shows a user's message in an agent's trail. New tests: a sharee's session.list refuses (Q1), and an agent bound to the shared thread, a chief run and the tester match no share (Q2). BUILD.md records the Q1 follow-up, Q2-Q4 and the live --migrate as Mos's go under Q173. README names the agent rule.
This commit is contained in:
1 parent
9049dab492
commit
4ae94c5c97
4 files changed
+75
-15
No files matched your search
@@ -10,10 +10,15 @@ sections 4.3, 5.2-5.6 and 8. Reviewer: Duckworth. Assignment: sage-2789.
|
|||||||
- 206 chains on 275 (193b1b, viewer verbs and board refusals), which Morgana is building. I rebase
|
- 206 chains on 275 (193b1b, viewer verbs and board refusals), which Morgana is building. I rebase
|
||||||
onto Morgana's 275 sha when it comes, and onto 253's landing under a new branch name.
|
onto Morgana's 275 sha when it comes, and onto 253's landing under a new branch name.
|
||||||
- At this base a user binds only with a collaborator grant (`#userGrants`, USER_LEVEL). Viewer
|
- At this base a user binds only with a collaborator grant (`#userGrants`, USER_LEVEL). Viewer
|
||||||
sessions arrive with 275, so the viewer column of design 4.3 is not testable here. On the 275
|
sessions arrive with 275, so the viewer column of design 4.3 is not testable here (sage-2818
|
||||||
|
Q4: the viewer column waits on the 275 rebase; 275 moves once more for Duckworth's B1,
|
||||||
|
duckworth-0166). On the 275
|
||||||
rebase, USER_VERBS and the viewer gate merge with the three share verbs, and the share verbs
|
rebase, USER_VERBS and the viewer gate merge with the three share verbs, and the share verbs
|
||||||
must refuse a viewer (design 4.3: share, unshare and shares are "refused" for viewers).
|
must refuse a viewer (design 4.3: share, unshare and shares are "refused" for viewers).
|
||||||
|
|
||||||
|
- The live `--migrate` (3l→3m on the running bus) is not part of this build. It is Mos's go under
|
||||||
|
Q173, with a backup first and a STATE report after (mos-1310 item 4).
|
||||||
|
|
||||||
## What changed
|
## What changed
|
||||||
|
|
||||||
- Schema 3l→3m. `thread_shares` as design 5.2, its index and three append-only triggers, in
|
- Schema 3l→3m. `thread_shares` as design 5.2, its index and three append-only triggers, in
|
||||||
@@ -41,16 +46,27 @@ sections 4.3, 5.2-5.6 and 8. Reviewer: Duckworth. Assignment: sage-2789.
|
|||||||
|
|
||||||
1. Label 3l→3m, not 3i-3j (design) or 3l (brief title). 264a took 3k-3l, and labels go by
|
1. Label 3l→3m, not 3i-3j (design) or 3l (brief title). 264a took 3k-3l, and labels go by
|
||||||
landing order (sage-2789).
|
landing order (sage-2789).
|
||||||
2. A share covers every session of the thread. The 264a comment on `session.list` says "there
|
2. A share covers every session of the thread, and `session.list` stays owner-only (sage-2818
|
||||||
are no shares (Q7)"; I left `session.list` owner-only, so a sharee reads any session it can
|
Q1, from Sage's Q7 ruling, agents/sage/work/queue-264/RULINGS.md:72-77). broker.mjs keeps its
|
||||||
name but can't list them. Flagged for a ruling: either a sharee gets `session.list`, or the
|
"no shares (Q7)" refusal: a sharee reads any session it can name but can't list them. A test
|
||||||
Console names the sessions some other way.
|
holds the refusal (human `thread-not-owned`, reader `read-only`, user `access-refused`).
|
||||||
3. A reader session matches shares as the business human. Design 4.3 says reader sessions "stay
|
**Follow-up**, bound to whichever of row 193 or the share wiring lands second: wire the sharee's
|
||||||
owner-only", and 5.5 says "messages in other users' unshared threads hidden" for human, reader
|
`session.list`. The bar is T1 sessions line 7 and REQ-USER-4 (candidate PRD 1509c7cc5,
|
||||||
and tester. I read that as: the reader sees what the owner sees, shares to the owner included.
|
docs/prd/mosaic-stack.md:1027-1052): read-only, and it needs current grants, the thread
|
||||||
4. Design 5.6's "30 s timer" and `access.reload` have no path at this base: runtime.mjs:263
|
owner's included.
|
||||||
passes `access: () => loadAccess(file, context)`, read on every request. The tests cover the
|
3. A reader session matches shares as the business human (sage-2818 Q2: accepted, for the human
|
||||||
per-request read (removed, expired, out of scope and unreadable), which is the stricter case.
|
principal only). Design 4.3 says reader sessions "stay owner-only", and 5.5 says "messages in
|
||||||
|
other users' unshared threads hidden" for human, reader and tester. The reader sees what the
|
||||||
|
owner sees, shares to the owner included. Q2 also found a gap in the first tip (16b9c6df6): an
|
||||||
|
agent session has no owner, but it matched a `*` share, so a shared user message showed in an
|
||||||
|
agent's trail. `#readable` now returns false unless the session is the business human, a
|
||||||
|
reader or a user. A test binds an assistant run to the shared thread and a chief run with no
|
||||||
|
thread, and checks that neither (nor the tester) sees the message after a `*` share and a share
|
||||||
|
to the owner.
|
||||||
|
4. Access is read per request (sage-2818 Q3). Design 5.6's "30 s timer" and `access.reload` have
|
||||||
|
no path at this base: runtime.mjs:263 passes `access: () => loadAccess(file, context)`, read on
|
||||||
|
every request. The tests cover the per-request read (removed, expired, out of scope and
|
||||||
|
unreadable), which is the stricter case.
|
||||||
5. Rule 3 asks the owner's grant for any level, like rule 2 (design: "still has a live grant
|
5. Rule 3 asks the owner's grant for any level, like rule 2 (design: "still has a live grant
|
||||||
covering t"). A user owner lowered to viewer keeps the thread readable to its sharees.
|
covering t"). A user owner lowered to viewer keeps the thread readable to its sharees.
|
||||||
6. An unshare checks no coverage, so an owner can end a share whose target lost its grant.
|
6. An unshare checks no coverage, so an owner can end a share whose target lost its grant.
|
||||||
@@ -60,10 +76,11 @@ sections 4.3, 5.2-5.6 and 8. Reviewer: Duckworth. Assignment: sage-2789.
|
|||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
New: tests/thread-shares.test.mjs, 13 tests: the read flow and the write refusals, `*`, rule 2,
|
New: tests/thread-shares.test.mjs, 15 tests: the read flow and the write refusals, `*`, rule 2,
|
||||||
rule 3 (with trail for human and reader), latest-row state, share-not-covered, target checks,
|
rule 3 (with trail for human and reader), latest-row state, share-not-covered, target checks,
|
||||||
lost owner coverage, tester and reader refusals (18 `action.refused` events, no rows), trail and
|
lost owner coverage, tester and reader refusals (18 `action.refused` events, no rows), trail and
|
||||||
replies, sessions, the 3l→3m migration and the 3l engine's refusal of a 3m store.
|
replies, sessions, the sharee's `session.list` refusal (Q1), agents and the tester matching no
|
||||||
|
share (Q2), the 3l→3m migration and the 3l engine's refusal of a 3m store.
|
||||||
|
|
||||||
Pins moved 3l→3m: account-, chief-of-staff, comment-, gitea-, grant-, name-, spec-, usage- and
|
Pins moved 3l→3m: account-, chief-of-staff, comment-, gitea-, grant-, name-, spec-, usage- and
|
||||||
thread-migration, sessions, schema (23 tables, 89 triggers) and user-principal (USER_VERBS).
|
thread-migration, sessions, schema (23 tables, 89 triggers) and user-principal (USER_VERBS).
|
||||||
@@ -91,6 +108,9 @@ test must fail on an assertion; a thrown error or a refusal before the assertion
|
|||||||
to user:carol must refuse).
|
to user:carol must refuse).
|
||||||
- M9. In `thread.share`/`thread.unshare`, replace `this.#thread(s, a.thread)` with
|
- M9. In `thread.share`/`thread.unshare`, replace `this.#thread(s, a.thread)` with
|
||||||
`this.#threadRow(s, a.thread)`. Killed by the first test (bob's share of A to `*` must refuse).
|
`this.#threadRow(s, a.thread)`. Killed by the first test (bob's share of A to `*` must refuse).
|
||||||
|
- Q2 probe (not a design mutant). In `#readable`, delete the human-principal line
|
||||||
|
(`if (!s.human && !s.reader && !s.user) return false;`). Killed by "only a human principal
|
||||||
|
matches a share" (the agent's trail shows the message after the `*` share).
|
||||||
|
|
||||||
## Run limits
|
## Run limits
|
||||||
|
|
||||||
|
|||||||
@@ -301,7 +301,10 @@ A session may read a thread it doesn't own only when all four hold (row 193 desi
|
|||||||
3. a thread owned by a user is still covered by that user's grant;
|
3. a thread owned by a user is still covered by that user's grant;
|
||||||
4. neither the caller nor the owner is the tester.
|
4. neither the caller nor the owner is the tester.
|
||||||
|
|
||||||
A reader session reads as the business human. The check runs on every request against the
|
Only a human principal matches a share: the business human, a reader session as that human, or
|
||||||
|
a user. An agent session matches none, `*` included, even when it is bound to the thread.
|
||||||
|
A reader session reads as the business human. A sharee can't call `session.list` on the
|
||||||
|
thread; it stays owner-only until row 193's share wiring. The check runs on every request against the
|
||||||
access document the broker reads then, so a removed, expired or unreadable grant or an
|
access document the broker reads then, so a removed, expired or unreadable grant or an
|
||||||
unshare ends reads on the next request. Only `thread.messages` admits a sharee; `thread.open`,
|
unshare ends reads on the next request. Only `thread.messages` admits a sharee; `thread.open`,
|
||||||
`thread.send` and the share verbs refuse it `thread-not-owned`. `trail` and `replies` show a
|
`thread.send` and the share verbs refuse it `thread-not-owned`. `trail` and `replies` show a
|
||||||
|
|||||||
@@ -827,9 +827,12 @@ export class Broker {
|
|||||||
}
|
}
|
||||||
// Row 206 (193b2, design 5.4): may a session read thread `t` it doesn't own? Rule 4 first: a
|
// Row 206 (193b2, design 5.4): may a session read thread `t` it doesn't own? Rule 4 first: a
|
||||||
// tester's thread is never shared, and a tester is never a share's target and holds no grant. A
|
// tester's thread is never shared, and a tester is never a share's target and holds no grant. A
|
||||||
// reader session is the owner's board view, so it reads what is shared with the owner.
|
// reader session is the owner's board view, so it reads what is shared with the owner. Only a
|
||||||
|
// human principal (the business human, a reader as that human, a user) matches a share: an agent
|
||||||
|
// session, even one bound to the thread, matches none, `*` included (sage-2818 Q2).
|
||||||
#readable(s, t) {
|
#readable(s, t) {
|
||||||
if (s.tester || t.owner === 'tester') return false;
|
if (s.tester || t.owner === 'tester') return false;
|
||||||
|
if (!s.human && !s.reader && !s.user) return false;
|
||||||
const me = s.reader ? this.#business(s.business).human : this.#owner(s);
|
const me = s.reader ? this.#business(s.business).human : this.#owner(s);
|
||||||
// Rule 1: a share in force to the caller or to everyone.
|
// Rule 1: a share in force to the caller or to everyone.
|
||||||
if (!this.#shared(t.id, me) && !this.#shared(t.id, '*')) return false;
|
if (!this.#shared(t.id, me) && !this.#shared(t.id, '*')) return false;
|
||||||
|
|||||||
@@ -304,6 +304,40 @@ test('a share covers every session of the thread', (t) => {
|
|||||||
assert.equal(code('bob', 'thread.messages', { thread: H, session: S }), 'thread-not-owned');
|
assert.equal(code('bob', 'thread.messages', { thread: H, session: S }), 'thread-not-owned');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a sharee can\'t list the thread\'s sessions: session.list stays owner-only (Q7, sage-2818 Q1)', (t) => {
|
||||||
|
const { call, code, A, H } = setup(t);
|
||||||
|
call('alice', 'thread.share', { thread: A, to: 'jason' });
|
||||||
|
call('human', 'thread.share', { thread: H, to: '*' });
|
||||||
|
assert.equal(code('human', 'session.list', { thread: A }), 'thread-not-owned');
|
||||||
|
assert.equal(code('reader', 'session.list', { thread: A }), 'read-only');
|
||||||
|
for (const who of ['alice', 'bob']) assert.equal(code(who, 'session.list', { thread: H }), 'access-refused', who);
|
||||||
|
// The owners still list their own.
|
||||||
|
assert.equal(call('human', 'session.list', { thread: H }).length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('only a human principal matches a share: an agent bound to the thread and the tester match none (sage-2818 Q2)', (t) => {
|
||||||
|
const { b, caps, call, code, A, H, sent } = setup(t);
|
||||||
|
// The assistant run serving alice's thread, and a chief run with no thread.
|
||||||
|
caps.agent = b.bindLaunch({ business: 'demo', role: 'assistant', run: 'assistant-run', harness: 'pi', thread: A });
|
||||||
|
caps.chief = b.bindLaunch({ business: 'demo', role: 'chief', run: 'chief-run', harness: 'pi' });
|
||||||
|
for (const who of ['agent', 'chief']) call(who, 'role.claim');
|
||||||
|
const seen = (who) => call(who, 'trail', { subject: sent }).some((r) => r.table === 'messages' && r.id === sent);
|
||||||
|
for (const who of ['agent', 'chief', 'tester']) assert.equal(seen(who), false, `${who} before`);
|
||||||
|
call('alice', 'thread.share', { thread: A, to: '*' });
|
||||||
|
call('alice', 'thread.share', { thread: A, to: 'jason' });
|
||||||
|
// The human principals now read it; the agents and the tester see what they saw before.
|
||||||
|
for (const who of ['human', 'reader']) assert.equal(seen(who), true, who);
|
||||||
|
for (const who of ['agent', 'chief', 'tester']) assert.equal(seen(who), false, `${who} after`);
|
||||||
|
for (const who of ['agent', 'chief']) {
|
||||||
|
assert.equal(code(who, 'thread.messages', { thread: A }), 'human-required', who);
|
||||||
|
assert.equal(code(who, 'replies', { id: sent }), 'human-required', who);
|
||||||
|
}
|
||||||
|
assert.equal(code('tester', 'thread.messages', { thread: A }), 'thread-not-owned');
|
||||||
|
assert.equal(code('tester', 'replies', { id: sent }), 'message-not-found');
|
||||||
|
call('human', 'thread.share', { thread: H, to: '*' });
|
||||||
|
assert.equal(code('tester', 'thread.messages', { thread: H }), 'thread-not-owned');
|
||||||
|
});
|
||||||
|
|
||||||
// The 3l→3m step. A 3l store from fixtures/schema-3l.sql, with one row in each thread table.
|
// The 3l→3m step. A 3l store from fixtures/schema-3l.sql, with one row in each thread table.
|
||||||
const AT = '2026-10-22T00:00:00.000Z';
|
const AT = '2026-10-22T00:00:00.000Z';
|
||||||
const ddl3l = readFileSync(here('./fixtures/schema-3l.sql'), 'utf8');
|
const ddl3l = readFileSync(here('./fixtures/schema-3l.sql'), 'utf8');
|
||||||
|
|||||||
Reference in new issue
Block a user