Appearance
Policy HTTP API
This is the reference for the project-scoped network Policy HTTP surface. It maps each operation to its OpenAPI schema, the per-call ReBAC gate, the audit relation it stamps, the outbox event it emits, and the closed Problem.code taxonomy. The wire-contract origin is api/openapi/plexsphere-v1.yaml; this doc is a map, not a duplicate contract — for the bounded-context narrative (the aggregate, the rule value objects, and the compile pipeline) see ../../contexts/policy/model.md.
A Policy is an aggregate that owns an ordered list of network rules behind a selector. Every mutation lands a new immutable revision: Create mints the head revision, Update appends a child revision and advances the head, and the revision history is read-only thereafter. A landed revision emits a policy_revision_created outbox event that drives the policy compile pipeline; Delete emits policy_deleted so the compiler purges the deleted Policy's contribution. The dry-run surface evaluates a candidate revision against the live mesh without persisting anything.
Operations
| Method | Path | Operation ID | ReBAC gate | Audit relation | Outbox event | Body cap |
|---|---|---|---|---|---|---|
| GET | /v1/projects/{project_id}/policies | ListPolicies | project#read on the parent Project (top-level) + per-row policy#read filter | policy.list | (none) | n/a |
| POST | /v1/projects/{project_id}/policies | CreatePolicy | policy#manage AND project#operator on the parent Project | policy.create | policy_revision_created | 64 KiB |
| GET | /v1/projects/{project_id}/policies/{policy_id} | GetPolicy | policy#read | policy.read | (none) | n/a |
| PATCH | /v1/projects/{project_id}/policies/{policy_id} | UpdatePolicy | policy#manage AND project#operator | policy.update | policy_revision_created | 64 KiB |
| DELETE | /v1/projects/{project_id}/policies/{policy_id} | DeletePolicy | policy#manage AND project#operator | policy.delete | policy_deleted | n/a |
| GET | /v1/projects/{project_id}/policies/{policy_id}/revisions | ListPolicyRevisions | policy#read | policy.read | (none) | n/a |
| GET | /v1/projects/{project_id}/policies/{policy_id}/revisions/{revision_id} | GetPolicyRevision | policy#read | policy.read | (none) | n/a |
| POST | /v1/projects/{project_id}/policies/{policy_id}/dry-run | DryRunPolicy | policy#edit | policy.dryrun | (none) | 64 KiB |
| GET | /v1/projects/{project_id}/policies/{policy_id}/diff | DiffPolicyRevisions | policy#read | policy.diff | (none) | n/a |
body_cap = 64 KiBis enforced before the JSON decoder runs on the three body-carrying operations (CreatePolicy,UpdatePolicy,DryRunPolicy); an over-cap body surfaces as413 request_body_too_large.- Every handler short-circuits to
501 policies_not_provisioneduntil the production composition root supplies the policy application service and the ReBAC authorizer.
Authorization model
All nine operations accept the same first-success-wins credential triple as the rest of /v1: a psk_… API token Bearer, an OIDC JWT Bearer, or a plexsphere_session cookie. A missing or unresolved credential is 401 unauthenticated; the ReBAC gate then decides the 403.
- Read paths (
GetPolicy,ListPolicyRevisions,GetPolicyRevision,DiffPolicyRevisions) gate onpolicy#readagainst the addressed Policy. ListPoliciesgates the request onproject#readagainst the parent Project, then applies a per-rowpolicy#readfilter so the page contains only the Policies the caller may see (a missing per-row relation drops the row, never turns the list into a5xx).- Mutating paths (
CreatePolicy,UpdatePolicy,DeletePolicy) run a dual gate:policy#manageANDproject#operator. At create time the new Policy has no id yet, sopolicy#manageis checked against the parent Project. DryRunPolicygates onpolicy#edit— a strictly weaker permission thanpolicy#manage, so an editor can preview a candidate revision without holding the manage relation that lets them commit it.
Every gate decision — granted or denied — and every body-shape rejection emits an audit row through the audit sink, stamped with the operation's audit relation (policy.create, policy.read, policy.list, policy.update, policy.delete, policy.dryrun, policy.diff).
Path & query parameters
| Operation | Parameter | Type | Required | Notes |
|---|---|---|---|---|
| every operation | project_id (path) | string (uuid) | yes | Owning Project. Malformed → 400 invalid_project_id. |
every /{policy_id} operation | policy_id (path) | string (uuid) | yes | Policy identifier. Malformed → 400 invalid_policy_id. |
| GetPolicyRevision | revision_id (path) | string (uuid) | yes | Revision identifier. Malformed → 400 invalid_revision_id. |
| ListPolicies / ListPolicyRevisions | cursor (query) | string | no | Continuation token from a prior page's next_cursor; malformed → 400 invalid_cursor. |
| ListPolicies / ListPolicyRevisions | limit (query) | integer | no | Page size; out-of-range → 400 invalid_limit. |
| DiffPolicyRevisions | from_revision (query) | string (uuid) | yes | Base revision of the diff. |
| DiffPolicyRevisions | to_revision (query) | string (uuid) | yes | Target revision of the diff. |
Schemas
The OpenAPI spec is the authoritative source for field shapes. The schemas this surface uses are:
- Policy:
Policy(id,project_id,slug,display_name,head_revision_id,created_at,updated_at, and — onGetPolicy— an embeddedheadrevision; list rows omithead). - Revision:
PolicyRevision(id,policy_id, optionalparent_id, optionalcorrelation_id,created_at,created_by,selector,rules). - Selector:
PolicySelector(source,destination) — both label-selector expressions; an empty or unparseable side is400 selector_syntax_error. - Rule:
PolicyRule(action—allow/deny/log;protocol—tcp/udp/icmp/any;source_cidr;destination_cidr; optionalportsPolicyPortRange{from, to}, omitted foricmp/any). - Requests:
PolicyCreateRequest(slug, optionaldisplay_name,selector,rules),PolicyUpdateRequest(optionaldisplay_name,selector,rules, and an optionalexpected_revision_idfor optimistic-concurrency control),PolicyDryRunRequest(selector,rules). - Responses:
PolicyListResponse/PolicyRevisionListResponse(items+ optionalnext_cursor),PolicyDiff(added,removed,modified— eachmodifiedentry aPolicyDiffPairofbefore/after),PolicyDryRunResponse(matched_node_ids,rule_diff,peer_pairs_affected,unreachable_node_ids).
Revisions, dry-run, and diff
- Create / Update land revisions.
CreatePolicymints the head revision;UpdatePolicyappends a child revision (itsparent_idpointing at the prior head) and advanceshead_revision_id. AnUpdatePolicybody carryingexpected_revision_idis rejected with409 expected_revision_mismatchwhen a concurrent edit has already advanced the head; anUpdatePolicythat changes nothing is400 empty_patch. - History is read-only.
ListPolicyRevisionspages the revision chain newest-first;GetPolicyRevisionfetches a single historical revision. Neither mutates. - Dry-run is non-persisting.
DryRunPolicyevaluates a candidateselector+rulesagainst the live mesh and returns thematched_node_ids, therule_diffversus the current head, thepeer_pairs_affected, and anyunreachable_node_ids— writing no rows and emitting no outbox event. - Diff compares two revisions.
DiffPolicyRevisionscomputes the canonical rule diff betweenfrom_revisionandto_revision, returning added / removed / modified rules.
Error taxonomy
All error responses use the shared Problem envelope (application/problem+json); the 403 path uses the PermissionDenied shape with reason = insufficient_relation.
| Code | Status | Where | Meaning |
|---|---|---|---|
invalid_project_id | 400 | every operation | Malformed Project UUID. |
invalid_policy_id | 400 | every /{policy_id} operation | Malformed Policy UUID. |
invalid_revision_id | 400 | GetPolicyRevision | Malformed revision UUID. |
invalid_cursor | 400 | ListPolicies / ListPolicyRevisions | cursor is not a valid continuation token. |
invalid_limit | 400 | ListPolicies / ListPolicyRevisions | limit is out of range. |
invalid_body | 400 | CreatePolicy / UpdatePolicy / DryRunPolicy | Body cannot be decoded as the operation's request envelope. |
invalid_policy | 400 | CreatePolicy | The Policy envelope failed a top-level shape check. |
empty_patch | 400 | UpdatePolicy | The patch changes nothing. |
selector_syntax_error | 400 | mutating + dry-run | selector source or destination is empty or unparseable. |
unauthenticated | 401 | every operation | Missing or unresolved credential. |
insufficient_relation | 403 | every operation | Caller lacks the required ReBAC relation (policy#read / policy#edit / policy#manage / project#operator / project#read). |
policy_not_visible | 404 | /{policy_id} operations | Policy not visible to the caller (post-authz; no id oracle). |
revision_not_visible | 404 | GetPolicyRevision / DiffPolicyRevisions | The named revision is not visible on the addressed Policy. |
revision_conflict | 409 | UpdatePolicy | A concurrent edit advanced the head before this request landed; refetch and retry. |
expected_revision_mismatch | 409 | UpdatePolicy | The supplied expected_revision_id no longer matches the head. |
policy_slug_taken | 409 | CreatePolicy | A Policy with this slug already exists in the Project. |
invalid_rule | 422 | mutating + dry-run | The rule list failed an aggregate invariant. |
cidr_family_mismatch | 422 | mutating + dry-run | A rule's source and destination CIDRs disagree on address family. |
port_range_inverted | 422 | mutating + dry-run | A rule's port range has from greater than to. |
unknown_action | 422 | mutating + dry-run | A rule action is not one of allow / deny / log. |
unknown_protocol | 422 | mutating + dry-run | A rule protocol is not one of tcp / udp / icmp / any. |
rule_count_exceeded | 422 | mutating + dry-run | The rule list exceeds the per-Policy maximum. |
request_body_too_large | 413 | CreatePolicy / UpdatePolicy / DryRunPolicy | Request body exceeded the 64 KiB policy envelope cap. |
policies_not_provisioned | 501 | every operation | The policy application service / authorizer is not wired in this build. |
internal | 500 | every operation | Server-side failure path; the wire body stays generic. |
Cross-references
../../contexts/policy/model.md— the Policy aggregate, the rule and selector value objects, and the ubiquitous language.../../contexts/policy/compiler.md— the compile pipeline a landed revision drives.../../contexts/policy/events.md— thepolicy_revision_created/policy_deletedoutbox events and the compile-consumer that recompiles or purges on each../projects.md— the parent Project surface; theproject#readandproject#operatorrelations gate Policy access../index.md— the HTTP API surface map.../../../api/openapi/plexsphere-v1.yaml— OpenAPI 3.1 spec; theListPolicies/CreatePolicy/GetPolicy/UpdatePolicy/DeletePolicy/ListPolicyRevisions/GetPolicyRevision/DryRunPolicy/DiffPolicyRevisionsoperations and theirPolicy/PolicyRevision/PolicyRule/PolicySelector/PolicyDiff/PolicyCreateRequest/PolicyUpdateRequest/PolicyDryRunRequest/PolicyListResponse/PolicyRevisionListResponse/PolicyDryRunResponseschemas.