API reference

Governance

Policy changes an agent may ask for but never make. An agent proposes after repeated refusals, a different identity ratifies, the system enacts, and the grant gate consults the result.

GET /v1/governance/proposals

List proposals

Every proposal on this account, newest first, with who proposed, ratified, enacted or rejected it and when.

Auth
session cookie or API key
Capability
governance
Success
HTTP 200

Parameters

status query, string or null
proposed, ratified, enacted or rejected
subagent query, string or null
Exact subject sub-agent name

Request body

None.

Example request

curl

curl -sS -X GET "https://api.afaprotocol.com/v1/governance/proposals" \
  -H "X-API-Key: afa-beta-EXAMPLE-e4qs"

Python

import requests

API = "https://api.afaprotocol.com"
headers = {"X-API-Key": "afa-beta-EXAMPLE-e4qs"}

r = requests.get(f"{API}/v1/governance/proposals", headers=headers, timeout=30)
r.raise_for_status()
print(r.json())

Example response

HTTP 200
{
  "count": 2,
  "proposals": [
    {
      "enacted_at": "2026-09-07T14:07:00+00:00",
      "enactor": {
        "identity": "b2e4d6f8-1a3c-4e5f-8b9d-0c1e2f3a4b5c",
        "kind": "api_key",
        "machine": "reviewer-laptop"
      },
      "evidence": {
        "blocked_attempts": 3,
        "threshold": 3
      },
      "kind": "deny_scope",
      "note": "worker-1 asked for Bash three times outside its grant",
      "proposal_id": "prop-4c1e9a7b2d5f8036",
      "proposed_at": "2026-09-07T14:00:00+00:00",
      "proposer": {
        "identity": "7a1c3e5b-2d4f-4a6b-9c8e-0f1a2b3c4d5e",
        "kind": "api_key",
        "machine": "worker-host"
      },
      "ratified_at": "2026-09-07T14:06:00+00:00",
      "ratifier": {
        "identity": "b2e4d6f8-1a3c-4e5f-8b9d-0c1e2f3a4b5c",
        "kind": "api_key",
        "machine": "reviewer-laptop"
      },
      "rejected_at": null,
      "rejector": null,
      "status": "enacted",
      "subject": {
        "subagent": "worker-1",
        "tool": "Bash"
      }
    },
    {
      "enacted_at": null,
      "enactor": null,
      "evidence": {
        "blocked_attempts": 3,
        "threshold": 3
      },
      "kind": "deny_scope",
      "note": "worker-1 asked for Bash three times outside its grant",
      "proposal_id": "prop-9d3f7b1e5a2c6048",
      "proposed_at": "2026-09-07T14:00:00+00:00",
      "proposer": {
        "identity": "7a1c3e5b-2d4f-4a6b-9c8e-0f1a2b3c4d5e",
        "kind": "api_key",
        "machine": "worker-host"
      },
      "ratified_at": null,
      "ratifier": null,
      "rejected_at": "2026-09-07T14:10:00+00:00",
      "rejector": {
        "identity": "7a1c3e5b-2d4f-4a6b-9c8e-0f1a2b3c4d5e",
        "kind": "api_key",
        "machine": "worker-host"
      },
      "status": "rejected",
      "subject": {
        "subagent": "worker-2",
        "tool": "Write"
      }
    }
  ]
}

Errors

StatusCodeMeaning
401missing_token / invalid_or_expired_api_keyNo credential, an expired session, or a revoked or expired key.
403scope_missingA key limited by scopes lacks governance; the body names it.

What would show this is false

Each transition is also an event on your chain (governance.proposed, governance.ratified, governance.enacted, governance.rejected). GET /v1/chain/verify covers them.

POST /v1/governance/proposals

Propose a policy change

An agent that was refused repeatedly asks for a rule, naming the sub-agent, the tool, and how many refusals it saw. It cannot enact the rule itself.

Auth
session cookie or API key
Capability
governance
Success
HTTP 201

