Skip to content
PaloNexus
Request access Request

HTTP API

The PaloNexus HTTP contract defines the methods, paths, bodies, and status codes for wiring a gateway, scripting the registry, approving an egress hold, and onboarding an agent.

The contracts are grouped by plane. All bodies are JSON unless noted. Short forms used throughout: ext_authz is Envoy’s external-authorization hook; a verifiable presentation (VP) is an agent’s signed wrapper around its Verifiable Credential (VC); TBAC is task-based access control; OPA is the Open Policy Agent; and a DID is a Decentralized Identifier. For the headers these endpoints read and stamp, see Headers.

The single decision point, on the control plane’s decision listener :9191. Its primary job is the agent-egress questionmay this agent make this call, on behalf of this human, for this task, right now? A request carrying X-Palonexus-Actor takes that egress path. The same endpoint also serves the foundational ingress decision (the north-south request the gateway calls via Envoy HTTP ext_authz); everything without X-Palonexus-Actor takes the ingress path. Either way: a 200 means allow (the gateway routes to the upstream); 403 means deny; 401 means an invalid credential or a needs-approval egress.

MethodPathPurpose
any/authzthe ext_authz decision (ingress or egress)

Status codes

CodeMeaning
200allow; X-Palonexus-Subject/-Upstream stamped (-Actor/-Agent-DID on egress)
401invalid credential, or (egress) needs human-approved delegation with X-Palonexus-Needs-Approval: true
403deny — unknown service/agent/target, not in allowlist, over budget, or a policy/OPA deny

Ingress decision order: identity.Verifyregistry.Getpolicy.Evaluate (inline then OPA veto) → audit → metrics.

Egress decision order: identity.Verify the agent token → verify the X-Palonexus-Agent-VP (required in vc mode) → resolve actor + target in the registry → policy.EvaluateEgress (allowlist → budget → delegation/TBAC → OPA veto) → audit (model.invoke / tool.read / agent.invoke) → metrics.

Terminal window
# Egress decision for a regulated runbook read
curl -s -o /dev/null -w "%{http_code} needs-approval=%header{x-palonexus-needs-approval}\n" \
-XPOST localhost:9191/authz \
-H 'Authorization: Bearer <agent-token>' \
-H 'X-Palonexus-Actor: triage-agent' \
-H 'X-Palonexus-On-Behalf-Of: sre@corp' \
-H 'X-Palonexus-Task: INC-123' \
-H 'X-Palonexus-Service: runbooks-api' \
-H 'X-Palonexus-Target-Kind: tool' \
-H 'X-Palonexus-Action: runbook:read' \
-H 'X-Palonexus-Resource: runbooks-api:/runbooks/db-failover'
# before delegation: 401 needs-approval=true · after approval: 200 needs-approval=

A standard HTTP forward proxy on EGRESS_PROXY_ADDR (default :9092), fronted by egress-proxy.palonexus.svc:80. It accepts a plaintext absolute-URI request or CONNECT host:port, reads identity from Proxy-Authorization: Bearer <Membership-VP>, and runs the same egress decision before forwarding. Audited as egress.proxy.

ConditionResult
missing Proxy-Authorization407 Proxy Authentication Required
invalid / revoked VP403
allowforward + stream the response
deny403 with X-Palonexus-Deny-Reason
needs-approval / externalcreate a pending egress request and hold (up to ApprovalTimeout, default 120s); approved → forward, denied/expired → 403
MethodPathPurpose
GET/healthz, /readyz200 liveness/readiness
GET/v1/registry/serviceslist all services (sorted by name)
POST/v1/registry/servicesupsert a service (audited registry.upsert)
GET/v1/registry/services/{name}get one service (404 if absent)
POST/v1/usagebroker reports per-call token/cost usage (telemetry; no audit row)
GET/v1/auditrecent hash-chained decision records; ?limit=N (default 200)
GET/v1/audit/verifyrecompute the chain; tamper-evidence
GET/v1/egress/requestslist egress-approval requests; ?status= filter, newest first
POST/v1/egress/requests/{id}/approveapprove a held egress request {approver}
POST/v1/egress/requests/{id}/denydeny a held egress request {approver, reason}
GET/metricsPrometheus exposition

The first block is north-south fields; the second is the additive agent-governance block (all omitempty).

