EKOSdocs
Docs / Architecture Learn

Permissions

Where EKOS enforces who may do what and what each reader may see, with the exact code lines and the tests that cover them.

Permissions are enforced in two places: one role check in the web console (who may do what) and one read filter in the Rust ledger (what someone may see of a project). The MCP gateway, the chat and the web command runner reuse those two checks instead of adding their own. Every reference below links to the exact line at commit 3df6fd0, and every rule names the tests that fail if it breaks.

Every path meets one role check and one ledger filter, except the bare CLI
Every path meets one role check and one ledger filter, except the bare CLI

The numbers match the sections below. Only the ekos command run directly skips them.

1. Who may do what: the console's role check

Every role comparison in the console happens in one function, authorize(). Each route declares which dependency calls it, and a test walks every route to prove none is missing.

Rule Where it is enforced
Four roles map to actions: viewer → read; reviewer → read, review; operator → read, operate; admin → all four ROLE_ACTIONS, authz.py:25
The only place a role is compared authorize(), authz.py:161
An API token (an agent's credential) may only read and operate, whatever role it was granted, so review and administration stay with people TOKEN_ACTIONS, authz.py:34
Platform owners hold every role roles_in(), authz.py:84
Routes reach the check through allow(action), allow_tenant, allow_platform or authenticated authz.py:300, authz.py:331, authz.py:348, authz.py:264
A refusal is 403 and is written to the audit log require(), authz.py:223
A project you have no role in answers 404, not 403, so its existence is not revealed workspace_for(), authz.py:277
A request becomes a principal (user, token, local owner) auth.py:130, auth.py:192
Local mode accepts only a loopback client address and a loopback Host header LoopbackOnly, main.py:115

Tests that fail if this breaks:

2. What someone may see: the read scope in the ledger

Limits inside a project (to some sources, paths or objects) are enforced in the Rust ledger, not in the console. ScopedStore wraps the knowledge store, and the query runtime, the EKL query language, graph export and every command read through that store interface, so a new reader cannot bypass the filter.

Rule Where it is enforced
The store wrapper that filters every read ScopedStore, scope.rs:326; its trait implementation at scope.rs:482
The decision for one object; a deny rule always wins ReadScope::visible, scope.rs:239
An object of unknown source or path counts as inside a denied area, and outside an allowed one denies, scope.rs:211; allows, scope.rs:224
The console turns a person's role bindings into a scope scope_for(), authz.py:124
Every MCP call a console route makes carries that scope; this is the only way a route gets an MCP client mcp_for_workspace, deps.py:26
The Rust MCP server reads the scope per call; a scope that does not parse fails the call instead of being ignored mcp.rs:1543
Under a restricting scope only the scoped tools are served; every other tool is refused SCOPED_TOOLS, mcp.rs:104; refusal at mcp.rs:1562

Tests that fail if this breaks:

3. Paths that reuse those two checks

The MCP gateway for agents, the chat and the web command runner each call the role check from section 1 and pass the scope from section 2. None of them decides permissions on its own.

Path What it enforces Code Tests
MCP gateway for agents Role check plus the scoped-tool list per tool call; the forwarded call is rebuilt from the tool name and arguments only, so a client cannot send its own scope _Caller.may, mcp_gateway.py:74; action_for, mcp_policy.py:36; mcp_gateway.py:115 test_a_viewer_token_sees_and_calls_only_read_tools, test_an_operator_token_may_write_a_note_and_still_cannot_review, test_a_scope_sent_by_the_client_is_dropped, test_the_scoped_tool_list_matches_the_server
Chat The model is offered only tools that need read and that the gateway's check allows, through the scoped client; a paid model needs the tenant's switch chat.py:167; chat.py:141 test_the_chat_never_names_a_review_tool, test_a_restricted_reader_chats_within_their_scope, test_a_paid_model_needs_the_tenant_switch
CLI commands from the web Each command's class maps to an action; admin and external commands need a typed confirmation; paths must stay inside the project; the reviewer is always the signed-in person ACTIONS, catalogue.py:26; CONFIRMED, :33; :85; build_argv, :89 test_rights_follow_the_class, test_a_token_cannot_review, test_a_review_names_the_person_not_the_form, test_a_path_must_stay_inside_the_project
Review decisions stay human-only No MCP code may reach a human-only decision: semantics review, conflict resolution, migration approval, session lifecycle Enforced by tests that read commands/mcp.rs no_mcp_code_can_reach_the_review_lifecycle, no_mcp_code_can_reach_the_resolve_path, no_mcp_code_can_reach_the_migration_lifecycle, no_mcp_code_can_reach_the_lifecycle_module

Limits

These checks cover the web console and its MCP gateway; four things fall outside them.

  • The ekos command checks no permissions. Whoever can run it against a project's files sees everything. The protection there is the operating system's file permissions on the EKOS home.
  • An MCP tool unknown to the console's policy is treated as a read (mcp_policy.py:36). A restricted reader still cannot reach it, because it is not in the scoped list. A new tool that writes must be added to that file, or viewers could call it; no test covers that case.
  • Derived objects carry no source (rollups, business-semantics items). A source-restricted reader therefore does not see them: safe, but those readers get less complete answers.
  • No outside audit. Real OIDC sign-in, roles, two tenants and a real MCP client run end to end against the Dex identity provider in deploy/e2e/run.sh, but all of this was written and tested by the project itself.

Check it yourself

Each group of tests runs on its own, from a clone of the repository:

# Console role checks, gateway, chat, web commands (Python; EKOS_BIN = a built ekos binary)
cd web/api && EKOS_BIN=$(realpath ../../ekos/target/release/ekos) uv run pytest tests/test_authz.py tests/test_authz_routes.py tests/test_mcp_gateway.py tests/test_chat.py tests/test_cli_route.py tests/test_local_mode.py

# Read scope: the ledger filter and the leak suite against a real MCP server (Rust)
cd ekos && cargo test -p ekos-ledger scope && cargo test -p ekos --test read_scope

# Human-only decisions and the per-tool scope classification
cd ekos && cargo test -p ekos --lib -- no_mcp_code_can_reach every_tool_has_a_read_scope_decision