morgana: row 295 r2 (dewey-0871): threadsState ends an expired grant's held claim from the cached read by access-expiry (B1, D7, M16, M11 claim row); access.reload scope as D8 (N1); bus README access.reload, revoked and revocation (N2); tests unrun
This commit is contained in:
1 parent
527002f975
commit
44b778e09a
6 files changed
+80
-7
No files matched your search
@@ -78,11 +78,12 @@ packages/bus/tests/access-revoke.test.mjs (B), the launcher file packages/cli/te
|
||||
| M8 | bindLaunch and role.claim refuse thread-revoked; another broker's rebind is allowed | B | drop the bindLaunch check |
|
||||
| M9 | the runtime timer ends the run at 30 000 ms and not at 29 999; intervalMs is checked; close stops it | B | the timer callback never calls `accessSweep` |
|
||||
| M10 | pending message + launcher restart starts no run, logs no launch and no refused start | L | threadPass ignores `revoked` |
|
||||
| M11 | a running revoked thread's run gets SIGTERM, ends `stopped` exit 0, with no thread-idle | L | the revoked branch drops the kill |
|
||||
| M11 | a running revoked thread's run gets SIGTERM, ends `stopped` exit 0, with no thread-idle; its last claim row is revoke/access-revoked/access-change, not a release (dewey-0871 B1) | L | the revoked branch drops the kill |
|
||||
| M12 | recover() rebinds an earlier host's run on a revoked thread and ends it host-lost (sage-3139 D1) | L | the bindLaunch check without `!old` |
|
||||
| M13 | grant and revoke call access.reload after the write; list and a refused write don't | C | drop the `reload` call |
|
||||
| M14 | a refused reload: exit 0, stdout as before, stderr names the code and the 30 s timer (sage-3139 Q2) | C | `reload` rethrows |
|
||||
| M15 | help keeps the provider-removal order with the reload sentence after it | C | drop the help sentence |
|
||||
| M16 | an expiry with no write and no sweep: threadsState ends the claim (revoke, access-revoked, by access-expiry) before it returns the row revoked with holder null; one revoke row only (dewey-0871 B1) | B | drop threadsState's `#revokeLost('access-expiry', true, doc)` call |
|
||||
|
||||
M11 relies on the runner's poll interval (set to 600 s in L), so only the signal ends the run: without
|
||||
the kill, a run that polls would exit 22 on not-holder and hide the mutant.
|
||||
@@ -116,3 +117,23 @@ the kill, a run that polls would exit 22 on not-holder and hide the mutant.
|
||||
A drained run of a revoked thread already had its claim ended by the sweep.
|
||||
- D6: `user remove` doesn't call access.reload: it refuses while the user holds a grant, so it
|
||||
never removes coverage. The brief names grant and revoke.
|
||||
- D7 (Dewey's B1, dewey-0871): an expiry changes no stamp, so the stamp check skips the sweep while
|
||||
`#revoked` with the current time already marks the row. threadsState therefore ends the lost
|
||||
claims itself when a lost row still has a holder, forced, from the cached document (no read, so
|
||||
sage-3139's cost rule holds), `by: 'access-expiry'`, and re-reads the rows. The launcher (2 s
|
||||
tick) then never sees `revoked: true` with a held claim, and the run's end is never a release.
|
||||
The request path is unchanged.
|
||||
- D8 (Dewey's N1): access.reload ends lost claims in every business, since the access file is
|
||||
shared, and writes the calling business's human as `by` on each. It returns only the caller's
|
||||
business's ended runs, so one business's human learns nothing of another's threads.
|
||||
|
||||
## Review round 1 (dewey-0871, CHANGES on 527002f97)
|
||||
|
||||
- B1 fixed: D7, test M16, M11 assertion. N1: D8. N2: packages/bus/README.md gets the access.reload
|
||||
row, `revoked` in threadsState, a revocation paragraph (timer, accessSweep, access.intervalMs,
|
||||
the four triggers) and the stamp-cached wording.
|
||||
- N3, at the rebase on 294 (c866a8a69): 294's accessFor passes `access` to the live boot, so the
|
||||
timer and request sweep go live with both rows; fix 294's "reads the file on every user request"
|
||||
comment to stamp-cached, and check its ACCESS_HELP edit against M15's ordering assertion.
|
||||
- N4: the touched whole files (user-principal, viewer, thread-shares) run whole in the bus suite;
|
||||
a first-run red goes per mos-1272.
|
||||
+20
-3
@@ -211,6 +211,7 @@ an operator inspect content and delivery evidence after an uncertain result.
|
||||
| `review.verdict` | `{task_ref,request,verdict,body,decision?}`; action `review.verdict`. `verdict` is `approve` or `changes`. `request` must be a `review.requested` event on the task addressed to the caller's role (`request-not-found`), with no verdict yet (`verdict-exists`). Writes a `review.verdict` event `{task_ref,request,commit,verdict,message}` and sends the requesting role a REVIEW-RESULT in reply to the request's message. Returns `{verdict,message,commit}`. |
|
||||
| `event.emit` | `{kind:'action.allowed',body:{action:'routine',target?},subject?}`; routine observations only. |
|
||||
| `launch.revoke`, `launch.restore` | No args; outside-agent human CLI only. |
|
||||
| `access.reload` | No args; the outside-agent human CLI only (row 295, #1801, FED R4): the tester and an agent get `human-required`, a reader `read-only`, a user `access-refused`. Reads the access file whatever its stamp and ends the lost claims of every business, since the file is shared, with `by` the human. Returns `{ended:[{thread,session,run}]}` for the caller's business only. `mosaic access grant` and `revoke` call it after the write. |
|
||||
| `inbox` | No args; plain JSON projection scoped to capability's business. |
|
||||
| `tasks` | Optional args, scoped to capability's business (row 89, #1583). None: every row of the tasks projection (below). `{task_ref}` alone: that one row, or `task-not-found`. Any of `{done,assignee,label,requirement,limit,after}`: `{tasks,total,next}`, the short form (below). An unknown arg or bad value refuses `invalid-request`. |
|
||||
| `agents` | No args; each held role's latest claim row in the capability's business. Human and reader sessions also get `state`, `working`, `inputNeeded` and `stranded` (Agent state, below); a role session gets the claim rows only. |
|
||||
@@ -305,8 +306,9 @@ Only a human principal matches a share: the business human, a reader session as
|
||||
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
|
||||
unshare ends reads on the next request. Only `thread.messages` admits a sharee; `thread.open`,
|
||||
access document as of that request: the broker stats the file each time and reads it again only
|
||||
when the stat changed (row 295), so a removed, expired or unreadable grant or an 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
|
||||
user's thread messages to the owner and readers while the check holds. The tester can't call
|
||||
the share verbs (`tester-refused`), and a viewer is refused them `access-refused`; a viewer
|
||||
@@ -316,7 +318,22 @@ reads a thread shared with it. Tests: `tests/thread-shares.test.mjs`.
|
||||
Per thread it returns `{thread,instance,pending,unread,lastMessage,lastOpen,lastClaim,holder}`:
|
||||
messages to the instance not yet delivered, messages delivered to the current holder and
|
||||
not yet read, the latest message, open and claim times, and the holding run or `null`. The
|
||||
launcher reads it to start and stop thread sessions (`packages/cli/README.md`).
|
||||
launcher reads it to start and stop thread sessions (`packages/cli/README.md`). A row adds
|
||||
`revoked: true` when its thread's user owner has lost the grant (below); the launcher never
|
||||
starts such a thread and sends a running one SIGTERM.
|
||||
|
||||
Revocation (row 295, #1801, FED R4, federation design 3.7). A user thread is lost when no grant
|
||||
in the access file covers it at collaborator level, so a downgrade to viewer is a loss. One
|
||||
transaction ends the held claim of every session of every lost thread, op `revoke`, reason
|
||||
`access-revoked`; the run's next bus call refuses `not-holder`, and a new run of the thread
|
||||
refuses `thread-revoked` at `bindLaunch` and at `role.claim`. A rebind of an earlier host's run is
|
||||
allowed, so the launcher's recovery can end it. Four triggers end the claims, each with its `by`:
|
||||
the runtime's timer, every 30 s (`access.intervalMs` sets it in process, as `librarySync.intervalMs`
|
||||
does), which reads the file whatever its stamp (`access-timer`); a user's request or a
|
||||
`threadsState` call after the file's stat changed (`access-change`); a `threadsState` row that is
|
||||
lost with its claim still held, as when a grant expired with no write, from the same read
|
||||
(`access-expiry`); and `access.reload` (the human). The timer calls `accessSweep()`, a trusted
|
||||
broker method, not a verb. Tests: `tests/access-revoke.test.mjs`.
|
||||
|
||||
`threadIdle({business,thread,run})` is the launcher's idle stop, also trusted and not a
|
||||
verb. It rechecks the thread and releases the run's claim in one call, and returns
|
||||
|
||||
@@ -1094,13 +1094,21 @@ export class Broker {
|
||||
// doesn't keep the holder from idling out.
|
||||
// FED R4 (design 3.7): a row adds revoked: true for a thread the access file no longer covers, as
|
||||
// of this call, so a launcher restart never starts one. A changed file also ends its lost runs here.
|
||||
// A grant that expired with no change to the file (Dewey's B1 in dewey-0871) leaves a revoked row
|
||||
// whose claim the stamp check didn't end: that row ends the lost claims from the same read, by
|
||||
// access-expiry, before the launcher sees it, so the run's end is never a release.
|
||||
threadsState(business) {
|
||||
this.#business(identifier(business));
|
||||
this.#revokeLost('access-change');
|
||||
const doc = this.#accessSeen?.doc ?? null;
|
||||
const now = new Date();
|
||||
return this.#threadRows('t.business=?', business).map(({ owner, project, ...r }) =>
|
||||
(this.#revoked({ business, owner, project, instance: r.instance }, doc, now) ? { ...r, revoked: true } : r));
|
||||
const lost = (r, owner, project) => this.#revoked({ business, owner, project, instance: r.instance }, doc, now);
|
||||
let rows = this.#threadRows('t.business=?', business);
|
||||
if (rows.some(({ owner, project, ...r }) => r.holder !== null && lost(r, owner, project))) {
|
||||
this.#revokeLost('access-expiry', true, doc);
|
||||
rows = this.#threadRows('t.business=?', business);
|
||||
}
|
||||
return rows.map(({ owner, project, ...r }) => (lost(r, owner, project) ? { ...r, revoked: true } : r));
|
||||
}
|
||||
// Row 121 round 2 (#1615, Dewey's F3 in 28591): the launcher's idle stop. The tick decides
|
||||
// from a threadsState snapshot, and a message the run took after it would be delivered to a
|
||||
|
||||
@@ -187,6 +187,24 @@ test('R4: threadsState marks revoked: true on the lost user\'s thread only, a pe
|
||||
assert.equal(Object.hasOwn(row(A), 'owner'), false, 'the row keeps its old fields');
|
||||
});
|
||||
|
||||
// Dewey's B1 (dewey-0871): the launcher ticks faster than the timer, so a grant that expires with no
|
||||
// write must not reach it as revoked: true with the claim still held.
|
||||
test('R4: threadsState after an expiry with no write and no sweep ends the claim before it marks the row revoked', (t) => {
|
||||
t.mock.timers.enable({ apis: ['Date'], now: Date.parse('2026-10-23T00:00:00Z') });
|
||||
const { b, code, claim, store, A, B } = setup(t, { grants: [grant({ expires: '2026-10-23T00:01:00Z' }), grant({ user: 'bob' })] });
|
||||
const row = (id) => b.threadsState('demo').find((x) => x.thread === id);
|
||||
assert.equal(row(A).revoked, undefined, 'live');
|
||||
t.mock.timers.tick(60_001);
|
||||
const a = row(A);
|
||||
assert.deepEqual([a.revoked, a.holder], [true, null]);
|
||||
assert.deepEqual(claim('a-run'), { op: 'revoke', reason: 'access-revoked', by: 'access-expiry' });
|
||||
assert.equal(code('a', 'message.receive'), 'not-holder');
|
||||
assert.deepEqual([row(B).holder, claim('b-run').op], ['b-run', 'claim']);
|
||||
// Ended once: the next call writes no second revoke.
|
||||
row(A);
|
||||
assert.equal(store.get("SELECT count(*) AS n FROM thread_claims WHERE holder_run='a-run' AND op='revoke'").n, 1);
|
||||
});
|
||||
|
||||
test('R4: a new run of a revoked thread refuses thread-revoked at bind and at claim; a rebind is allowed', (t) => {
|
||||
const { b, call, edit, record, broker, A, B } = setup(t, { runs: false });
|
||||
const late = b.bindLaunch(record('late', A));
|
||||
|
||||
@@ -702,7 +702,7 @@ export function createLauncher({
|
||||
if (clearing.has(t.session)) continue;
|
||||
const s = registry.running(b.id, t.instance, t.thread, t.session);
|
||||
// FED R4 (#1801, design 3.7): the broker marks a thread whose user owner lost the grant, and has
|
||||
// ended its claim. A revoked thread never starts, and a running one gets the idle stop's
|
||||
// ended its claim before it returns the row, whether the file changed or a grant expired. A revoked thread never starts, and a running one gets the idle stop's
|
||||
// start-time-checked SIGTERM at once, with no threadIdle: there is no claim left to release.
|
||||
if (t.revoked) {
|
||||
if (s && !stopping.has(s.run)) {
|
||||
|
||||
@@ -17,6 +17,7 @@ import { fileURLToPath } from "node:url";
|
||||
import { launchDoc } from "../../business/tests/helpers.mjs";
|
||||
import { BusError } from "../../bus/src/broker.mjs";
|
||||
import { startBroker } from "../../bus/src/runtime.mjs";
|
||||
import { Store } from "../../bus/src/store.mjs";
|
||||
import { gitEnv } from "../../seat/src/checkout.mjs";
|
||||
import { sandboxProblem } from "../../seat/src/sandbox.mjs";
|
||||
import { launchLogFile, readSessions, Registry, SESSION_CREDENTIALS } from "../../seat/src/session.mjs";
|
||||
@@ -199,6 +200,14 @@ test("REQ-LAUNCH-1 R4: a running thread's run gets SIGTERM at the first tick aft
|
||||
// More ticks start nothing.
|
||||
for (let i = 0; i < 3; i++) await host.launcher.tick();
|
||||
assert.equal(events(f, "launch").length, 1);
|
||||
// Dewey's B1: the broker ended the claim before the launcher's kill, so the run's end didn't release it.
|
||||
await host.close();
|
||||
const store = new Store(f.dataRoot);
|
||||
t.after(() => store.close());
|
||||
assert.deepEqual(
|
||||
store.get("SELECT op, reason, by FROM thread_claims WHERE holder_run=? ORDER BY seq DESC LIMIT 1", launch.run),
|
||||
{ op: "revoke", reason: "access-revoked", by: "access-change" },
|
||||
);
|
||||
});
|
||||
|
||||
// sage-3139, D1: the bind refusal skips a rebind, so recover() ends the earlier host's run.
|
||||
|
||||
Reference in new issue
Block a user