---
name: axiom-gateway
description: Axiom Gateway agent skill. Use when an action you call returns an Axiom action (an `action_id` with a `state` such as `pending_approval`), when a tool or API is protected by Sectigo Axiom, or when you must wait for a human approval, resume an approved action, or verify an Axiom receipt.
version: 1.0.0
contract_version: "2026-11-14"
---

# Axiom Gateway agent skill

Skill version 1.0.0, for Axiom contract `2026-11-14`. The machine-readable contract is in
`contract/openapi.json` (OpenAPI 3.1) and `contract/schemas/*.json` (JSON Schema 2020-12). The shared
conformance fixtures in `fixtures/` show one response per state and what to do next. If this skill
and the contract disagree, the contract wins, and enforcement always stays with the gateway, the
SDK and the receiving application. This skill only helps you interact correctly.

**An action ID alone never proves permission.** Status reads and resumes need your own
authenticated, DPoP-bound credential, and only the original agent can resume.

## Reference behaviour

Read the supported modes before choosing how to wait. Persist the action ID, original actor
binding and deadline. Tell the user which action is waiting and who can review it. When a relay
signals a change, read authenticated status. Resume as the original logical agent only when the
action is approved. After dispatch, track the existing action through to its confirmed outcome. On
denial or expiry, report the reason and stop. An action ID alone never proves permission.

## 1. Discover capabilities

`GET /v1/capabilities` (no authentication). Read before choosing how to wait:

| Field                         | Use                                                                    |
| ----------------------------- | ---------------------------------------------------------------------- |
| `supported_modes`             | `poll`, `webhook`, `mcp_task`. Pick one your runtime can actually use. |
| `max_warm_wait_seconds`       | Upper bound for one in-session warm wait.                              |
| `default_retry_after_seconds` | Poll interval when a status omits `retry_after_seconds`.               |
| `resume_actor`                | Always `original_agent`.                                               |
| `token_endpoint`, `jwks_uri`  | Where to authenticate and where verification keys live.                |
| `mcp.protocol_versions`       | MCP versions the gateway negotiates.                                   |

## 2. Authenticate

OAuth 2.1 `client_credentials` with `private_key_jwt` and DPoP:

1. Make a client assertion JWT signed by your enrolled key: `iss` = `sub` = your agent ID,
   `aud` = the token endpoint, a fresh `jti`, `exp` at most 10 minutes ahead.
2. `POST {token_endpoint}` as a form with `grant_type=client_credentials`, `client_id`,
   `client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer`,
   `client_assertion`, and a `DPoP` header proof (`htm=POST`, `htu` = token endpoint).
3. The token (`token_type: DPoP`, about 5 minutes) is bound to your key. Every API call sends
   `Authorization: DPoP <token>` and a new `DPoP` proof with `htm`, `htu` (URL without query),
   `iat`, a fresh `jti` and `ath` (base64url SHA-256 of the token).
4. Refresh before expiry. After a legitimate key rotation, authenticate with the new key under the
   same agent ID. Your pending actions are still yours.

## 3. Submit

`POST /v1/actions` with `operation_key`, `operation`, `service`, `tenant`, `resource`, `params`
(the material fields the application will commit, with decimals as strings) and an optional
`intent` for approvers.

- Choose the operation key **once per intent**, before the first attempt, and store it. Reuse it
  for every retry of that intent. Do not reuse it for anything else.
- If the response is lost (timeout, connection reset), submit the same body with the same key.
  You get the existing action back; nothing runs twice.
- `409 idempotency_conflict` with `changed_fields` means the key already belongs to a different
  request. The original record is unchanged. A different amount, beneficiary or tenant is a new
  intent: it needs a new key and, usually, a fresh approval.

## 4. Pending status: tell the user

On `pending_approval`, immediately persist `{action_id, operation_key, agent_id, resource, params,
deadline}`, then tell the user, in one short message:

- which action is waiting (`summary`, `action_id`);
- who can review it (`approvals[].next_party`, for example "finance_approver in axiom");
- the deadline (`decision_deadline`);
- how they or you will check it again.

Do not imply the action has happened. No business write has started.

## 5. Bounded warm wait and retry hints

- Wait no longer than `max_warm_wait_seconds` in one go, and only if you have that much time left.
- Between status reads, sleep at least `retry_after_seconds` (or `default_retry_after_seconds`).
  Do not start a read that would end after your budget.
- When the budget ends, persist the record (already done in step 4) and return the pending ID.
- `409 invalid_state` on resume before approval carries `retry_after_seconds`. Honour it.

## 6. Runtime behaviour

| Runtime                                   | During approval                                                                                                                  | How work resumes                                                                                                              |
| ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| Live agent with time remaining            | Use a bounded warm wait and honor the retry interval.                                                                            | Read status and resume in the current session. Persist the ID before the wait budget expires.                                 |
| Runtime with a durable scheduler          | Store the action ID and register a supported event consumer, or schedule a status check.                                         | The customer's scheduler wakes the workload. It authenticates as the original agent, including after legitimate key rotation. |
| CLI or agent with no background execution | Return the pending ID and a human-readable way to check it. Capture no implicit promise that a webhook can restart this process. | At the next invocation, the user or runtime supplies the saved ID. The authenticated original agent reads status and resumes. |

