Skip to content

Latest commit

 

History

History
429 lines (353 loc) · 26.7 KB

RFC-0026-Escalation-and-Grant-Requests.md

File metadata and controls

429 lines (353 loc) · 26.7 KB

RFC-0026: Escalation and Grant Requests

Status: Accepted (semantics + audit trail) — promoted to SPEC.md v0.28. The audit/record half of the manifest-vs-API question is resolved (a new §17 grant_request_events table); the active request/response coordination mechanism (new MCP tools) remains open, deferred to a future RFC — see "Resolution: manifest vs. API," below. Authors: eXOReaction AS (Thor Henning Hetland) Date: 2026-07-24 Related: RFC-0002 (Auth and Delegation), RFC-0009 (Visibility and Authority Declarations), RFC-0025 (Authority Level and Multi-Source Grant Ceiling) — this RFC builds on all three and replaces none of them. Motivating case: Mynder Orchestration Canvas v1 (Cantara customer deployment) — same deployment as RFC-0025, completing the mechanism RFC-0025 deliberately left open (its Open Question 6).


What This RFC Proposes

Three prior mechanisms declare when a human must be involved, but none defines what happens next as a concrete, auditable object:

  • RFC-0002's delegation.human_in_the_loop.required: true — a human must approve before a unit may be loaded at all.
  • RFC-0009's authority.<action>: requires_approval — a human must approve before a specific action on a specific unit.
  • RFC-0025's grant_ceiling — computes an effective authority_level ceiling; a task that needs to exceed it currently has no defined path to ask for more.

All three currently stop at "a human should be asked" without specifying: asked how, asked who (by name or role, not just "a human"), what the answer looks like, how long an answer is valid for, and how a granted answer feeds back into evaluation. This RFC defines that object — a GrantRequest — and its lifecycle, plus a fourth trigger type that RFC-0025's Open Question 6 anticipated but did not define: escalation on low model-reported confidence in a task's own output, evaluated after synthesis rather than before.

