Skip to content
WIP

This guide is a work in progress. Barry is also a work in progress.

Policy resolution

This page explains how Barry turns an identity, the traits a session selects, and every bound in play into one effective policy: the list of tools the session sees and the bound that guards each call. Knowing the order and the merge rules lets you predict a session’s tools before you start it, and explains why selecting one read-only trait can make a whole session read-only.

Barry computes a session’s policy in a single pass, in the identities bag’s sessionPolicy. The pass produces two things:

  • The trait set, which decides which tools the session may see, and at what access.
  • The effective bound, which takes tools, write access, paths, commands, and network destinations back from that set.

The MCP tool filter, the call-time guards, the native tool ceiling, and the egress sandbox all read the same result, so they can’t disagree about what a session may do.

Barry gathers traits from three sources and keeps each name once:

  1. Base defaults. The base identity’s traits: artifacts, bash, filesystem, instructions, and sessions. An identity can turn one off by listing it as optional.
  2. Identity traits. The traits in the identity’s traits: list.
  3. Session traits. The traits you select when the session starts, such as with --traits.

A trait you select explicitly must exist, or the session refuses to start. A base default this installation can’t resolve grants nothing: Barry drops it and records a warning rather than failing the session.

Barry then expands the set. A trait’s includes: pulls in other traits, and Barry rejects a cycle. A trait with per-harness policies expands to the branch for the harness the session runs, and fails when it has no branch for that harness.

Packing a bag doesn’t put its trait in the set. The base identity sets bag_grant: session, the default, which means a packed bag’s tools reach a session only when a trait selects them. An identity that sets bag_grant: identity adds the automatic trait of each of its packed root bags to every session.

A root bag that provides tool namespaces or skills gets two automatic traits:

  • <bag> grants every namespace and skill the bag provides at readwrite, including those of the bags it lists under bags:.
  • <bag>:read includes <bag> and carries the readonly bound. Because a bound applies to the whole session, selecting <bag>:read makes the entire session read-only, not only that bag’s tools.

Some bags get no automatic trait: a bag with only services or only actions, and a bag that extends another with extends:. Those bags reach a session through traits that someone wrote by hand. A trait in bag.yaml with the bag’s own name, or with the name read, replaces the matching automatic trait. ADR 0045 records this rule.

Barry resolves the expanded traits into grants, keyed both by namespace and by qualified tool id. Traits only add, so the grants union. When two traits grant the same namespace at different access levels, the more permissive one wins: readwrite beats read. An explicit grant for one tool takes precedence over its namespace’s grant.

Barry decides every tool by its qualified id, namespace:tool, never by its bare name. Two bags can both export a tool named status, and a grant for one never admits the other. ADR 0037 records the reason. Prefer qualified ids in a trait’s tools: list; a bare name still works, matches that name in every namespace, and logs a deprecation warning.

Barry builds the effective bound by merging, in this order:

  1. Every bound a selected trait carries (inline bounds and named ones).
  2. The base identity’s required bounds. The bounds base ships, such as readonly and no-network, are optional, so they apply only when something selects them.
  3. The identity’s named bounds, from its bounds: list.
  4. The named bounds the session selects, such as with --bound.
  5. The identity’s inline bound:.
  6. The session’s inline bound.

The order doesn’t change the result, because every rule moves toward more restriction:

  • Denials union. A tool, access level, path, command, or network action that any source denies stays denied.
  • Allowlists intersect. files.allowOnly and network.allowDomains keep only the entries every source permits.
  • The strictest enforcement wins. network.enforce: sandbox beats guard.

A named bound that doesn’t exist stops the session from starting. A network.allowDomains list in the merged bound requires network.enforce: sandbox, because only the egress sandbox can enforce an allowlist.

Barry keeps a tool only when every check passes:

  1. A trait grants its namespace or its qualified id.
  2. The bound doesn’t deny its namespace or name in deniedTools. A bound with file rules also withholds the bash tool.
  3. A write tool needs a readwrite grant, and the bound mustn’t deny write access to it.
  4. The bound’s network actions don’t imply denying it.

What survives becomes the session’s tool list. The MCP server lists only those tools, so the model never sees the rest.

A request that reaches the MCP server without a session resolves under an anonymous scope. It gets only the default-trait grant, filtered the same way. When resolution fails for any reason, such as a session that names an identity that doesn’t exist, the MCP server serves no tools rather than every tool. ADR 0018 records both rules.

Suppose an ops identity packs two bags, github and slack, each with one read tool and one write tool:

Tool Access
github:list_issues read
github:create_issue write
slack:read_channel read
slack:post_message write

The identity grants slack to every session and requires one bound:

name: ops
bags: [github, slack]
traits: [slack]
bounds: [no-network-write]

You start a session as ops and select one more trait:

Terminal
<identity> session start --traits github:read

Barry resolves it in four steps:

  1. Traits. The base defaults, slack from the identity, and github:read from the session. github:read expands to include github.
  2. Grants. github and slack both grant their namespaces at readwrite.
  3. Bound. github:read carries readonly, which denies the Bash, Write, and Edit tools, denies write access, and denies network writes. The identity’s no-network-write also denies network writes. The union equals readonly.
  4. Filter. Write access is now denied session-wide, so both write tools drop out.

The session sees two of the four tools:

Tool Result Why
github:list_issues Listed Granted through github:read
github:create_issue Withheld readonly denies write access
slack:read_channel Listed Granted through slack
slack:post_message Withheld readonly applies to the whole session

The default traits pass through the same filter, so the session keeps the filesystem’s read tools and loses its write tools and bash. To post to Slack while reading GitHub, select github instead of github:read and add a bound of your own that denies github:create_issue.

Primitives defines each term this page uses, and Bounds lists every bound field. Session modes and guarantees explains how much of this policy each harness lets Barry enforce.