Skip to content

agent_lifecycle.rego

Governs the two lifecycle events: an agent creating a child agent (agent.spawn), and an agent handing a task to another agent (agent.delegate). The action layer for those — what the resulting child can do — lives in delegation.rego.

Package: kitelogik.agent_lifecycle · Source: kitelogik/policies/agent_lifecycle.rego

Rules

agent.spawn

OutcomeTrigger
Allowdelegation_depth ≤ 2 (and is a number) and every requested capability is in the parent's session_scopes
Denydelegation_depth > 2
Denydelegation_depth missing, null, or non-numeric
DenyA non-empty requested_capabilities list contains any scope not in the parent's session_scopes

agent.delegate

OutcomeTrigger
Allowdelegation_depth ≤ 1 (and is a number) and every requested scope is in the parent's session_scopes
Denydelegation_depth > 1
Denydelegation_depth missing, null, or non-numeric
DenyA non-empty requested_capabilities list contains any scope not in the parent's session_scopes

The depth ceilings differ by event type — agent.spawn allows up to depth 2 (the parent can produce a child, which can produce a grandchild), but agent.delegate caps at depth 1 (only direct hand-offs from the original session).

Why the is_number(...) guards matter

OPA structural ordering treats null < number, so null <= 2 evaluates to true — meaning a missing delegation_depth would silently allow spawn at any depth. Both allow rules wrap the comparison in is_number(...) and there are dedicated deny rules for malformed events:

rego
# Malformed event — hard-deny rather than fall through
deny if {
    input.event_type == "agent.spawn"
    not is_number(object.get(input.context, "delegation_depth", null))
}

object.get(..., null) ensures the value is always bound, so not is_number(...) also catches the missing-key case.

Capability and scope subset rules

The _capabilities_valid and _delegate_scopes_valid helpers both require every requested capability to be in the parent's session scopes:

rego
_capabilities_valid if {
    every cap in input.requested_capabilities {
        cap in input.context.session_scopes
    }
}

This enforces scope-narrowing — a child can never gain a scope the parent didn't already have. The parent can still grant fewer scopes (narrowing) without changing this policy.

How main.rego routes events to this module

rego
# Route agent.spawn and agent.delegate to agent_lifecycle policy
allow if {
    not deny
    input.event_type in {"agent.spawn", "agent.delegate"}
    agent_lifecycle.allow
}

deny if {
    input.event_type in {"agent.spawn", "agent.delegate"}
    agent_lifecycle.deny
}

So agent_lifecycle.rego is the canonical lifecycle gate — its allow flows up directly, its deny escalates to the aggregator.

Extending in your own project

Same-package extension:

rego
package kitelogik.agent_lifecycle

import future.keywords.if

# Restrict spawn to specific user roles
deny if {
    input.event_type == "agent.spawn"
    not input.context.user_role in {"orchestrator", "supervisor"}
}

# Cap spawn rate per session (requires writing context.spawns_in_session
# from your runtime — not part of the OSS context shape)
deny if {
    input.event_type == "agent.spawn"
    input.context.spawns_in_session > 5
}

Released under the Apache 2.0 License.