Tool access precedence
Which layer wins when the org dashboard, a local .prismor/agents.yaml, and
a session's synthesized scope disagree about whether an agent may call a
specific tool. Companion to docs/enterprise-tool-access.md
and docs/policy-layers-and-exemptions.md.
The rule
Org policy is authoritative. An admin's decision on prismor.dev (Admin →
Connections → an agent's Tool access panel) always wins over a developer's
local .prismor/agents.yaml deny list and over a session's synthesized scope
(prismor scope edit, the scoped-agent allowlist, the dashboard's MCP Servers
toggles):
kill switch ── agent paused (local OR org) ── ALWAYS blocks, no override
│
org deny ── admin denied this tool ── ALWAYS blocks
│
org allow ── admin explicitly allowed this tool ── overrides local deny_tools
│ AND session scope for
│ this tool
local ── .prismor/agents.yaml deny_tools, session-scoped allowlist
- The kill switch (an agent paused, either locally or by the org) is the one condition nothing can override. It blocks every tool for that agent, not just one.
- An org deny for a tool always blocks, regardless of what a local file says.
- An org allow is a real override, not just "no deny exists." Toggling a
tool to "Allowed" on the dashboard writes a signed rule that lifts a local
.prismor/agents.yamldeny and a session's scoped-agent restriction for that exact tool. It does not lift a different org-level deny (that's a separate admin decision) and it never resurrects a killed-switched agent. - Local restrictions (the per-agent deny list, a synthesized session scope) are the default floor for a single machine or session when the org hasn't spoken. They no longer silently out-rank the org once an admin makes an explicit call.
This only applies to tool access (allow/deny a specific tool tag). The non-overridable security floor — destructive commands, secret exfiltration, RCE, privilege escalation, DoS, tool-category crossover — is a different, always-on layer that no admin action can weaken; see docs/policy-layers-and-exemptions.md.
Why this changed
Earlier, the dashboard's "Allowed" badge meant only "no org-level deny
exists for this tool" — it said nothing about local state. An admin could
toggle a tool to "Allowed" and watch the agent still get blocked, because a
developer's local .prismor/agents.yaml deny or a stale session scope was
still in effect and nothing could lift it remotely. That made the org console
look broken and put the developer's local toggle in charge of a decision that
should belong to the org.
How it's enforced (runtime)
Both deny and allow rows for a tool ship in the same signed list
(settings.tool_denies, entries disambiguated by action: "deny" | "allow")
so they arrive and expire together — see resolveToolDenies in
prismor-web/lib/tool-policy.ts.
On-device, prismor/runtime/runtime.py::evaluate_tool_call:
- Evaluates the local per-agent kill switch, the local
deny_toolslist, the session's scoped-agent rules, and the org's own tool denies — same as before. - Then applies any matching org tool-allow entry: it drops findings with
ruleId in ("agent-tool-deny", "scoped-agent")for the tool the current event is about. It does not touch the kill switch (ruleId "agent-disabled"), a differentruleId "org-tool-deny"finding, or a scoped-agent finding about a different concern (path or network access — only the tool-name check is lifted).
Set from the console:
POST /api/admin/tool-policy
Content-Type: application/json
{"orgId":"<org>","tool":"mcp__github__create_issue","action":"allow","scope":"agent","scopeId":"checkout-bot"}
scope may be org, agent, device, or session — same scoping as tool
denies. Tests: prismor/tests/test_org_tool_denies.py
(test_org_allow_overrides_local_agent_deny,
test_org_allow_overrides_session_scoped_deny,
test_org_allow_does_not_lift_kill_switch,
test_org_allow_does_not_lift_scoped_network_denial).