FieldTypeMeaning
namestringrequired. Unique registry key.
upstreamstringrequired. In-cluster DNS target, e.g. orders.apps.svc.cluster.local:8080.
ownerstringOwning team (audit + paging).
requireScopestringOAuth scope a caller must hold; "" = any authenticated.
publicboolIf true, unauthenticated callers allowed.
registeredAtRFC3339Set by the store if zero.
kindstring"" · service · agent · model · tool.
allowModels[]stringkind=agent: model names it may invoke.
allowTools[]stringkind=agent: tool names it may call.
allowAgents[]stringkind=agent: peer agents it may hop to.
budgetBudgetkind=agent: egress rate ceilings (below).
dataClassstring"" · public · internal · regulated. regulated → TBAC (delegation required).

Budget (a zero field means no limit on that dimension):

FieldTypeMeaning
tokensPerHourint64rolling LLM-token ceiling
callsPerHourint64rolling call-count ceiling
costUsdPerDayfloat64rolling USD-spend ceiling

Allowlist semantics (MayReach) — deny-by-default: for a model/tool/agent target the caller must list the target’s name in the matching allow* list.

Terminal window
curl -fsS -XPOST localhost:8181/v1/registry/services -H 'content-type: application/json' -d '{
"name":"triage-agent",
"upstream":"triage-agent.apps.svc.cluster.local:80",
"owner":"sre",
"requireScope":"agent:triage:invoke",
"kind":"agent",
"allowModels":["model-openai"],
"allowTools":["runbooks-api"],
"budget":{"tokensPerHour":2000000,"callsPerHour":500}
}'
# -> 200, echoes the stored Service; also appends a registry.upsert audit row.

The model broker’s per-call token/cost report. It feeds the agent’s rolling budget meter and bumps the Prometheus token + cost counters. It is telemetry, not a decision, so it emits no audit record.

{ "agent": "triage-agent", "model": "model-openai", "tokens": 1234, "costUsd": 0.0123 }

Response 200 {"ok": true}; a malformed body → 400 {"error":"invalid body"}.

Returns the most recent hash-chained records (newest last); ?limit=N caps the count. Each record carries: seq, action (authz / egress / model.invoke / tool.read / agent.invoke / egress.proxy / egress.approval / registry.upsert), subject, actor (egress only), task, service, allow, reason, rule, plus the chained hash/prevHash.

Recomputes the chain and reports tamper-evidence:

{ "ok": true, "brokenAtSeq": -1 }

ok=false with brokenAtSeq set to the sequence where the chain first breaks.

The pending-egress queue the forward-proxy parks a needs-approval request on (an external/unmatched host, or a regulated registry target with no standing delegation). Record JSON:

{ "id": "uuid", "status": "pending|approved|denied|expired",
"actor": "incident-triage", "actorDid": "did:key:z…",
"target": "hooks.acme.io:443", "service": "external|<registry-name>",
"action": "egress.invoke", "resource": "https://hooks.acme.io/incident",
"reason": "post incident summary", "requestedAt": "ISO8601",
"approver": null, "decidedAt": null }
Terminal window
curl -s 'localhost:8181/v1/egress/requests?status=pending'
curl -s -XPOST localhost:8181/v1/egress/requests/$ID/approve -d '{"approver":"sre@corp"}'
curl -s -XPOST localhost:8181/v1/egress/requests/$ID/deny -d '{"approver":"sre@corp","reason":"not allowed"}'

Approve/deny each appends an egress.approval audit row and wakes the proxy’s hold (forward on approve, 403 on deny/expiry). A timed-out hold transitions to expired (fail-closed). If the egress queue is disabled, approve/deny return 503.

FastAPI service. Issuer/root is did:web:agent-idp.agent-idp.svc; agents are did:key:z… subjects. Errors use the envelope {"error":{"code","message"}}.

MethodPathResult
GET/healthz, /readyz{"status":"ok"} / {"status":"ready"}
GET/.well-known/did.jsonthe issuer did:web DID document
GET/v1/issuer{issuerDid, issuerPubMultibase}
MethodPathBodyResult
POST/v1/agents{name, role, capabilities:[{action,resource,constraints?}]}201 {name, status:"registered"} (idempotent on name)
POST/v1/agents/{name}/provision200 {name, did, privateKeyB64, membershipVc, capabilityVcs, issuerDid}privateKeyB64 returned once (404 if unknown)
GET/v1/agents[{name, role, did, capabilities, provisioned}] (no private keys)
GET/v1/agents/{name}one record (no private key; 404 if absent)
Terminal window
curl -s -XPOST localhost:8090/v1/agents -H 'content-type: application/json' -d '{
"name":"triage","role":"incident-triage",
"capabilities":[{"action":"runbook:read","resource":"runbooks-api:/runbooks/*"}]}'
curl -s -XPOST localhost:8090/v1/agents/triage/provision

