Source policy contract
This contract is licensed under CC-BY-4.0 (docs/contracts/LICENSE).
The source policy is the operator’s policy file: which sources an agent may
take in, under what licence and at what cost, and which work may be reported
beyond the machine. This contract states what the file may contain, what
each field does, the order in which admission applies it, and every check
the loader makes before a policy is used. Terms such as scope, engagement,
principal and policy mode are defined in
docs/GLOSSARY.md.
There is one policy engine. The runtime’s loader and admission check are the implementation, and this contract describes them; it is not a second implementation. Two artefacts published beside it let anything that writes or checks a policy outside this repository be held to the same answers:
docs/contracts/source-policy.schema.json, the JSON Schema of the file’s structure (§The schema);docs/contracts/source-policy-vectors.json, accepted and refused documents and the rulings a policy produces (§The vectors).
commonmeasure policy check <file> loads a candidate through the loader and
prints what it accepted or the refusal, so the question of whether a policy
is valid can always be settled by the engine itself.
The file
Section titled “The file”$COMMONMEASURE_HOME/policy.json, or ~/.commonmeasure/policy.json. A JSON
object. An absent file is not an error: the mode is observe, every
crossing is recorded and nothing is refused. A file that is present and
cannot be loaded is an error, reported where the policy would be used, and
never replaced by a permissive default. A policy containing an unknown field
refuses every mediated crossing until the policy is corrected.
On a managed edge the file is the desired policy the hub distributed, and
the edge replaces it only through
docs/contracts/policy-envelope.md.
Fields
Section titled “Fields”Top level:
| Field | Type | Default | What it does |
|---|---|---|---|
policy_mode |
observe, prefer or strict |
observe |
What happens to a breach (§Admission). |
constraints |
array of constraints | [] |
The rules admission applies (§Constraints). |
scopes |
array of scopes | [] |
Overlays selected by working directory (§Scopes and principals). |
principals |
array of principal bindings | [] |
Overlays selected by authenticated identity (§Scopes and principals). |
allow_private_hosts |
boolean | false |
Whether the mediated tools may reach private addresses (§Recording). |
record_internal_prefixes |
array of URL prefixes | [] |
Internal prefixes whose crossings may be recorded (§Recording). |
terms |
array of terms | [] |
Operator references for the basis of use, by host (§Terms). |
A scope:
| Field | Type | Default | What it does |
|---|---|---|---|
match |
string, required | Matched as a substring of the session’s working directory. | |
principal |
string | none | The principal this scope belongs to. |
engagement |
string | none | The governing engagement: the name under whose clearance a crossing in this scope may leave the machine. |
allow_telemetry_egress |
boolean | false |
Whether witnessed public crossings in this scope may enter a configured telemetry projection. |
policy_mode |
mode | inherited | Replaces the mode. |
constraints |
array of constraints | inherited | Replaces the constraint list; it is not merged with it. |
A principal binding names exactly one of os_user, subject and
edge_token; the loader refuses a binding naming more or none:
| Field | Type | Default | What it does |
|---|---|---|---|
principal |
string, required | The name the binding gives the identity. | |
os_user |
integer | The effective numeric operating-system user id the binding applies to. | |
subject |
string | The sub claim of an access token the hosted edge verified: the hub’s opaque user id (host integration). |
|
edge_token |
string | The label of a bearer token the edge issued for a host with no OAuth. | |
require_scope |
boolean | false |
Whether this principal may work only in a scope bound to it. |
allowances |
array | [] |
Periodic spending limits, at most one per period: {"period": "day" or "month", "amount": {"currency", "micros"}, "timezone": "<IANA zone>"}. |
policy_mode |
mode | inherited | Replaces the mode. |
constraints |
array of constraints | inherited | Replaces the constraint list. |
allow_private_hosts |
boolean | inherited | Replaces the setting. |
An optional field of a scope or a principal binding given as null is the
field left out. An unknown field at the top level, in a scope, in a
principal binding, in terms, in an allowance or in an amount refuses the
whole file. An unknown member inside a constraint is dropped, because a
constraint’s kind decides its members and every member a kind needs is
required.
Constraints
Section titled “Constraints”{"kind": "denied_source_host", "host": "tracker.example"}{"kind": "allowed_source_host", "host": "www.gov.uk"}{"kind": "allowed_source_provider", "provider": "ozone"}{"kind": "required_licence", "licence": "cc-by-4.0"}{"kind": "maximum_acquisition_cost", "amount": {"currency": "GBP", "micros": 2500000}}{"kind": "maximum_context_tokens", "tokens": 8000}{"kind": "maximum_latency_ms", "milliseconds": 30000}{"kind": "allowed_provider", "provider": "exa"}{"kind": "denied_provider", "provider": "exa"}{"kind": "access_rule", "host": "*.example.com", "action": "refuse"}allowed_source_provider permits sources delivered by the named adapter past
the allowed-host list. The dispatch path supplies that identity; a URL or
supplier-native metadata cannot claim it. Direct fetches and observed sources
do not inherit this permission. Explicit host denials, ordered access rules,
provider eligibility and required licences still apply. This rule supplies no
licence or cost evidence.
Every kind but one is a set, and the order of its entries means nothing.
access_rule is the exception. Access rules are read in the order written
and the first whose host matches the source’s host decides; a later rule
for the same host is never reached.
{ "constraints": [ {"kind": "access_rule", "host": "docs.example.com", "action": "allow"}, {"kind": "access_rule", "host": "*.example.com", "action": "refuse"}, {"kind": "access_rule", "host": "publisher.example", "action": "require_licence", "licence": "rsl:publisher/2026"}, {"kind": "access_rule", "host": "*", "action": "refuse"} ]}A host pattern is one of three forms. An exact host matches that host
alone. *.example.com matches example.com and every host beneath it.
* matches every source that has a host. A source with no host, such as a
document from the operator’s own corpus, matches no pattern. A pattern is
compared in lower case, IDNA-encoded and without a trailing dot, whatever
case it was written in. A wildcard anywhere but the front refuses the file.
The actions:
allowpasses the host, including past anallowed_source_hostlist that does not name it. A required licence still applies.refusemakes the source a breach.require_licenceadmits the source only when the supplier declared exactly the named licence; an undeclared licence is unknown, never permitted.require_mediationpasses every crossing admission sees, because every such crossing is one Common Measure ruled on before it happened. An observed crossing is recorded without being checked against it. It states an intent in the file; a host that must not be read at all takesrefuse.
A refusal names the rule that decided it as access rule N (pattern), where
N is the rule’s position in its constraint list, counted from one and
counting every constraint in that list, not only access rules.
Scopes and principals
Section titled “Scopes and principals”A session’s effective policy is resolved once, from the policy, the session’s authenticated identity and the working directory:
- Principal. Where
principalsis not empty, the binding whose key is the session’s authenticated identity overlays the top level: itspolicy_mode,constraintsandallow_private_hostsreplace the top level’s where present. A key matches a binding of its own kind only. The stdio server’s identity is the process’s effective user id, matched againstos_user; it is read from the process, not fromUSER,LOGNAMEor any other variable the process could set. The hosted edge’s identity is the subject of the access token it verified, matched againstsubject, or the label of the edge-issued token presented, matched againstedge_token; the service process’s own user is never its principal. An identity with no binding holds no authority: every mediated crossing is refused and the refusal names the reason, whatever the mode. Declaring one principal therefore changes what happens to every other identity reaching the edge, service and CI accounts included. Windows has no user id the runtime reads, so a policy declaringprincipalsrefuses every mediated crossing from a stdio server there. - Scope. The first scope whose
matchappears anywhere in the working directory is selected; no match leaves the top level. A match is a substring, not a path prefix:/mattersalso selects/home/op/matters-archive, so a narrower scope is listed before a broader one. - Ownership. A selected scope that names a principal other than the
session’s is not passed over for a later one: the session is refused
there. A principal with
require_scope: truewhose session selects no scope bound to it is refused. - Overlay. The selected scope’s
policy_modeandconstraintsreplace the values resolved so far where present. Itsengagementandallow_telemetry_egressapply to that scope alone and are never inherited.
Admission
Section titled “Admission”For each source a crossing would take in, admission applies, in this order:
- a refusal from resolution (an unbound user, a scope owned by another principal), whatever the mode;
- the
denied_source_hostset, which no rule allows past; - the access rules, first match deciding;
- the
allowed_source_hostset, where one is declared and no access rule allowed the host and noallowed_source_providerpermission applies; - the
required_licenceset, where one is declared.
Each step either passes the source to the next or produces a breach. The
mode decides what a breach does: strict refuses the crossing before any
bytes reach the model; observe and prefer admit it with the breach on
the record. Nothing is ever left unrecorded because of the mode.
The source’s own terms (an RSL licence, a declared AI-use preference, a
reporting demand) refuse a crossing in every mode, with one exception: a
fetch of an origin’s own robots.txt, at exactly /robots.txt with no
query, is not ruled on them, because the file states them; admission above,
the file’s own access rule, the origin’s answer and the record apply to it as
to any page, and a redirect from it to another URL is ruled on that URL’s
terms.
Recording
Section titled “Recording”-
A private address is a
file://URL, a URL with no host,localhostor a name ending.localhost,.localor.internal, or an IP address the IANA IPv4 and IPv6 Special-Purpose Address Registries mark not globally reachable. An IPv6 address that embeds an IPv4 address (IPv4-mapped, IPv4-compatible, NAT6464:ff9b::/96, 6to42002::/16) is judged as that IPv4 address. Multicast and deprecated IPv6 site-local addresses are private as well. A name is judged as written (10.example.comis public), and the mediated tools judge the addresses it resolves to by the same rule before connecting. The one classifier iscrates/commonmeasure-types/src/address.rs, whose doc comment lists where it departs from the registry and why: an embedded IPv4 address is judged as that address, stricter or looser as it is, and every other departure is stricter. -
Two ranges hold names on purpose, and a fetch by name is refused there:
100.64.0.0/10, shared address space, where Tailscale’s MagicDNS puts tailnet names. To reach one tailnet host, name its prefix inrecord_internal_prefixes, which also keeps its crossings from being projected.198.18.0.0/15, which a fake-IP proxy (Clash, Surge, sing-box and similar) answers every name with. Common Measure cannot check where a name leads through such a proxy. Set the proxy to return real addresses to the machine running the edge.commonmeasure doctor --resolve <name>shows the addresses a name resolves to and what the floor makes of them.
allow_private_hostsadmits both, and every other private address with them. A hosted edge in service mode refuses both whatever the policy says. -
allow_private_hostslets the mediated tools reach private addresses. It does not affect observed capture, which never records a private address whatever this says. -
record_internal_prefixesnames internal prefixes, each an absolute URL ending in/without a query or a fragment, whose crossings are recorded, observed and mediated alike, and markedinternalso no projection sends them. Every spelling of a URL under a prefix is under it, apart from the spellings thatsession-evidence.md§Source declarations names. Host case, a trailing dot, a default port, credentials, a percent-encoded unreserved character (/%70rivate/is/private/), lower-case hex and a fragment do not change the answer, on the URL’s side or the prefix’s. The prefix is read as written: its slashes are never merged, its%2Fand%3Bare never decoded and its;is never stripped. The URL is read as requested and as every path that decoding%2Fand%5Cas/and%3Bas;, merging runs of/, resolving dot segments and stripping;path parameters reach from it, in any order and including only some of them, and it is under the prefix when any reading is, so//private/a,/private%2Fa,/x/..%2Fprivate/a,/x//..%2Fprivate/a,/private;x=1/a,/x/..;/private/a,/private%3Bx/aand/private%5Caare under/private/. Readings are capped at 64, and a crafted URL can reach the cap; a URL past it is under every prefix on its origin. The form and the readings are the onesrobots.txtand licence scopes are matched in. Observed capture records no internal or private address the list does not match; the mediated tools reach and record one only underallow_private_hosts. -
The personal-data detector’s finding is recorded on every mediated crossing.
strictrefuses one on an internal or private source; on a public source it admits the crossing with the finding recorded, because a public page’s published contact details are not the personal data the detector exists to keep out of a model. The detector does not read a PDF the edge hands over as a file, sostrictrefuses every PDF (a fetched file). -
A witnessed public crossing leaves the machine only where its scope names an
engagementand setsallow_telemetry_egress: true; an absent policy, an unmatched directory or a scope without both keeps it local. What leaves isdocs/contracts/telemetry-projection.md. The scope’sengagementis the governing engagement; the name the console reports work under is resolved separately, from the attribution rules.
{ "terms": [{ "host": "publisher.example", "reference": "agreement-42", "requires_reporting": true, "access_context": [{"scheme": "ror", "value": "https://ror.org/013meh722"}] }]}A terms entry names a source host and the operator’s reference for an agreement with it. The host is compared as a host pattern’s is, and matched exactly: a subdomain needs its own entry. One entry per host is accepted.
The reference is recorded on every mediated crossing of the host under
declarations.terms (session evidence):
it does not override the source’s published preference, it
supplies no declared licence, and a supplier API subscription or access
reference alone supplies no reuse permission. requires_reporting says the
agreement requires usage reporting, and that duty binds: each crossing of
the host carries a telemetry reporting demand ruled as a licence’s is, in
every policy mode, met only through the hub the edge is enrolled with, with
the operator’s reporting consent and subject to the private-address,
delivery, session-log and access_context checks a licence’s demand passes,
and refused before the request otherwise (session evidence §Source
declarations). access_context
names the institution identifiers the agreement attributes usage to, never a
person; the relay withholds each crossing of a host under terms naming
identifiers until session-document delivery is built, so an entry that sets
both requires_reporting and access_context refuses every crossing of its
host until then. Which agreement permits what, and how an edge proves it,
is settled by the network design (roadmap NET-14 and NET-15), which these
fields wait on.
A terms entry may also name the Anchor the source accepts anonymous passes from:
{ "terms": [{ "host": "news.example", "reference": "agreement-43", "anchor": {"origin": "https://anchor.example", "credential_env": "NEWS_ANCHOR_CREDENTIAL"} }]}This is the anonymous access profile, under the product decision “Anonymous access profile”. Where a source asks for an anonymous pass, the edge may present one instead of its named Web Bot Auth signature (bot identity). The pass comes from an Anchor, in the sense of the Anonymous Bot Authentication draft, that the source accepts. It says only that the Anchor found the agent meets a policy the Anchor publishes, such as public interest news requirements (integrity, citation, provenance, reporting); it identifies neither the operator, the reader nor the session. The requirements are kept outside Common Measure, and Common Measure may be one Anchor among several, never the only one. The mechanism is an RFC 9578 publicly verifiable token until the draft settles. The profile applies only to a host whose entry names an anchor.
origin is the Anchor’s origin, https://host[:port] with nothing after
it. The edge reads the Anchor’s issuer directory there
(/.well-known/private-token-issuer-directory) and sends token requests
only to a request URI on the same origin. Cleartext http is admitted only
at a loopback IP address and only where allow_private_hosts is set, which
is how a stand-in Anchor on the operator’s own machine is reached.
credential_env names the environment variable that holds the credential
the edge registered under with the Anchor. The policy names the variable
and never holds the value, because a policy is recorded and printed whole;
the value is read when passes are refilled and sent to the Anchor alone.
commonmeasure passes refill <host> obtains passes for the host ahead of
any fetch and keeps them in the Anchor’s wallet, <home>/passes/<anchor>.json,
owner-only, whose name is the origin without https:// and with every
character other than a letter, a digit, . or - written as _. Requests
to the Anchor carry no Web Bot Auth signature and no CommonMeasureBot
user agent, so the Anchor does not learn the edge key; it learns the
credential, by which it rations passes. A ration or other refusal is
recorded as the Anchor stated it, in the passes_refilled event
(session evidence §Events). A fetch of the
host goes under the profile: its requests and probes go unsigned, a
PrivateToken challenge is answered once with a held pass and never with
one obtained at the time, and the crossing is never cleared for relay
egress (bot identity §The anonymous access profile).
What the loader refuses
Section titled “What the loader refuses”A policy is refused in two stages, and the whole file is refused at the first fault.
Structure. The document must parse into the fields above: no unknown
field where §Fields says one refuses, every required member present, every
value of its type, a known mode, a known constraint kind, a known action, a
host pattern that names a host. The refusal is <file> is not a valid policy: followed by the parser’s message, whose wording is not part of
this contract.
Checks. A document that parses is then checked. Each check refuses with
the sentence below, where <file> names the file as the loader was given
it and <list> is the top-level policy, scope "<match>" or
principal "<name>" (terms are declared at the top level only):
| Check | Refused when | Refusal |
|---|---|---|
internal_prefix_not_absolute |
a recordable prefix is not an absolute URL | <file>: record_internal_prefixes entry "<prefix>" is not an absolute URL prefix and would never match anything |
internal_prefix_query_or_fragment |
a recordable prefix has a query or a fragment | <file>: record_internal_prefixes entry "<prefix>" has a query or a fragment; a prefix names a path, so write it without them |
internal_prefix_unterminated |
a recordable prefix does not end in / |
<file>: record_internal_prefixes entry "<prefix>" must end with "/" — without it the prefix also matches hosts and paths it merely starts, which would record more than the operator named |
scope_match_empty |
a scope’s match is empty |
<file> has a scope with an empty "match", which would govern everything |
scope_match_duplicate |
two scopes have one match |
<file> declares two scopes matching "<match>"; only the first would govern or take an edit, so the second must be merged into it or renamed |
principal_name_empty |
a principal’s name is empty | <file> has a principal with an empty name |
principal_key_count |
a binding names no key, or more than one | <file> principal "<name>" names none of os_user, subject and edge_token; a binding names exactly one; <file> principal "<name>" names <first> and <second>; a binding names exactly one of os_user, subject and edge_token; <file> principal "<name>" names os_user, subject and edge_token; a binding names exactly one |
principal_key_empty |
a binding’s subject or edge_token is empty |
<file> principal "<name>" names an empty subject; <file> principal "<name>" names an empty edge_token |
principal_name_duplicate |
two bindings share a name | <file> declares principal "<name>" twice, for OS users <first> and <second>, or for <key> and <key> where the keys are not both user ids |
principal_os_user_duplicate |
two bindings share a key | <file> binds <key> to both "<first>" and "<second>"; one identity cannot hold two principals, where <key> is OS user <id>, subject "<id>" or edge token "<label>" |
principal_scope_missing |
require_scope is set and no scope names the principal |
<file> requires principal "<name>" to work in a directory scope bound to it and declares none, so nothing could ever be admitted for it; bind a scope or drop require_scope |
allowance_timezone_unknown |
an allowance names an unknown zone | <file> principal "<name>" <period> allowance names timezone "<zone>", which is not an IANA zone this build recognises |
allowance_period_duplicate |
one principal has two allowances for one period | <file> principal "<name>" declares two <period> allowances of <first> and <second>; one period holds one amount, and the second would silently never bind |
access_rule_licence_empty |
a require_licence rule names no licence |
<file> <list> constraint <N>: a require_licence access rule must name the licence it requires |
scope_engagement_empty |
a scope’s engagement is empty |
<file> has a scope with an empty "engagement" |
scope_egress_without_engagement |
a scope clears egress and names no engagement | <file> scope "<match>" allows telemetry egress without naming an engagement |
scope_principal_undeclared |
a scope names a principal no binding declares | <file> scope "<match>" names undeclared principal "<name>" |
terms_host_empty |
a terms entry names no host | <file> <list> terms entry <N> names no host |
terms_reference_empty |
a terms entry names no reference | <file> <list> terms entry <N> for host "<host>" names no reference; the reference is what the record and the wire carry |
terms_host_duplicate |
one list has two entries for one host | <file> <list> declares terms for host "<host>" twice; one host holds one agreement |
terms_identifier_incomplete |
an institution identifier lacks a scheme or a value | <file> <list> terms entry <N> for host "<host>" has an access_context identifier without a scheme or a value |
terms_anchor_not_origin |
an anchor is not written as its own origin | <file> <list> terms entry <N> for host "<host>" names anchor "<anchor>", which is not an origin; an anchor is written as scheme://host[:port] with nothing after it |
terms_anchor_not_https |
an anchor is not https, and is not http at a loopback IP address with allow_private_hosts set |
<file> <list> terms entry <N> for host "<host>" names anchor "<anchor>", which is not https; an anchor is reached over https, or over http at a loopback address where allow_private_hosts is set |
terms_anchor_credential_empty |
an anchor names no credential variable | <file> <list> terms entry <N> for host "<host>" names an anchor without credential_env; the anchor issues passes to the credential the edge registered under |
Amounts in allowance_period_duplicate are written as the major unit with
six decimal places (1.000000). A value that is only spaces counts as empty
for the licence, the reference, an identifier’s scheme and value, and an
anchor’s credential_env.
With this saved as refused.json:
{"policy_mode": "strict", "scopes": [{"match": "/matters", "allow_telemetry_egress": true}]}$ commonmeasure policy check refused.jsoncommonmeasure: refused.json scope "/matters" allows telemetry egress without naming an engagementThe command exits non-zero on a refusal and prints nothing else.
The loader’s form
Section titled “The loader’s form”The loader’s form of a policy is the policy as the loader writes it back after parsing:
policy_mode,constraints,scopes,allow_private_hostsandrecord_internal_prefixesare always present;principalsandtermsare present only when not empty;- a scope always carries
allow_telemetry_egress, and carries its other optional fields only when set; a binding always carriesrequire_scope, andallowancesonly when not empty; a terms entry always carriesrequires_reporting,access_contextonly when not empty, andanchoronly when set; - a host pattern is written in its compared form and a currency in upper case;
- an unknown member inside a constraint is gone.
The digest of a policy is the SHA-256 of the canonical JSON of its loader’s
form (docs/contracts/canonical-json.md), which is the
digest a fleet-status document reports for the file in force.
commonmeasure policy check prints it; here policy.json holds the policy
of the vector “a firm’s policy as an owner writes it”:
$ commonmeasure policy check policy.jsonaccepted policy.jsonmode strictconstraints 4scopes 2 /matters/confidential- — engagement client-confidential, telemetry egress not cleared /matters — engagement matters, telemetry egress clearedprincipals 0terms 0digest sha256:7e0249949df8ae01662b97eb21ef7e877945aa8bda144383dbf29367bf1a86edThe schema
Section titled “The schema”docs/contracts/source-policy.schema.json is JSON Schema 2020-12, derived
from the types the loader parses with and printed by
commonmeasure policy schema. A test fails when the committed file is not
what the binary prints.
The schema decides structure and nothing after it. It accepts a host pattern with a misplaced wildcard, which the parser refuses, and it accepts every document refused by a check in §What the loader refuses. A document the schema accepts is therefore not a document the loader accepts; the vectors record the schema’s verdict and the loader’s separately, and the difference between them is this section.
The vectors
Section titled “The vectors”docs/contracts/source-policy-vectors.json holds three lists.
validation: each entry has a name, a policy, schema_valid (the
schema’s verdict) and accepted (the loader’s). An accepted entry carries
loader_form. A refused entry carries check, one of structure or the
check names in §What the loader refuses, and, for every check but
structure, the refusal the loader produces for a file named by the
top-level source_name. Every check has at least one refused entry.
ruling_policies names the policies the rulings use, and rulings holds
one entry per ruling:
| Member | Meaning |
|---|---|
policy |
a key of ruling_policies |
cwd |
the session’s working directory |
url |
the source |
licence |
the licence the supplier declared, or null for none |
scope |
the match of the scope that governs, or null |
mode |
the mode that governs |
ruling |
allowed, refused or allowed_with_breach |
reason |
the runtime’s sentence for a breach, or null |
No ruling policy declares principals, because a principal is resolved from
the user running the check and a vector must give one answer everywhere.
An implementation that writes, checks or explains policies outside this
repository runs the vectors and reproduces schema_valid, accepted,
loader_form, check, refusal, scope, mode and ruling. reason is
the sentence an explanation can quote. The vectors are checked against the
binary and the runtime in crates/commonmeasure-cli/tests/source_policy_contract.rs.
Policy written for many machines
Section titled “Policy written for many machines”A policy is written for one machine or distributed to many from a hub, and two of its fields mean something different on each machine they reach:
os_useris a number each machine assigns. The same id names a different person on each machine, and on a personal workstation it is usually that machine’s only account. A binding distributed to many machines applies its constraints and allowances to whoever holds that id on each of them, and every crossing their sessions record carries the binding’s name. Asubjectnames one hub user wherever it is read, and anedge_tokenone label the edge that holds the token issued; neither is matched by a stdio server, whose identity is always anos_user.matchis compared with each machine’s own directory layout. A distributed scope governs the same work on every machine only where the organisation lays its directories out alike.
A policy meant for many machines is therefore written without os_user
bindings, and its scopes are written against a directory layout the
organisation keeps. A subject binding names the same person on every
hosted edge and is matched by nothing else.
docs/contracts/policy-envelope.md §What a distributed
policy carries states the same for the hub.
Directory selection and managed reporting
Section titled “Directory selection and managed reporting”Directory enrolment does not edit this schema or a managed policy file. A separate local root selection and signed edge-bound reporting approval narrow egress. Every winning false scope vetoes reporting, including an omitted bool; reporting approvals never replace admission rules. Absolute existing directory matchers also recognise canonical targets, and conflicting symlink/worktree scopes fail closed. Resolver version 2 records these semantics. See directory enrolment.