mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-09 12:17:13 +00:00
Qdrant ran with no authentication at all: its environment set the HTTP port and disabled telemetry and nothing else, so every container that reached its network held full read, write and delete access to every collection, and the MCP surface's read-only promise rested entirely on the adapter's contract naming only GET routes. Qdrant 1.19 forces authentication on every request once a read-only key is set, so the engine now declares both keys, the adapter presents the read-only one, and the full key goes only to consumers that own data here. A write route is now refused by the engine rather than only by the adapter. Three defects in the Flowise registry path. Its tool-name check compared against a contract the role always passed as empty, so it could not fail; it now compares against the provider's declared allowlist. Its managed flow pointed a customMCP node at an input that exists on no node at this tag, while the node that does take a registry id is customMcpServerTool, and the one that takes an inline config would have embedded the bearer in the flow; the flow is now an Agentflow whose Tool node calls one named tool with fixed arguments and is executed on every deploy. Its module docstring claimed only SSE providers could be registered, while the pinned MCPToolkit connects Streamable HTTP first and falls back to SSE, so the docstring contradicted the role's own declaration. Reaching a provider inside the container network needs HTTP_SECURITY_CHECK=false, which replaces the built-in deny list rather than merging into it, so a typo there disables every protection silently while the registry keeps working. The deploy now points a throwaway entry at loopback and at the metadata address and fails unless both are refused. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
208 lines
6.7 KiB
Python
208 lines
6.7 KiB
Python
"""Lookup ``engine``: resolve a central/embedded engine connection for a consumer.
|
|
|
|
{{ lookup('engine', 'redis', application_id) }} # full dict
|
|
{{ lookup('engine', 'redis', application_id, 'url') }} # one field
|
|
|
|
Mirrors the ``database`` lookup but for the non-RDBMS engines registered in
|
|
:data:`utils.roles.applications.services.engines.ENGINES` (redis, memcached,
|
|
rabbitmq, elasticsearch, typesense, qdrant, unbound). When the consumer declares
|
|
``services.<engine>.shared: true`` the connection points at the central
|
|
``svc-db-*`` / ``svc-dns-*`` stack with the consumer's own provisioned credentials;
|
|
otherwise it points at the embedded sidecar inside the consumer's own stack.
|
|
|
|
See ``docs/architecture/central-engines.md``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from ansible.errors import AnsibleError
|
|
from ansible.plugins.loader import lookup_loader
|
|
from ansible.plugins.lookup import LookupBase
|
|
|
|
from utils.manager.credential_key import OVERRIDE_SECTION
|
|
from utils.roles.applications.config import get
|
|
from utils.roles.applications.services.engines import (
|
|
ENGINES,
|
|
engine_service,
|
|
is_engine_enabled,
|
|
is_engine_shared,
|
|
)
|
|
from utils.roles.entity.name import get_entity_name
|
|
|
|
_FIELDS = (
|
|
"engine",
|
|
"enabled",
|
|
"shared",
|
|
"local",
|
|
"host",
|
|
"port",
|
|
"username",
|
|
"password",
|
|
"instance",
|
|
"prefix",
|
|
"url",
|
|
"container",
|
|
)
|
|
|
|
|
|
def qdrant_consumer_key(applications: dict[str, Any], svc_id: str) -> str:
|
|
"""Return the engine-wide key a data-owning qdrant consumer presents.
|
|
|
|
Args:
|
|
applications: the resolved applications mapping.
|
|
svc_id: the qdrant service role id.
|
|
|
|
qdrant has no per-consumer principals, only two engine-wide keys, so a
|
|
consumer that owns data here holds the engine's own key. The read-only key
|
|
is deliberately never returned: it belongs to the MCP adapter, whose whole
|
|
read-only guarantee rests on being unable to present anything else.
|
|
"""
|
|
return str(
|
|
get(
|
|
applications,
|
|
svc_id,
|
|
f"{OVERRIDE_SECTION}.api_key",
|
|
strict=False,
|
|
default="",
|
|
skip_missing_app=True,
|
|
)
|
|
or ""
|
|
).strip()
|
|
|
|
|
|
class LookupModule(LookupBase):
|
|
def run(
|
|
self,
|
|
terms: list[Any],
|
|
variables: dict[str, Any] | None = None,
|
|
**kwargs: Any,
|
|
) -> list[Any]:
|
|
terms = terms or []
|
|
if len(terms) not in (2, 3):
|
|
raise AnsibleError(
|
|
"engine: requires engine_type, consumer_id [, want_path]"
|
|
)
|
|
engine = str(terms[0]).strip()
|
|
consumer_id = str(terms[1]).strip()
|
|
want = str(terms[2]).strip() if len(terms) == 3 else "all"
|
|
if engine not in ENGINES:
|
|
raise AnsibleError(
|
|
f"engine: unknown engine '{engine}'; known: {', '.join(ENGINES)}"
|
|
)
|
|
if not consumer_id:
|
|
raise AnsibleError("engine: consumer_id must not be empty")
|
|
|
|
vars_ = variables or self._templar.available_variables
|
|
applications = lookup_loader.get(
|
|
"applications", loader=self._loader, templar=getattr(self, "_templar", None)
|
|
).run([], variables=vars_)[0]
|
|
|
|
descriptor = ENGINES[engine]
|
|
svc_id = descriptor["svc"]
|
|
port = int(descriptor["port"])
|
|
entity = get_entity_name(consumer_id)
|
|
|
|
enabled = is_engine_enabled(applications, consumer_id, engine)
|
|
shared = is_engine_shared(applications, consumer_id, engine)
|
|
svc_cfg = engine_service(applications, consumer_id, engine)
|
|
|
|
central_name = str(
|
|
get(
|
|
applications,
|
|
svc_id,
|
|
f"services.{engine}.name",
|
|
strict=False,
|
|
default=engine,
|
|
skip_missing_app=True,
|
|
)
|
|
or engine
|
|
).strip()
|
|
|
|
username = entity
|
|
prefix = entity
|
|
password = str(
|
|
svc_cfg.get("password")
|
|
or get(
|
|
applications,
|
|
consumer_id,
|
|
f"{OVERRIDE_SECTION}.{engine}_password",
|
|
strict=False,
|
|
default="",
|
|
)
|
|
or ""
|
|
).strip()
|
|
host = central_name if shared else engine
|
|
instance = central_name if shared else entity
|
|
|
|
# Exception: shared redis authenticates as the central `default` user, not
|
|
# a per-consumer ACL user — ACL users live only in memory and are lost when
|
|
# a deploy recreates the central redis after the consumer was provisioned.
|
|
if engine == "redis" and shared:
|
|
username = "default"
|
|
password = str(
|
|
get(
|
|
applications,
|
|
svc_id,
|
|
f"{OVERRIDE_SECTION}.REDIS_PASSWORD",
|
|
strict=False,
|
|
default="",
|
|
skip_missing_app=True,
|
|
)
|
|
or ""
|
|
).strip()
|
|
|
|
if engine == "qdrant" and shared:
|
|
password = qdrant_consumer_key(applications, svc_id)
|
|
|
|
if engine == "rabbitmq":
|
|
url = (
|
|
f"amqp://{username}:{password}@{host}:{port}/{entity}"
|
|
if shared
|
|
else f"amqp://{host}:{port}/"
|
|
)
|
|
elif engine in ("redis", "memcached"):
|
|
scheme = "redis" if engine == "redis" else "memcached"
|
|
auth = f"{username}:{password}@" if (shared and password) else ""
|
|
url = f"{scheme}://{auth}{host}:{port}"
|
|
elif engine in ("elasticsearch", "typesense", "qdrant"):
|
|
url = f"http://{host}:{port}"
|
|
else:
|
|
url = host
|
|
|
|
consumer_name = str(
|
|
get(
|
|
applications,
|
|
consumer_id,
|
|
f"services.{engine}.name",
|
|
strict=False,
|
|
default="",
|
|
skip_missing_app=True,
|
|
)
|
|
or ""
|
|
).strip()
|
|
container = central_name if shared else (consumer_name or f"{entity}-{engine}")
|
|
|
|
resolved = {
|
|
"engine": engine,
|
|
"enabled": enabled,
|
|
"shared": shared,
|
|
"local": bool(enabled and not shared),
|
|
"host": host,
|
|
"port": port,
|
|
"username": username if shared else "",
|
|
"password": password if shared else "",
|
|
"instance": instance,
|
|
"prefix": prefix,
|
|
"url": url,
|
|
"container": container,
|
|
}
|
|
if want == "all":
|
|
return [resolved]
|
|
if want not in _FIELDS:
|
|
raise AnsibleError(
|
|
f"engine: unknown want '{want}'; known: {', '.join(_FIELDS)}"
|
|
)
|
|
return [resolved.get(want, "")]
|