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).
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 effectiveauthority_levelceiling; 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).
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'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.
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.
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.
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 | 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.
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.
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_levelrequest raises only the specificgrant_ceiling.sources[]entries named inbinding_source_refs, to (at most)requested_level, for the declaredgrant_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 agrantedstatus 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_levelMUST 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 freshGrantRequestnaming 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_approvalrequest permits exactly the onerequested_actionon the one unit/task combination the request named, pergrant_scope. It does not grant other RFC-0009 actions, and does not touchgrant_ceilingat all — the two mechanisms compose the same way they do without this RFC (RFC-0025's normative capping table still applies to whateverauthority_levelis in effect). - A granted
confidence_below_thresholdrequest permits the specific output to be surfaced as a decision. It is inherentlysingle_use— see Conformance;time_boundandstandinggrant 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: standingagainst amandatory_sources-protected source (RFC-0025 §3.13) MUST carry anexpires_atregardless ofgrant_scope.mandatory_sourcesexists specifically to stop a policy ceiling from being silently weakened; an indefinitestandinggrant 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 amandatory_sourcesentry.standinggrants against non-mandatory sources are unaffected by this rule.
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.
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.
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.
- RFC-0002 (Auth and Delegation):
grantor.approval_mechanismreusesdelegation.human_in_the_loop.approval_mechanism's vocabulary verbatim rather than defining a parallel one. This RFC giveshuman_in_the_loop.required: truethe runtime object it previously lacked. - RFC-0009 (Visibility and Authority Declarations):
requires_approvalactions resolve through aGrantRequestwithtrigger: 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_refsand the recomputation rule depend directly on RFC-0025's named-binding-source requirement (§3.13) — aGrantRequestcannot state what it is raising without RFC-0025 having named what was binding in the first place.
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).
| 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.
- v3 (2026-07-24): Resolved the audit/record half of the manifest-vs-API question (former
Open Question 1): a new §17
grant_request_eventstable, mirroring the existingrender_events/quarantine_eventspattern, 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_refwas singular, but RFC-0025'sgrant_ceilingallows 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 tobinding_source_refs(a list) with an explicit atomicity rule — tied sources must be raised together, never partially. Added an explicit rejection consequence forrequested_levelfailing to exceedcurrent_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. Tightenedstandinggrants againstmandatory_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.