Parameters

None.

Request body

evidence EvidenceBody required
No description in the schema.
EvidenceBody fields
blocked_attempts integer required
Blocked attempts observed for this subject; must reach the configured threshold
kind string required
deny_scope, allow_scope or policy_note
note string or null
No description in the schema.
subject SubjectBody required
No description in the schema.
SubjectBody fields
scope string or null
Synonym for tool, for callers that name scopes; stored as tool
subagent string required
Exact sub-agent name the rule applies to
tool string or null
Optional tool name; absent means the whole sub-agent

Example request

curl

curl -sS -X POST "https://api.afaprotocol.com/v1/governance/proposals" \
  -H "X-API-Key: afa-beta-EXAMPLE-e4qs" \
  -H "Content-Type: application/json" \
  -d '{
  "evidence": {
    "blocked_attempts": 3,
    "threshold": 3
  },
  "kind": "deny_scope",
  "note": "worker-1 asked for Bash three times outside its grant",
  "subject": {
    "subagent": "worker-1",
    "tool": "Bash"
  }
}'

Python

import requests

API = "https://api.afaprotocol.com"
headers = {"X-API-Key": "afa-beta-EXAMPLE-e4qs"}
payload = {
    "evidence": {
        "blocked_attempts": 3,
        "threshold": 3
    },
    "kind": "deny_scope",
    "note": "worker-1 asked for Bash three times outside its grant",
    "subject": {
        "subagent": "worker-1",
        "tool": "Bash"
    }
}

r = requests.post(f"{API}/v1/governance/proposals", headers=headers, json=payload, timeout=30)
r.raise_for_status()
print(r.json())

Example response

HTTP 201
{
  "proposal": {
    "enacted_at": null,
    "enactor": null,
    "evidence": {
      "blocked_attempts": 3,
      "threshold": 3
    },
    "kind": "deny_scope",
    "note": "worker-1 asked for Bash three times outside its grant",
    "proposal_id": "prop-4c1e9a7b2d5f8036",
    "proposed_at": "2026-09-07T14:00:00+00:00",
    "proposer": {
      "identity": "7a1c3e5b-2d4f-4a6b-9c8e-0f1a2b3c4d5e",
      "kind": "api_key",
      "machine": "worker-host"
    },
    "ratified_at": null,
    "ratifier": null,
    "rejected_at": null,
    "rejector": null,
    "status": "proposed",
    "subject": {
      "subagent": "worker-1",
      "tool": "Bash"
    }
  }
}

Errors

StatusCodeMeaning
401missing_token / invalid_or_expired_api_keyNo credential, an expired session, or a revoked or expired key.
403scope_missingA key limited by scopes lacks governance; the body names it.
422validation errorA required field is missing or a value has the wrong type.
422threshold_not_metfewer blocked attempts than the configured threshold (default 3)
422kind outside deny_scope, allow_scope, policy_note

What would show this is false

Read the proposal back with GET. Its status is proposed and the grant gate is unchanged; GET /v1/grants/status still answers as before.

GET /v1/governance/proposals/{proposal_id}

One proposal

The full record of one proposal, including the identities behind each transition.

Auth
session cookie or API key
Capability
governance
Success
HTTP 200

Parameters

proposal_id path, string required
No description in the schema.

Request body

None.

Example request

curl

curl -sS -X GET "https://api.afaprotocol.com/v1/governance/proposals/prop-4c1e9a7b2d5f8036" \
  -H "X-API-Key: afa-beta-EXAMPLE-e4qs"

Python

import requests

API = "https://api.afaprotocol.com"
headers = {"X-API-Key": "afa-beta-EXAMPLE-e4qs"}

r = requests.get(f"{API}/v1/governance/proposals/prop-4c1e9a7b2d5f8036", headers=headers, timeout=30)
r.raise_for_status()
print(r.json())

Example response

