awslabs / awslabs/cli-agent-orchestrator
A profile's `tools:` allowlist is not enforced for CAO MCP tools on any provider
- Dominant language
- Python
- Stars
- 1.3k
- Forks
- 267
- Avg merge
- 1d 23h
- Merged PRs (30d)
- 70
Description
## Scope — verifiable in one command
This is pre-existing and unrelated to any feature work in flight. `src/cli_agent_orchestrator/utils/tool_mapping.py` is **byte-identical to `origin/main`** (`f6c5794`), so everything below reproduces on a clean main checkout.
Measured against `origin/main f6c5794`, `tool_mapping.py` sha256 prefix `524ddff3a3fa1f19`. If the mapping table changes, re-derive rather than trust these numbers.
## Summary
An agent profile can declare a narrow `tools:` allowlist, but that list is only translated into *provider-native* tool restrictions. It is never enforced for CAO's own MCP tools, on any provider. An agent whose profile declares one tool therefore retains the full CAO tool surface — including `assign` and `handoff`, which spawn a terminal under a caller-chosen `agent_profile`, minting a new identity with its own memory scope.
## The correct pattern already exists in this codebase and was simply not extended
`mcp_server/server.py:2294-2308` gates cross-agent `store_lesson` writes exactly right:
```python
caller_profile = terminal_context.get("agent_profile") # from the terminal record, never tool args
if target != caller_profile:
if not _caller_has_store_lesson_capability(caller_profile): # operator-owned profile frontmatter
return {"success": False, "error": "...not authorized to store lessons for..."}
```
Server-side check, identity from the registered record rather than from arguments, capability from a file a worker cannot edit through MCP, fails closed on lookup error. `assign` and `handoff` have no equivalent.
## Evidence
Run from a repo checkout with dependencies installed:
```
uv run python -c '
from cli_agent_orchestrator.utils.tool_mapping import get_disallowed_tools
for p in ("claude_code","codex","kiro_cli","copilot_cli","cursor_cli"):
d = get_disallowed_tools(p, ["memory_recall"])
print(p, len(d), "assign denied?", "assign" in [x.lower() for x in d])'
```
```
claude_code 14 assign denied? False
codex 0 assign denied? False
kiro_cli 0 assign denied? False
copilot_cli 5 assign denied? False
cursor_cli 0 assign denied? False
mcp__ entries in the denied set, any provider: NONE
```
Output is identical under a clean `CAO_HOME_DIR`, and the function reads no environment, settings, or filesystem state — so the numbers are frame-independent.
Two enforcement mechanisms exist, neither covering CAO tools:
- `providers/claude_code.py:474-482` — translates the allowlist into `--disallowedTools`, native names only.
- `providers/codex.py:881-886` — a prompt sentence, with the code stating *"Codex has no native tool restriction mechanism."* Advisory by construction.
And there is **no general MCP-level check**. The only caller-side allowlist inspections in `mcp_server/server.py` are at `1609-1622` (scoped to `DISCOVERY_TOOL_MARKER`) and the `store_lesson` capability check above. Every other `allowed_tools` reference resolves a *child's* list during `assign`/`handoff` (`134-168`, `241-273`). The server does not consult the calling terminal's allowlist before executing a tool.
## Impact
A profile's allowlist is documentation rather than a control. `agent_store/memory_manager.md` declares `tools: [memory_recall]`; that agent can nonetheless call `assign(agent_profile="developer", ...)`, and the child — not being the restricted profile — reads its own agent-scoped memories and reports back. On codex/kiro_cli/cursor_cli it additionally retains shell and filesystem access.
## Bound, stated so severity is not over-read
The child reaches only the identity it was *spawned as*: `memory_recall` has no `scope_id` parameter and the child's `scope_id` derives from its own terminal context. So this is "an agent can mint an arbitrary identity and read that identity's memories," **not** "an agent can name a third party's scope."
## Suggested direction
Extend the `store_lesson` pattern: check the calling terminal's `allowed_tools` server-side before dispatching any tool — the one place that behaves uniformly regardless of provider. Provider-native restriction stays as defence in depth for non-CAO tools. If profiles may declare lists the system cannot enforce, say so in the profile documentation.
Contributor guide
Research direction
Start in mcp_server/server.py with the existing store_lesson capability check at lines 2294-2308, then trace the server-side tool dispatch path and the allowed_tools references around lines 134-168, 241-273, and 1609-1622. Ensure the calling terminal's allowlist is enforced for CAO MCP tools while retaining the provider restrictions in utils/tool_mapping.py and providers/claude_code.py and codex.py. Done means a restricted profile cannot invoke unlisted CAO tools such as assign or handoff, with failures handled closed.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- authorization, security
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 65/100