awslabs / awslabs/cli-agent-orchestrator

A profile's `tools:` allowlist is not enforced for CAO MCP tools on any provider

Open
#671 3 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.