HTTP 200
{
  "proposal": {
    "enacted_at": "2026-09-07T14:07:00+00:00",
    "enactor": {
      "identity": "b2e4d6f8-1a3c-4e5f-8b9d-0c1e2f3a4b5c",
      "kind": "api_key",
      "machine": "reviewer-laptop"
    },
    "evidence": {
      "blocked_attempts": 3,
      "threshold": 3
    },
    "kind": "deny_scope",
    "note": "worker-1 asked for Bash three times outside its grant",
    "proposal_id": "prop-4c1e9a7b2d5f8036",
    "proposed_at": "2026-09-07T14:00:00+00:00",
    "proposer": {
      "identity": "7a1c3e5b-2d4f-4a6b-9c8e-0f1a2b3c4d5e",
      "kind": "api_key",
      "machine": "worker-host"
    },
    "ratified_at": "2026-09-07T14:06:00+00:00",
    "ratifier": {
      "identity": "b2e4d6f8-1a3c-4e5f-8b9d-0c1e2f3a4b5c",
      "kind": "api_key",
      "machine": "reviewer-laptop"
    },
    "rejected_at": null,
    "rejector": null,
    "status": "enacted",
    "subject": {
      "subagent": "worker-1",
      "tool": "Bash"
    }
  }
}

Errors

StatusCodeMeaning
401missing_token / invalid_or_expired_api_keyNo credential, an expired session, or a revoked or expired key.
403scope_missingA key limited by scopes lacks governance; the body names it.
422validation errorA required field is missing or a value has the wrong type.
404no proposal with that id on this account

What would show this is false

proposer.identity and ratifier.identity differ. If they were equal the ratify call would have been refused.

POST /v1/governance/proposals/{proposal_id}/enact

Enact

Puts a ratified rule into force. From this call on, the grant gate answers denied_by_governance for the named sub-agent and tool, naming the proposal.

Auth
session cookie or API key
Capability
governance
Success
HTTP 200

Parameters

proposal_id path, string required
No description in the schema.

Request body

TransitionBody or null

Example request

curl

curl -sS -X POST "https://api.afaprotocol.com/v1/governance/proposals/prop-4c1e9a7b2d5f8036/enact" \
  -H "X-API-Key: afa-beta-EXAMPLE-e4qs" \
  -H "Content-Type: application/json" \
  -d '{}'

Python

import requests

API = "https://api.afaprotocol.com"
headers = {"X-API-Key": "afa-beta-EXAMPLE-e4qs"}
payload = {}

r = requests.post(f"{API}/v1/governance/proposals/prop-4c1e9a7b2d5f8036/enact", headers=headers, json=payload, timeout=30)
r.raise_for_status()
print(r.json())

Example response

HTTP 200
{
  "proposal": {
    "enacted_at": "2026-09-07T14:07:00+00:00",
    "enactor": {
      "identity": "b2e4d6f8-1a3c-4e5f-8b9d-0c1e2f3a4b5c",
      "kind": "api_key",
      "machine": "reviewer-laptop"
    },
    "evidence": {
      "blocked_attempts": 3,
      "threshold": 3
    },
    "kind": "deny_scope",
    "note": "worker-1 asked for Bash three times outside its grant",
    "proposal_id": "prop-4c1e9a7b2d5f8036",
    "proposed_at": "2026-09-07T14:00:00+00:00",
    "proposer": {
      "identity": "7a1c3e5b-2d4f-4a6b-9c8e-0f1a2b3c4d5e",
      "kind": "api_key",
      "machine": "worker-host"
    },
    "ratified_at": "2026-09-07T14:06:00+00:00",
    "ratifier": {
      "identity": "b2e4d6f8-1a3c-4e5f-8b9d-0c1e2f3a4b5c",
      "kind": "api_key",
      "machine": "reviewer-laptop"
    },
    "rejected_at": null,
    "rejector": null,
    "status": "enacted",
    "subject": {
      "subagent": "worker-1",
      "tool": "Bash"
    }
  }
}

