Files

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-enroll enrollBinding. 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 with approvalChoice by the business's human, via 'cli', or via 'console' with the broker's own human.input (row 252).
  • Start: decision.resolve (:5653-5659) runs #approvedGrant after the close and, once the transaction commits, starts #runEnrollment, #runHostGrant or #startGitea with setImmediate.
  • Spend and records: each executor calls #consumeGrant and 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.mjs checkLaunch, 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.