Skip to content

04. Per-call scope resolution

Purpose says why the access happens. Scope says how narrow it should be. A purpose support:read_own_orders is a category; the per-call scope is "this user's order in this conversation." Scope resolution is the per-call narrowing that turns wide credentials into operation-specific access.


A platform engineer at a Bengaluru SaaS company implements purpose binding (chapter 03) and is pleased with the audit clarity. Three weeks later, a customer-success rep accidentally types another customer's account ID into the support agent's chat to "test something." The agent reads the other customer's data — the purpose check passes (support:read_own_orders is valid) — and surfaces it. The leak is small but real. The audit shows the purpose was declared, the credential was authorised, the data was read. None of the layers caught the problem because the scope was not tied to the active user.

The fix is in this chapter. Per-call scope resolution narrows the access to the smallest set of records the operation's context permits. The purpose says read_own_orders; the scope says "own means the user_id in the active session, not the user_id passed in the argument." Even if the agent or the user passes a different user_id, the scope refuses.


What scope resolution is

Scope resolution is the per-call narrowing of access to the smallest record set the operation's context permits, enforced by the data layer regardless of what the caller claims.

Three properties.

Per-call. Each call is scoped independently. Two calls in the same session can have different scopes if their contexts differ.

Context-driven. The scope is derived from the active context — the session's authenticated user, the active resource, the workflow's tenant. Not from arguments the agent constructs; arguments are checked against the scope.

Data-layer enforced. The data layer (storage, query engine) refuses any access outside the scope, regardless of how the call was constructed above it. The agent layer is not the security boundary.


The scope sources

Three categories of scope, layered for typical agent calls.

Tenant scope

Multi-tenant platforms bind every call to a tenant. The tenant comes from the caller's authenticated identity, not from arguments.

scope:
  tenant: acme-corp           # from session, not from request body

A call to read_customer(customer_id="c_42") is resolved against tenant=acme-corp. The data layer adds WHERE tenant_id = 'acme-corp' to every query. A customer in a different tenant is refused — the query simply does not return the row.

This is the most important scope layer for multi-tenant platforms. Without it, cross-tenant leaks are a regular incident class.

User / actor scope

Within a tenant, the active user constrains many operations. The scope sources from the session:

scope:
  tenant: acme-corp
  active_user_id: u_42        # the user the agent is helping

For support:read_own_orders, the data layer adds WHERE customer_id = 'u_42'. The agent can supply customer_id as an argument, but it is checked against the active_user_id; mismatches refuse.

Resource scope

For purposes where the operation is bound to a specific resource (e.g., consultation:read_active_patient), the active resource constrains:

scope:
  tenant: clinical-care
  active_patient_id: p_318   # the patient being consulted on this session

The data layer enforces WHERE patient_id = 'p_318' on relevant queries.


How the scope flows through a call

For a tool call from an agent:

1. Session establishes active context:
     - tenant_id (from authenticated session)
     - active_user_id (from chat session start)
     - active_resource_id (from session state — e.g., which patient is being consulted)

2. Agent constructs a tool call with arguments.

3. Tool wrapper declares the purpose (chapter 03).

4. Mediator resolves the scope from the purpose + active context:
     - purpose support:read_own_orders, active_user_id u_42
     -> scope { tenant: acme-corp, customer_id: u_42 }

5. Mediator validates the call's arguments against the scope:
     - call passes customer_id="c_99" but scope says customer_id="u_42"
     -> refuse with SCOPE_VIOLATION

6. Mediator passes the scope to the data layer.

7. Data layer executes the query with scope clauses:
     - SELECT ... FROM orders
       WHERE tenant_id = 'acme-corp'
         AND customer_id = 'u_42'   -- from scope, not from request

8. Result returned; audit recorded with the full scope.

Step 5 is the load-bearing check that prevents the chapter-opening incident. The agent's argument is treated as a claim, validated against the scope derived from session context. Mismatches refuse.

Step 7 is the data layer's enforcement — defence in depth. Even if step 5 were bypassed, the query the data layer executes is scoped.


Common scope patterns

Five patterns cover most agent operations.

Pattern Scope sources from Example purpose
single-resource Active resource (active_user_id, active_patient_id) consultation:read_active_patient
owner-bound The owning user identified by session support:read_own_orders
tenant-only Just the tenant analytics:tenant_aggregate
cohort-bounded A pre-approved cohort selector marketing:campaign_export
cross-resource-related Resources related to an active resource (e.g., orders of the active customer) support:read_active_customer_recent_orders

For each pattern, the access mediator knows the scope template; the per-call scope is derived from the active context.


What the data layer does

The data layer's job is to enforce the scope regardless of what is claimed above.

For SQL databases, scope clauses are appended to queries. Some platforms use row-level security (Postgres RLS, SQL Server RLS) where the scope is stored in a session variable and the database itself enforces. Others use ORM-level mediation where every query is constructed via a scope-aware builder.

For document stores, the scope is applied to the document path or the query filter.

For vector stores, the scope filters the candidates: a vector search retrieves only candidates whose scope clauses match.

For object stores, the scope governs path prefixes; queries outside the prefix refuse.

The discipline is the same across stores: the scope is enforced by the storage layer, not just the application layer. An attacker who bypasses the application layer still hits the storage's enforcement.


What happens on a scope violation

A scope violation is a structured refusal:

