feat: add source config ownership policy

This commit is contained in:
lda
2026-06-04 17:49:05 +07:00 Unverified
parent b211eef1c7
commit 00305c1398
15 changed files with 1112 additions and 36 deletions
@@ -38,6 +38,9 @@ class FakeRegistryProvider:
def config_source_ids(self) -> set[str]:
return self._config_ids
def config_source_ownership(self) -> dict[str, str]:
return {source_id: "locked" for source_id in self._config_ids}
def _api(
*entries: FakeRegistryEntry,
@@ -118,6 +121,20 @@ def test_list_shadowed_by_config() -> None:
assert sl["shadowed_by_config"] is False
def test_list_registry_entries_reports_config_ownership_and_mutability() -> None:
api = _api(
FakeRegistryEntry(id="github.work"),
config_ids={"github.work"},
)
payload = asyncio.run(api.list_registry_entries())
entry = payload["entries"][0]
assert entry["shadowed_by_config"] is True
assert entry["config_ownership"] == "locked"
assert entry["mutable"] is False
def test_inspect_returns_full_entry_and_shadow_flag() -> None:
api = _api(
FakeRegistryEntry(
@@ -1,5 +1,7 @@
from __future__ import annotations
from pathlib import Path
from wf_mcp.broker import WfMcpService
from wf_mcp.broker.service.connection_service import ConnectionService
from wf_mcp.broker.service.events import BrokerEventRecorder
@@ -258,6 +260,118 @@ def test_connection_service_sync_registry_disabled_entry_hydrates_disabled_sourc
assert catalog.capability_sources["demo.registry"].enabled is False
def test_connection_service_sync_locked_config_shadows_registry_entry(
tmp_path: Path,
) -> None:
service = ConnectionService(events=BrokerEventRecorder(EventBus()))
_source_catalog(service)
store = FileSourceRegistryStore(tmp_path / "locked_shadow")
store.save_registry(
SourceRegistryFile(
sources=[
McpSourceRegistryEntry(
id="demo.default",
provider="registry",
account="stored",
transport=StdioSourceTransport(command="demo-server"),
)
]
)
)
config = BrokerConfig(
store_root=local_temp_root(),
connections=[
ConnectionConfig(
id="demo.default",
server="config",
account="locked",
source_config_ownership="locked",
)
],
)
service.sync_connections_from_config(config, source_registry_store=store)
connection = service.get("demo.default")
assert connection.server == "config"
assert connection.account == "locked"
assert any(
event.kind == "source_registry_ignored_config_shadow"
for event in service.events.list_events()
)
def test_connection_service_sync_seed_config_materializes_registry_entry(
tmp_path: Path,
) -> None:
service = ConnectionService(events=BrokerEventRecorder(EventBus()))
_source_catalog(service)
store_root = tmp_path / "seed_materialized"
store = FileSourceRegistryStore(store_root)
config = BrokerConfig(
store_root=local_temp_root(),
connections=[
ConnectionConfig(
id="demo.default",
server="demo",
account="default",
metadata={"transport": {"kind": "stdio", "command": "demo-server"}},
source_config_ownership="seed",
)
],
)
service.sync_connections_from_config(config, source_registry_store=store)
registry = store.load_registry()
assert registry.sources[0].id == "demo.default"
assert registry.sources[0].provider == "demo"
assert service.get("demo.default").metadata["source_registry"] is True
all_events = service.events.list_events()
assert any(
event.kind == "source_registry_seeded_from_config"
for event in all_events
)
def test_connection_service_sync_seed_existing_registry_entry_wins(
tmp_path: Path,
) -> None:
service = ConnectionService(events=BrokerEventRecorder(EventBus()))
_source_catalog(service)
store = FileSourceRegistryStore(tmp_path / "seed_existing")
store.save_registry(
SourceRegistryFile(
sources=[
McpSourceRegistryEntry(
id="demo.default",
provider="registry",
account="stored",
transport=StdioSourceTransport(command="demo-server"),
)
]
)
)
config = BrokerConfig(
store_root=local_temp_root(),
connections=[
ConnectionConfig(
id="demo.default",
server="config",
account="seed",
metadata={"transport": {"kind": "stdio", "command": "config-server"}},
source_config_ownership="seed",
)
],
)
service.sync_connections_from_config(config, source_registry_store=store)
connection = service.get("demo.default")
assert connection.server == "registry"
assert connection.account == "stored"
def test_wfmcpservice_sync_connections_delegates_registry_store() -> None:
service = WfMcpService(store=FileStore(local_temp_root() / "facade_registry"))
store = FileSourceRegistryStore(local_temp_root() / "facade_registry_store")
@@ -48,12 +48,16 @@ def _provider(
tmp_path: Path,
entries: list[McpSourceRegistryEntry] | None = None,
config_ids: frozenset[str] | None = None,
config_connections: list[ConnectionConfig] | None = None,
) -> SourceRegistryAdminProvider:
store = _store_with_entries(tmp_path / "reg", *(entries or []))
connections = [
ConnectionConfig(id=cid, server="s", account="a")
for cid in (config_ids or frozenset())
]
if config_connections is not None:
connections = config_connections
else:
connections = [
ConnectionConfig(id=cid, server="s", account="a")
for cid in (config_ids or frozenset())
]
return SourceRegistryAdminProvider(
source_registry_store=store, config_connections=connections
)
@@ -120,12 +124,30 @@ def test_add_persists_and_round_trips(tmp_path: Path) -> None:
def test_add_rejects_config_shadowed_id(tmp_path: Path) -> None:
provider = _provider(tmp_path, config_ids=frozenset({"config.server"}))
with pytest.raises(ValueError, match="shadowed by a config connection"):
with pytest.raises(ValueError, match="locked by a config connection"):
provider.add_registry_entry(_entry_dict("config.server"))
assert provider.list_registry_entries() == []
def test_add_allows_seed_config_shadow_when_registry_missing(tmp_path: Path) -> None:
provider = _provider(
tmp_path,
config_connections=[
ConnectionConfig(
id="github.work",
server="github",
account="work",
source_config_ownership="seed",
)
],
)
result = provider.add_registry_entry(_entry_dict("github.work"))
assert result.id == "github.work"
def test_add_rejects_duplicate_registry_id(tmp_path: Path) -> None:
provider = _provider(tmp_path, entries=[_entry("existing.server")])
+47
View File
@@ -549,6 +549,53 @@ def test_build_service_from_config_config_shadows_registry() -> None:
)
def test_broker_config_connection_defaults_to_locked() -> None:
tmp_path = local_temp_root() / "broker_config_locked_default"
tmp_path.mkdir(parents=True, exist_ok=True)
config_path = tmp_path / "wf_mcp.config.json"
config_path.write_text(
json.dumps(
{
"store_root": str(tmp_path / "store"),
"connections": [
{"id": "demo.default", "server": "demo", "account": "default"}
],
}
),
encoding="utf-8",
)
config = load_broker_config(config_path)
assert config.connections[0].source_config_ownership == "locked"
def test_broker_config_connection_accepts_seed_policy() -> None:
tmp_path = local_temp_root() / "broker_config_seed_policy"
tmp_path.mkdir(parents=True, exist_ok=True)
config_path = tmp_path / "wf_mcp.config.json"
config_path.write_text(
json.dumps(
{
"store_root": str(tmp_path / "store"),
"connections": [
{
"id": "demo.default",
"server": "demo",
"account": "default",
"source_config_ownership": "seed",
}
],
}
),
encoding="utf-8",
)
config = load_broker_config(config_path)
assert config.connections[0].source_config_ownership == "seed"
def _artifact() -> WorkflowArtifact:
return WorkflowArtifact(
id="summarize_docs",
+35
View File
@@ -10,8 +10,10 @@ from wf_mcp.source_registry import (
McpSourceRegistryEntry,
SourceRegistryFile,
StdioSourceTransport,
connection_config_to_registry_entry,
registry_entry_to_connection_config,
)
from wf_mcp.models import ConnectionConfig
def _entry(source_id: str = "github.work") -> McpSourceRegistryEntry:
@@ -116,3 +118,36 @@ def test_registry_entry_to_connection_config_disabled_entry() -> None:
config = registry_entry_to_connection_config(entry)
assert config.enabled is False
def test_connection_config_to_registry_entry_preserves_transport_metadata() -> None:
connection = ConnectionConfig(
id="github.work",
server="github",
account="work",
enabled=False,
metadata={
"transport": {"kind": "stdio", "command": "npx", "args": ["server"]},
"profile": "corp",
"auth_ref": "secret://github/work",
"region": "us",
},
)
entry = connection_config_to_registry_entry(connection)
assert entry.id == "github.work"
assert entry.provider == "github"
assert entry.account == "work"
assert entry.enabled is False
assert entry.profile == "corp"
assert entry.auth_ref == "secret://github/work"
assert entry.transport.kind == "stdio"
assert entry.metadata["region"] == "us"
def test_connection_config_to_registry_entry_requires_transport_metadata() -> None:
connection = ConnectionConfig(id="github.work", server="github", account="work")
with pytest.raises(ValueError, match="requires metadata.transport"):
connection_config_to_registry_entry(connection)