Errors

StatusCodeMeaning
401missing_token / invalid_or_expired_api_keyNo credential, an expired session, or a revoked or expired key.
403scope_missingA key limited by scopes lacks governance; the body names it.
422validation errorA required field is missing or a value has the wrong type.
409not_ratifiedthe proposal has not been ratified
404unknown proposal

What would show this is false

Call GET /v1/grants/status?subagent=worker-1&tool=Bash. The answer is denied_by_governance with this proposal_id. Before enact it was not.

POST /v1/governance/proposals/{proposal_id}/ratify

Ratify (a different identity)

A second identity, a different key or machine than the proposer, approves the proposal. The proposer's own key is refused: the party that was constrained never holds the pen.

Auth
session cookie or API key
Capability
governance
Success
HTTP 200

Parameters

proposal_id path, string required
No description in the schema.

Request body

TransitionBody or null

Example request

curl

curl -sS -X POST "https://api.afaprotocol.com/v1/governance/proposals/prop-4c1e9a7b2d5f8036/ratify" \
  -H "X-API-Key: afa-beta-EXAMPLE-e4qs" \
  -H "Content-Type: application/json" \
  -d '{}'

Python

import requests

API = "https://api.afaprotocol.com"
headers = {"X-API-Key": "afa-beta-EXAMPLE-e4qs"}
payload = {}

r = requests.post(f"{API}/v1/governance/proposals/prop-4c1e9a7b2d5f8036/ratify", headers=headers, json=payload, timeout=30)
r.raise_for_status()
print(r.json())

Example response

HTTP 200
{
  "proposal": {
    "enacted_at": null,
    "enactor": null,
    "evidence": {
      "blocked_attempts": 3,
      "threshold": 3
    },
    "kind": "deny_scope",
    "note": "worker-1 asked for Bash three times outside its grant",
    "proposal_id": "prop-4c1e9a7b2d5f8036",
    "proposed_at": "2026-09-07T14:00:00+00:00",
    "proposer": {
      "identity": "7a1c3e5b-2d4f-4a6b-9c8e-0f1a2b3c4d5e",
      "kind": "api_key",
      "machine": "worker-host"
    },
    "ratified_at": "2026-09-07T14:06:00+00:00",
    "ratifier": {
      "identity": "b2e4d6f8-1a3c-4e5f-8b9d-0c1e2f3a4b5c",
      "kind": "api_key",
      "machine": "reviewer-laptop"
    },
    "rejected_at": null,
    "rejector": null,
    "status": "ratified",
    "subject": {
      "subagent": "worker-1",
      "tool": "Bash"
    }
  }
}

Errors

StatusCodeMeaning
401missing_token / invalid_or_expired_api_keyNo credential, an expired session, or a revoked or expired key.
403scope_missingA key limited by scopes lacks governance; the body names it.
422validation errorA required field is missing or a value has the wrong type.
409separation_of_powersthe caller is the identity that proposed it
409invalid_transitionalready ratified, enacted or rejected
404unknown proposal

What would show this is false

Ratify with the proposer's key first and read the 409. Then ratify with another key and read status ratified. The rule is still not in force until enact.

POST /v1/governance/proposals/{proposal_id}/reject

Reject

Closes a proposal without enacting it. Allowed from proposed or ratified; an enacted rule cannot be rejected, it can only be superseded by a new proposal.

Auth
session cookie or API key
Capability
governance
Success
HTTP 200

Parameters

proposal_id path, string required
No description in the schema.

Request body

TransitionBody or null

Example request

curl

curl -sS -X POST "https://api.afaprotocol.com/v1/governance/proposals/prop-9d3f7b1e5a2c6048/reject" \
  -H "X-API-Key: afa-beta-EXAMPLE-e4qs" \
  -H "Content-Type: application/json" \
  -d '{
  "reason": "withdrawn"
}'

Python

import requests

