Part 4: Composition Functions as Agentic Provisioning Gates
Continue the series
You are reading Part 4 of 4
Building the Agentic Internal Developer Platform

The reconciliation engine from Part 3 solves classification: every agent action lands in one of four buckets — MATCHED, ORPHAN_CREDENTIAL, ORPHAN_CLAIM, or DIVERGENT_OUTCOME. What it does not solve is enforcement at the point of provisioning. An agent can submit a Crossplane claim the instant it receives a credential from the broker, well before the reconciliation window closes. If that credential turns out to be orphaned or replayed, cloud resources may already exist. This part closes that gap using Crossplane Composition Functions as a synchronous policy gate inside the provisioning pipeline itself, rather than treating reconciliation as a purely after-the-fact audit trail.
#The Provisioning Race Condition
Crossplane’s control loop is optimistic by design: a Composite Resource (XR) transitions toward Ready: True as soon as its managed resources report healthy status. There is no native concept of “was this claim authored by a legitimately reconciled agent action.” Given the tolerance window (t_{window} = 5s) established in Part 3, an agent’s claim can reach the API server, get scheduled, and begin provisioning a VPC or IAM role well inside that window — before the ledger has confirmed a MATCHED state. This is the exact race the reconciliation engine was built to detect, but detection alone doesn’t stop the managed resource from being created.
The fix is to move enforcement upstream, into the composition pipeline, using Crossplane Composition Functions as the checkpoint. Rather than trusting the claim at admission time, the pipeline calls out to the reconciliation service on every reconcile pass and refuses to emit desired resources until the associated action is confirmed MATCHED.
Detecting Toxic Access with Graph-Based SoD Engines
#Architectural Breakdown: Gating with Crossplane Composition Functions
Since Crossplane v1.14, Compositions run as an ordered pipeline of gRPC functions (RunFunctionRequest / RunFunctionResponse), each receiving the observed and desired state and returning a mutated desired state plus status conditions. This pipeline model is what makes Crossplane Composition Functions suitable as an enforcement layer: a function can inspect the composite’s spec, call an external service synchronously, and either pass desired resources through unchanged or strip them entirely while setting Ready: False.
The gate function sits early in the pipeline, before function-patch-and-transform generates managed resources. If the gate fails, downstream functions never see resources to compose — the XR stalls in a Waiting condition rather than partially provisioning infrastructure. This is the same fail-closed posture used by the credential broker in Part 2, applied one layer up the stack. For teams standardising on this pattern across an internal provisioning architecture, the gate function becomes a reusable primitive rather than a one-off check.
#Implementation Logic
#Step 1: Carrying the Ledger Key on the Claim
Every claim submitted by an agent must carry the agentActionId issued at credential time, so the gate function has a key to query against the ledger.

