mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 19:01:54 +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>
57 lines
1.8 KiB
Python
57 lines
1.8 KiB
Python
"""The read-only qdrant key never leaves the MCP adapter.
|
|
|
|
qdrant authenticates with two engine-wide keys and has no per-consumer
|
|
principals, so the only thing keeping the adapter unable to write is which key
|
|
it is handed. A consumer lookup that reached for the read-only key, or an
|
|
adapter that received the full one, would erase that boundary without any
|
|
declaration changing.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
import unittest
|
|
|
|
plugin_module = importlib.import_module("plugins.lookup.engine")
|
|
qdrant_consumer_key = plugin_module.qdrant_consumer_key
|
|
|
|
SVC = "svc-db-qdrant"
|
|
|
|
BOTH_KEYS = {
|
|
SVC: {
|
|
"secrets": {
|
|
"credentials": {
|
|
"api_key": "full-access-key",
|
|
"read_only_api_key": "read-only-key",
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
class TestQdrantConsumerKey(unittest.TestCase):
|
|
def test_a_data_owning_consumer_gets_the_full_key(self) -> None:
|
|
self.assertEqual("full-access-key", qdrant_consumer_key(BOTH_KEYS, SVC))
|
|
|
|
def test_the_read_only_key_is_never_handed_to_a_consumer(self) -> None:
|
|
self.assertNotIn(
|
|
"read-only-key",
|
|
qdrant_consumer_key(BOTH_KEYS, SVC),
|
|
"the adapter's read-only guarantee is that it holds this key and "
|
|
"nothing else; handing it out elsewhere makes the key meaningless",
|
|
)
|
|
|
|
def test_an_engine_without_a_declared_key_yields_nothing(self) -> None:
|
|
self.assertEqual("", qdrant_consumer_key({SVC: {}}, SVC))
|
|
|
|
def test_an_absent_service_role_does_not_raise(self) -> None:
|
|
self.assertEqual("", qdrant_consumer_key({}, SVC))
|
|
|
|
def test_a_blank_key_does_not_render_as_whitespace(self) -> None:
|
|
blank = {SVC: {"secrets": {"credentials": {"api_key": " "}}}}
|
|
self.assertEqual("", qdrant_consumer_key(blank, SVC))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|