This RFC is scoped narrowly: it defines the request/response object and lifecycle. It does not define a specific approval-mechanism transport (RFC-0002's oauth_consent / uma / custom taxonomy already covers that, and this RFC reuses it rather than inventing a second one).


The Problem

"A human should approve" is not a protocol

A GrantRequest-shaped gap already exists in production. Mynder's own Policy Agent module (GDPR supplier-document-retention review) independently built exactly this: a persisted state machine (scanning → pending_review → executed | dismissed), a named approver (reviewedByUserId), and full timestamping — because RFC-0002/0009's declarative "approval required" flags gave it no runtime object to use. Every deployment that takes requires_approval seriously ends up building its own version of the same lifecycle object. That is exactly the kind of convergent, independently-invented mechanism a protocol should capture once rather than leave to N reinventions.

RFC-0025 computes a ceiling; nothing defines what happens when a task needs more

RFC-0025's grant_ceiling produces an effective authority_level and names the binding source. It stops there deliberately (its Open Question 6). A task whose effective level is explain but whose actual work requires suggest has no defined next step: today, an implementation must invent its own escalation mechanism, and — this is the part RFC-0025 could not specify on its own — that mechanism needs to know it can raise only the specific binding source, not the whole computed minimum, or a naive implementation collapses grant_ceiling's entire multi-source discipline the first time someone approves anything.

Confidence is a different kind of trigger — it does not exist before synthesis

Every mechanism in RFC-0009 and RFC-0025 is evaluated from declared, static metadata, before an agent generates anything — authority, authority_level, and grant_ceiling are all computable by a parser with no model in the loop. A model's own confidence in what it just produced is categorically different: it does not exist until after synthesis, and no static manifest field can express it in advance. Treating "low confidence" as if it were just another static ceiling source (as an earlier informal review of this design space initially assumed) is a category error. It needs its own trigger type, evaluated at a different point in the pipeline, funneling into the same GrantRequest object rather than a separate one — the shape of "agent hit a wall, needs a named human to lift it" is identical whether the wall is an authority ceiling or the agent's own uncertainty.

Concrete scenario

Scenario A — Mynder, continued from RFC-0025: A task-type's effective authority_level (per RFC-0025) is explain, bound by the task-type's own manifest ceiling. The agent determines the actual work would require suggest. Today, nothing in the manifest says who to ask, what form the ask takes, how long a "yes" is good for, or how the "yes" is supposed to interact with the five other grant_ceiling sources that were not the binding constraint. Separately, the same deployment wants: if an agent's own confidence in a generated compliance determination falls below a declared threshold, that must trigger the identical human-in-the-loop pause — evaluated per-task after generation, not derivable from any pre-declared manifest field.


Design

GrantRequest — the object

grant_request:
  id: "gr-2026-07-24-0031"                  # unique per request, implementation-assigned
  trigger: insufficient_authority_level      # see Trigger vocabulary, below
  task_type_ref: change-formal-status
  agent_ref: lara-compliance

  # populated for trigger: insufficient_authority_level
  binding_source_refs: [task-type-ceiling]   # ALL grant_ceiling sources (§3.13, RFC-0025) tied for the binding minimum — a list even when only one ties
  current_effective_level: explain
  requested_level: suggest

  grantor:
    role: "kundeansvarlig"                  # named role or person, not just "a human"
    approval_mechanism: oauth_consent        # reuses RFC-0002's §3.4 vocabulary — oauth_consent | uma | custom

  grant_scope: single_use                   # single_use | time_bound | standing
  status: pending                           # pending | granted | denied | expired

  requested_at: "2026-07-24T09:15:00Z"
  resolved_at: null
  resolved_by: null
  justification: "Customer-supplied evidence contradicts the auto-classified status; need to propose a correction, not just explain the discrepancy."

grant_request fields:

Field Required Type Description
id REQUIRED string Unique per request. Implementation-assigned; format is implementation-defined but MUST be stable for the request's lifetime.
trigger REQUIRED enum One of the Trigger vocabulary values, below.
task_type_ref REQUIRED string The task_types[].id (RFC-0025 §3.13) this request concerns.
agent_ref OPTIONAL string The agents[].id (RFC-0025 §3.13) making the request, if applicable.
binding_source_refs REQUIRED for insufficient_authority_level list of strings All grant_ceiling.sources[].id (§3.13) tied for the binding minimum in the original ceiling computation — a list even when only one source ties. RFC-0025 itself requires reporting the full tied set (§3.13's "which source(s) produced the binding value"); a singular field here would silently break the moment a tie occurs (see Resolution semantics).
current_effective_level REQUIRED for insufficient_authority_level string The authority_level computed before this request.
requested_level REQUIRED for insufficient_authority_level string The authority_level the task needs. MUST exceed current_effective_level. A request where this does not hold MUST be rejected as a request-validation error before being surfaced to a grantor — it is not a judgment call for a human to make.
requested_action REQUIRED for requires_approval string The RFC-0009 §4.17 action (modify, execute, etc.) awaiting approval.
confidence_observed REQUIRED for confidence_below_threshold float 0.0–1.0 The agent-reported confidence that triggered escalation.
confidence_threshold_ref REQUIRED for confidence_below_threshold string Reference to the task_types[].confidence_threshold (below) that was breached.
grantor.role REQUIRED string Named role or person expected to resolve this request. MAY reference a role vocabulary the deployment maintains outside KCP (KCP does not define an identity/role directory).
grantor.approval_mechanism REQUIRED enum oauth_consent | uma | custom — reuses RFC-0002 §3.4's vocabulary exactly; this RFC does not define a second transport taxonomy.
grant_scope REQUIRED enum single_use (this task instance only — default and safest) | time_bound (valid until an expiry, below) | standing (persistent until explicitly revoked).
expires_at REQUIRED if grant_scope: time_bound ISO 8601 datetime When a time_bound grant stops being valid.
status REQUIRED enum pending | granted | denied | expired.
requested_at / resolved_at REQUIRED / OPTIONAL ISO 8601 datetime Request and resolution timestamps. resolved_at is null while status: pending.
resolved_by REQUIRED once resolved string Identity of whoever actually resolved the request. MUST be recorded even if it matches grantor.role — this is the audit fact, not the request's expectation.
justification OPTIONAL string Free-text reason for the request, surfaced to the grantor.

Trigger vocabulary

Trigger Evaluated Source
insufficient_authority_level Before synthesis (static) RFC-0025 grant_ceiling computes an effective level lower than the task requires.
requires_approval Before the specific action RFC-0009 authority.<action>: requires_approval is hit.
confidence_below_threshold After synthesis (runtime, model-reported) This RFC's task_types[].confidence_threshold (below) is breached by the agent's own reported confidence in its output.

The third trigger is categorically different from the first two and MUST NOT be treated as just another grant_ceiling source (RFC-0025 §3.13): it cannot be computed by a manifest parser with no model in the loop, because the value being compared does not exist until after the agent has generated its output. Implementations MUST evaluate it as a distinct, post-synthesis check.

confidence_threshold — declared per task-type

task_types:
  - id: change-formal-status
    authority_level: explain
    confidence_threshold: 0.70   # OPTIONAL — agent-reported confidence below this triggers a GrantRequest
Field Required Type Description
task_types[].confidence_threshold OPTIONAL float 0.0–1.0 If the acting agent reports confidence in its output below this value for this task-type, a GrantRequest with trigger: confidence_below_threshold MUST be raised before the output is surfaced as a decision. Absence means no confidence-based escalation is required for this task-type — this is a declared opt-in, not a default-on check, since not every task-type has a well-defined notion of "confidence" to report.

KCP does not define how an agent computes its own confidence value — that is the runtime's responsibility, the same division of labor RFC-0009 draws between declaring execute: denied and an alignment-dependent model actually respecting it (see RFC-0009 Appendix D). This field only defines the threshold and the resulting obligation to escalate.

Honest admission — self-reported confidence is gameable, and unlike authority: denied it fails silently. RFC-0009 Appendix D is explicit that authority declarations are policy, not enforcement, and documents specific models that failed to respect them under adversarial testing. confidence_threshold has the identical enforcement gap, but a worse failure mode: an agent (or a compromised or simply poorly-calibrated runtime) that always reports high confidence never triggers escalation, and — unlike a denied execute action, which at least fails loudly under agent-drift-style probing (RFC-0009 Appendix D) — there is no manifest signal that this is happening. A confidence value that is always 0.95 looks identical, from the manifest's perspective, to an agent that is genuinely always confident. Deployments relying on this trigger SHOULD independently audit the distribution of reported confidence values against outcomes (does a "high confidence" output actually turn out right as often as claimed) rather than trusting the number in isolation — this is a runtime operational practice, not something this RFC can enforce structurally.

Resolution semantics — how a grant feeds back into grant_ceiling

This is the part that must be precise, or the whole discipline RFC-0025 built collapses the first time someone clicks "approve":

  • A granted insufficient_authority_level request raises only the specific grant_ceiling.sources[] entries named in binding_source_refs, to (at most) requested_level, for the declared grant_scope. It does not touch, override, or bypass any other source.
  • Tied binding sources MUST be raised together, atomically, as a single grant decision. If RFC-0025's ceiling computation reported more than one source tied for the minimum, all of them appear in binding_source_refs, and a granted status raises all of them in the same resolution. Raising only a subset of a tied set MUST NOT change the effective level — the untouched tied source(s) still hold the minimum down, so a partial grant is a no-op by construction. There is no such thing as "granting half a tie."
  • The effective authority_level MUST be recomputed as the minimum across all sources, with the named source(s) now raised. If a different source (or a new tied set) is now the binding constraint, the task remains capped there, and a fresh GrantRequest naming that new set is required — an escalation grant is never a blanket override of the whole ceiling, and granting one tie does not pre-authorize whatever ties next.
  • A granted requires_approval request permits exactly the one requested_action on the one unit/task combination the request named, per grant_scope. It does not grant other RFC-0009 actions, and does not touch grant_ceiling at all — the two mechanisms compose the same way they do without this RFC (RFC-0025's normative capping table still applies to whatever authority_level is in effect).
  • A granted confidence_below_threshold request permits the specific output to be surfaced as a decision. It is inherently single_use — see Conformance; time_bound and standing grant scopes MUST NOT be used for this trigger, since a confidence observation is about one generated output, not a standing property of the task-type or agent.
  • A denied or expired request leaves the effective level, action permission, or output suppression exactly as it was before the request — denial is not itself a demotion, it is a refusal to raise.
  • grant_scope: standing against a mandatory_sources-protected source (RFC-0025 §3.13) MUST carry an expires_at regardless of grant_scope. mandatory_sources exists specifically to stop a policy ceiling from being silently weakened; an indefinite standing grant against exactly such a source is that same silent weakening, one layer up, achieved through a single unreviewed approval instead of a missing manifest entry. A "SHOULD periodically review" obligation is not sufficient protection for a MUST-level mechanism — this RFC therefore requires a forced expiry (and re-approval to renew) specifically for grants touching a mandatory_sources entry. standing grants against non-mandatory sources are unaffected by this rule.

Complete Example

Continuing RFC-0025's worked example (§3.13): change-formal-status's effective authority_level was explain, bound by task-type-ceiling. The agent's actual task needs suggest:

grant_request:
  id: "gr-2026-07-24-0031"
  trigger: insufficient_authority_level
  task_type_ref: change-formal-status
  agent_ref: lara-compliance
  binding_source_refs: [task-type-ceiling]   # sole binding source at request time — no tie yet
  current_effective_level: explain
  requested_level: suggest
  grantor:
    role: "kundeansvarlig"
    approval_mechanism: oauth_consent
  grant_scope: single_use
  status: granted
  requested_at: "2026-07-24T09:15:00Z"
  resolved_at: "2026-07-24T09:22:00Z"
  resolved_by: "user:kari.nordmann@example.com"
  justification: "Customer-supplied evidence contradicts the auto-classified status; need to propose a correction, not just explain the discrepancy."

Recomputation: task-type-ceiling is raised to suggest for this single task instance only. Recomputing the minimum across RFC-0025's six sources (org-risk-policy: prepare, org-data-policy: suggest, regulatory-constraint: suggest, task-type-ceiling: suggest [was explain, now raised], agent-capability-ceiling: prepare, customer-setting: prepare) — new effective level: suggest, now bound jointly by org-data-policy and regulatory-constraint (both at suggest, tied for the new minimum).

If the agent's task later needed prepare, the new GrantRequest must name both tied sources: binding_source_refs: [org-data-policy, regulatory-constraint]. Per the atomicity rule above, a grant that raised only org-data-policy (say, because only that grantor was reachable) would leave the effective level at suggest — still capped by regulatory-constraint — and MUST NOT be reported as a successful escalation to prepare until the second source is also raised. This is precisely the failure mode a singular binding_source_ref field would have hidden.


Resolution: manifest vs. API (formerly Open Question 1)

GrantRequest is not a manifest field, and never should be — a signed, distributed, cached knowledge.yaml cannot be rewritten every time someone clicks approve without breaking the entire §16 trust model this protocol is built on. But "not a manifest field" splits into two genuinely different halves, one resolved here and one still open.

Resolved: the audit/record half fits §17 Observability (RFC-0017) directly, as a new table. §17 already defines a local-first, append-only event system (~/.kcp/usage.db, WAL mode) with exactly the shape a GrantRequest audit trail needs — timestamped rows, a correlation_id for tracing, "MUST NOT transmit without explicit user consent" as the existing default. This RFC adds a fourth table, grant_request_events, one row per state transition (not one row per request — a request's full history is reconstructed by grouping rows on id):

grant_request_events(
  id TEXT,                   -- the GrantRequest's own id (see the object shape above)
  timestamp TEXT,             -- ISO 8601, when THIS transition happened
  event_type TEXT,            -- created | granted | denied | expired
  trigger TEXT,                -- requires_approval | insufficient_authority_level | confidence_below_threshold
  task_type_ref TEXT,
  agent_ref TEXT,
  binding_source_refs TEXT,   -- JSON array, present for insufficient_authority_level
  current_effective_level TEXT,
  requested_level TEXT,
  grantor_role TEXT,
  resolved_by TEXT,           -- present on granted/denied rows only
  correlation_id TEXT
)

This is decisively settled — a mechanical, low-risk extension of an already-accepted pattern, not a new design.

Still open: active request/response coordination is genuinely new protocol surface. An agent that raises a GrantRequest needs to know when it's resolved; a grantor needs to see pending requests and act. §17's tables are write-only telemetry — nothing in them has a mutable pending → granted state anything can wait on. Today's bridges only serve static manifest content over MCP; they have no "pause and wait for a human decision" primitive at all. This needs new MCP tools (something like create_grant_request / list_pending_grant_requests / resolve_grant_request) with their own open design questions this RFC does not resolve: who is authorized to call resolve_grant_request (auth binding, see Open Question 1 below), push vs. poll for the requesting agent, and multi-bridge consistency (a request created via one language's bridge must be visible to an agent talking to a different bridge). This is real enough design work to deserve its own RFC rather than being rushed as a footnote here.


Open Questions

1. Does resolved_by need to be verifiably bound to grantor.role, or is recording it enough?

The design requires recording resolved_by regardless of whether it matches the expected grantor.role, for audit honesty. It does not currently require verifying that whoever resolved the request was actually authorized to hold that role — that verification is left to the approval_mechanism (OAuth/UMA token claims, the same trust boundary RFC-0009 Appendix C already draws for agent_role). Is that division of responsibility sufficient, or does this RFC need its own authorization check independent of the approval mechanism?

2. Should confidence_below_threshold support a threshold that varies by authority_level, not just by task-type?

The current design ties confidence_threshold to a task-type only. A deployment might reasonably want a stricter confidence bar at suggest than at observe for the same task-type. Deferred as unnecessary complexity for v1; worth revisiting once real deployments report whether a single per-task-type threshold is too coarse.

3. Expiry semantics for time_bound grants that outlive the manifest version they were granted against

If a time_bound grant is still active when the manifest's grant_ceiling sources change (a new manifest version ships with a different task-type-ceiling value), does the grant still apply to the old ceiling value, the new one, or does a manifest version change implicitly expire all outstanding time_bound grants? This RFC does not currently address version/grant interaction and probably should before this leaves Draft.


Relationship to Other RFCs

  • RFC-0002 (Auth and Delegation): grantor.approval_mechanism reuses delegation.human_in_the_loop.approval_mechanism's vocabulary verbatim rather than defining a parallel one. This RFC gives human_in_the_loop.required: true the runtime object it previously lacked.
  • RFC-0009 (Visibility and Authority Declarations): requires_approval actions resolve through a GrantRequest with trigger: requires_approval. This RFC does not change RFC-0009's capping semantics.
  • RFC-0025 (Authority Level and Multi-Source Grant Ceiling): answers RFC-0025's own Open Question 6. binding_source_refs and the recomputation rule depend directly on RFC-0025's named-binding-source requirement (§3.13) — a GrantRequest cannot state what it is raising without RFC-0025 having named what was binding in the first place.

Conformance

The semantics and the §17 grant_request_events audit table below are normative. The active request/response coordination mechanism (how a request is actually created and resolved, not just recorded) remains provisional — deferred to a future RFC — so a conformance claim here covers "correctly computes triggers and records the audit trail," not "provides a working create/resolve API."

Feature Level Notes
Trigger computation: requires_approval Level 2 Reuses existing RFC-0009/RFC-0002 mechanisms
Trigger computation: insufficient_authority_level Level 3 Requires RFC-0025 grant_ceiling (Level 3)
Trigger computation: confidence_below_threshold Level 3 Requires runtime (post-synthesis) evaluation, not just manifest parsing
grant_request_events audit table (§17) Level 3 Normative — one row per state transition
grant_scope: single_use Level 2 Default, simplest to reason about
grant_scope: time_bound / standing Level 3 Requires expiry/revocation handling

confidence_below_threshold grants MUST use grant_scope: single_use; a time_bound or standing grant on this trigger type is a conformance violation (see Resolution semantics).


Backward Compatibility

Addition Pre-RFC-0026 behaviour Risk
task_types[].confidence_threshold (manifest field) Silently ignored per SPEC.md §2 None
grant_request_events table (§17, new — additive to ~/.kcp/usage.db) Absent; observability consumers reading only the existing three tables are unaffected None
Manifests using only RFC-0002/0009/0025 Fully valid, unaffected None

This RFC adds no new required fields to any existing block and deprecates nothing.


Changelog

  • v3 (2026-07-24): Resolved the audit/record half of the manifest-vs-API question (former Open Question 1): a new §17 grant_request_events table, mirroring the existing render_events/quarantine_events pattern, one row per state transition. The active request/response coordination half remains genuinely open — new MCP tools, deferred to a future RFC rather than rushed here. Renumbered the remaining Open Questions (1-3) accordingly.
  • v2 (2026-07-24): Revised after adversarial review. Fixed a genuine correctness bug: the original binding_source_ref was singular, but RFC-0025's grant_ceiling allows multiple tied binding sources, and this RFC's own worked example demonstrated the singular field silently breaking (raising one of two tied sources does not change the effective level). Changed to binding_source_refs (a list) with an explicit atomicity rule — tied sources must be raised together, never partially. Added an explicit rejection consequence for requested_level failing to exceed current_effective_level (previously an unenforced MUST). Added an honest admission, mirroring RFC-0009 Appendix D, that self-reported confidence is gameable and fails silently (no manifest-visible signal), with an operational recommendation to audit confidence-vs-outcome. Tightened standing grants against mandatory_sources-protected sources to require a forced expiry, closing a gap where a single unreviewed approval could permanently defeat RFC-0025's silent-erosion protection. Flagged the Conformance table as provisional pending Open Question 1.
  • v1 (2026-07-24): Initial draft.

Knowledge Context Protocol — eXOReaction AS, Oslo, Norway.