1apiVersion: platform.kby.io/v1alpha1
2kind: AgenticClaim
3metadata:
4 name: agent-provision-vpc-7421
5spec:
6 agentActionId: "act_9f1c2e-7421"
7 parameters:
8 region: eu-west-2
9 cidrBlock: "10.40.0.0/16"#Step 2: Wiring the Gate into the Composition Pipeline
The function-agentic-gate runs before patch-and-transform. Its input config carries the reconciliation service endpoint and the maximum wait budget before the XR is marked failed rather than pending.
1apiVersion: apiextensions.crossplane.io/v1
2kind: Composition
3metadata:
4 name: vpc-agentic.aws.platform.kby.io
5spec:
6 mode: Pipeline
7 pipeline:
8 - step: agentic-ledger-gate
9 functionRef:
10 name: function-agentic-gate
11 input:
12 apiVersion: gate.fn.kby.io/v1beta1
13 kind: GateConfig
14 reconciliationEndpoint: "reconciler.internal:9443"
15 requiredStatus: MATCHED
16 maxWaitSeconds: 8
17 - step: patch-and-transform
18 functionRef:
19 name: function-patch-and-transform
20 - step: auto-ready
21 functionRef:
22 name: function-auto-ready#Step 3: The Gate Function Logic
Written against function-sdk-go, the function extracts agentActionId, queries the reconciliation service, and either forwards the request untouched or strips the desired composite entirely while attaching a condition.
1func (f *Function) RunFunction(ctx context.Context, req *fnv1.RunFunctionRequest) (*fnv1.RunFunctionResponse, error) {
2 rsp := response.To(req, response.DefaultTTL)
3 xr, _ := request.GetObservedCompositeResource(req)
4 actionID, _, _ := unstructured.NestedString(xr.Object, "spec", "agentActionId")
5
6 status, err := f.ledgerClient.Lookup(ctx, actionID)
7 if err != nil {
8 response.Fatal(rsp, errors.Wrap(err, "ledger unreachable"))
9 return rsp, nil
10 }
11
12 if status != "MATCHED" {
13 response.ConditionFalse(rsp, "Ready", "AwaitingReconciliation").
14 WithMessage(fmt.Sprintf("action %s status=%s", actionID, status))
15 response.SetDesiredCompositeResource(rsp, xr) // no managed resources attached
16 return rsp, nil
17 }
18
19 return rsp, nil // fall through, allow patch-and-transform to compose resources
20}#Failure Modes and Edge Cases
The gate introduces new failure surfaces that did not exist in a pure audit-only reconciliation model:
Reconciliation service unavailability. If the ledger lookup times out, the function must fail closed via response.Fatal, forcing Crossplane to retry with backoff rather than defaulting to MATCHED. Fail-open here would defeat the entire purpose of the gate.
Post-gate divergence. A status of MATCHED at query time does not guarantee the action stays MATCHED. If the ledger later reclassifies the same action as DIVERGENT_OUTCOME — for instance because the downstream cloud provider call failed asynchronously — the XR has already progressed past the gate on that reconcile pass. Mitigation requires a secondary watch: the reconciliation service publishes reclassification events, and a controller subscribed to that stream can force-requeue the affected XR to re-enter the pipeline.
Replay via claim resubmission. An agent resubmitting the same agentActionId after a MATCHED result has already been consumed should be rejected at the ledger layer, not the gate. The gate function should treat a second lookup on an already-consumed action ID as ORPHAN_CLAIM, consistent with the classification rules from Part 3.

Deletion during pending gate. If the claim is deleted while the XR sits in Waiting, Crossplane’s finalizer logic must still allow the pipeline to run once more so the gate function can clean up any partial state — an easy oversight if the function assumes it only ever runs on create/update events.
#Scaling and Security Trade-offs
Introducing a synchronous external call into every Crossplane reconcile loop changes both the latency and the blast radius profile of the control plane:
- Latency vs correctness: a synchronous gate adds (RTT_{ledger}) to every reconcile pass, typically 15–40ms under normal load, against near-zero enforcement latency in the ungated model. This is the direct cost of eliminating the provisioning race.
- Fail-closed vs fail-open posture: fail-closed (used here) guarantees no orphaned infrastructure but risks stalling legitimate claims during a ledger outage; fail-open restores availability but reopens the exact race condition this design exists to close.
- Fan-out at scale: thousands of XRs polling the reconciliation service on every reconcile interval creates read amplification against the ledger’s Postgres backend. A short-TTL cache (2–3s) in front of the lookup path absorbs repeated queries for the same
agentActionIdwithout weakening the tolerance window guarantees. - Blast radius of function compromise: the gate function holds a credential capable of querying the full ledger. It must run with a scoped read-only service account and mTLSto the reconciliation endpoint, never the broker-issuing credential from Part 2.The KBY LexiconmTLS (Mutual Transport Layer Security)mTLS is a TLS handshake extension in which both the client and server present X.509 certificates to cryptographically authenticate each other before establishing an encrypted channel, rather than only the server proving its identity as in standard TLS. It forms the cryptographic identity layer underpinning zero-trust network architectures, replacing implicit network-location trust with explicit, per-connection workload authentication.
- Operational observability: every
AwaitingReconciliationcondition should emit a metric labelled byagentActionIdprefix and status, so a stuck queue of pending XRs is visible before it becomes a provisioning backlog.
Full technical detail on the pipeline execution model is documented in the official Crossplane Composition Functions documentation, which covers the gRPC contract this gate function implements against.
With enforcement now sitting inside the provisioning pipeline rather than bolted on as an audit step, the next part turns to what happens when the gate itself needs to reason about partial trust — agent actions that are MATCHED but scoped to resources the agent’s role should not touch.
Comments
Add a thoughtful note on Part 4: Composition Functions as Agentic Provisioning Gates. Comments are checked for spam and held for moderation before appearing.




