Skip to main content

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) or opa (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, viewer or can_invoke on 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:

FieldTypeDefaultDescription
namestringrequired^[a-z0-9][a-z0-9_.-]{0,99}$.
descriptionstring""What the policy enforces.
versionstring1.0.0Version label of the policy.
engineenumbuiltinbuiltin or opa.
regostringnoneRego module, required for opa.
rulesarray[]Rules of a builtin policy (table below).
priorityinteger100Evaluation order, lowest first.
statusenumactivedraft, active or disabled.

Each built-in rule:

FieldTypeDefaultDescription
idstringnoneRule identifier, quoted in reasons.
effectenumallowallow or deny.
actionsarray["*"]Actions the rule applies to, such as agent.run.
resource_typesarray[]Resource types, such as agent or connector.
conditionsobject{}Conditions on the subject, the resource and the context.
obligationsarray[]Each item: type and params.
reasonstringnoneExplanation 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​

  1. Create the policy project. The policy-pack template writes policies/*.rego with their _test.rego, an OpenFGA model authz/model.fga with its tests authz/model.fga.yaml, and a sample request inputs/tool-invoke.json.

    enterprise-ai create policy residency

    Expected result: the files are listed and the project is valid.

    If it fails: exit code 1 with "exists"; add --force.

  2. Write the rules. Write Rego for engine opa, or a JSON rule list for engine builtin. 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.

  3. Test locally. test authz runs opa test on the policies and fga model test on every *.fga.yaml. A missing opa or fga binary is an error, never a silent pass.

    cd residency
    enterprise-ai test authz

    Expected result: each suite is reported PASS.

    If it fails: exit code 1 names the failed suite, or the missing binary.

  4. Register the policy as a draft. POST /v1/policies stores the policy and records revision 1. Register it with status: draft so 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), 503 policy_engine_unavailable when OPA refuses the module.

  5. 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; recorded also 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_deny and deny_to_allow.

    If it fails: 422 simulation_engine_unsupported (only builtin candidates are simulated; test Rego with opa test), 422 validation_error when neither inputs nor recorded is given.

  6. Activate. PUT /v1/policies/{policy_id} with status: active records a new revision. With the default delivery EAI_OPA_DELIVERY=push, an opa policy 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; check EAI_OPA_URL and the OPA service.

  7. 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 of EAI_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, 503 policy_bundle_signing_unavailable (no signing key).

  8. 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_found for an unknown revision.

  9. Disable or delete. PUT with status: disabled takes 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 reasons of new decisions.

    If it fails: 503 policy_engine_unavailable on delete; the policy is kept, try again.

warning

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​

SymptomCauseFix
422 validation_error on regoEngine opa without a Rego module.Send rego, or use engine builtin.
422 validation_error on inputsSimulation without inputs and without recorded.Give sample requests or a number of recorded decisions.
422 validation_error with contract: policy-contractThe evaluated request does not match the Policy Contract.Check it against contracts/v1/policy-contract.schema.json.
422 simulation_engine_unsupportedThe candidate uses engine opa.Test Rego with enterprise-ai test authz; simulate builtin rules only.
503 policy_engine_unavailableOPA refused or did not answer.Check OPA and EAI_OPA_URL; read reason.
409 policy_bundle_emptyNo policy to put in the bundle.Create or activate a policy first.
503 policy_bundle_signing_unavailableNo bundle signing key.Set EAI_POLICY_BUNDLE_SIGNING_KEY_FILE.
404 not_foundUnknown policy id (a UUID) or revision.List the policies with enterprise-ai get policies.
403 on POST /v1/policiesThe caller has neither tenant_admin nor security_officer.Ask for the role; a developer may only simulate.

Next steps​