Skip to content
Hub documentation

Policy distribution

An owner manages the organisation’s policy under Record and policy → Policy in the sidebar, or in the same group under Menu on smaller screens. Members do not see it. The edge enforces policy locally and keeps its applied policy if the hub cannot be reached; publishing does not establish that an edge has fetched or applied the revision.

The page opens on the Published policy. Revision, publication time and publisher appear beside its status. Directory groups show Engagement, Mode and Reporting separately; expand Directory details for rules and inheritance. Full published policy retains access to all fields. Reporting permitted by policy still needs the applicable consent and directory approval.

A new organisation starts on revision 1, { "policy_mode": "observe" }, which the hub publishes when it creates the organisation. Its publisher reads Common Measure Hub, at organisation creation, and the page shows Guided setup beside it as well as Edit policy.

Edit policy opens a Draft based on that revision. An organisation with no published revision shows No published policy: Create policy opens guided setup and Start from JSON opens a general-purpose draft in the editor. The form edits default and directory modes and adds hosts to refuse. In Observe and Prefer, a rule breach is recorded and the source is admitted; Strict refuses rule breaches. Private-network restrictions and reporting clearance still apply when no source rules are configured.

Full policy JSON edits all fields, including ordered rules, directory matches, engagement, reporting permissions, terms and other settings. Use JSON draft validates the JSON before returning to the simple controls. Scope rules replace the default list; absent scope rules inherit it. Source rules and directory scopes retain their order, with the first match winning. Values outside the browser’s exact integer range cannot be edited here.

Create a replacement draft → Guided setup offers the general-purpose agent, named-source observation and strict named-source starting points. It asks for sources and licences as appropriate. Directory rules are optional and start empty; Add a path rule (advanced) holds both path-rule lists. The hosts reported by edges in the last 30 days are suggestions. Use this policy puts the result in the JSON input for validation and review; it does not publish. Carry forward existing confidential scopes and other requirements when replacing a policy.

Review changes validates with the Hub and shows the changed values before and after. Back to edit retains the draft. Cancel discards it without publishing. Validation failures retain input. If another owner publishes a revision meanwhile, copy your draft JSON before reloading the latest policy and incorporating your changes.

Test a source takes a directory, source URL and declared licence. Review changes or use JSON draft to run the probes against that draft. Results are labelled Last check; they do not update until the next validation. A probe checks the rules without fetching content.

Publish appears after review when the draft differs from its starting snapshot. The Hub also checks for an unchanged normalised policy before publishing. Publishing makes the revision available on each edge’s next fetch; it does not establish application by a running session.

Expand Edge fetch status for each edge’s member, last fetch and revision served. A fetch does not prove application: run commonmeasure policy on that machine to confirm. Fetch the latest policy on an edge gives the sync command. Earlier revisions contains publication history, and Policy digest contains the current revision’s digest.

Start with this policy in Full policy JSON, or choose General-purpose agent in Guided setup:

{
"policy_mode": "observe",
"constraints": [],
"scopes": []
}

This template has no source allowlist, required licences or assumed agreements. Supported tools and hosts record public source activity; private-network access remains off. Members choose reporting per project on their own machines, and an owner approves each request under Project reporting (member setup). An approved enrolled project can report with no directory scopes in the policy.

Use Add a path rule (advanced) only for a firm-wide convention that must hold on every machine, such as keeping /clients/ confidential. Confidential directories comes first; Directories permitted to report adds reporting scopes with an engagement. Full policy JSON exposes the same scope fields: match, engagement and allow_telemetry_egress. Path matches use text contained in the working directory’s path. Keep confidential scopes above broader reporting scopes. A matching scope that omits reporting permission or sets it to false vetoes reporting even with project approval. Reporting scopes can clear existing evidence, subject to applicable consent and project approval.

Replacing a legal policy with this template removes its existing rules, scopes and agreements. Carry forward any confidential scopes or other requirements you still need, and inspect Review changes before publishing.

Narrow the policy in Edit policy as you learn which sources agents use. While the mode is Observe, source-rule breaches are recorded and the sources are admitted. Switch to Strict when those rules should refuse a source. Changing only the mode to Strict adds no restrictions to an empty rule set. Private-network restrictions and reporting clearance apply in either mode.

