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
Theoperator_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.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_.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.Success response
Theoperator_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.