mutating_tools_enabled: false was a claim with nothing behind it. The agent include list is built by subtracting tools.mutating from tools.allowlist, and the role named no mutating tool, so the subtraction removed nothing and every client was offered HassTurnOn, HassTurnOff, HassBroadcast, HassCancelAllTimers and the three list writers while the declaration said mutations were off.
Naming the seven turns the flag into the control it claims to be: a client now sees GetDateTime, GetLiveContext and todo_get_items, and setting the flag to true restores the full surface. The adapter already refuses a contract whose tool carries no mutating verdict; this brings the one native provider that serves actuating tools under the same rule. Entity exposure still bounds what the read tools reach.
The README described entity exposure as the only control and said the write tools stay listed; it now describes both layers.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Production block never set one. inventory provision writes only
ansible_connection for a local host, so DOMAIN_PRIMARY stayed at the
group_vars default of infinito.localhost and the deployment served a zone its
own resolver knows nothing about. CoreDNS templates answers for
INFINITO_DOMAIN_RE and nothing else, so every lookup of the deployment's own
name came back NXDOMAIN.
Mastodon is where that surfaced. tootctl accounts create validates the address
through EmailMxValidator, which resolves MX and then, for the domain itself,
A and AAAA. All six queries returned NXDOMAIN in run 33574056480, the
validator recorded email :unreachable, and the admin account was never created.
Only the guide rows were affected: the matrix rows write
DOMAIN_PRIMARY: "{{ lookup('env', 'INFINITO_DOMAIN') }}" into their host_vars
and resolve fine.
The template now carries DOMAIN=<your-domain> in both the host and the
container form and threads it into --vars, with the container form passing it
through the docker run boundary as well. The two guide runners substitute it
the way they already substitute the SSH key, so the replay deploys under
INFINITO_DOMAIN like every other CI deploy. compose_deploy.sh moves its env
load ahead of the substitutions to have the value; host_deploy.sh had no env
load at all.
A production reader is asked for a domain now instead of silently inheriting
infinito.localhost, which no real deployment wants either. The shipped default
keeps its value: sys-svc-nss documents that *.localhost resolves through the
myhostname NSS module without any zone, and host-side tasks lean on it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The dependency diagrams are drawn from the service edges, so dropping the
client-side homeassistant and baserow declarations changes both ends of each
relation. The four client roles lose the provider node, its service node and
the edge between them; homeassistant loses its whole Dependents subgraph,
because nothing pulls it in from that side any more.
The provider-side edges that carry the MCP pairing are untouched, which is
the point: the relation is now drawn once, from the provider, instead of
from both ends.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
enabled and shared were hand-maintained in 24 roles and wrong in both
directions. Both now resolve from the mcp_consumer edges already declared
in meta/services.yml: a server takes the consumers it admits, a client
takes the servers that admit it, and the pair renders as "'peer' in
group_names or ...", or False when no peer is admitted at all. setdefault
keeps meta/mcp.yml and meta/variants.yml as the override channel, so
pinning either key still wins.
The resolved block replaces the mapping instead of being written into it.
_build_role_base_config parks the YAML cache's own object under mcp, so an
in-place setdefault injected the derived keys into every later raw read and
made test_mcp_schema pass alone but fail in a directory run.
The derivation keys on peer presence, never on a peer's own value: a
re-entrant read under _RENDER_GUARD returns the unrendered dict.
Four lints now ask the resolver instead of reading meta/mcp.yml, which they
could not do while the key was hand-written. That is what exposed the eleven
missing hostile-path specs the previous commit closed.
Carries one change that belongs to the WordPress probe but shares a file
with this one: the tool allowlist moves to the hyphenated names the adapter
actually serves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
allowed_consumers was twenty hand-kept lists of which fourteen were identical, so every new client meant an edit in every provider. A client now declares services.<own-entity>.mcp_consumer: true once and a provider refuses one with mcp_consumer: false, which is also what tells a deliberate exclusion apart from a forgotten entry.
The group_names-gated consumer entries give the inventory resolver the co-deploy edge meta/mcp.yml never had: one of sixty-nine matrix rounds carried an MCP client before, twenty-three role/round pairs do now. bond 0.5 keeps the clients out of round 0 so the host memory budget still holds.
BREAKING CHANGE: meta/mcp.yml no longer accepts allowed_consumers; declare services.<entity>.mcp_consumer on the client instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The READMEs are generated from the role declarations. Six role READMEs gain
svc-net-tor in their dependency graph and the top-level table gains the Tor
Onion Service column entries, following cd1f07a91. The remaining lines are
template output: the author name becomes a link, two mermaid linkStyle rows,
and a reworded setup sentence in web-svc-mirror.
Left unregenerated the documentation would disagree with the meta/services.yml
it is derived from.
The generator was not re-run here: this is the output that was already in the
tree, checked to match the committed declarations.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
834 commits from the Tor onion-service branch against 299 of ours, from merge
base 7b646bdece: 127 conflicted files, 3506 files changed.
The two sides are largely orthogonal. Theirs adds svc-net-tor and a `tor` service
across the role tree, plus two sweeping refactors: shipped scripts sorted by
language (files/ruby/, files/python/, files/shell/, scripts/lint/*.sh) and the
unit suite moved under tests/unit/python/. Ours is requirement 036, the native AI
gateway, plus the MCP role work. Most conflicts were both sides appending to the
same service map, resolved as unions.
Resolutions that were not unions:
Credentials moved namespace. Theirs renamed meta/schema.yml to meta/secrets.yml
and re-addressed credentials as secrets.credentials.<name>. Git carried our
litellm_api_key blocks through the rename, but every consumer we added still read
'credentials.<name>', which no longer resolves. All 45 call sites are migrated.
plugins/filter/container_volumes.py is deleted by theirs and relocated to
plugins/lookup/container_volumes.py, where it carries the same tmpfs defect our
f85996b43 had just fixed: a hard-coded {"size": ...} that silently drops a per
mount `mode`. The fix is re-applied in the relocated file, as that commit's merge
note anticipated.
compose.yml keeps only INFINITO_PLAYWRIGHT_REPORTS_BASE_DIR. Theirs also forwards
INFINITO_PLAYWRIGHT_STAGE_BASE_DIR; that line is deliberately dropped, per the
analysis recorded in 45804e9ad -- STAGE has a drift-proof role-vars fallback,
while forwarding it promotes a setdefault-pinnable .env value above that SPOT.
tasks/stages/01_constructor.yml takes their include, ./tasks/utils/setup/group.yml,
because their refactor removed tasks/groups/*-roles.yml, leaving our include
target nonexistent. Our task name is kept, since svc-virt is in the loop.
Makefile swarm-zombie is a union: their validate plus teardown, then our
lab_subnet cleanup, which their side dropped while leaving lab_subnet.sh as the
only orphaned reference in the file.
01_bootstrap.sh keeps our deterministic --ip pinning, which distinguishes an
already-attached node from a real failure, over their `|| true`.
Their language-sorted layout orphaned references our side still held: zammad's
two scripts/apply/*.rb, jenkins' java-with-ca-trust.sh, and the notify topics
their handlers renamed to hyphenated form. All corrected.
roles/sys-svc-compose/files/shell/swarm/stack_ready.sh gains our latched-update
block: paused, rollback_paused, rollback_started and rollback_completed exit 2
rather than polling a state that cannot resolve without another deploy.
config_from_run is unioned to (title, logged, jobs); the production caller in
trigger/__main__.py now passes jobs, without which our suite suppression would
have been inert.
READMEs are regenerated rather than hand-merged: 15 LiteLLM rows and 91 Tor rows
now derive from the merged metadata, which is itself evidence the meta merge is
coherent.
Verified: 5161 unit tests pass. The single failure, test_query.py TestSortSpec,
also fails on fork/feature/svc-net-tor at b51d2f9919, measured in a baseline
worktree -- so this merge introduces no new unit-test failure. Two modules are
excluded from that run because `baudolo`, a dependency their branch introduces,
is not installed on this host.
Not verified: no deploy has run against the merged tree, and make quality has not
been run end to end here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The block sat under services but never behaved like a service. A service has
exactly one providing role, declared with provides, so the registry can map the
key to that role and consumers bind their flag to its presence. No role declares
provides: mcp. There are seventeen providers and the pairing is done by
allowed_consumers inside the block itself.
Every rule that enforces the service model therefore needed an exemption for it:
a shared-provider suppression in fourteen roles, a playwright-service-flag
suppression, and two freshly added exemptions in the dynamic-flag and
variants-static-override checks. Four independent rules, four escape hatches,
one cause. They are all gone now, together with a bond key that only the
resource budgeter reads and only for entries under services, and a wordpress
addon bridge that pointed at a service block that no longer exists.
The block moves to meta/mcp.yml. The applications loader maps meta/<name>.yml to
applications.<app>.<name> generically, so the move needs no wiring; the file
takes the same shape as meta/rbac.yml. Every config path follows, the variant
overrides are lifted out of their services map, and the vocabulary module leaves
the services package because it no longer describes one. roles_with_service
gains an explicit topic parameter rather than an implicit fallback, so nothing
silently resolves against the wrong container.
Files that are otherwise unrelated to this move are included where the committed
tree would otherwise keep pointing at a path that no longer resolves: thirteen
vars files, eleven playwright env templates and one task file.
Four roles declared auth_subject: user while handing the client an
account-bound credential. Nextcloud and Home Assistant mint against the
administrator account, Mattermost issues the administrator's personal access
token, and Baserow creates a dedicated mcp@<domain> account with its own
workspace. The declarations now name what the minting tasks do; Baserow's
README additionally claimed the administrator owns its endpoint, which it
never did.
The tools declarations follow one convention instead of two: the field records
the deployment's intent and the README names the missing enforcement, which is
what GitLab already did and what Baserow and Mattermost now do.
The discovery lookup carries auth_subject through so clients can see the
subject. No consumer branches on it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Move source, minimum version and upstream notes into the role's own mcp
block, so the facts sit next to the surface they describe.
Drop the mcp_token credential declaration. Home Assistant issues that
token itself and it is persisted through sys-token-store, so a vault
credential of that name was never read and pointed at a place the value
does not live.
Document the surface: the endpoint answers an unauthenticated request
with 404 rather than 401, and the enforced control is entity exposure,
not a read-only tool policy. tools/list offers write tools, but an entity
that is not exposed to Assist stays unreachable through them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Home Assistant exposes no per-tool filter: enabling the Assist API
publishes its actuating intents regardless of the services.mcp tool
flags. The enforced control is entity exposure, and the role exposes
none, so the actuating tools have nothing to reach.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The role body runs per host, so the mint ran on every node in the play.
mint_long_lived_token deletes the existing token under the same client
name before creating a new one, so each node invalidated its
predecessor while the store persisted the first one. Clients then got a
401 on every call. A deploy-time probe now asks the hub whether the
stored token still authenticates, re-mints only when it does not, and
refuses the deploy when even the fresh token is rejected.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The guest spec requested /mcp_server/sse, a path Home Assistant does not
route. The hub answers 401 on any unknown path, so the assertion passed
without ever reaching the MCP surface. Upstream serves it at /api/mcp.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The role declared a server-direction MCP service but never provisioned one, and the declared endpoint pointed at /mcp_server/sse, the legacy path. Home Assistant serves MCP under /api/mcp, and its mcp_server integration has no YAML schema, so the config entry only exists through the config-entries flow API.
The provisioning script drives that path over the container loopback: onboard the administrator when the hub is fresh (falling back to the login flow when it is not), mint a long-lived token over the websocket API, then create the mcp_server config entry with it. The token is persisted through sys-token-store, which is why meta/users.yml registers the key; a second role registering a token for the same user only works since the per-key user merge.
The whole path is skipped when a token already exists, so a redeploy costs one lookup. Handlers flush before it runs because the hub container has to exist before anything talks to its API.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two defects, the second only visible once the first was gone.
The hub answered every proxied request with HTTP 400. In swarm it joins the
proxy's own overlay, so the forwarded request arrives from that network and not
from the hub's, while `trusted_proxies` listed only the hub's own subnet. The
container log named the address it rejected, and the two networks resolve to
192.168.107.0/24 for the proxy against 192.168.79.0/24 for the hub. Compose has
no such split, which is why this never surfaced there. Use the project-wide
trusted-proxy CIDRs instead, the same list three other roles already consume.
With the proxy trusted the failure moved to HTTP 502: the backend was
crash-looping. Home Assistant guards its config directory with a PID lock, so
the second and third replica on the shared volume abort with "Another Home
Assistant instance is already running" and never come up. The service sat at
2/3 with `task: non-zero exit (1)` cycling over all three nodes. Upstream
supports no multi-instance mode, so the hub is pinned to a single replica like
xwiki and jellyfin before it.
Verified by a full swarm run: deploy, e2e and the drain-and-reschedule step all
green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The first MCP server role: where the agent roles consume tools, this one
offers them. Its services.mcp block declares an internal streamable-HTTP
endpoint gated on an agent being deployed, so the surface only exists
when something can use it, and it stays read-only because a privileged
auth subject must not carry mutating tools.
Home Assistant rejects any request carrying X-Forwarded-For from an
undeclared proxy, answering 400 for every call behind the platform's
reverse proxy. The role therefore renders a configuration.yaml that
trusts the stack subnet, keeping default_config so mounting the file does
not drop the integrations Home Assistant generates on first start.
As a shared_net provider its compose file references its own network as
external, which nothing creates in compose mode, so the role pre-creates
it the way prometheus, minio and seaweedfs already do. The vhost declares
status_codes [200, 302, 405] because the health probe sends HEAD and the
hub answers 405.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>