API = "https://api.afaprotocol.com"
headers = {"X-API-Key": "afa-beta-EXAMPLE-e4qs"}
payload = {
    "reason": "withdrawn"
}

r = requests.post(f"{API}/v1/governance/proposals/prop-9d3f7b1e5a2c6048/reject", headers=headers, json=payload, timeout=30)
r.raise_for_status()
print(r.json())

Example response

HTTP 200
{
  "proposal": {
    "enacted_at": null,
    "enactor": null,
    "evidence": {
      "blocked_attempts": 3,
      "threshold": 3
    },
    "kind": "deny_scope",
    "note": "worker-1 asked for Bash three times outside its grant",
    "proposal_id": "prop-9d3f7b1e5a2c6048",
    "proposed_at": "2026-09-07T14:00:00+00:00",
    "proposer": {
      "identity": "7a1c3e5b-2d4f-4a6b-9c8e-0f1a2b3c4d5e",
      "kind": "api_key",
      "machine": "worker-host"
    },
    "ratified_at": null,
    "ratifier": null,
    "rejected_at": "2026-09-07T14:10:00+00:00",
    "rejector": {
      "identity": "7a1c3e5b-2d4f-4a6b-9c8e-0f1a2b3c4d5e",
      "kind": "api_key",
      "machine": "worker-host"
    },
    "status": "rejected",
    "subject": {
      "subagent": "worker-2",
      "tool": "Write"
    }
  }
}

Errors

StatusCodeMeaning
401missing_token / invalid_or_expired_api_keyNo credential, an expired session, or a revoked or expired key.
403scope_missingA key limited by scopes lacks governance; the body names it.
422validation errorA required field is missing or a value has the wrong type.
409invalid_transitionalready enacted or rejected
404unknown proposal

What would show this is false

The grant gate is unchanged after a reject. A rejected proposal never appears as a reason in GET /v1/grants/status.

POST /v1/policy/test

What would the dedup gate decide

Answers hit, miss, bypass or uncacheable for a hypothetical call, writing nothing.

Auth
session cookie or API key
Capability
governance
Success
HTTP 200

The key and the window are the ones the real gate uses, so the answer predicts what a real call would get. The response also reports the window in events and seconds.

Parameters

None.

Request body

intent string or null
fresh | cached_ok
parent_event_id string or null
No description in the schema.
tool string required
No description in the schema.
tool_input object
No description in the schema.

Example request

curl

curl -sS -X POST "https://api.afaprotocol.com/v1/policy/test" \
  -H "X-API-Key: afa-beta-EXAMPLE-e4qs" \
  -H "Content-Type: application/json" \
  -d '{
  "tool": "Read",
  "tool_input": {
    "file_path": "/srv/app/config.toml"
  }
}'

Python

import requests

API = "https://api.afaprotocol.com"
headers = {"X-API-Key": "afa-beta-EXAMPLE-e4qs"}
payload = {
    "tool": "Read",
    "tool_input": {
        "file_path": "/srv/app/config.toml"
    }
}

r = requests.post(f"{API}/v1/policy/test", headers=headers, json=payload, timeout=30)
r.raise_for_status()
print(r.json())

Example response

HTTP 200
{
  "cached_at": "2026-09-01T14:10:21+00:00",
  "cached_from_event_id": "evt-5e6f708192a3b4c5",
  "cached_from_sequence": 4,
  "dedup_action": "reuse",
  "dedup_status": "hit",
  "semantic_key": "sha256:12ba8b0e2703a0156eb1339f44574881babf4b3af933d2b56faa3b6fd6f3d04f",
  "would_hit": true
}

Errors

StatusCodeMeaning
401missing_token / invalid_or_expired_api_keyNo credential, an expired session, or a revoked or expired key.
403scope_missingA key limited by scopes lacks governance; the body names it.
422validation errorA required field is missing or a value has the wrong type.

What would show this is false

Ask with intent fresh: dedup_status bypass and would_hit false regardless of history. Then POST /v1/events/search shows no new row.