{
  "ok": false,
  "error": {
    "code": "SCOPE_VIOLATION",
    "retriable": false,
    "human_hint": "That information is not available for the current context.",
    "model_action": "Do not retry. Ask the user to clarify what they meant; do not assume the requested ID is correct.",
    "fields": {
      "purpose": "support:read_own_orders",
      "expected_scope": { "customer_id": "u_42" },
      "attempted_resource": { "customer_id": "c_99" },
      "audit_id": "aud_..."
    }
  }
}

The model receives the error and acts per the model_action. Typically the model asks the user for clarification, or surfaces "I cannot find that order; could you check the order number?"

The audit captures the violation. A high rate of scope violations is a signal — either the agent's prompt is leading to wrong-resource calls, or there is an active probing attempt.


When the scope is wrong

Sometimes the agent legitimately needs to access a resource outside the default scope. Example: a support agent escalating to a different customer's complaint where the two are related. The right pattern is:

  • A different purpose with a different scope template (e.g., support:read_related_customer with an explicit relationship check)
  • Approval gating for higher-risk purposes (requires_approval: true in the purpose registry)
  • Explicit context update — the active context shifts to the new resource via a deliberate operation, not by argument

Avoid the temptation to make a purpose "flexible." A flexible purpose has no scope; the discipline collapses.


Cross-tenant scope: never

Cross-tenant access through an agent is the single largest incident class. The discipline:

  • Every access is tenant-scoped. There is no "all tenants" mode for agent operations.
  • A cross-tenant operation (e.g., a platform admin tool) is a separate purpose with explicit cross-tenant scope, used by platform-only credentials, audited at higher fidelity.
  • The agent's credential is never granted cross-tenant scope; the agent acts within one tenant per session.

The model-gateway layer (02_ai_infrastructure/01 chapter 10) enforces region; this layer enforces tenant; both work together.


How scope interacts with the other surfaces

  • Classification (chapter 02) — scope and tier compose; the data layer applies both.
  • Purpose (chapter 03) — purpose drives the scope template.
  • PII (chapter 05) — fields outside the scope's allowed tier are stripped or refused.
  • Audit (chapter 07) — the full scope is recorded per call.
  • Leak detection (chapter 08) — scope violations are first-class signals.
  • Cross-tenant/region (chapter 10) — tenant scope is the core defence against cross-tenant leaks.

How to recognise missing scope discipline in the wild

  • Queries against the data layer do not include tenant or user filters at the storage level
  • Agent tools receive user_id as an argument and trust it
  • The audit shows accesses without scope fields
  • Cross-tenant access is "documented as forbidden" but not enforced
  • Scope violations are not a metric anyone watches
  • A scope rule lives in the application's code, not in the data layer

Interview Q&A

Q1. The agent passes customer_id="c_99" to a read tool, but the active user is u_42. What should happen? The access mediator validates the argument against the scope derived from the active context. The scope says customer_id=u_42; the argument says c_99; mismatch. The call is refused with SCOPE_VIOLATION. The error tells the model "do not retry; ask the user." The data layer would also refuse if the call somehow bypassed the mediator — defence in depth. The user's experience is "I cannot find that order"; the platform's experience is a structured refusal recorded in audit. Wrong-answer notes: "trust the argument; the agent must be right" is exactly the chapter-opening incident.

Q2. Where should tenant filtering be enforced — application or storage layer? Both. The application layer (the mediator) applies the filter as part of every query; the storage layer enforces it independently (via row-level security, or a query builder that cannot omit the filter). Defence in depth: the application's filter is the first line; the storage's enforcement is the floor below which the system cannot fall. If only one is in place, a code-level bypass leaks data; if both, the bypass still produces no rows. Wrong-answer notes: "the application is sufficient" misses the bypass risk.

Q3. Walk through how a "support agent escalating to a related customer" operation should work. A separate purpose with a different scope template — support:read_related_customer — that explicitly checks the relationship. The scope might be: customer_id IN (SELECT related_id FROM customer_relationships WHERE primary_id = active_user_id). The data layer enforces the subquery; the agent's call is checked against it. For higher-risk relations, the purpose may require approval (the registry's requires_approval: true). A flat "expand the scope" is not the answer; a named purpose with a defined relationship rule is. Wrong-answer notes: "let the agent read any related record" loses the per-call discipline.

Q4. The data store does not support row-level security at the database level. How do you still get storage-level enforcement? A query-builder layer that all access flows through, designed so the tenant/user scope cannot be omitted. The builder accepts a scoped context object; queries are built with the scope clauses baked in; there is no API path that bypasses. Code review confirms no direct database access outside the builder. Where the storage system later supports row-level security, you migrate to native enforcement; until then, the builder is the storage-layer-equivalent boundary, kept in a small library with security review on every change. Wrong-answer notes: "we'll be careful in the application code" is the application-level-only failure mode.


What to do differently after reading this

  • For every purpose, define the scope template explicitly. The active context drives scope; the agent's arguments are claims.
  • Enforce the scope at the data-storage layer, not just the application.
  • Refuse calls where arguments contradict the scope. The refusal is the audit signal.
  • Treat cross-tenant access as a separate, restricted purpose; never enable it on agent credentials.
  • Monitor scope-violation rate; it is a leading indicator of probing or prompt issues.

Bridge. Scope bounds which records are touched. The next discipline is what content enters logs, prompts, and responses once those records are touched. Personal data needs detection, redaction, hashing, and minimisation across every storage surface. The next chapter is the PII discipline. → 05-pii-detection-and-redaction.md