Harden connector registry plugin records

This commit is contained in:
Danny Zhang
2026-05-18 19:36:22 -07:00
parent 67694d2e39
commit 53d24ca4fa
5 changed files with 347 additions and 20 deletions
+22 -4
View File
@@ -29,25 +29,36 @@ registry = ConnectorRegistry.from_plugin_records(
{
"id": "plugin_orders",
"name": "orders",
"package_path": "./orders-plugin",
"mount": {
"path": "orders-plugin",
},
"policyLabels": ["read_only"],
},
{
"id": "plugin_calendar",
"name": "calendar",
"policy": {
"labels": ["read_only"],
},
"apps": {
"calendar": {
"id": "connector_googlecalendar",
"connectorId": "connector_googlecalendar",
"authorizationAlias": "calendar_connection",
"serverLabel": "google_calendar",
"allowedTools": ["events_search"],
"requireApproval": "never",
}
},
},
]
],
package_root="./mounted-plugins",
)
orders = Connector.from_installed_plugin("plugin_orders", registry)
calendar = Connector.from_installed_plugin(
"plugin_calendar",
registry,
authorization={"calendar": "conn_calendar_access_token"},
authorization={"calendar_connection": "conn_calendar_access_token"},
hosted_mcp_require_approval="always",
)
@@ -62,6 +73,13 @@ This keeps package discovery, marketplace installation, workspace sharing, admin
sync outside the SDK runtime while giving those systems a stable place to hand installed plugin
records to the SDK.
Registry records accept either direct package paths such as `package_path` or nested mounted-package
paths such as `mount.path`. When `package_root` is supplied, relative and absolute package paths
must resolve inside that root. Hosted app records can declare auth aliases, server labels, allowed
tools, approval settings, and deferred loading flags; auth aliases are resolved through the
`authorization` mapping passed to `Connector.from_installed_plugin()`. Top-level `policyLabels` or
`policy.labels` are merged into the connector's policy labels.
## SDK tool connectors
Use [`Connector.from_tools()`][agents.connectors.Connector.from_tools] when your integration is
+20 -6
View File
@@ -43,9 +43,17 @@ def build_hosted_connector() -> Connector:
"id": "plugin_calendar",
"name": "calendar",
"description": "Hosted Google Calendar connector shape.",
"policy": {
"labels": ["read_only"],
},
"apps": {
"calendar": {
"id": "connector_googlecalendar",
"connectorId": "connector_googlecalendar",
"authorizationAlias": "calendar_connection",
"serverLabel": "google_calendar",
"allowedTools": ["events_search"],
"requireApproval": "never",
"deferLoading": True,
}
},
}
@@ -54,8 +62,8 @@ def build_hosted_connector() -> Connector:
return Connector.from_installed_plugin(
"plugin_calendar",
registry,
authorization={"calendar": "demo_access_token"},
hosted_mcp_require_approval="never",
authorization={"calendar_connection": "demo_access_token"},
hosted_mcp_require_approval="always",
)
@@ -119,10 +127,14 @@ def build_package_connector(package_root: Path) -> Connector:
{
"id": "plugin_orders",
"name": "orders",
"package_path": str(package_root),
"mount": {
"path": package_root.name,
},
"policyLabels": ["read_only"],
"source": "unified_plugins_demo",
}
]
],
package_root=package_root.parent,
)
return Connector.from_installed_plugin("plugin_orders", registry)
@@ -177,6 +189,7 @@ async def verify_connector_demo() -> dict[str, Any]:
"package_registry_source": package_connector.metadata["unified_plugin"]["source"],
"hosted_connector_label": hosted_tool.tool_config["server_label"],
"hosted_connector_id": hosted_tool.tool_config["connector_id"],
"hosted_allowed_tools": hosted_tool.tool_config["allowed_tools"],
}
@@ -238,7 +251,8 @@ async def main(*, verify: bool) -> None:
expected = {
"direct_tool_result": "discount=25.00",
"mcp_tool_result": "order demo_order_1001: fulfilled",
"hosted_connector_label": "calendar",
"hosted_connector_label": "google_calendar",
"hosted_allowed_tools": ["events_search"],
}
mismatches = {
key: (summary.get(key), expected_value)
+169 -9
View File
@@ -25,6 +25,17 @@ ConnectorPolicyLabel = Literal[
]
"""Coarse policy labels callers can use to route connector approval and sandbox decisions."""
_CONNECTOR_POLICY_LABELS: set[str] = {
"read_only",
"write",
"destructive",
"external_send",
"network",
"secret_access",
"local_execution",
"sandbox_required",
}
HostedConnectorAuthorization = (
str | Mapping[str, str] | Callable[[str, str, Mapping[str, Any]], str | None]
@@ -47,6 +58,7 @@ class ConnectorPlugin:
package_path: Path | None = None
hosted_connectors: Mapping[str, Mapping[str, Any]] = field(default_factory=dict)
metadata: Mapping[str, Any] = field(default_factory=dict)
policy_labels: tuple[ConnectorPolicyLabel, ...] = ()
@classmethod
def from_record(
@@ -66,6 +78,7 @@ class ConnectorPlugin:
description = _optional_record_str(record, ("description",), "Plugin description")
package_path = _plugin_package_path(record, package_root=package_root)
hosted_connectors = _plugin_hosted_connector_configs(record)
policy_labels = _plugin_policy_labels(record)
metadata = {
key: value
for key, value in record.items()
@@ -83,6 +96,9 @@ class ConnectorPlugin:
"packagePath",
"package_path",
"path",
"policy",
"policyLabels",
"policy_labels",
"pluginId",
"plugin_id",
"slug",
@@ -96,6 +112,7 @@ class ConnectorPlugin:
package_path=package_path,
hosted_connectors=hosted_connectors,
metadata=metadata,
policy_labels=policy_labels,
)
@@ -440,6 +457,7 @@ class ConnectorRegistry:
authorization=authorization,
require_approval=hosted_mcp_require_approval,
)
connector.policy_labels.update(plugin_record.policy_labels)
connector.tools.extend(hosted_tools)
if hosted_tools:
connector.policy_labels.add("network")
@@ -611,6 +629,20 @@ def _load_hosted_connector_tools(
tools: list[Tool] = []
for app_name, raw_config in app_configs.items():
connector_id = _hosted_connector_id(raw_config, f"App id for {app_name!r}")
server_label = (
_optional_record_str(raw_config, ("server_label", "serverLabel"), "App server label")
or app_name
)
allowed_tools = _optional_record_str_list(
raw_config, ("allowed_tools", "allowedTools"), f"Allowed tools for {app_name!r}"
)
app_require_approval = cast(
RequireApprovalSetting,
raw_config.get("require_approval", raw_config.get("requireApproval", require_approval)),
)
defer_loading = _optional_record_bool(
raw_config, ("defer_loading", "deferLoading"), f"Defer loading for {app_name!r}"
)
resolved_authorization = _resolve_authorization(
authorization, app_name, connector_id, raw_config
)
@@ -620,8 +652,10 @@ def _load_hosted_connector_tools(
app_name,
connector_id=connector_id,
authorization=resolved_authorization,
server_label=app_name,
require_approval=require_approval,
server_label=server_label,
allowed_tools=allowed_tools,
require_approval=app_require_approval,
defer_loading=defer_loading,
)
tools.extend(connector.tools)
@@ -633,17 +667,66 @@ def _plugin_package_path(
*,
package_root: str | Path | None,
) -> Path | None:
path_value = _plugin_package_path_value(record)
if path_value is None:
return None
root_path = Path(package_root).expanduser().resolve() if package_root is not None else None
path = Path(path_value).expanduser()
candidate = (
path.resolve() if path.is_absolute() else ((root_path or Path.cwd()) / path).resolve()
)
if root_path is not None and not _is_relative_to(candidate, root_path):
raise UserError(
f"Plugin package path must stay inside the connector package root: {path_value}"
)
return candidate
def _plugin_package_path_value(record: Mapping[str, Any]) -> str | None:
path_value = _optional_record_str(
record,
("package_path", "packagePath", "local_path", "localPath", "path"),
"Plugin package path",
)
if path_value is None:
return None
path = Path(path_value).expanduser()
if not path.is_absolute() and package_root is not None:
path = Path(package_root).expanduser() / path
return path.resolve()
if path_value is not None:
return path_value
for key in ("package", "mount", "local_package", "localPackage"):
nested = record.get(key)
if nested is None:
continue
if not isinstance(nested, Mapping):
raise UserError(f"Plugin {key} must be an object")
path_value = _optional_record_str(
nested,
("path", "package_path", "packagePath", "local_path", "localPath"),
f"Plugin {key} path",
)
if path_value is not None:
return path_value
return None
def _plugin_policy_labels(record: Mapping[str, Any]) -> tuple[ConnectorPolicyLabel, ...]:
policy_labels = _optional_record_policy_labels(
record, ("policy_labels", "policyLabels"), "Plugin policy labels"
)
if policy_labels is not None:
return policy_labels
policy = record.get("policy")
if policy is None:
return ()
if not isinstance(policy, Mapping):
raise UserError("Plugin policy must be an object")
return (
_optional_record_policy_labels(
policy,
("labels", "policy_labels", "policyLabels"),
"Plugin policy labels",
)
or ()
)
def _plugin_hosted_connector_configs(
@@ -712,6 +795,8 @@ def _plugin_metadata(plugin: ConnectorPlugin) -> dict[str, Any]:
metadata["hosted_connectors"] = {
app_name: dict(config) for app_name, config in plugin.hosted_connectors.items()
}
if plugin.policy_labels:
metadata["policy_labels"] = sorted(plugin.policy_labels)
return metadata
@@ -726,10 +811,38 @@ def _resolve_authorization(
if isinstance(authorization, str):
return authorization
if isinstance(authorization, Mapping):
return authorization.get(app_name) or authorization.get(connector_id)
for key in _authorization_lookup_keys(app_name, connector_id, app_config):
token = authorization.get(key)
if token is not None:
return token
return None
return authorization(app_name, connector_id, app_config)
def _authorization_lookup_keys(
app_name: str,
connector_id: str,
app_config: Mapping[str, Any],
) -> tuple[str, ...]:
keys = [app_name, connector_id]
for field_name in (
"authorization_alias",
"authorizationAlias",
"authorization_ref",
"authorizationRef",
"auth_alias",
"authAlias",
"auth_reference",
"authReference",
"connection_id",
"connectionId",
):
value = app_config.get(field_name)
if isinstance(value, str) and value:
keys.append(value)
return tuple(dict.fromkeys(keys))
def _read_json_object(path: Path) -> dict[str, Any]:
try:
value = json.loads(path.read_text())
@@ -786,6 +899,53 @@ def _optional_record_str(
return None
def _optional_record_str_list(
record: Mapping[str, Any],
keys: tuple[str, ...],
field_name: str,
) -> list[str] | None:
for key in keys:
value = record.get(key)
if value is not None:
return _expect_str_list(value, field_name)
return None
def _optional_record_bool(
record: Mapping[str, Any],
keys: tuple[str, ...],
field_name: str,
) -> bool:
for key in keys:
value = record.get(key)
if value is not None:
if not isinstance(value, bool):
raise UserError(f"{field_name} must be a boolean")
return value
return False
def _optional_record_policy_labels(
record: Mapping[str, Any],
keys: tuple[str, ...],
field_name: str,
) -> tuple[ConnectorPolicyLabel, ...] | None:
for key in keys:
value = record.get(key)
if value is not None:
return _expect_policy_labels(value, field_name)
return None
def _expect_policy_labels(value: Any, field_name: str) -> tuple[ConnectorPolicyLabel, ...]:
if not isinstance(value, list) or not all(isinstance(item, str) for item in value):
raise UserError(f"{field_name} must be a list of policy label strings")
unknown = sorted(set(value) - _CONNECTOR_POLICY_LABELS)
if unknown:
raise UserError(f"{field_name} contains unknown labels: {', '.join(unknown)}")
return tuple(cast(ConnectorPolicyLabel, item) for item in value)
def _expect_str_list(value: Any, field_name: str) -> list[str]:
if not isinstance(value, list) or not all(isinstance(item, str) for item in value):
raise UserError(f"{field_name} must be a list of strings")
+2 -1
View File
@@ -12,6 +12,7 @@ async def test_connector_package_demo_verifies_end_to_end() -> None:
assert summary["direct_tool_result"] == "discount=25.00"
assert summary["mcp_tool_result"] == "order demo_order_1001: fulfilled"
assert summary["package_registry_source"] == "unified_plugins_demo"
assert summary["hosted_connector_label"] == "calendar"
assert summary["hosted_connector_label"] == "google_calendar"
assert summary["hosted_allowed_tools"] == ["events_search"]
assert "apply_discount" in summary["agent_tool_names"]
assert "mcp_orders__lookup_order" in summary["agent_tool_names"]
+134
View File
@@ -343,6 +343,140 @@ def test_connector_registry_loads_hosted_app_connector_record() -> None:
assert tool_config["require_approval"] == "always"
def test_connector_registry_resolves_auth_alias_and_hosted_options() -> None:
registry = ConnectorRegistry.from_plugin_records(
[
{
"id": "plugin_workspace",
"name": "workspace",
"apps": {
"calendar": {
"connectorId": "connector_googlecalendar",
"authorizationAlias": "google_calendar_connection",
"serverLabel": "google_calendar",
"allowedTools": ["events_search"],
"requireApproval": "never",
"deferLoading": True,
}
},
}
]
)
connector = Connector.from_installed_plugin(
"plugin_workspace",
registry,
authorization={"google_calendar_connection": "conn_calendar"},
hosted_mcp_require_approval="always",
)
tool = connector.tools[0]
assert isinstance(tool, HostedMCPTool)
tool_config = cast(dict[str, Any], tool.tool_config)
assert tool_config["server_label"] == "google_calendar"
assert tool_config["connector_id"] == "connector_googlecalendar"
assert tool_config["authorization"] == "conn_calendar"
assert tool_config["allowed_tools"] == ["events_search"]
assert tool_config["require_approval"] == "never"
assert tool_config["defer_loading"] is True
def test_connector_registry_merges_policy_labels_from_plugin_record() -> None:
registry = ConnectorRegistry.from_plugin_records(
[
{
"id": "plugin_workspace",
"name": "workspace",
"policy": {
"labels": ["read_only", "external_send"],
},
"apps": {
"calendar": {
"id": "connector_googlecalendar",
}
},
}
]
)
connector = Connector.from_installed_plugin(
"plugin_workspace",
registry,
authorization={"calendar": "conn_calendar"},
)
assert connector.policy_labels == {"read_only", "external_send", "network"}
assert connector.metadata["unified_plugin"]["policy_labels"] == [
"external_send",
"read_only",
]
def test_connector_registry_resolves_mounted_package_paths(tmp_path) -> None:
plugins_root = tmp_path / "mounted-plugins"
plugin_dir = plugins_root / "orders"
plugin_config_dir = plugin_dir / ".codex-plugin"
plugin_config_dir.mkdir(parents=True)
(plugin_config_dir / "plugin.json").write_text(
json.dumps(
{
"name": "orders",
"version": "1.0.0",
"description": "Mounted order plugin.",
"mcpServers": "./.mcp.json",
}
)
)
(plugin_dir / ".mcp.json").write_text(
json.dumps(
{
"mcpServers": {
"orders": {
"command": "python",
"args": ["server.py"],
"cwd": ".",
}
}
}
)
)
registry = ConnectorRegistry.from_plugin_records(
[
{
"id": "plugin_orders",
"name": "orders",
"mount": {
"path": "orders",
},
}
],
package_root=plugins_root,
)
connector = Connector.from_installed_plugin("plugin_orders", registry)
assert connector.metadata["unified_plugin"]["package_path"] == str(plugin_dir.resolve())
assert connector.description == "Mounted order plugin."
assert len(connector.mcp_servers) == 1
def test_connector_registry_rejects_mounted_paths_outside_package_root(tmp_path) -> None:
with pytest.raises(UserError, match="must stay inside the connector package root"):
ConnectorRegistry.from_plugin_records(
[
{
"id": "plugin_orders",
"name": "orders",
"mount": {
"path": "../outside",
},
}
],
package_root=tmp_path / "mounted-plugins",
)
def test_connector_registry_skips_hosted_apps_without_authorization() -> None:
registry = ConnectorRegistry.from_plugin_records(
[