Files
teknium1 f86a484f78 test(security): per-profile secret scope E2E on a multiplexed serve (#120310 KNOWN)
One real `hermes serve` hosting two profiles (default + profiles/beta), each
with a distinct canary under the same .env variable name:

- control: the fixed sibling route GET /api/mcp/servers?profile=X resolves
  each profile's own ${SECRET} reference (multiplex active, scoping works)
- plugin_route (KNOWN #120310, strict xfail): a user dashboard plugin route
  reading get_secret() must return the requesting profile's value A->B->A;
  on main it raises UnscopedSecretError for every profile
- each profile's stdio MCP server env carries only its own configured
  secret: none of the other profile's canaries, never the launch provider key

Sabotage: PRs #120332/#120333/#120325 each turn plugin_route into
XPASS(strict); launch scope for ?profile= (web_server_profiles) reds the
control; unfiltered MCP env (mcp_tool_config._build_safe_env) and
os.environ ${VAR} lookup (config._env_ref_lookup) red the MCP env test.
2026-09-24 04:30:29 -07:00

89 lines
3.1 KiB
Python

"""Private harness for ``test_profile_secret_scope.py``: one real ``hermes serve`` hosting two
profiles, a user dashboard plugin whose API route reads a credential through ``get_secret``, and a
stdio MCP server per profile that dumps the environment it was spawned with.
Everything here writes plain files into the fake home; no Hermes code is imported or patched.
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
PLUGIN_ID = "scopecanary"
SECRET_ENV = "SCOPE_CANARY_SECRET" # same NAME in every profile's .env, distinct VALUE
MCP_ENV_KEY = "SCOPE_MCP_OWN_SECRET" # the one variable a profile deliberately hands its MCP server
MCP_SERVER_NAME = "envdump"
# The plugin is third-party code: it only uses the documented credential API and folds every
# failure into its reply (the plugin "no data" contract), so the test sees exactly what it saw.
_PLUGIN_API = f'''
from fastapi import APIRouter
router = APIRouter()
@router.get("/secret")
def secret() -> dict:
try:
from agent.secret_scope import get_secret
return {{"ok": True, "value": get_secret("{SECRET_ENV}")}}
except Exception as exc: # plugin contract: never raise
return {{"ok": False, "error": type(exc).__name__}}
'''
# A stdio MCP server that records os.environ at spawn into the file named by argv[1], then serves
# one trivial tool so the client's handshake completes like any real server's.
_ENV_DUMP_SERVER = '''
import json, os, sys
out = sys.argv[1]
tmp = out + ".part"
with open(tmp, "w", encoding="utf-8") as fh:
json.dump(dict(os.environ), fh)
os.replace(tmp, out)
from mcp.server import MCPServer
server = MCPServer("envdump")
@server.tool()
def envdump_ping(nonce: str = "") -> str:
"""Echo the nonce."""
return nonce
server.run(transport="stdio")
'''
def install_plugin(hermes_home: Path) -> None:
"""A user plugin with a backend API under the launch home's plugins dir."""
dash = hermes_home / "plugins" / PLUGIN_ID / "dashboard"
dash.mkdir(parents=True, exist_ok=True)
(hermes_home / "plugins" / PLUGIN_ID / "plugin.yaml").write_text(
f"name: {PLUGIN_ID}\nversion: 0.0.1\ndescription: secret-scope canary\n", encoding="utf-8")
(dash / "manifest.json").write_text(json.dumps({
"name": PLUGIN_ID, "label": "Scope canary", "version": "0.0.1",
"tab": {"path": f"/{PLUGIN_ID}", "hidden": True}, "entry": "dist/index.js", "api": "plugin_api.py",
}), encoding="utf-8")
(dash / "dist").mkdir(exist_ok=True)
(dash / "dist" / "index.js").write_text("// no UI\n", encoding="utf-8")
(dash / "plugin_api.py").write_text(_PLUGIN_API, encoding="utf-8")
def write_env_dump_server(path: Path) -> Path:
path.write_text(_ENV_DUMP_SERVER, encoding="utf-8")
return path
def mcp_config(server_script: Path, dump_file: Path) -> dict:
"""``mcp_servers`` entry: the profile passes exactly one secret, by reference to its own .env."""
return {MCP_SERVER_NAME: {
"command": sys.executable,
"args": [str(server_script), str(dump_file)],
"env": {MCP_ENV_KEY: "${" + SECRET_ENV + "}"},
}}