After publishing, run commonmeasure policy sync, then commonmeasure relay --dry-run to preview outgoing counts and hosts. Once a machine has enrolled a project for reporting, a policy scope alone no longer clears its work: each project directory also needs an owner’s reporting approval (Enrol a project). Run commonmeasure relay to deliver the cleared evidence.

The hub holds the policy your organisation wants its edges to enforce and serves it as a signed envelope. A Common Measure edge in managed mode fetches that envelope and applies it through its own loader: it takes the policy only under a mode set in a local file on the edge, from the signing key pinned in that file, and keeps the policy already in force whenever a check fails or the hub cannot be reached. The hub is never in the path of a crossing.

The edge authenticates to the endpoint by signing the request with the key it registered at enrolment (an HTTP message signature, RFC 9421, Ed25519) — the same signature it presents to publishers. There is no credential to issue: the key id identifies the edge and resolves its organisation, and revoking the key ends its access at the moment the key leaves the public directory. The edge decides whether a source may cross into an agent.

GET /api/v1/policy/signer (signed in, an identity:read key, or an enrolled edge’s ingest key) answers with what the edge’s deployment.json needs:

{
"key_id": "hub-policy-3f9c2a1b7d0e",
"algorithm": "ed25519",
"public_key": "<hex, 32 bytes>",
"organisation": "11111111-1111-1111-1111-111111111111",
"policy_path": "/api/v1/policy/desired",
"policy_url": "https://<your hub>/api/v1/policy/desired"
}

policy_url is the endpoint on the hub’s public address, which is the address an edge’s signature must name, whatever address the edge reached the hub by. The enrolment exchange answers the same object as policy_signer, which commonmeasure connect --managed pins.

The hub has one policy-signing key; every organisation’s envelopes are signed by it, and an edge accepts envelopes from that key and from no other. organisation is your organisation’s identifier as the envelope names it. The key is derived from the hub’s deploy secret: rotating that secret changes the key id, and every managed edge must be re-pinned before it accepts a new envelope.

An owner of your organisation publishes the desired policy with POST /api/v1/policy/revisions, the policy file under policy, written the way you would write the edge’s policy.json by hand. Fields with a default can be left out. For a firm that refuses every source except named ones, requires its licence for one host, and clears one matter for telemetry egress:

{
"policy": {
"policy_mode": "strict",
"constraints": [
{ "kind": "access_rule", "host": "statutes.example", "action": "allow" },
{ "kind": "access_rule", "host": "guidance.example", "action": "allow" },
{
"kind": "access_rule",
"host": "www.lawlibrary.example",
"action": "require_licence",
"licence": "example-firm/lawlibrary-subscription"
},
{ "kind": "access_rule", "host": "*.news.example", "action": "allow" },
{ "kind": "access_rule", "host": "*", "action": "refuse" }
],
"scopes": [
{
"match": "/matters/confidential-",
"engagement": "client-confidential",
"constraints": [{ "kind": "access_rule", "host": "*", "action": "refuse" }]
},
{
"match": "/matters/MR-2026-014",
"engagement": "MR-2026-014",
"allow_telemetry_egress": true
}
]
}
}

Interactive clients send expected_revision alongside policy: the revision the draft was based on, or 0 before the first publication. The Hub compares it under the organisation publication lock and returns 409 if it has changed. Omitting it retains the API’s unconditional next-revision publish. The Policy page always supplies it. An X-Expected-Organization-Id header also prevents a session’s organisation switch from retargeting a draft.

The answer is 201 with the revision number and the policy digest:

{ "revision": 1, "digest": "sha256:…" }

The hub reads the policy the way the edge’s policy loader reads it and stores it in the loader’s own form: every defaulted field present (allow_private_hosts false and record_internal_prefixes empty above, and allow_telemetry_egress false on the first scope), each access rule’s host in lower case with any trailing dot removed and a Unicode name in its ASCII form, and each currency in upper case. The digest is over that form, in the canonical JSON of RFC 8785, so formatting, member order and the defaults you left out make no difference. GET /api/v1/policy/revisions/{revision} shows the stored form.