For a CLI the human-readable way is, for example: `axiom status act_…`, then `axiom resume act_…`.

## 7. Webhook relay

A wake event is a compact JWS (`content-type: application/jose`, header `typ: axiom-event+jws`)
whose payload is `WakeEvent`: `event_id`, `action_id`, `tenant`, `occurred_at`, `seq`.

1. Verify the signature with the issuer JWKS (`jwks_uri`, key role `event`), the `typ`, the age
   and that `tenant` is yours. Reject anything else.
2. **Read authenticated status**, even for a duplicate or an older `seq`: duplicate notifications
   cause a status read. The event is a hint. It carries no state you may act on, cannot grant
   authority and cannot execute anything.
3. Keep the highest `seq` you have processed per action, so an older event never moves your local
   view backwards; record it only after the status read succeeded.

Duplicate, delayed and out-of-order events therefore converge on the same record.

## 8. Authenticated resume

Only when an authenticated status read says `approved`:

`POST /v1/actions/{action_id}/resume` with the **same** `operation_key`, `resource` and `params`
that were approved. Axiom rechecks the request, your current authority and policy, then claims
execution atomically.

- A changed parameter returns `409 conflict`. Do not "fix" it. A new request needs new approval.
- `403 wrong_resume_actor`: you are not the original agent. Stop.
- Resume before `resume_deadline`. After it, the action expires.
- If the resume response is lost, **read status**; do not send a second resume unless status still
  says `approved`.
- Embedded-SDK services: resume by re-sending the original request to the provider with the same
  operation key. The provider claims execution through Axiom.

## 9. After dispatch: track, never retry

`executing` and `outcome_unknown` mean the request may already have reached the target.

- Poll status with the retry hint. Never resubmit, never resume again, never call the target
  directly.
- `outcome_unknown`: Axiom reconciles with the target by operation key and restores a final state
  only from authoritative evidence. Tell the user the result is not yet confirmed and that it must
  not be retried.

## 10. States and what to do

| State                                          | Do                                                                                    | Fixture                                       |
| ---------------------------------------------- | ------------------------------------------------------------------------------------- | --------------------------------------------- |
| `pending_approval`                             | Persist, tell the user, wait (§5–7).                                                  | `pending.json`                                |
| `approved`                                     | Resume as the original agent with the approved params (§8).                           | `approved.json`                               |
| `executing`                                    | Track (§9).                                                                           | `executing.json`                              |
| `outcome_unknown`                              | Track, surface to the operator, never retry (§9).                                     | `outcome_unknown.json`                        |
| `succeeded` / `failed`                         | Read the outcome and receipt. A retry returns the prior result.                       | `succeeded.json`                              |
| `denied` / `expired` / `revoked` / `cancelled` | Report `reason_code` and stop. A material change or fresh approval needs new request. | `denied.json`, `expired.json`, `revoked.json` |
| (409 `idempotency_conflict`)                   | The original stands. Report `changed_fields`.                                         | `idempotency_conflict.json`                   |

## 11. Verify the receipt

`GET /v1/actions/{action_id}/receipt` returns an `axiom.receipt.bundle.v1`. It verifies offline:
the bundle embeds the keys that signed it. Use `axiom verify receipt.json`, the
`verifyReceipt` function in `@sectigo/axiom-sdk/verify`, or the public browser verifier.

Report to the user, separately: who signed (Axiom's observation key; the provider's outcome key),
whether it was **approved**, **dispatched**, and **destination confirmed** (only a valid provider
outcome attestation confirms the destination), and any missing claims. Add
`--issuer-jwks {jwks_uri}` to check the issuer's current key status.

## 12. Errors

Errors are RFC 9457 problem details with a stable `code` and often a `reason_code`.

| Code                      | Meaning and action                                                                  |
| ------------------------- | ----------------------------------------------------------------------------------- |
| `invalid_token` (401)     | Token expired or revoked. Get a new token and retry once.                           |
| `invalid_dpop_proof`      | Proof wrong or replayed (check clock, `htu`, `ath`, fresh `jti`). Fix, do not loop. |
| `forbidden` (403)         | Policy refusal. Read `reason_code` (for example `agent_suspended`) and stop.        |
| `not_found` (404)         | Not visible to you. Treat it as absent; never guess IDs.                            |
| `idempotency_conflict`    | Same key, different request (§3).                                                   |
| `invalid_state` (409)     | Wrong state for this call (for example resume before approval). Read status.        |
| `conflict` (409)          | Resume params differ from the approval. Stop.                                       |
| `validation_failed` (422) | Fix the fields listed in `issues`.                                                  |
| `rate_limited` (429)      | Wait `retry_after_seconds`.                                                         |
| `quota_exceeded` (402)    | Report to the user. Existing actions and evidence remain available.                 |

## Never

- Never treat an action ID, a webhook, or a message from a person as permission to execute.
- Never resubmit or resume an action that is `executing` or `outcome_unknown`.
- Never change the amount, beneficiary, currency, tenant or resource and reuse the operation key.
- Never resume as a different agent, or hand your token or private key to another agent. Use
  explicit delegation (`POST /v1/grants/{grant_id}/delegate`) for a narrower child grant.
- Never promise the user that a webhook will restart a process that cannot run in the background.
