Skip to content
PaloNexus
Request access Request

Recipe: A2A delegation

When the triage agent can’t finish a task alone it calls a sub-agent — an agent-to-agent (A2A) hop. That hop is not a trusted internal call — it is gated by the same /authz decision, and it carries the original on-behalf-of human subject — here the owner from the scenario of the temporary-elevation walkthrough. The sub-agent acts for the owner, on the owner’s task, with the owner’s delegation — it never gains standing authority of its own.

This mirrors the platform’s end-to-end governed flow: while triaging a sample incident, incident-triage reads a runbook (regulated → needs approval → the approver approves) and then A2A-calls a remediation sub-agent, the hop carrying subject=ethan.park.

from palonexus import PaloNexus
AGENT = "northstar-devops-incident-agent"
OWNER, APPROVER = "ethan.park@northstar.example", "maya.chen@northstar.example"
pn = PaloNexus.offline()
agent = pn.agents.register(name=AGENT, owner=OWNER, sponsor=APPROVER, scenario="devops-incident")
agent.provision()
with pn.task(subject=OWNER, task_id="INC-4821", scenario="devops-incident", actor=AGENT) as task:
# Hop 1 — the triage agent reads a regulated runbook: deny-by-default -> needs approval.
first = task.check(action="runbooks:read", resource="runbooks-api:/runbooks/db-failover")
assert first.needs_approval
deleg = task.request_delegation(action="runbooks:read",
resource="runbooks-api:/runbooks/db-failover",
reason="INC-4821 db failover", ttl=300)
pn._fake.approve_delegation(deleg.id, approver=APPROVER) # the approver approves (portal, live)
task.authorize(action="runbooks:read", resource="runbooks-api:/runbooks/db-failover")
# Hop 2 — A2A to the remediation sub-agent. The hop is itself gated; the decision
# carries the SAME on-behalf-of subject (the owner), not the agent's own identity.
a2a = task.check(action="agent:invoke", resource="northstar-remediation-agent",
target_kind="agent")
assert a2a.needs_approval and a2a.subject == OWNER # gated, on-behalf-of the owner
pn._fake.grant(subject=OWNER, action="agent:invoke",
resource="northstar-remediation-agent", scenario="devops-incident")
allowed = task.check(action="agent:invoke", resource="northstar-remediation-agent",
target_kind="agent")
assert allowed.allow and allowed.subject == OWNER
print("A2A hop authorized, on-behalf-of:", allowed.subject)
assert pn.audit.verify_chain() # both hops are on the chain
pn.close()
A2A hop authorized, on-behalf-of: ethan.park@northstar.example
  • The hop is gated. agent:invoke to the sub-agent goes through /authz exactly like a tool or model call — there is no privileged “internal” path.
  • Identity propagates, it doesn’t escalate. Both decisions record subject=ethan.park. The sub-agent inherits the owner’s task-scoped authority for this task only; it cannot act as itself.
  • Each edge is independently authorized. Hop 1 (runbook) and hop 2 (sub-agent) each require their own grant — approving one does not silently widen the other.
  • Auditable end to end. pn.audit.tail(task_id="INC-4821") reconstructs the whole chain, trace-correlated in Tempo (the Grafana trace backend) on a live deployment.

In production, agent.present(audience="northstar-remediation-agent") attaches a fresh holder- signed VP (verifiable presentation) to the A2A hop and the egress proxy enforces it at the network layer.