6.3 KiB
Row 298 (#1804, Q228): the chief-of-staff grant scope
Owner Megavolt, reviewer Dewey. Assigned in sage-3584 (mos-1489), 2026-10-23 18:53Z.
Base: origin/refactor 45dca69f6. Branch: megavolt/row298.
Brief: docs/plans/2026-10-23_mvp-readiness-rows.md, "Brief: the chief-of-staff grant scope (Q228)".
Decision: DECISIONS Q228 (jason-master-restart DECISIONS.md:2278). It replaces Mos's Q190 reading.
What exists at 45dca69f6 (broker.mjs)
- Raise:
#raiseGrant(:2558) is chief-only (grant-chief-only) and binds a grant body per method: gitea#giteaGrantBinding(:2665), host#hostGrantBinding(:2599), or host-enrollenrollBinding. The decision goes through#dispatch(..., 'decision.raise', ...), routed to the human. - Approval:
#approvedGrant(:1525) returns the binding only for a credential.mint routed to the human and closed withapprovalChoiceby the business's human, via 'cli', or via 'console' with the broker's own human.input (row 252). - Start: decision.resolve (:5653-5659) runs
#approvedGrantafter the close and, once the transaction commits, starts#runEnrollment,#runHostGrantor#startGiteawith setImmediate. - Spend and records: each executor calls
#consumeGrantand writes the 236 records (grant.call, grant.result, grant.step, grant.applied or grant.failed, gitea_grants or host_grants). Revoke goes through grants.revoke and hosts.revoke, as before. - Human-only settings:
#human(s)(cli, outsideAgent) plus#mayHuman, as in veto.set (:4713). The organization file is checked whole by business.mjscheckLaunch, and a bad file refuses every read.
Proposed design (waits on Sage's rulings, Q1-Q6 below)
D1. Storage. The scope gets its own key, launch.grantScope, in the organization file. The
whole-file check doesn't refuse on it. A separate reader, grantScope(document), returns the
parsed scope or, when the key is missing or bad, the empty scope {}, plus a fallback flag.
A bad value can't break every organization read, and it falls back to "everything needs
Approve", which is the fail-closed direction. (Q1)
D2. Shape. { gitea?: { agents: [id], projects: [id] }, host?: { agents: [id], hosts: [id], commands: [[argv prefix]] } }. A request is in scope only if its method has an entry, its agent
is listed, and every target is listed: each gitea project, the host, and each command prefix
equal to a listed prefix. host-enroll is never in scope, because it adds a host to the registry.
(Q2)
D3. Writer. A new verb, grants.scope.set { revision, scope }, CLI-only for the human: #human(s),
then #mayHuman, with the verb added to LIFECYCLE_HUMAN_VERBS. The tester, the reader and every
agent are refused. organization.set's ORGANIZATION_KEYS don't include it, so the chief's tool
never reaches it, the same way instances.set and veto.set stay human-only. The write validates
strictly and refuses a bad value; only the read falls back. Receipt: grants.scope with the old
and new sha256. grants.scope reads the value for the human, the tester and the chief. (Q3)
D4. In-scope raise. Inside the raise transaction, #raiseGrant checks the binding against the scope
and, if it's in scope, closes the new decision at once: op resolved, choice approvalChoice, by
grant-scope, via scope. It writes a grant.scope.used event carrying the scope sha256 and
revision. #approvedGrant accepts via 'scope' only when that event exists for the close's seq
and its sha256 matches the scope as read in the same transaction. After the commit, the same
setImmediate starts the same executor, so 236's spend, records and revoke are unchanged. The
decision is closed before any inbox read, so no inbox item ever shows. (Q4)
D5. Out-of-scope raise: unchanged. The decision stays open, routed to the human, for the Console
and CLI Approve.
D6. Executor recheck. At the spend, #consumeGrant reruns the scope check for a via-'scope'
approval. If the scope changed between raise and run and the grant is now out of scope, it
fails with grant.failed scope-changed and the grant is spent. (Q5)
Open questions for Sage
Q1. Storage: launch.grantScope in the organization file with a lenient reader (D1), or a separate
file under the business directory?
Q2. Shape: is per-method agent, project, host and command lists right? Should host-enroll stay
out of scope always?
Q3. "A scope change needs Approve": is the human's CLI verb itself the Approve (D3)? Or must the
chief also be able to raise a scope change that Jason approves in the inbox (a new decision
action, more code)? I recommend D3 alone. Should Console get a scope page in this row? I
propose no.
Q4. Recording an in-scope grant as a broker close via 'scope' with a scope event (D4), versus a
decision that is never raised (a new table). D4 keeps 236's records and revoke unchanged.
Q5. Scope narrowed between raise and run: fail scope-changed (D6), or honor the raise-time check?
Q6. Chief enrollment ("Jason's Approve stays for the chief of staff's enrollment"): I read this
as the chief's own instance and credentials, which the human's instances.set and the
credential lifecycle already cover. So nothing new is needed in this row. Agree?
Tests (access by brief; all on scratch stores, fake Gitea, fake host)
T1 agent, chief and tester callers of grants.scope.set are refused, and organization.set with grantScope is refused. T2 a bad or missing grantScope falls back to empty and the grant waits. T3 an in-scope gitea request issues with no open decision and no inbox item, and writes the 236 records. T4 the same for a host request. T5 out of scope by agent, by project, or by command waits for Approve. T6 host-enroll always waits. T7 a scope change by the human's CLI writes a receipt; a scope change by any other path is refused. T8 an in-scope grant is logged and revoked with grants.revoke or hosts.revoke. T9 scope narrowed before run gives scope-changed.
Mutants (brief)
M1 the scope check skipped (every chief raise auto-closes): killed by T5 and T6.
M2 the fallback skipped (a bad value is used as-is or throws): killed by T2.
M3 the #human check removed from grants.scope.set: killed by T1.
Holds
Darkwing holds the slot: no push, no claim, no queue write, no node and no test run until Sage says so. Q188: no agent creates a grant; tests use scratch stores only.