Pular para o conteúdo

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 (#193 Fase 1) is scaffolded to host the store + UI. Owner ratification flips this to Normative.

Reference correction: meta/docs/stack#385 cites "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
}
  • ActionId is a capability action id from the control plane catalogue (RFC 004 Amendment A1). A grant is resolved per scope (§6.1), so within a scope-resolved grant the action ids are the bare capability action ids that the scope namespaces — e.g. in a scope: "desk" grant, ticket.reply (not desk.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:

  1. its membership in deny (→ never) / propose / allow,
  2. its default disposition by blast radius (RFC 004 A1 §16.3: high defaults to propose even when listed in allow unless the tenant has explicitly opted that action into autonomous allow),
  3. the max_blast_radius ceiling (an action whose blast radius Exceeds the ceiling is forced to deny, regardless of lists),
  4. all conditions holding.

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

TierScopeComposition
owner-individualthe principal's own objectsstands alone
org owner/adminorg-wide grant + a ceilinga 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_id is enforced as the RLS-GUC (app.current_tenant_id) on the store, per multi-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 Grant type (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:desk MAY 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 (like deploy: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#025 slice 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:

  1. Resolve the principal's effective grant (§4 intersection) for (tenant_id, scope).
  2. For a candidate ActionId, compute the disposition by §3 (most-restrictive).
  3. allow → the action is eligible; it still passes dry-run / rate-limit / approval / circuit-breaker before firing (RFC 006). propose → route via propose_action (RFC 005 Confirm/Reject card). deny → drop.
  4. Emit koder.admin.action (RFC 004 §7 / A1 §16.6) with the grant_id, blast_radius, decision, and correlation trace_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 (#193 Fase 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.