Waxell

Play · Developers and platform teams

Prove which agent called your API

The call sayswhich agent, which run,and for whom.

A domain is how an agent calls back into your application, and every domain call arrives with a short-lived Ed25519-signed identity: the agent, its pushed version, the run, the person it acts for, and the parent that delegated it. You verify it against your workspace's public keys; no shared secret leaves Waxell.

30 minutes. No commitment.

SOC 2 Type II attested · ISO/IEC 27001:2022 certified · HIPAA with a BAA · aligned to the EU AI Act and NIST AI RMF

identity · overview
identity · overview
  1. This play uses
  2. 03Enforce
  3. 04Run
  4. 05Prove
The situation

Who

A platform team exposing refund and lookup endpoints to agents, where the only caller identity today is a shared bearer token.

What happened

A refund went out and the service log said 'the agent did it'. Which agent, which version, which run, and for which support rep took a day to reconstruct.

What's at stake

Endpoints that cannot tell one agent from another, actions that cannot be tied to a person, and a shared secret that proves the call came from somewhere, not who made it.

The play
  1. 01Run

    Register a domain. The agent gets an interface, not your database.

    When an agent runs ctx.domain('company', 'get', company_id=...), Waxell routes it to POST {base_url}/domains/company/get on an HTTPS endpoint you control; SDK agents see each action as a tool named company__get. Write the contract as company.yaml: a bearer auth block whose token is a shared secret so only Waxell can reach your /domains/* routes, the endpoint's base_url (public HTTPS, so a tunnel during local development), and one action per route with its request_schema, which becomes the input schema the model sees. Omit request_schema and the agent has no idea what fields the action needs; your endpoint rejects the call with a 422. Each action can also declare data_classification, human_in_the_loop and rate_limit_per_minute. wax push-domain company.yaml creates a new version and makes it the default, and the agent reaches it only if its manifest lists domains: [company].

    ctx.domain('company', 'get', ...) · POST {base_url}/domains/company/get · company__get · request_schema per action · wax push-domain company.yaml · domains: [company]

    company.yaml · domain contract
    name: companyper_user_auth: trueauth:  type: bearer  config:    header: Authorization    prefix: Bearer    token: <shared secret>endpoint:  base_url: https://api.acme.com  path_template: /domains/{domain}/{action}actions:  - name: get    request_schema:      type: object      properties:        company_id: {type: string}      required: [company_id]    data_classification: confidential    human_in_the_loop: false    rate_limit_per_minute: 120$ wax push-domain company.yaml
  2. 02Enforce

    Verify the AXID on every call.

    Every run, on any framework and any execution tier, is minted an Agent Execution Identity when it starts, and every domain call it makes carries X-Waxell-AXID: a 60-second JWT with alg EdDSA and typ axid+jwt, which you refuse if either differs. The claims name the agent (name, version and a hash of its manifest), the run (id and workflow), scope as domain:<name>.* for each domain the agent declared, the call (id, domain, action), and act: the person the run acts for, with assurance tenant_asserted or idp_verified, or null for a system-triggered run. Fetch the key by kid from your workspace's JWKS at /api/v1/tenants/<workspace-id>/.well-known/axid-jwks.json, which wax axid show prints; check exp, tenant_id, that aud contains domain:<your domain>, and that call.action is the action being invoked; then authorize on agent.name, agent.version, act and scope as your policy requires. Keys live 90 days and rotate with an overlap, so cache the JWKS for up to 10 minutes and refetch on an unknown kid.

    X-Waxell-AXID · alg EdDSA · typ axid+jwt · 60 s · agent.name / agent.version / run.id / act / scope / call.action · wax axid show · wax axid verify

    identity · overview
    identity · overview
  3. 03Enforce

    Verify the person too. Reject if the two disagree.

    When the domain sets per_user_auth: true and the signal that started the run carried _sub_user_identity, the same call also carries X-Waxell-Sub-User-Token, minted fresh for each domain call. Its format is wax_su_v2_<kid>.<payload>.<signature>; the Ed25519 signature covers the base64url payload string exactly as received, and it is not a JWT, so a generic JWT library will not verify it. The claims give you sub (the sub_user_id you sent), email, roles, perms and groups, aud equal to the domain name, exp at iat plus 300 seconds, and a jti to reject reused tokens. Authorize on the verified token, never on the plain X-Waxell-Sub-User-Id and X-Waxell-Sub-User-Email headers. Read the two tokens together: the end-user token's axid claim must equal the AXID's sub, and the AXID's act.sub must equal the token's sub_user_id. Reject the request if either pair disagrees.

    per_user_auth: true · _sub_user_identity · wax_su_v2_<kid>.<payload>.<signature> · exp = iat + 300 · jti · waxell-sub-user-jwks.json · wax sub-user-auth rotate

    POST /domains/company/get · headers
    Authorization: Bearer <shared secret>X-Waxell-AXID: <agent identity token>X-Waxell-Sub-User-Token: wax_su_v2_<kid>.<payload>.<signature># Ed25519 over the payload string as received. Not a JWT.sub:   alice            # the sub_user_id you sentaud:   company          # the domain nameexp:   iat + 300jti:   reject replaysaxid:  must equal the AXID's sub$ wax sub-user-auth rotate
  4. 04Prove

    Store the sub. Trace it back later.

    The AXID's sub, axid:<hex>, is durable: the same value appears on the run's spans, its lineage edges, its domain call records and the person's activity log, so store it and call.id on any row the call changes. From any sub you stored, wax axid trace prints the person, the agent and version, the run, every domain call that presented the identity with the call.id you logged, the lineage edges, the person's activity entries, and any child identities a spawned run minted; GET /api/v1/axid/<sub>/ returns the same data. A run spawned by another run carries parent_axid, a chain_depth one higher, and a chain_hash that commits to the actor and the parent so a chain cannot be spliced; a verifier that trusts only depth-1 identities can refuse chain_depth above 1.

    store sub and call.id · wax axid trace axid:5f1a9c… · GET /api/v1/axid/<sub>/ · parent_axid · chain_depth · chain_hash

    observe · run detail
    observe · run detail
What the record shows
  1. company.refund · order 1042 · $140refunds · v1.2.0 · run 4471 · for u_42 · AXID verified · act.sub u_42 · perms orders:refundallow
  2. company.refund · token with aud billingrefunds · v1.2.0 · run 4471 · for u_42 · wrong audience · endpoint serves companyblock
  3. company.export_customersrefunds · v1.1.0 · run 4466 · system · act null · system-triggered run · action requires a personblock
  4. company.get · chain_depth 2researcher · run 4480 · parent run 4471 · parent_axid run 4471 · chain_hash verifiedallow

Illustrative entries in the shape the product writes them.

Policies involved

Products in this play

Questions teams ask about this play

The domain's bearer token is a shared secret that proves a call came from Waxell at all, and you should verify it and fail closed. Who made the call is proved by the AXID and the end-user token, both signed with Ed25519 and verified against your workspace's public JWKS. There is nothing to rotate on your side; keys live 90 days and rotate with an overlap.

Related plays

Run this play this week.

Free for your first agents and devices, live in about thirty seconds. Or walk through it with someone who has run it before.

30 minutes. No commitment.

SOC 2 Type II attested · ISO/IEC 27001:2022 certified · HIPAA with a BAA · aligned to the EU AI Act and NIST AI RMF