PR-3 of the multi-workspace MCP rollout. PR-1 made the MCP server itself
multi-workspace aware (one process, N workspace memberships). PR-2 added
source_workspace_id threading to delegate_task / list_peers. This change
closes the remaining workspace-scoped tools so a single agent registered
into multiple workspaces no longer leaks memories or chat history across
tenants.
Tools now accepting `source_workspace_id`:
- tool_commit_memory(content, scope, source_workspace_id=None) —
routes POST to /workspaces/{src}/memories with the source workspace's
Bearer token. Body still embeds source_workspace_id for the platform's
audit + namespace-isolation enforcement.
- tool_recall_memory(query, scope, source_workspace_id=None) —
GET /workspaces/{src}/memories with the source workspace's token and
?workspace_id={src} query so the platform scopes the read to the
caller's tenant view (PR-1 / multi-workspace mode).
- tool_chat_history(peer_id, limit, before_ts, source_workspace_id=None)
— auto-routes via the _peer_to_source cache populated by list_peers,
with explicit override winning. Falls back to module-level WORKSPACE_ID
if neither is available. URL: /workspaces/{src}/chat-history.
- tool_get_workspace_info(source_workspace_id=None) — GET /workspaces/{src}
with the source workspace's token. Useful for introspecting any
workspace the agent is registered into, not just the primary.
In every path, `src = source_workspace_id or WORKSPACE_ID`, so
single-workspace operators see no behavior change. Tokens are resolved
per-workspace via auth_headers(src) / _auth_headers_for_heartbeat(src),
which fall through to the legacy AUTH_TOKEN env when not in
multi-workspace mode.
Also updates input_schemas in platform_tools/registry.py so the new
optional parameter is advertised to LLM clients (claude-code,
hermes-agent, langchain wrappers).
Tests (4 new classes in test_a2a_multi_workspace.py, 21 new tests):
- TestCommitMemorySourceRouting — URL + Authorization header per source
- TestRecallMemorySourceRouting — URL + query param + Authorization
- TestChatHistorySourceRouting — peer-cache auto-route + explicit override
- TestGetWorkspaceInfoSourceRouting — URL + Authorization
Inbox tools (peek/pop/wait_for_message) already multi-workspace aware
since PR-1 — inbox.py spawns per-workspace pollers and tags every
InboxMessage with arrival_workspace_id. No further plumbing needed.
Suite: 1700 passed, 3 skipped, 2 xfailed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|---|---|---|
| .. | ||
| __init__.py | ||
| README.md | ||
| registry.py | ||
Platform tool registry
Single source of truth for every tool the platform exposes to agents (A2A delegation, hierarchical memory, broadcast, introspection).
Why this exists
Pre-#2240, three places independently declared each tool:
- MCP server (
workspace/a2a_mcp_server.py) — theTOOLSJSON list - LangChain
@toolwrappers (workspace/builtin_tools/{delegation,memory}.py) - Agent-facing system-prompt docs (
workspace/executor_helpers.py)
Adding a tool to one and forgetting the others happened repeatedly. The
canonical case: send_message_to_user was registered in MCP TOOLS but
the executor_helpers doc string never mentioned it, so agents saw the
tool as available but had no usage guidance — a silent capability
regression.
What the registry does
registry.py defines each tool ONCE as a frozen ToolSpec:
ToolSpec(
name="delegate_task",
short="Delegate a task to a peer workspace via A2A and WAIT for the response.",
when_to_use="Use for QUICK questions and small sub-tasks where you can afford to wait inline...",
input_schema={...}, # JSON Schema, consumed by MCP server
impl=tool_delegate_task, # the actual coroutine
section="a2a", # which prompt section it belongs to
)
Adapters consume specs; no hardcoded names anywhere else:
- MCP server builds its
TOOLSlist from_PLATFORM_TOOL_SPECSat import time - LangChain
@toolwrappers readname=spec.namefrom the registry - Doc generator (
executor_helpers._render_section()) produces the system-prompt block fromspec.short(bullet) +spec.when_to_use(heading + paragraph)
CLI subprocess block — special case
Non-MCP runtimes (ollama, custom subprocess adapters) use a separate
hand-maintained block in executor_helpers._A2A_INSTRUCTIONS_CLI because
the CLI subcommand vocabulary (peers, delegate, status, info)
differs from the MCP tool names (list_peers, delegate_task, etc.).
Auto-generation would lose the readable invocation syntax.
Alignment is enforced via _CLI_A2A_COMMAND_KEYWORDS (in
executor_helpers.py): every a2a-section spec must be keyed there with
either a CLI subcommand keyword OR an explicit None if the tool is
intentionally not exposed via subprocess (e.g.
send_message_to_user because its structured attachments field
doesn't survive positional-arg shell invocation).
Tests that catch drift
workspace/tests/test_platform_tools.py:
| Test | What it catches |
|---|---|
test_mcp_server_registers_every_registry_tool |
MCP TOOLS list out of sync with registry |
test_mcp_tool_descriptions_match_registry_short |
hand-edited MCP description that drifted |
test_mcp_tool_input_schemas_match_registry |
schema duplicated in server file |
test_a2a_instructions_text_includes_every_a2a_tool |
doc generator missed a tool |
test_old_pre_rename_names_not_present_in_docs |
stale name leaked back in |
test_a2a_mcp_instructions_match_snapshot |
rendered shape (bullet ordering, headings, footers) drifted |
test_a2a_cli_instructions_match_snapshot |
CLI block edited in a way that changes shape |
test_hma_instructions_match_snapshot |
HMA section drifted |
test_cli_keyword_mapping_covers_every_a2a_tool |
tool added to registry without a CLI mapping decision |
test_cli_keyword_substrings_appear_in_cli_block |
CLI keyword in the mapping but missing from the doc block |
The snapshot files at workspace/tests/snapshots/*.txt are LF-pinned
in .gitattributes so a Windows contributor with core.autocrlf=true
doesn't get mysterious test failures.
Adding a new tool
- Append a
ToolSpec(...)toTOOLSinregistry.py. - Add the LangChain
@toolwrapper inworkspace/builtin_tools/(the wrapper body just callsspec.impl). - Update
_CLI_A2A_COMMAND_KEYWORDSinexecutor_helpers.py— set the value to the CLI subcommand keyword, or toNoneif the tool isn't exposed via the subprocess interface. - Regenerate snapshots — see the comment block at the top of
workspace/tests/test_platform_tools.pyfor the one-liner. - Run
pytest workspace/tests/test_platform_tools.py --no-cov.
Renaming a tool
Edit name in registry.py only. Then:
- The MCP TOOLS list rebuilds automatically.
- The doc generator regenerates automatically (snapshots will fail the diff — regenerate them).
- Search
workspace/for the old literal in case a non-adapter consumer (tests, plugin code) hardcoded the old name; update those. - Update any
_CLI_A2A_COMMAND_KEYWORDSkey + the literal substring in_A2A_INSTRUCTIONS_CLIif applicable.
Removing a tool
Delete the ToolSpec and the _CLI_A2A_COMMAND_KEYWORDS key. Adapters
and doc generators stop registering it automatically; the structural
tests prevent stale references from surviving.