Policy Development Guide
Version française : ../../fr/guides/policy-development.md
This guide shows how to write, test, register, simulate, activate, publish, version and retire an authorization
policy (OPA Rego or built-in rules) and a relationship model (OpenFGA). Writing a policy needs tenant_admin or
security_officer; a developer may simulate and read the revisions.
Concept
The Control Plane is the policy decision point of the platform: every sensitive action (run an agent, call a tool,
sync a connector, load a skill) asks a decision through the Policy Contract (contracts/v1/policy-contract.schema.json)
and gets allow, reasons and obligations (contracts/v1/policy-decision.schema.json). Two kinds of policy
coexist:
- Attribute policies, engine
builtin(JSON rules) oropa(Rego), stored in the policy registry with an immutable revision for every change. - Relationship checks (ReBAC), answered by OpenFGA from tuples such as
owner,editor,viewerorcan_invokeon an object.
A policy is draft, active or disabled; only active policies take part in the decisions, in priority order.
Policy fields
The body of POST /v1/policies:
| Field | Type | Default | Description |
|---|---|---|---|
name | string | required | ^[a-z0-9][a-z0-9_.-]{0,99}$. |
description | string | "" | What the policy enforces. |
version | string | 1.0.0 | Version label of the policy. |
engine | enum | builtin | builtin or opa. |
rego | string | none | Rego module, required for opa. |
rules | array | [] | Rules of a builtin policy (table below). |
priority | integer | 100 | Evaluation order, lowest first. |
status | enum | active | draft, active or disabled. |
Each built-in rule:
| Field | Type | Default | Description |
|---|---|---|---|
id | string | none | Rule identifier, quoted in reasons. |
effect | enum | allow | allow or deny. |
actions | array | ["*"] | Actions the rule applies to, such as agent.run. |
resource_types | array | [] | Resource types, such as agent or connector. |
conditions | object | {} | Conditions on the subject, the resource and the context. |
obligations | array | [] | Each item: type and params. |
reason | string | none | Explanation returned with the decision. |
Rego and OpenFGA
The template sdks/python/src/enterprise_ai/templates/policy-pack contains Rego policies with their tests and an OpenFGA model with model.fga.yaml; the platform policies live in deploy/policies/opa and the relationship model in deploy/policies/openfga/model.fga.
A Rego decision rule returns allow, reasons, obligations and, for a product package (eai.rag_agent, eai.compliance...), applies: false when the action is not its business (audit.record, knowledge.query...). The Control Plane then skips that policy instead of reading its fail-closed allow: false as a refusal; without applies, the package decides every action of the tenant.
enterprise-ai create policy residency
cd residency
enterprise-ai test authz
Steps
-
Create the policy project. The
policy-packtemplate writespolicies/*.regowith their_test.rego, an OpenFGA modelauthz/model.fgawith its testsauthz/model.fga.yaml, and a sample requestinputs/tool-invoke.json.enterprise-ai create policy residencyExpected result: the files are listed and the project is valid.
If it fails: exit code 1 with "exists"; add
--force. -
Write the rules. Write Rego for engine
opa, or a JSON rule list for enginebuiltin. In the portal: Developer Studio > Policy studio > New; the editor checks the JSON of the rules as you type.Expected result: every rule has a test that allows and a test that denies.
If it fails: a rule without a deny test usually allows too much; add the case.
-
Test locally.
test authzrunsopa teston the policies andfga model teston every*.fga.yaml. A missingopaorfgabinary is an error, never a silent pass.cd residencyenterprise-ai test authzExpected result: each suite is reported PASS.
If it fails: exit code 1 names the failed suite, or the missing binary.
-
Register the policy as a draft. POST /v1/policies stores the policy and records revision 1. Register it with
status: draftso that it takes no part in the decisions yet. In the portal: Create.policy = client.policies.create({"name": "residency", "engine": "opa", "rego": open("policies/residency.rego").read(), "status": "draft"})Expected result: 201 with the policy id.
If it fails: 422
validation_error(rego is required for engine opa), 503policy_engine_unavailablewhen OPA refuses the module. -
Evaluate and simulate. POST /v1/policies/evaluate gives the decision of the active policies for one request. POST /v1/policies/simulate compares, input by input, the active set with the same set plus the candidate, without any change;
recordedalso replays the last recorded decisions (0 to 500). In the portal: Test request > Evaluate with the active policies and Simulate the edited policy.client.policies.evaluate({"action": "tool.invoke", "resource": {"type": "tool", "id": "crm-search"}})client.policies.simulate({"name": "residency", "rules": []}, policy_id="<policy-id>", inputs=[{"action": "tool.invoke"}])<policy-id>: id of the policy the candidate replaces; without it the candidate is added.Expected result: the simulation counts
changed,allow_to_denyanddeny_to_allow.If it fails: 422
simulation_engine_unsupported(onlybuiltincandidates are simulated; test Rego withopa test), 422validation_errorwhen neitherinputsnorrecordedis given. -
Activate. PUT /v1/policies/{policy_id} with
status: activerecords a new revision. With the default deliveryEAI_OPA_DELIVERY=push, anopapolicy is uploaded to OPA on write.Expected result: GET /v1/policies/decisions shows the new decisions with their
reasons.If it fails: 503
policy_engine_unavailable; checkEAI_OPA_URLand the OPA service. -
Publish a signed bundle. With
EAI_OPA_DELIVERY=bundle, OPA polls signed bundles instead: POST /v1/policies/bundles publishes a new revision, signed with the key ofEAI_POLICY_BUNDLE_SIGNING_KEY_FILE, and OPA downloads GET /v1/policies/bundles/{revision}/bundle.tar.gz. GET /v1/policies/engines shows the version each engine really runs.Expected result: the engines report the published revision.
If it fails: 409
policy_bundle_empty, 503policy_bundle_signing_unavailable(no signing key). -
Version and compare. Each PUT appends a revision. GET /v1/policies/{policy_id}/revisions lists them and GET /v1/policies/{policy_id}/revisions/diff compares two (by default the previous one against the latest). In the portal: Policy studio > Revisions > Compare the decisions of the two revisions.
Expected result: the changed fields and the decisions that move.
If it fails: 404
not_foundfor an unknown revision. -
Disable or delete. PUT with
status: disabledtakes the policy out of the decisions and keeps it; DELETE /v1/policies/{policy_id} removes it (and the module from OPA in push mode).Expected result: the policy no longer appears in the
reasonsof new decisions.If it fails: 503
policy_engine_unavailableon delete; the policy is kept, try again.
Disabling a deny policy can open access at once. Simulate on recorded decisions before you disable it in production.
Governance
POST /v1/policies creates a policy, POST /v1/policies/simulate simulates a candidate revision, POST /v1/policies/evaluate evaluates a request, POST /v1/policies/bundles publishes a signed OPA bundle, POST /v1/authz/check checks a relationship.
client.policies.simulate({"rules": []}, policy_id="<policy-id>", recorded=True)
client.policies.diff("<policy-id>", from_revision=1, to_revision=2)
Relationships are managed with POST /v1/authz/tuples, GET /v1/authz/tuples and DELETE /v1/authz/tuples, and checked with POST /v1/authz/check. In the portal: Security and governance > Permissions (ReBAC) and Policy simulator.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
422 validation_error on rego | Engine opa without a Rego module. | Send rego, or use engine builtin. |
422 validation_error on inputs | Simulation without inputs and without recorded. | Give sample requests or a number of recorded decisions. |
422 validation_error with contract: policy-contract | The evaluated request does not match the Policy Contract. | Check it against contracts/v1/policy-contract.schema.json. |
422 simulation_engine_unsupported | The candidate uses engine opa. | Test Rego with enterprise-ai test authz; simulate builtin rules only. |
503 policy_engine_unavailable | OPA refused or did not answer. | Check OPA and EAI_OPA_URL; read reason. |
409 policy_bundle_empty | No policy to put in the bundle. | Create or activate a policy first. |
503 policy_bundle_signing_unavailable | No bundle signing key. | Set EAI_POLICY_BUNDLE_SIGNING_KEY_FILE. |
404 not_found | Unknown policy id (a UUID) or revision. | List the policies with enterprise-ai get policies. |
| 403 on POST /v1/policies | The caller has neither tenant_admin nor security_officer. | Ask for the role; a developer may only simulate. |
Next steps
- Action context and execution context: the attributes a policy sees.
- Pack development guide for the policies of a pack.
- Testing guide and API reference guide.