Cedar is the permission layer: compiled data evaluated in-process by the gateway, default-deny, never a network hop per decision. These routes move one workspace’s Cedar source through draft, compile, and publish. See Policy overview and Publishing. Three rules shape this surface.
1

A read never writes

GET /policy does not create a Policy row. A workspace with no row gets a read-only projection of what a first publish would produce — the default Cedar source and the stable pol_default_… id — not a claim that anybody published anything.
2

A draft is not live

PUT /policy stores source and stamps draft_updated_at. It does not compile, does not create a revision, and does not touch the workspace’s snapshot version.
3

Publish confirms a stored draft

POST /policy/publish compiles the stored draft outside every lock, then commits only if nothing moved underneath. It never accepts Cedar in its body, so nothing can go live that was not first stored and audited as a draft.
POST /policy/test borrows policy.draft rather than workspace.read, so a frozen workspace answers 409 even though nothing is written. Testing is part of authoring, a frozen workspace cannot publish the result, and a read-only role should not be able to spend compiler capacity.

Policy object

The operator_policy contract, frozen in packages/proto/operator_policy.schema.json. It never carries entities_json, the workspace snapshot, or a revision’s compiled bundle — those are gateway documents.
integer
required
Currently 1.
string
required
Prefixed pol_. For a workspace with no stored policy this is the stable pol_default_<workspace suffix>.
string
required
Prefixed ws_.
string
required
The stored draft source, or the default policy when no row exists.
string
required
sha256:<64 hex> over the canonical UTF-8 Cedar source only.
string
required
RFC 3339 UTC. With no Policy row this is when the workspace acquired its default policy, not a claim that an operator edited anything.
string
required
not_run, valid, or invalid. A plain read reports valid only when the stored draft is byte-equal to a source that did compile, because it is the one that was published; otherwise not_run.
array
required
Compiler diagnostics. Empty on a plain read. Each entry has severity (error or warning), code, message, and optional line and column.
object
required
state (published or pending), requested_version (at least 1), and published_version when a version has been published.
string
Prefixed rev_. Present once a revision exists.
string
RFC 3339 UTC. Present once a revision exists.
string
sha256:<64 hex> over the compiled bundle — source plus the entity snapshot as of publish time. Present once a revision exists.
draft_hash and published_hash cover different inputs and are never comparable. published_hash is also not the hash of what is currently live: it is frozen at publish time, while the running bundle is rebuilt from the revision’s source plus fresh entities, so the two diverge after any key or connection change without the policy moving. “Is my draft the one that is live?” is answered by current_revision_id plus publication, never by comparing hashes.

Get the policy

GET /api/v1/workspaces/{workspace_id}/policy Requires workspace.read. Returns 200 with the policy object. Writes nothing.
string
required
Workspace id, prefixed ws_.
The draft_hash above is a placeholder. The real value is the SHA-256 of the exact cedar_text bytes returned in the same response.

Errors

Store a draft

PUT /api/v1/workspaces/{workspace_id}/policy Requires policy.draft. Returns 200 with the policy object reflecting the new draft. Stores source and audits it. Does not compile.
string
required
Workspace id, prefixed ws_.
string
required
Cedar source, 1–262144 bytes. Additional properties are rejected. The bound is part of the compiler-pool budget: a megabyte of Cedar is a payload, not a policy — the shipped contract policy is under 2 KB.

Errors

Test a draft

POST /api/v1/workspaces/{workspace_id}/policy/test Requires policy.draft. Compiles the stored draft against the workspace’s current entities and reports diagnostics. Writes nothing. No body. It uses the same compiler seam as publish, and therefore the same process-level concurrency bound and the same closed failure vocabulary, so a developer who cannot publish can still find out why a draft will be rejected before an admin tries.
string
required
Workspace id, prefixed ws_.
Returns 200 with validation_state set to valid or invalid and diagnostics populated. An invalid draft is a successful 200 response, not an error status — the request succeeded, the draft did not compile.

Errors

policy_compiler_busy is distinct from policy_compiler_unavailable on purpose: nothing is wrong with the compiler or the draft, the process is over its concurrency budget. Sending an operator to debug a healthy toolchain is the failure mode being avoided. Both are closed messages — they name a condition, never a queue depth, a lane, or a workspace. The compiler’s stderr, exit code, and exception text never reach a response.

Publish

POST /api/v1/workspaces/{workspace_id}/policy/publish Requires policy.publish. Compiles the stored draft, creates a revision, stages a snapshot for the gateway, and audits the publish. The route runs in three phases: it snapshots (policy_id, cedar_text, draft_hash, current_revision_id) and closes the read transaction; compiles with no transaction open and no lock held; then re-opens a write transaction that locks the workspace and the policy and re-checks the same values. Anything that changed underneath is a 409 with no writes.
string
required
Workspace id, prefixed ws_.
string
required
The draft_hash of the draft you intend to publish, matching ^sha256:[a-f0-9]{64}$. Take it from GET /policy or the PUT /policy response. Publishing is a confirmation of a stored draft, never a way to submit one — this body accepts no Cedar.
string
Optional. When supplied, the publish is rejected unless the policy’s current_revision_id still matches. Use null or omit it when publishing the first revision.
Additional properties are rejected.

Success response

The operator_policy_publish contract, frozen in packages/proto/operator_policy_publish.schema.json.
integer
required
Currently 1.
string
required
Prefixed pol_.
string
required
The revision just committed, prefixed rev_.
string
required
The source hash that was published — the same value you confirmed.
string
required
RFC 3339 UTC.
object
required
state (published or pending), requested_version, and published_version when set. A Redis outage after the commit makes this pending; it does not delete the revision or the audit row.

Errors