From 7be4714631928418b894491a4b748ddec99f30c9 Mon Sep 17 00:00:00 2001 From: Tomas Tormo Date: Tue, 20 Oct 2026 00:21:48 +0000 Subject: [PATCH 1/8] docs: mesh.catalog lists what a node hosts, discovery is the directory The authorization concept said system://mesh.catalog answers discovery queries, the policy reference called it the built-in discovery service, and the exposing-services guide called it discovery. The catalog is an MCP server on the node that answers list_local_services for a peer whose credential grants it (internal/node/gate.go:79-114, internal/node/mcp.go:402-415). The routers' directory answers who provides a name (internal/node/node.go:2263-2270). A lookup by type asks the directory for the peers and then each peer's catalog for the names (internal/node/node.go:2284-2318). The guide's troubleshooting list also blamed a missing service on the caller's lack of system://mesh.catalog. A lookup by name never opens the catalog (internal/node/node.go:2166-2169). The list now names a record that the router refused because the node's role does not grant publishing it (internal/router/directory.go:216-218, 253-269). --- site/content/docs/concepts/authorization.md | 4 +++- site/content/docs/guides/exposing-services.md | 11 +++++++---- site/content/docs/reference/policy.md | 5 +++-- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/site/content/docs/concepts/authorization.md b/site/content/docs/concepts/authorization.md index 5eb7971a..0c20f2b6 100644 --- a/site/content/docs/concepts/authorization.md +++ b/site/content/docs/concepts/authorization.md @@ -28,7 +28,9 @@ fails a check, such as the target check, or an appended task block. A node publishes services, each with a type and a name: `mcp://calculator`, `inference://vllm-eu`, `a2a://triage`, `egress://bigquery.googleapis.com`, and -the built-in `system://mesh.catalog` that answers discovery queries. +the built-in `system://mesh.catalog`, which lists the services the node hosts +to a peer that asks. The routers' directory, not the catalog, tells a caller +who provides a name. Standing mesh roles grant access to services by name (`allowed_services`), grant the right to serve them (`allowed_publish`) and can narrow HTTP diff --git a/site/content/docs/guides/exposing-services.md b/site/content/docs/guides/exposing-services.md index c225d00c..913433d1 100644 --- a/site/content/docs/guides/exposing-services.md +++ b/site/content/docs/guides/exposing-services.md @@ -187,9 +187,10 @@ curl -fsS -X POST https://mesh.example.com/policies \ --data @policy.json ``` -Discovery (`system://mesh.catalog`) follows the same rule. A role that should -be able to browse what a node offers needs `system://mesh.catalog` in its -grants. On a mesh where everyone may browse, grant it to `mesh:role:node`. +The node's catalog (`system://mesh.catalog`) follows the same rule. A role +that should be able to browse what a node offers needs +`system://mesh.catalog` in its grants. On a mesh where everyone may browse, +grant it to `mesh:role:node`. ## Labels @@ -246,7 +247,9 @@ If the service is missing, check these causes in order: 1. The backend did not answer the probe. Look for "Withholding" in the node log. -2. The caller's role lacks `system://mesh.catalog`. +2. This node's role does not grant `mcp://calculator` in `allowed_publish`, + so the router refused its directory record. Look for "Record denied" in + the router log. 3. The two nodes have not exchanged discovery state yet. Wait a few seconds and try again. diff --git a/site/content/docs/reference/policy.md b/site/content/docs/reference/policy.md index 3565fb52..793610e8 100644 --- a/site/content/docs/reference/policy.md +++ b/site/content/docs/reference/policy.md @@ -94,8 +94,9 @@ A fact whose only meaning is that a grant exists carries the single term `true`, because the Biscuit grammar requires at least one term per predicate. The same holds for `target_unrestricted(true)` below. -`system://mesh.catalog` is the built-in discovery service that every node -runs. A role that should be able to list the tools of a node needs it. +`system://mesh.catalog` is the built-in service on every node that lists the +services the node hosts. A role that should be able to list the tools of a +node needs it. ### Publishing From f637abb47fe8fe6a7974e6f850e2aecab2dce81a Mon Sep 17 00:00:00 2001 From: Tomas Tormo Date: Tue, 20 Oct 2026 00:21:48 +0000 Subject: [PATCH 2/8] docs: served_by also grants publishing an egress destination The authorization concept and the policy reference said that without allowed_publish, holders may publish nothing. An egress destination's served_by selects the nodes that serve it, by role or by label. The control plane adds egress:// to what those nodes may publish when it mints their credential (api/egress.go:239-265, called from internal/controlplane/server.go:2031-2037). The same grant travels in the policy rules as granted_publish_exact("egress", ) <- role(r) or <- label(k, v) (api/egress.go:277-300). --- site/content/docs/concepts/authorization.md | 9 ++++++--- site/content/docs/reference/policy.md | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/site/content/docs/concepts/authorization.md b/site/content/docs/concepts/authorization.md index 0c20f2b6..4fa1dd61 100644 --- a/site/content/docs/concepts/authorization.md +++ b/site/content/docs/concepts/authorization.md @@ -70,9 +70,12 @@ from a file (`--policy-file`). service. - `allowed_publish`: the services holders may serve, in the same syntax. A member announces a service to the directory, and is accepted as its - provider by callers, only when its own credential grants it here. If - absent, holders may publish nothing. Calling and serving are granted - separately: a role that may call `mcp://calculator` does not serve it. + provider by callers, only when its own credential grants serving it. If + absent, holders may publish nothing but an egress destination whose + `served_by` selects them. Such a destination needs no entry here, because + the control plane grants `egress://` to the nodes that `served_by` + selects. Calling and serving are granted separately: a role that may call + `mcp://calculator` does not serve it. - `allowed_targets`: the nodes holders may call, as facts about the destination node's identity: `node:`, `user:`, `email:`, `group:`, `idp_role:`, or `*`. If absent, diff --git a/site/content/docs/reference/policy.md b/site/content/docs/reference/policy.md index 793610e8..b0679901 100644 --- a/site/content/docs/reference/policy.md +++ b/site/content/docs/reference/policy.md @@ -67,7 +67,7 @@ notice. |---|---|---| | `name` | string, required, unique | The role name. `mesh:role:node` and `mesh:role:router` are the roles that the two binaries request at enrollment. Any other name is an ordinary role. | | `allowed_services` | list of service patterns | Services that holders may call. | -| `allowed_publish` | list of service patterns | Services that holders may serve. A router keeps a member's directory record only for a service its credential grants here, and a caller sends a request to a provider it reached by name only when the provider's credential does. If absent, holders may publish nothing. See [Publishing](#publishing). | +| `allowed_publish` | list of service patterns | Services that holders may serve. A router keeps a member's directory record only for a service its credential grants here, and a caller sends a request to a provider it reached by name only when the provider's credential does. If absent, holders may publish nothing but an egress destination whose `served_by` selects them. See [Publishing](#publishing). | | `allowed_targets` | list of target patterns | Nodes that holders may call. `*` means any node. If absent, holders may call no node. | | `allowed_labels` | list of label patterns | Labels that a node holding this role may declare at enrollment. If absent, no labels may be declared. | | `custom_datalog` | list of Datalog statements | Facts are minted into the credentials of holders. Rules are distributed to nodes and applied when a holder is verified. | From 686da6fb433d3386dab0b5b31eec525bee2fde6d Mon Sep 17 00:00:00 2001 From: Tomas Tormo Date: Tue, 20 Oct 2026 00:21:48 +0000 Subject: [PATCH 3/8] docs: the attenuation block holds rules, checks and policies The authorization concept said the node's local rules add extra facts, extra checks and policies. The attenuation block has three lists, policies, checks and rules (api/policy.go:63-67), and the node parses each with the matching Biscuit parser (internal/node/config.go:121-146). A rule needs a body after "<-" (library biscuit-go v2.2.2: ~/go/pkg/mod/github.com/eclipse-biscuit/biscuit-go/v2@v2.2.2/parser/grammar.go:175-178), so a bare fact is refused at start. --- site/content/docs/concepts/authorization.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/content/docs/concepts/authorization.md b/site/content/docs/concepts/authorization.md index 4fa1dd61..06ba8624 100644 --- a/site/content/docs/concepts/authorization.md +++ b/site/content/docs/concepts/authorization.md @@ -161,8 +161,8 @@ The node builds a Biscuit authorizer over Block 0 and adds, in this order: `target_fact("group", "dev-nodes")` and similar. The caller's `allowed_targets` are matched against these facts. 4. **The node's local rules**, from the `attenuation` block of its - configuration file: extra facts, extra checks, and `allow` and `deny` - policies. + configuration file: the `rules`, the `checks`, and the `allow` and + `deny` policies. 5. **The baseline policies**: `allow if service($t,$n), granted_service_exact($t,$n)` and the equivalent policies for sets, prefixes, suffixes, per-type and global wildcards, plus the target check From fe98a7c670b833cc6e9b0ab7ac9a175003569fe8 Mon Sep 17 00:00:00 2001 From: Tomas Tormo Date: Tue, 20 Oct 2026 00:21:48 +0000 Subject: [PATCH 4/8] docs: a task block expires after expire_time, not at it The authorization concept said the current time must be strictly before every task block's expire_time. The policy reference, the security architecture page and the agent architecture page said a request must come before it. The verifier rejects a block only when the current time is after expire_time, so a request at the instant of expire_time still passes (api/tar.go:500, internal/identity/biscuit.go:711). The JS and Python verifiers agree (sdk/js/src/tar.ts:427, sdk/python/src/agent_mesh/tar.py:347). --- site/content/docs/concepts/authorization.md | 2 +- site/content/docs/contributing/security-architecture.md | 4 ++-- site/content/docs/preview/agent-architecture.md | 4 ++-- site/content/docs/reference/policy.md | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/site/content/docs/concepts/authorization.md b/site/content/docs/concepts/authorization.md index 06ba8624..aa6a5b63 100644 --- a/site/content/docs/concepts/authorization.md +++ b/site/content/docs/concepts/authorization.md @@ -183,7 +183,7 @@ block has **0 rules, 0 checks, and 1 `tar_block("")` fact**. After Stage 1 succeeds, the verifier evaluates the decoded `TaskAuthorizationRule` chain against the request: -- The current time must be strictly before every block's `expire_time` (when +- The current time must be at or before every block's `expire_time` (when set). - In **every** appended `TaskAuthorizationRule` block, at least one `TaskRule` must match the request's `service`, HTTP `method` and `path` (when diff --git a/site/content/docs/contributing/security-architecture.md b/site/content/docs/contributing/security-architecture.md index ffb965b5..52f52164 100644 --- a/site/content/docs/contributing/security-architecture.md +++ b/site/content/docs/contributing/security-architecture.md @@ -274,8 +274,8 @@ and evaluates it in host code alongside Block 0's Datalog policy. Within a single `TaskAuthorizationRule`, a request matches if it satisfies at least one `TaskRule` (an empty `rules` list denies everything). Across blocks `1..k`, semantics are strict intersection (logical AND): the request must satisfy -Block 0 Datalog RBAC, arrive before every block's `expire_time`, and match at -least one `TaskRule` in every appended block. +Block 0 Datalog RBAC, arrive at or before every block's `expire_time`, and +match at least one `TaskRule` in every appended block. #### Per-service-type PEP matching semantics diff --git a/site/content/docs/preview/agent-architecture.md b/site/content/docs/preview/agent-architecture.md index 3482535b..27380487 100644 --- a/site/content/docs/preview/agent-architecture.md +++ b/site/content/docs/preview/agent-architecture.md @@ -138,8 +138,8 @@ single pass, and evaluate the rules directly in host code. **at least one** `TaskRule`. An empty `rules` list denies everything. - Across appended blocks `1..k`, semantics are **strict intersection (logical AND)**: the request must be permitted by Block 0's standing Datalog policy, - arrive before every block's `expire_time`, and match at least one `TaskRule` - in **every** appended `TaskAuthorizationRule` block. + arrive at or before every block's `expire_time`, and match at least one + `TaskRule` in **every** appended `TaskAuthorizationRule` block. Each `TaskRule` scopes: diff --git a/site/content/docs/reference/policy.md b/site/content/docs/reference/policy.md index b0679901..ad30e925 100644 --- a/site/content/docs/reference/policy.md +++ b/site/content/docs/reference/policy.md @@ -345,8 +345,8 @@ a single `tar_block("")` fact encoding `TaskAuthorizationRule`: ``` Across appended blocks `1..k` (up to `MaxAttenuationBlocks = 8`), evaluation is -strict intersection: a request must pass Block 0 Datalog policy, be before -every block's `expire_time`, and match at least one `TaskRule` in every +strict intersection: a request must pass Block 0 Datalog policy, arrive at or +before every block's `expire_time`, and match at least one `TaskRule` in every appended `TaskAuthorizationRule` block. ## Evaluation From 49419c9d7a1c5066b69191946351aca48ef562b8 Mon Sep 17 00:00:00 2001 From: Tomas Tormo Date: Tue, 20 Oct 2026 00:21:48 +0000 Subject: [PATCH 5/8] docs: the node honours X-Mesh-Required-Labels on every forwarding route The authorization concept said a caller sends X-Mesh-Required-Labels on the /v1 inference endpoints or on a proxied A2A request. The node API reference said the proxy path honours it, and so does /v1/*. The Go comment on api.HeaderMeshRequiredLabels named only /v1/*. The node reads the header on /v1/chat/completions and /v1/completions (internal/node/openai_facade.go:357) and on every /mesh//// request (internal/node/sidecar.go:790, with the A2A gate at internal/node/a2a_service.go:106). The node also reads it on /egress// (internal/node/egress.go:481) and on CONNECT (internal/node/egress_tunnel.go:349). call_remote_tool takes the same requirement as required_labels (internal/node/mcp_handlers.go:175). /v1/models never reads the header (internal/node/openai_facade.go:302-328). On /egress// the node serves the destination itself, so it compares the requirement with its own labels (internal/node/egress.go:494-504). A CONNECT to a destination the node serves does the same (internal/node/egress_tunnel.go:376). This commit also rewrites the comment on api.HeaderMeshRequiredLabels in api/network.go to name these routes. --- api/network.go | 13 +++---------- site/content/docs/concepts/authorization.md | 7 +++++-- site/content/docs/reference/node-api.md | 2 +- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/api/network.go b/api/network.go index ffc1d612..b723c438 100644 --- a/api/network.go +++ b/api/network.go @@ -283,16 +283,9 @@ const ( // between a root path "/" and an empty path "". HeaderMeshNoTrailingSlash = "X-Mesh-No-Trailing-Slash" - // HeaderMeshRequiredLabels constrains an inference request on the sidecar's - // OpenAI-compatible endpoints (/v1/*) to providers attested with every - // pair of a comma-separated list of "key=value" label requirements (see - // api/labels.go and LabelCheck); invalid entries are rejected with HTTP - // 400. It can only narrow what mesh policy allows, never widen it. Absent - // means any provider permitted by policy. - // - // Reserved as part of the sidecar contract; enforced by the provider - // scorer. Label declarations are routing hints until attested via the - // node's Biscuit (see api/labels.go). + // HeaderMeshRequiredLabels lists "key=value" pairs the provider must carry. + // It only narrows what policy allows. The node honours it on /v1 + // completions, /mesh/*, /egress/* and CONNECT, and strips it there. HeaderMeshRequiredLabels = "X-Mesh-Required-Labels" // HeaderMeshPrincipal, HeaderMeshRoles, HeaderMeshTask, and HeaderMeshTaskID carry diff --git a/site/content/docs/concepts/authorization.md b/site/content/docs/concepts/authorization.md index aa6a5b63..8a9d6e67 100644 --- a/site/content/docs/concepts/authorization.md +++ b/site/content/docs/concepts/authorization.md @@ -271,8 +271,11 @@ Labels are used in four places: to its `attenuation.checks`. Every caller's credential must then carry that label. - **A caller choosing providers** sends `X-Mesh-Required-Labels: region=eu` - on the node's `/v1` inference endpoints or on a proxied A2A request, or - passes `required_labels` to `call_remote_tool`. Before it sends any request + on a request to the node, or passes `required_labels` to + `call_remote_tool`. The node honours the header on `/v1/chat/completions` + and `/v1/completions`, on every `/mesh////` request, + on `/egress//` and on `CONNECT`. A node that serves the + request itself checks its own labels. Before it sends any request data, the calling node fetches the provider's credential through the mutual handshake, verifies it, and confirms that every label named is attested in it. A provider that cannot show them all is skipped. diff --git a/site/content/docs/reference/node-api.md b/site/content/docs/reference/node-api.md index e9278b81..67b98ad4 100644 --- a/site/content/docs/reference/node-api.md +++ b/site/content/docs/reference/node-api.md @@ -248,7 +248,7 @@ and `.well-known/agent-card.json` is rewritten for the mesh. | Header | Effect | |---|---| -| `X-Mesh-Required-Labels: k=v[,k=v]` | Forward only to a peer whose credential attests every listed pair. Otherwise `403`. Removed before forwarding. Also honoured on `/v1/*`. | +| `X-Mesh-Required-Labels: k=v[,k=v]` | Forward only to a peer whose credential attests every listed pair. Otherwise `403`. Removed before forwarding. Also honoured on `/v1/chat/completions`, `/v1/completions` and `CONNECT`. On `/egress/*` this node is the provider, so it checks its own labels. | ### Talking MCP through the proxy From ea449199e48abf24441cf2f761286a8a1c6fece2 Mon Sep 17 00:00:00 2001 From: Tomas Tormo Date: Tue, 20 Oct 2026 00:21:48 +0000 Subject: [PATCH 6/8] docs: the target check is a baseline check, not a policy The authorization concept listed the target check under the baseline policies. It is a Biscuit check, "check if allow_network_target($fact, $val) or target_unrestricted(true)" (api/datalog.go:599). The node adds it with AddCheck after its local rules and before the baseline policies (internal/node/middleware.go:352-353). A check fails the request whatever the policies say, so it belongs with the other baseline checks (internal/node/middleware.go:303, 332). --- site/content/docs/concepts/authorization.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/site/content/docs/concepts/authorization.md b/site/content/docs/concepts/authorization.md index 8a9d6e67..a1d6aba8 100644 --- a/site/content/docs/concepts/authorization.md +++ b/site/content/docs/concepts/authorization.md @@ -155,8 +155,12 @@ The node builds a Biscuit authorizer over Block 0 and adds, in this order: destination outside the mesh (`egress://`) it adds `host(...)` and `port(...)`. 2. **The baseline checks**: `client_peer_id($id), connection_peer_id($id)` - (the token belongs to the peer that presents it), and the expiration - check against the current time. + (the token belongs to the peer that presents it), the expiration check + against the current time, and the target check + `allow_network_target($fact, $val) or target_unrestricted(true)`. The + node adds the target check after its local rules. The position of a + check does not change its result, because Biscuit runs every check after + it has applied all rules. 3. **The node's own identity facts**, taken from its own credential, as `target_fact("group", "dev-nodes")` and similar. The caller's `allowed_targets` are matched against these facts. @@ -165,10 +169,9 @@ The node builds a Biscuit authorizer over Block 0 and adds, in this order: `deny` policies. 5. **The baseline policies**: `allow if service($t,$n), granted_service_exact($t,$n)` and the equivalent policies for sets, prefixes, suffixes, per-type and - global wildcards, plus the target check - `allow_network_target(...) or target_unrestricted(true)`, and the rules - that turn an [HTTP grant](../../reference/policy/#http-grants) into a - service grant when the request's method and path match it. + global wildcards, and the rules that turn an + [HTTP grant](../../reference/policy/#http-grants) into a service grant + when the request's method and path match it. 6. **The synced mesh policy rules** described above. Biscuit evaluates every `check` and requires all of them to pass. It then From 23d8d07125edc8525735e43f9757130ac5472b17 Mon Sep 17 00:00:00 2001 From: Tomas Tormo Date: Tue, 20 Oct 2026 00:21:48 +0000 Subject: [PATCH 7/8] docs: a bootstrap-token credential carries only the token's role The authorization concept said the control plane resolves the identity's roles from the bindings and writes them into the authority block. It said so for enrollment, refresh and token exchange. The policy reference and the identity concept said the same. The control plane resolves roles from the bindings only for an identity that the identity provider authenticates (internal/controlplane/server.go:1027-1074, 1417-1438, internal/controlplane/sts.go:917-943). A member that enrolled with a bootstrap token is minted with the token's role alone (internal/identity/biscuit.go:683, called from internal/controlplane/server.go:1449, 2405, 3019, 3299). A role bound to node: or to mesh:system:authenticated reaches that member through the policy rules the verifying node syncs (api/policy_rules.go:154-171). The authorization concept also said that removing a grant takes effect through the credential. That holds for a grant compiled into a token. A grant that reaches a member only through the policy rules ends when the verifying node next fetches the policy. --- site/content/docs/concepts/authorization.md | 20 +++++++++++++------- site/content/docs/concepts/identity.md | 10 ++++++---- site/content/docs/reference/policy.md | 8 +++++--- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/site/content/docs/concepts/authorization.md b/site/content/docs/concepts/authorization.md index a1d6aba8..6a25a713 100644 --- a/site/content/docs/concepts/authorization.md +++ b/site/content/docs/concepts/authorization.md @@ -118,15 +118,21 @@ evaluate. ## From policy to facts At enrollment, refresh, and token exchange (`POST /token/exchange`), the -control plane resolves the identity's roles from the bindings and writes the -result into the credential's authority block as Datalog facts: one `role(...)` -fact per role, plus the grants compiled from the lists of every role. For -example, `allowed_services: ["mcp://calculator"]` becomes +control plane writes the credential's authority block as Datalog facts: one +`role(...)` fact per role, plus the grants compiled from the lists of those +roles. For example, `allowed_services: ["mcp://calculator"]` becomes `granted_service_set("mcp", ["calculator"])`, `mcp://*` becomes `granted_service_all("mcp")`, and `mcp://*.internal` becomes `granted_service_suffix("mcp", ".internal")`. Wildcards keep their dot, so `*.acme.example` matches `svc.acme.example` but not `evil-acme.example`. +The roles in the block depend on how the member enrolled. For an identity +that the identity provider authenticates, the control plane resolves the +roles from the bindings. For a member that enrolled with a bootstrap token, +the authority block carries only the token's role. A role bound to +`node:` or to `mesh:system:authenticated` reaches that member +through the policy rules described next, not through its credential. + The control plane also renders the policy as Datalog rules such as `role("developer") <- group("eng")` and `granted_service_set("mcp", ["calculator"]) <- role("developer")`. Nodes @@ -135,9 +141,9 @@ sooner when a policy update event reaches them) and add it to their authorizer as it arrives. When a node verifies a credential, these rules run against the identity facts in it. A grant added to the policy therefore reaches every node within the sync interval, with no need to reissue -credentials. Removing a grant takes effect through the credential instead: -the facts already in a token stay valid until the token is refreshed or -expires. +credentials. A removed grant stays in the tokens that already carry it, +until each token is refreshed or expires. A grant that reaches a member only +through these rules ends when each node next fetches the policy. ## What the hosting node checks diff --git a/site/content/docs/concepts/identity.md b/site/content/docs/concepts/identity.md index b13c823f..636d0312 100644 --- a/site/content/docs/concepts/identity.md +++ b/site/content/docs/concepts/identity.md @@ -112,10 +112,12 @@ contains: | `label("region", "eu")` | One fact per declared and permitted label. | | `granted_service_*`, `granted_target_*` | What the roles allow, compiled from `allowed_services` and `allowed_targets`. [Authorization](../authorization/) describes them. | -The node does not set any of these facts. Roles come from the bindings in -the mesh policy, not from the identity provider: an issuer's `roles` claim -is stored as `idp_role()`, which grants nothing by itself. Labels are the -ones the node asked for and the policy allowed. Grants come from the policy. +The node does not set any of these facts. For an OIDC enrollment, roles +come from the bindings in the mesh policy, never from the identity provider. +A bootstrap enrollment gets only the role of its bootstrap token. An +issuer's `roles` claim is stored as `idp_role()`, which grants nothing by +itself. Labels are the ones the node asked for and the policy allowed. +Grants come from the policy. ### 2. Delegated Session Biscuit (`POST /token/exchange`, `/oauth/token`) diff --git a/site/content/docs/reference/policy.md b/site/content/docs/reference/policy.md index ad30e925..4cdad7b8 100644 --- a/site/content/docs/reference/policy.md +++ b/site/content/docs/reference/policy.md @@ -351,9 +351,11 @@ appended `TaskAuthorizationRule` block. ## Evaluation -At enrollment, refresh, and token exchange, the control plane resolves the -identity's roles from the bindings and mints into the credential one `role()` -fact per role and the compiled `granted_*` facts of every role. The control +At enrollment, refresh, and token exchange, the control plane mints into the +credential one `role()` fact per role and the compiled `granted_*` facts of +every role. For an identity that the identity provider authenticates, the +control plane resolves the roles from the bindings. A member that enrolled +with a bootstrap token gets only the token's role. The control plane also renders the policy as Datalog rules served as `datalog_rules` at `GET /policies`. Nodes, routers and SDK members fetch this text (`--control-plane-sync-interval`, 15 minutes for a node, 5 for a router) and From 579088c07b030d2396d0ae7a20a257a945f4ca84 Mon Sep 17 00:00:00 2001 From: Tomas Tormo Date: Tue, 20 Oct 2026 08:17:52 +0000 Subject: [PATCH 8/8] docs: say who asks a node's catalog --- site/content/docs/concepts/authorization.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/site/content/docs/concepts/authorization.md b/site/content/docs/concepts/authorization.md index 6a25a713..f4bf8684 100644 --- a/site/content/docs/concepts/authorization.md +++ b/site/content/docs/concepts/authorization.md @@ -28,9 +28,9 @@ fails a check, such as the target check, or an appended task block. A node publishes services, each with a type and a name: `mcp://calculator`, `inference://vllm-eu`, `a2a://triage`, `egress://bigquery.googleapis.com`, and -the built-in `system://mesh.catalog`, which lists the services the node hosts -to a peer that asks. The routers' directory, not the catalog, tells a caller -who provides a name. +the built-in `system://mesh.catalog`. A peer asks a node's catalog for the +services that node hosts. The routers' directory, not the catalog, tells a +caller who provides a name. Standing mesh roles grant access to services by name (`allowed_services`), grant the right to serve them (`allowed_publish`) and can narrow HTTP