Policy engine
The Policy Engine gives Jentrix one enforceable home for safety rules. Every
risky action — a risky MCP tool, an outbound webhook, a repo push, a bulk
mutation, a credential grant — is evaluated by a deterministic
evaluatePolicy(action, context) before it runs. A denial returns the same
structured error envelope agents already understand, with the policy reason and
the next step.
How resolution works
Policies are layered and the most-restrictive decision wins:
system → workspace → project → agent profile → work order → one-time grant
For a given action the engine collects every policy whose layer applies to the
context and whose template matches the action, then takes the most restrictive
outcome. The ordering is DENY > REQUIRE_ > ALLOW*; same-restrictiveness ties
break by priority (higher wins), then by layer specificity. When no policy
matches, the action is allowed (low-risk paths default to ALLOW).
Outcomes
| Outcome | Meaning |
|---|---|
ALLOW | The action may proceed. |
DENY | The action cannot proceed. |
REQUIRE_APPROVAL | The action pauses and an admin must approve it (Control Tower). |
REQUIRE_STRONGER_CONTEXT | The Work Order must include missing fields/sources. |
REQUIRE_SAFER_MODE | Convert the action to draft / dry-run / read-only. |
REQUIRE_CREDENTIAL_BROKER | The action must use a brokered grant, not raw credentials. |
A denied MCP/admin action returns { error: { code: "FORBIDDEN", message, hint } }
where message is the policy reason and hint is the next step.
Templates (the v1 policy "language")
There is no DSL. A policy is a typed template plus a small, validated JSON condition object. The built-in templates:
| Template | Domain | Gates | Default outcome |
|---|---|---|---|
read_only_workspace | (all) | every mutating action — a read-only client workspace | DENY |
draft_vs_send | communication | comm:send (drafting is allowed) | REQUIRE_SAFER_MODE |
repo_commit_deny | repo | repo:commit (optional branch) — forbid the harness local-commit boundary (runner-performed, not brokered; no approval path) | DENY |
repo_push_approval | repo | repo:push (optional branch) | REQUIRE_APPROVAL |
repo_branch_create_approval | repo | repo:branch_create | REQUIRE_APPROVAL |
repo_pr_create_approval | repo | repo:pr_create (optional baseBranch) | REQUIRE_APPROVAL |
merge_approval | repo | repo:merge (optional baseBranch) — the production boundary under gate_before_merge | REQUIRE_APPROVAL |
deploy_environment_gate | repo | deploy:preview / deploy:production (optional environment) | REQUIRE_APPROVAL |
high_risk_change | repo | a push/PR/merge over a diff-size cap or touching a sensitivePaths entry | REQUIRE_APPROVAL |
webhook_admin_approval | webhook | creating an outbound webhook | REQUIRE_APPROVAL |
admin_tool_approval | mcp tool | listed admin tools (tools: [...]) | REQUIRE_APPROVAL |
max_spend_per_run | job | a job whose estimatedCostUsd exceeds maxUsd | REQUIRE_APPROVAL |
harness_advance_approval | automation | harness:advance — govern the harness orchestrator advancing a run (put a human in the outer loop) | REQUIRE_APPROVAL |
harness_freeze_deny | automation | harness:freeze — forbid an automated/policy-initiated run freeze (freeze stays a manual operator control; no approval path) | DENY |
The delivery action classes are open strings (no schema change), evaluated under
the repo domain: repo:merge, deploy:preview, deploy:production,
ci:rerun (an explicit Actions re-run — off by default; the CI-triage path
pushes a commit instead), release:approve, and release:rollback. The merge,
deploy, and rollback boundaries are also brokered and leased (M8.3 Stage 3.4) —
the policy gate authorizes the action; a short-lived scoped credential carries it
out. An agent never holds a standing merge/deploy token.
Two SYSTEM-scope defaults (webhook_admin_approval, repo_push_approval) are
seeded off; admins enable or add policies at Settings → Policies.
Approvals and overrides
A REQUIRE_APPROVAL outcome surfaces in the Control Tower policy queue. An
admin resolves it, which mints an ApprovalBinding bound to the exact
action-payload hash + actor + an expiry. The binding:
- authorizes only that exact payload — it cannot be reused for a different payload (the hash differs);
- is single-use and time-bounded;
- relaxes a
REQUIRE_APPROVALtoALLOWonly when the matched policy setsallowOverride.DENYis never relaxed.
A partial unique index guarantees at most one active binding per
(workspace, actionHash, actor), so the same payload can be re-approved later
once a binding is consumed/expired/revoked.
What gets evaluated
Enforcement is wired into the ops core (so both the UI and MCP surfaces are
covered) and surfaced through safe() as a FORBIDDEN envelope for MCP tools:
- Webhook creation (
create_webhook/ settings). - Bulk mutations (
bulk_create_tasks/bulk_update_tasks/bulk_move_tasks). - Board-kind conversion (
convert_board_kind). - GitHub outbound sync (held back, not denied loudly — the sync is skipped).
- Credential grant issuance (the
CREDENTIALdomain, before any grant).
Every evaluation writes an append-only PolicyEvaluation with redacted inputs.
MCP tools
| Tool | Returns |
|---|---|
list_policies | Workspace policies with template, domain, outcome, priority, enabled. Filter by domain/scope. |
get_policy_evaluation | One evaluation: action class, outcome, reason, matched policy, and redacted inputs. |
list_policy_evaluations | The append-only evaluation feed (newest first). Filter by runId/outcome/domain/since; paginated. |
Reads never surface credential backend references or any secret material. The
related admin-scope write tools are live: resolve_approval resolves a
require_approval evaluation by minting a scoped, time-bounded approval binding,
and request_credential_grant issues a short-lived credential grant (see
Credential broker).