grant-contract
autonomy specs/autonomy/grant-contract.kmd
Corpo da especificação
// Delegated-autonomy grant contract — the single, normative shape of "what an // agent may do autonomously, for whom, under what conditions, up to what blast // radius, before a human Confirm is required". The Agent Console OWNS the store // and UI (meta/docs/stack#385, owner §6.4); this spec is the CONTRACT both the // store (producer) and every consumer (Kortex first, kortex#035) bind to.
Delegated-autonomy grant contract
[!badge orange "Draft"] [!badge gray "Awaiting ratification"] [!badge blue "Consumer: Kortex (kortex#035)"]
Status: Draft. The contract is defined contract-first so the first consumer (Kortex,
kortex#035) can compile a delegation guard against a stable shape before the Agent Console product (#193Fase 1) is scaffolded to host the store + UI. Owner ratification flips this to Normative.Reference correction:
meta/docs/stack#385cites "stack-RFC-020 §A4" for the scoped/revocable token — that RFC is the AI-compute strategy and does not define capability scopes; this spec does not bind to RFC-020.Substrate correction (2026-07-01): an earlier draft named Koder ID conditional scopes (
id-RFC-015) as the substrate. That mechanism is coarse group-gating for human logins and does not project per-principal per-action grants (see §5). The canonical substrate is the Agent Console grant store +GET /v1/grants(RLS-scoped, one-model); conditional scopes are an OPTIONAL enhancement (§5).
1. Why one contract
agent-guardrails.kmd forbids divergent per-agent safety models ("kode allows
what kortex blocks"). There is therefore one grant contract for every
Koder-built agent over every system — internal tickets and customer tickets
alike. scope + conditions distinguish cases (RFC 016 §6.2); there are no
two parallel contracts.
2. The grant
grant := {
tenant_id : string // RLS-GUC tenant scope (multi-tenant-by-default)
principal : string // the responsible principal (Koder ID subject)
scope : ScopeSelector // which systems/objects this grant covers
allow : [ActionId] // may fire autonomously (subject to all guards)
propose : [ActionId] // must route to a human Confirm/Reject card
deny : [ActionId] // never, even proposed
conditions : [Condition] // predicates that must hold (time, object class, …)
max_blast_radius : BlastRadius // ceiling the grant cannot exceed (low|medium|high)
audit : "required" // every resolved action emits koder.admin.action
}
ActionIdis a capability action id from the control plane catalogue (RFC 004 Amendment A1). A grant is resolved perscope(§6.1), so within a scope-resolved grant the action ids are the bare capability action ids that the scope namespaces — e.g. in ascope: "desk"grant,ticket.reply(notdesk.ticket.reply). The fully-qualified<scope>.<action>form is only for referring to actions across scopes.BlastRadius∈ {low,medium,high}, keyed to irreversibility (RFC 004 A1 §16.3).
3. Default posture — deny/propose, never allow
The empty/initial grant is deny everything; granting is explicit opt-in,
mirroring deploy-authz model-a (default false). An action's effective
disposition is the most restrictive of:
- its membership in
deny(→ never) /propose/allow, - its default disposition by blast radius (RFC 004 A1 §16.3:
highdefaults toproposeeven when listed inallowunless the tenant has explicitly opted that action into autonomousallow), - the
max_blast_radiusceiling (an action whose blast radiusExceedsthe ceiling is forced todeny, regardless of lists), - all
conditionsholding.
So allow is necessary but never sufficient: a high-blast action in allow
still proposes unless explicitly opted in, and is denied if it exceeds the ceiling.
4. Two principal tiers — composition by intersection
| Tier | Scope | Composition |
|---|---|---|
| owner-individual | the principal's own objects | stands alone |
| org owner/admin | org-wide grant + a ceiling | a member's effective grant = intersect(member_grant, org_grant) |
Composition is tighten-never-widen: a member grant can only narrow the org grant (drop actions, lower the ceiling, add conditions), never widen it. An attempt to widen is clamped to the org grant. This is the same intersection discipline RLS uses for tenant rows.
5. Expression — the store + API is canonical (owner-decision 2026-07-01)
Revised. The original §5 said a grant is "carried as Koder ID conditional scopes on the token — not a bespoke store format." Implementation proved that aspiration unworkable at this granularity, and it contradicted §8 (which mandates a store). The system of record is the Agent Console (Koder Kli) grant store, read by consumers over
GET /v1/grants; this is the canonical expression.
A grant is a row in the Kli grant store, resolved per request:
tenant_idis enforced as the RLS-GUC (app.current_tenant_id) on the store, permulti-tenancy/contract.kmd; a grant never crosses tenants.- Revocation is instant: editing or deleting the grant takes effect on the next resolution — no token-TTL lag.
- One model: the store, every consumer's guard, and the wire form all bind to
the one
Granttype (engines/sdk/autonomy); there is a single source of truth. - A consumer resolves the effective grant (§4) via the API and compiles it (§6). Reads are fail-closed (an unreachable store denies), so the API dependency never widens autonomy.
Why not conditional scopes on the token
The id-RFC-015 conditional-scope mechanism gates a scope by group membership,
evaluated only in human login flows (oauth/.../scope_grants.go
filterGrantedScopes). deploy:prod works because it is ONE coarse scope → ONE
group. Projecting a per-action grant (desk:ticket.note:allow per principal)
would need a group per (scope, action, disposition, principal) tuple
(unmanageable) or a new per-principal conditional-scope store in the ID engine —
which would duplicate this store as a second source of truth, and give only
eventual revocation (token TTL) plus token bloat. So it is structurally weaker
here, not "more complete".
Optional future enhancements (not required; unbuilt)
- (hybrid) Coarse participation scope — a single group-gated scope such as
autonomy:deskMAY be surfaced on a principal's token to gate whether it participates in delegated autonomy at all; the fine-grained grant stays in the store. This fits the existing mechanism (likedeploy:prod) without the per-action explosion. - (full) Per-principal scope projection — only if the ID engine grows a
per-principal conditional-scope capability (id-RFC-015 extension). Gated on that
cross-component work; tracked in
kli#025slice 3.
6. Consumer contract (Kortex first — kortex#035)
A consumer never trusts the wire; it compiles the grant into a deterministic guard and composes it with the rest of the RFC 006 chain:
- Resolve the principal's effective grant (§4 intersection) for
(tenant_id, scope). - For a candidate
ActionId, compute the disposition by §3 (most-restrictive). allow→ the action is eligible; it still passes dry-run / rate-limit / approval / circuit-breaker before firing (RFC 006).propose→ route viapropose_action(RFC 005 Confirm/Reject card).deny→ drop.- Emit
koder.admin.action(RFC 004 §7 / A1 §16.6) with thegrant_id,blast_radius,decision, and correlationtrace_id, whatever the outcome.
Kortex's internal/controlplane already provides the BlastRadius/
DefaultDisposition/Exceeds primitives (kortex#033/#034); kortex#035 adds the
grant resolver + the §3 disposition function + the propose routing. The
deterministic gate (Execute refuses unless allow) stays unchanged.
7. Audit
audit: required is non-negotiable: every resolved action (allowed, proposed,
or denied) emits one koder.admin.action. The audit boundary is the governance
layer, not the transport (RFC 004 A1 §16.4).
8. Slicing (what is built where)
- This contract (slice 1, here) — unblocks
kortex#035. - Grant store + UI (slice 2) — built in the Agent Console once its product
is scaffolded (
#193Fase 1:/k-new-product+ headless core). Until then the store is absent; a consumer with no grant resolves to the default-deny posture (§3), which is safe.