An edge digests the policy exactly as the envelope carries it, before parsing it. Because the hub stores the loader’s form, that digest also equals the digest of the edge’s own serialisation of the policy. A revision’s digest is fixed when it is published: an edge compares it when the same revision is offered again, and reports it in fleet status.

The policy identity an edge reports (contextops-policy-identity) is a separate digest, computed on the edge from the policy in force for a working directory. The hub stores and shows no policy identity.

A policy whose structure the edge’s loader would refuse is refused with 400 and the loader’s reason: a missing field, an unknown mode or constraint kind, a host pattern that names no host, or an unknown or misspelled field in the policy, a scope, a principal, an allowance, an amount, a terms entry or an access_context identifier. Inside a constraint the loader does not refuse an unknown member: it drops it, and so does the hub, so a misspelled constraint member is not an error and is not kept. The checks the loader makes once a policy has parsed are made here too, with the loader’s own sentence: a scope that clears egress without naming an engagement, an empty or repeated scope match, a recordable prefix that is not an absolute URL ending in /, a require_licence rule naming no licence, a terms entry naming no host or no reference, and the rest the Common Measure product repository’s source policy contract (docs/contracts/source-policy.md) lists. The hub holds itself to that contract’s validation vectors, so a policy it publishes is one every edge loads.

A policy with any entry in principals is refused with 400, although an edge’s loader accepts one. A principal binding is keyed by an operating-system user id, which each machine assigns: the same binding sent to every edge of an organisation would apply to whoever holds that id on each machine and record that person under the binding’s name. Principals belong in a policy file written on the machine it governs.

Revisions only increase, and a revision is never edited: publishing again writes the next one. An edge refuses a revision earlier than the one it applied, so a captured earlier envelope cannot move it backwards.

GET /api/v1/policy/revisions lists revisions with their digests, publication times and publishers: published_by is the publisher’s email address, or null when there is none to name, and published_at_creation is true for the revision the hub published when it created the organisation. GET /api/v1/policy/revisions/{revision} returns one with its policy. Every publication is recorded in the audit log as policy.publish; the revision published at creation is recorded with the hub as its actor and published_at_creation in its detail.

POST /api/v1/policy/check takes the same policy, and optionally probes, a list of {directory, url, licence}, and publishes nothing. It answers 200 with valid: false and the refusal publishing would give, or with valid: true, the policy in the loader’s form, its digest, the revision it would become, the current revision, whether the draft is unchanged from it, the changes against it, and each probe explained as {scope, mode, ruling, reason}, where ruling is allowed, refused or allowed_with_breach and is absent for a probe with no URL. Owners only, as publishing is.

GET /api/v1/policy/desired answers with the current revision as a signed envelope. Set the edge’s policy_url to this path on the hub’s address, https://<your hub>/api/v1/policy/desired. The request must carry a Web Bot Auth signature from an edge enrolled in your organisation: the Signature, Signature-Input and Signature-Agent headers the edge sends, covering the hub’s authority. An unsigned request, an API key and a signed-in session are all refused, as is a signature from a revoked key or one made for another origin. Each 401 carries a code beside detail: key_unknown and key_revoked refuse the key itself; signature_invalid, signature_expired, authority_mismatch and components_missing refuse one request, and a later request may succeed. An organisation has revision 1 from its creation; one with no published revision answers 404, which an edge records as the hub being unreachable, and keeps its applied policy.

An edge that is not enrolled makes no request at all, and reports that it has no key to authenticate with rather than reporting this hub as unreachable.

The envelope is signed afresh at each fetch, never cached, with a seven-day validity window from that fetch. The revision and digest, which the edge compares against what it applied, are stable; the times are not. It is bound to the key id that fetched it, so an envelope captured from one edge is refused by another. The policy inside is the organisation’s: every enrolled edge of the organisation is served the same revision.

Publishing needs a signed-in owner. Reading the signer needs a signed-in member, an identity:read key or an enrolled edge’s ingest key; reading the revisions needs a signed-in member or an identity:read key. Fetching the envelope needs an enrolled edge’s signature and nothing else; the signature an edge sends here opens this one endpoint and no other, so an edge can never read the fleet, the revisions or another organisation’s policy.