VP verification (cryptographic egress identity)

Section titled “VP verification (cryptographic egress identity)”
MethodPathBodyResult
POST/v1/agents/verify-presentation{vp, audience?}{ok, agentName, agentDid, reason}

It (1) verifies the holder did:key signature + audience (default palonexus-egress)

  • nonce, (2) finds the issuer-signed Membership VC, verifies it chains to the issuer for this holder and is not revoked, and (3) maps the proven did:key back to the registered agent name. Fail-closed: any failure → ok=false with a reason.
Terminal window
curl -s -XPOST localhost:8090/v1/agents/verify-presentation \
-H 'content-type: application/json' -d '{"vp":"<membership-vp-jwt>"}'
# -> {"ok":true,"agentName":"triage","agentDid":"did:key:z6Mk…","reason":"ok"}
MethodPathBody / QueryResult
POST/v1/delegations/request{actorName, task, action, resource, reason, ttlSeconds?}201 record (404 if agent unknown/unprovisioned)
GET/v1/delegationsall, newest first
GET/v1/delegations/{id}one (404 if absent)
POST/v1/delegations/{id}/approve{approver}issues the Delegation VC → {id, status:"approved", vc, vcJti, notAfter} (409 if not pending)
POST/v1/delegations/{id}/deny{approver, reason}{… status:"denied"} (409 if not pending)
GET/v1/delegations/{id}/vc{vc} (the JWT to present at the resource gate; 409 if not approved)
GET/v1/delegations/check?actor=&task=&action=&resource={ok, reason, vcJti, notAfter} — called by the control plane on every regulated egress

/v1/delegations/check returns ok=true iff an approved, non-expired (notAfter > now), non-revoked delegation exists matching actor, task, action, and resource (trailing /* glob). Otherwise ok=false with a reason: no approved delegation / delegation expired / delegation revoked.

Terminal window
ID=$(curl -s -XPOST localhost:8090/v1/delegations/request -H 'content-type: application/json' -d '{
"actorName":"triage","task":"INC-42","action":"runbook:read",
"resource":"runbooks-api:/runbooks/db-failover","reason":"sev1","ttlSeconds":300}' | jq -r .id)
curl -s -XPOST localhost:8090/v1/delegations/$ID/approve -d '{"approver":"alice@oncall"}'
curl -s "localhost:8090/v1/delegations/check?actor=triage&task=INC-42&action=runbook:read&resource=runbooks-api:/runbooks/db-failover"
# -> {"ok":true,"reason":"","vcJti":"…","notAfter":"…"}
MethodPathBodyResult
GET/status/{list}{"revoked":[<vcJti>,…]} (default list default)
POST/v1/revoke{vcJti}{revoked:true, vcJti}
GET/v1/revocations{revoked:[…]}
Terminal window
JTI=$(curl -s localhost:8090/v1/delegations/$ID | jq -r .vcJti)
curl -s -XPOST localhost:8090/v1/revoke -d "{\"vcJti\":\"$JTI\"}"
# the next /v1/delegations/check (and /authz) for that delegation now denies.

A thin LiteLLM proxy that holds the provider key and is the choke point for model egress. Agents call it with a logical model name and an OpenAI-compatible payload; after each completion it POSTs usage back to the control plane’s /v1/usage.

Terminal window
curl localhost:8080/v1/chat/completions -H 'content-type: application/json' \
-H 'x-palonexus-actor: triage-agent' \
-d '{"model":"model-openai","messages":[{"role":"user","content":"hi"}]}'

Logical models: model-openai (openai/gpt-4o-mini), model-openai-large (openai/gpt-4o). Health probes: GET /health/liveliness, GET /health/readiness.

Exposed on :8181/metrics:

MetricTypeLabels
palonexus_authz_decisions_totalcounterservice, decision (allow/deny), rule
palonexus_authz_duration_secondshistogramservice
palonexus_token_usage_totalcounteragent, model
palonexus_agent_cost_usd_totalcounteragent