Skip to content

Access decisions

Section 7 · From docs/MCP_PRIVACY.md on GitHub

7.1 Order

Policy.decide(spec, principal) returns (allowed, reason):

  1. Collect the matching rules: roles first (in the order of the roles mapping), then rules in order.
  2. Explicit denials. For each matching rule, if its deny list for the primitive's family matches, or its deny_categories matches, or the primitive declares a capability in its deny_capabilities, the call is denied with that rule's name in the reason. Nothing can override this.
  3. Sensitivity ceiling. If any matching rule sets max_sensitivity, the highest of those values is the ceiling; otherwise the policy's own. A primitive above the ceiling is denied.
  4. Grants. If any matching rule's allow list for the family, or its allow_categories, matches, the policy-level lists are skipped. Otherwise the policy's deny list, deny categories, allow list and allow categories are checked in that order.
  5. Capabilities. Every capability the primitive declares that is in the policy's effective deny set must be granted by a matching rule's allow_capabilities, or the call is denied.

A rule's allow_tools therefore cannot get around a capability denial or a sensitivity ceiling. Those need allow_capabilities and max_sensitivity in the rule.

Decisions are cached per primitive and per set of roles (plus the principal id when some rule matches on principals).

Reasons look like these. The last four come from {"name": "lists", "allow_tools": ["find_*", "get_*"], "deny_tools": ["get_secret*"], "allow_categories": ["inventory"], "max_sensitivity": "confidential"} and tools called get_secret_x, find_live (category live), list_x and a restricted get_raw:

capability 'reveal' denied by rule 'never-reveal'
capability 'cloud_access' is denied by policy 'strict-visible'
tool denied by policy 'lists'
category 'live' not allowed by policy 'lists'
tool not in the allow list of policy 'lists'
sensitivity 'restricted' exceeds the allowed 'confidential'

7.2 hide_denied

With hide_denied: true (every built-in profile), a denied primitive is filtered from tools/list, resources/list, resources/templates/list and prompts/list, and calling it gives the same error as a name that does not exist:

NotFoundError: Unknown tool: reveal_token

Hiding beats refusing: the model does not waste turns on tools it cannot use, and a prompt-injected "call reveal_token" has nothing to call. The layer still reports each attempt to the policy, which records it as a not_found decision (section 15.1), so probing for hidden tools shows up in the audit trail.

With hide_denied: false, the tool stays listed and the call returns an isError result. From {"extends": "strict", "name": "strict-visible", "hide_denied": false}, calling map_inventory:

{
  "isError": true,
  "content": [{"type": "text", "text": "Access to tool 'map_inventory' denied: capability 'cloud_access' is denied by policy 'strict-visible'"}],
  "_meta": {
    "cloudg/error_code": -31001,
    "cloudg/error_data": {"policy": "strict-visible", "reason": "capability 'cloud_access' is denied by policy 'strict-visible'"}
  }
}

A denied resource read or prompt raises AccessDeniedError (code -31001) as a protocol error instead.

7.3 Sensitivity ceilings

Each primitive has a sensitivity (see MCP_TOOLS.md). In 0.6.0 the restricted tools are get_asset_metadata (raw metadata, where secrets tend to live), privacy_audit_log and reveal_token. strict and soc-analyst cap callers at confidential, which hides those three. The lead role raises its own ceiling to restricted; a rule ceiling replaces the policy ceiling instead of tightening it.

The annotator also uses the sensitivity for MCP audience hints: restricted output is marked for the user only (section 11.6).