Repository navigation
fix(auth): an agent key reaches only itself, its edges and the agents it spawned — on the raw backend routes too (trinity-enterprise#629) - #3346
Conversation
… it spawned — on the raw backend routes too (trinity-enterprise#629) An agent-scoped MCP key resolves to its OWNER carrying the owner's role (Invariant #8), so every access/owner predicate said yes for any sibling the owner holds — on an admin-owned install, every agent. The agent_permissions edge the MCP layer checks (P-02) did not exist one hop down: with its injected key and TRINITY_BACKEND_URL, agent A could chat with, task, loop, read the history and logs of, or replace the git token of any sibling B. Ruling (operator, 2026-10-06): fix the hole. Narrowed in the shared gate, not per endpoint (the #1890 lesson): - dependencies.agent_may_reach(current_user, target, manage=): * use tier — get_authorized_agent[_by_name], assert_agent_access, A2A inbound: itself, an edge target, an agent it spawned with its current key; * manage tier — get_owned_agent[_by_name], assert_agent_owner: itself or an agent it spawned. An edge means "may call", never "may manage". Outside its reach reads as the uniform 404 (#186). Every other principal is unchanged. The edge read fails closed, like the MCP layer's. - get_skill_managed_agent_by_name keeps the plain owner fence: skills.manage (ent#596) IS the grant to manage the owner's other agents. - Every inline can_user_access_agent / can_user_share_agent call (16 files) ANDs the reach in; nevermined's admin shortcut no longer skips it. - The operator queue had its own predicate (admin role → no filter): an agent key now always gets a set narrowed to its reach. - MCP: every `baselined: ENT629` row in TOOL_ACCESS_POLICY is `enforce` on its agent parameter; the "tool-surface gate only" notes now name the backend fence. Collection reads (the agent list, event-subscription list, MCP key list) are not narrowed: they disclose names, never act on an agent. Tests: tests/unit/test_ent629_agent_reach.py — the reach truth table on a real schema, every helper, A2A inbound, the operator queue, the skills capability keeping its reach, the dispatch routes resolving through the narrowed gate, and an AST guard that fails the build on an inline access check without the reach (exemptions carry their reason). Mutation-checked: reach always-true, edge-as-manage, the access dependency dropping it, and an inline site dropping it each turn tests red. Docs: Invariant #8, security.md §5, P-02, the J10 docstring, mcp-server.md and the affected feature flows and requirements. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…b; regression fixes (trinity-enterprise#629) From the regression run over every unit file touching a changed module (14 new failures vs dev, all traced): - The skill fence's owner check is its own seam again, `get_capability_owned_agent_by_name` (not narrowed: the capability is the grant), so the ent#596 recorder patches a named function. - files.py: a skills.manage holder writing a sibling's skills path reaches it without an edge (`_may_reach_for_path`); every other path is narrowed. The capability gate still decides the write. - share_agent_file checks "an agent key shares only its own volume" before the (now narrowed) owner gate, so a sibling gets the specific refusal. - The depth suites (#2806, #2973) seed the A→B edge their sibling calls need. - The edge read fails closed (a fixture schema without agent_permissions refused instead of raising — the enterprise assignment suites). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Local regression runI ran 257 unit files: every file that imports or exercises a module this PR changes.
After the fixes: every affected file passes (ent#596, ent#629, ent#549, #1310, #2806, #2973: 235 passed). The enterprise assignment suites pass too (112). The edge read fails closed, so a fixture schema with no 🤖 Generated with Claude Code |
|
merge-train: ejected from this batch — rides the next train once fixed. It conflicts with |
Conflicts with #3236 (ent#164) and #3208/#753 (skill gates): imports and the MCP access table keep both sides; files.py keeps both helpers. Semantic fix: ent#629's narrowed owner gate refused what ent#164 and ent#753 grant — an agents.manage holder reconfiguring its owner's other agents, and a skills.manage holder setting a sibling's skill gate (7 tests red after the textual merge). A capability whose grant IS the reach keeps it, bounded to agents the owner OWNS: - agent_may_reach(manage=True) admits an agents.manage holder on an owner-owned target (_manages_owners_agent, fail-closed); reads are not widened. - The skill-gate writes resolve the agent with get_capability_owned_agent, the path-dependency form of the capability owner fence, after require_person_or_capability has refused a non-holder. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Merged Textual conflicts, with both sides kept: the Semantic conflict. The textual merge left 7 tests red. ent#629's narrowed owner gate refused two things that #3236 and #3208 grant:
Fix: a capability whose grant is the reach keeps that reach, but only to agents its owner owns (
Local: ent164, ent629, ent596, 2996 ×2, ent753 routes, 1310 ×2, 2806, 186 and 293 give 449 passed. MCP 🤖 Generated with Claude Code |
…pawned, read directly (trinity-enterprise#629) dev's ent#611/#715/#815 queue suites (29 tests) went red after the merge: an admin-owned agent key's scope was built from the owner's accessible-agent list and then filtered agent by agent, so a self or permitted agent without an ownership row (every one in those fixtures) dropped out. The scope now starts from {self} ∪ the agent's edges — the MCP's own rule — bounded by a non-admin owner's set, plus the owner's agents it spawned. The edge read fails closed. The router also gets the logger it was missing. ent#715's heads-ups fixture seeds the agent key's edge to the agent it reads, so the test still checks that person rows are withheld, not that the whole queue is refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
merge-train: ejected from this batch — rides the next train once fixed. The narrowed reach removes cross-agent reach from capabilities that
CI is green because no test drives these holders against a sibling. The fix is a design call — how a passed capability fence tells the reach gate to stand down ( Mechanical, same push:
Lane C also wants |
|
merge-train (2026-10-10): ejected — the branch has not moved since the 2026-10-08 ejection and both criticals reproduce on the merged tree: (1) |
|
Resolve by merging |
# Conflicts: # docs/memory/architecture.md # src/backend/routers/agents.py # src/mcp-server/src/access.ts
…ity-enterprise#629)
Merge-train review (2026-10-08, 2026-10-10): the narrowed gate revoked the
cross-agent reach that ent#164 capabilities exist to grant. A
`schedules.manage` holder got 404/403 on a sibling's schedule routes, an
`instructions.manage` holder on its instruction files and git reset, and an
`agents.manage` holder on `PUT /{name}/model`, because `agent_may_reach`
never saw which capability the route had checked.
- The two capability fences (`capability_fence`,
`require_person_or_capability`) record the target they admitted on the
request's principal (`User._capability_reach`, a private attribute), after
the grant check and the owner bound. `agent_may_reach` honours that record
on both tiers. This replaces `_manages_owners_agent`, which widened every
owner check for an `agents.manage` holder on every route.
- `files._may_reach_for_path`: the path exception covers `instructions.manage`
as well as `skills.manage`, needs the grant for each file class the path
touches, and is bounded to agents the owner OWNS (`owner_owns_agent`). It
used to reach any agent the owner could access.
- `loops._check_loop_access`: reach is checked before the admin and
initiator shortcuts, so an agent key cannot read or stop a sibling's loop
by id.
- Operator queue: the spawned set is one query
(`list_agents_spawned_by_key`), not two lookups per held agent.
Also merges dev: the agents.py activity routes stay deleted (#3483), and
access.ts takes dev's owner constants alongside this PR's enforce rows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Answering the 2026-10-08 and 2026-10-10 ejections. Critical 1: capabilities lost their reach. I went with a per-request record instead of widening the gate.
Manual schedule trigger has no capability fence on Critical 2: Mechanical items:
Tests: 8 new in
Mutation check: dropping the record in Locally, 950 passed and 7 xfailed across ent164, ent629, ent596, 2996 ×4, ent753 routes, 1310 ×2, 186, 293, 2806, 2973, operator-queue, loops, file-path, facade-parity and models-centralized. MCP 🤖 Generated with Claude Code |
…sal (trinity-enterprise#629)
J10 `test_without_permission_an_agent_cannot_read_or_stop_a_loop_on_another`
failed on journey-smoke. Since the backend checks an agent key's reach before
the admin shortcut in `_check_loop_access`, `GET /api/loops/{id}` answers 403
for a loop on an agent outside the caller's reach. `get_loop_status` and
`stop_loop` resolve the loop through that read before their own gate, so the
caller got a raw `API error (403)` instead of "Loop '<id>' not found or not
accessible" and the hint.
A 403 on the read now returns the same uniform body as the MCP gate, with the
same log line and audit row. The stop is still never sent. A 404 (no such
loop) keeps the backend's own shape, as the existing test pins.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… owner fence (trinity-enterprise#629) The backend regression diff caught 4 new failures in `test_3435_credential_tool_gates::test_agent_key_is_refused_human_only_even_for_its_owner`. With the narrowed owner fence, `get_owned_agent_by_name` answers an agent key on a sibling with the uniform 404, so the handler-body `reject_agent_principal` never ran. The agent was told "Agent not found" instead of the rule it hit. `dependencies.human_only` is the route-level form of `reject_agent_principal`. On export and import it now runs before the ownership dependency. The refusal depends on the principal alone, before any lookup, so it discloses nothing about the target (#186). The inline call stays as belt-and-braces. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@dolho, I reviewed Verdict: it does what trinity-enterprise#629 asks. The six per-agent reads my release probe made with a sibling's key now all go through the narrowed checks: agent detail, schedules, files, skills, and capability grants on two agents. So do the sibling file downloads. Nothing here blocks the merge. There are three things I'd fix first, some test gaps, a design question, and the description to refresh. What I ran
1.
2. MCP
3. The new
4. Test gaps (mutations that survive)
5. Design question: an edge also lets an agent read everyone's chats with the target
6. PR description
Learnings fragment for the "bound fixed on one of two paths" class. Suggested file: ## 2026-10-12 — pitfall — a capability's owner bound must live in ONE helper, not in each path that honours the grant
**Context**: abilityai/trinity#3346 (trinity-enterprise#629 × #596/#164). `skills.manage` is honoured on two paths: the skills routes (`dependencies.get_skill_managed_agent_by_name`) and the files tab (`files._may_reach_for_path`). A review asked for the owner bound on the files path; it was added there only, so on an admin-owned install the skills routes still admitted another user's agent — `can_user_share_agent` short-circuits True for `role == "admin"`, and an agent key carries its owner's role.
**Lesson**: when a grant is honoured on more than one path, put its bound (grant check → `owner_owns_agent` → record) in one helper every path calls, and test each path with an agent owned by ANOTHER user under an admin owner. `can_user_share_agent` / `can_user_access_agent` are never an owner bound for an agent principal.Minor: |
… the #3346 review (trinity-enterprise#629) Addresses webmixgamer's review of f9cd838. 1. `skills.manage` reached another user's agent through the skills routes on an admin-owned install. `get_skill_managed_agent_by_name` went through the unnarrowed owner fence, which `can_user_share_agent` answers True for any agent when the owner is admin; the files tab refused the same target. The skills fence now applies the bound every capability fence applies (grant, then `owner_owns_agent`, then record the reach) and returns the narrowed `get_owned_agent_by_name`. `can_manage_agent_skills` takes the same bound. The skill-gate writes use `get_owned_agent_by_name` (`_gate_writer` records first), so `get_capability_owned_agent[_by_name]` and `_owned_agent_or_404(narrow=False)` are gone: one mechanism. 2. The MCP `enforce` gate read the lenient permissions list, which is built from the Docker-filtered fleet. A ghost with no container, a child being recreated, a spawned child whose edge was removed, and every call during a Docker fault all read as "not permitted". New `GET /api/agents/{caller}/reach/{target}` answers from the edge or the spawn record in the database; `TrinityClient.isAgentPermitted` reads it and fails closed. 3. security.md no longer says collection reads "disclose names, never act": fleet-level routes still answer an agent key with its owner's reach, some of them write, and some return task text and emails. A private follow-up tracks them. 4. Tests for the review's surviving mutations: - credential export's `human_only`; - the queue's non-admin bound; - `deleted_at` in the spawned set; - a schedules holder through the real route via TestClient (same principal object for the fence and the path dependency); - another user's agent on the skills fence; - the reach endpoint. Each mutation now fails a test. 5. An agent key never gets the admin "every user" chat history or sessions view: "may call" is not "may read everyone's conversations with it". Also: learnings fragment (capability bound in one helper). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@webmixgamer thanks, all six are addressed in
I added the learnings fragment as you proposed ( Locally: ent629 41/41; related suites 777 passed + 7 xfailed; MCP 885/885, 🤖 Generated with Claude Code |
|
@dolho thanks, I checked
Three things left, the first one red on CI:
Optional: I'll approve once CI is green. The fleet-level follow-up is filed privately as trinity-enterprise#872. |
…inity-enterprise#629) The backend regression diff caught `test_probe_is_a_persons_even_for_a_key_that_may_manage_skills[agent_key_with_skills_manage]`. Since the #3346 review, an agent key's `skills.manage` reaches only agents its owner OWNS (`owner_owns_agent`), not every agent `can_user_share_agent` admits. The fixture stubbed "may share" but never said who owns `gated-agent`, so the bound read "unknown owner" and refused. The test's premise is unchanged: the key's owner owns the agent, so the manage check admits it and the probe is still refused for a key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
webmixgamer
left a comment
There was a problem hiding this comment.
Approving 2cc17964e: CI is green and the ent754 fixture is fixed. Points 2 and 3 from my last comment are still open: isAgentPermitted drops the workspace_only refusal, and the credential-import guard has no test like the export one. Both are small, and fine as a quick follow-up.
…eraction with #3346) #3346 (trinity-enterprise#629) bounds an agent key to its own agent, its permission edges and the agents it spawned. The read-tier tests here use a 'sibling' agent key against 'shared-agent' with only the access layer stubbed, so seven of them hit the new reach refusal before the owner-tier gate they test. Stub reach alongside access; reach refusal on the file routes stays pinned by test_ent629_agent_reach.py. Mechanical, per the merge-train note on the PR. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… follow a link; runtime config files join the write deny lists (#3304) * docs(files): credential files are owner-tier to read; reads never follow a link; runtime config files join the write lists Requirements (content-files §13.1, GUARD-002 §28) and the file-routes bullet in the architecture area file, ahead of the code. Refs Abilityai/trinity-enterprise#819 Refs Abilityai/trinity-enterprise#823 * fix(files): credential files are owner-tier to read on download and preview One credential constant feeds the write list and a new owner-tier read set: the credential paths plus .git/config, the /proc and /dev spellings, the Trinity-managed copies (.trinity/git-credential, .trinity/backup/*) and the secret file classes (.kube/config, .config/gcloud/*, *.key, *.pem, *.p12, *.pfx). Download and preview refuse an owner-tier path unless the caller is a person who passes the owner tier (owner or admin); the PERSON gate runs first. A NUL byte is refused with 400 before the agent is called, and the agent is sent the normalised path that was checked. The 403 body says what a teammate can still do and whom to ask. Refs Abilityai/trinity-enterprise#819 * fix(files): audit owner-tier read decisions Every refused owner-tier read writes one file_read_refused row, and every allowed one by anyone other than the owner's own signed-in session writes file_read_allowed (AUTHORIZATION, routed endpoint, normalised path, rule). A person is filed as actor_user; an agent key as its agent; other keys by scope and key, never as the owner. The audit is best-effort: a failure never changes the answer. Refs Abilityai/trinity-enterprise#819 * fix(files): runtime config files join the credential constant ~/.claude.json, ~/.claude/.credentials.json, ~/.gemini/settings.json and ~/.tmp/codex/* (the Codex runtime's login and MCP config) are refused on write, mkdir and delete at the backend and are owner-tier to read. Deleting .tmp or .tmp/codex through the Files routes is refused, since they hold the Codex login. Refs Abilityai/trinity-enterprise#823 * fix(base-image): the file guardrail denies the runtime config files path_deny gains ~/.claude.json, ~/.claude/.credentials.json, ~/.gemini/settings.json and ~/.tmp/codex/*, and the image-build smoke gains a ~/.claude.json row, so the build proves the baseline. Refs Abilityai/trinity-enterprise#823 * fix(base-image): the agent server never writes runtime config through its file routes PROTECTED_PATHS and EDIT_PROTECTED_PATHS gain .claude.json and .credentials.json. PUT, mkdir and DELETE also refuse any resolved target inside the runtime config paths (Claude Code's two login files, Gemini's settings.json, Codex's .tmp/codex), so a link elsewhere in the home that points at one is refused too; DELETE refuses a directory above one. The home is a module constant so the handlers run over a temporary home in tests. Snapshot refreshed. Refs Abilityai/trinity-enterprise#823 * fix(files): file reads never follow a link The agent server opens a download or preview path one component at a time with O_NOFOLLOW and serves the descriptor it opened, so the checked file is the file read. A path that is a link, or passes through one, is refused with 403 resolved_path_mismatch for every caller, the owner and the platform's own reads included. The backend turns that refusal into a file_read_refused audit row and a structured 403; a platform read that meets one logs a warning naming the file. The fence is resolved-path containment, not a string prefix. A source scan pins that Trinity itself creates no links in an agent home. Refs Abilityai/trinity-enterprise#819 * fix(files): agents not yet on the current image serve no below-owner reads The backend probes each agent container once, per container and image, for the agent server's no-link read (ent#708-style: grep its source for the read function). Until an agent is stopped and started on the current image, download and preview by anyone below the owner tier are refused with 403 agent_restart_required; the owner and admins are unaffected and never trigger the probe. A missing target fails safe and is remembered for that image; an exec that raises or times out fails safe and is probed again on the next read. Each refusal is logged with a running count. Refs Abilityai/trinity-enterprise#819 * fix(frontend): a refused file read shows the server's message The download and preview store calls parse a text or blob error body back into {detail, code, path}, so a structured refusal reads as its message instead of "Request failed with status code 403". The Credentials panel reads first and opens the editor only on content or a 404; any other failure keeps it closed and shows the message in an InlineError beside the file list. The Files panel's Download shows the message too. Refs Abilityai/trinity-enterprise#819 * docs(files): feature flows and user docs for owner-tier credential reads, no-link reads and runtime config files file-browser (owner-tier set, link refusal, older-image refusal, audit, error bodies and what the UI shows), agent-guardrails (path_deny + 4), agent-sharing and credential-injection, the index; user docs say what a teammate on a shared agent can and cannot do and whom to ask, that the Files tab does not open links, and that the runtime login files and .tmp are not editable there. Refs Abilityai/trinity-enterprise#819 Refs Abilityai/trinity-enterprise#823 * fix(work): the pipeline reader names a linked file in the log The Work card reads pipeline files over its own httpx client, so the platform-read warning in agent_client.read_file never reached it. A 403 body is now read under the same byte budget and passed to that warning. Refs Abilityai/trinity-enterprise#819 * fix(files): the old-image refusal asks for a restart to apply an update The 403 agent_restart_required message now reads: "This agent needs a restart to apply an update. Ask the agent's owner or an admin to stop and start it in Trinity." It is shown on agents not yet on the current image. Refs Abilityai/trinity-enterprise#819 * fix(files): only a person at the owner tier skips the older-image check The older-image check on download and preview now exempts the same caller the credential tier serves: a person who passes the owner gate (the agent's owner, or an admin). Agent keys, the owner's own included, are refused with 403 agent_restart_required like any caller below the owner tier until the agent restarts on the current image; on a verified image they read as before. Refs Abilityai/trinity-enterprise#819 * fix(files): the older-image check caches only grep's own answer The read-policy probe caches a verdict only when grep answered: exit 0 (verified), or exit 1 or 2 with no output (token or file absent; grep -qs prints nothing). The exec helper reports a Docker fault as exit 1 with text in its output, so that, a timeout or any other code now reads as "not verified" for this read, is logged, and is probed again next time. Refs Abilityai/trinity-enterprise#819 * test(files): the older-image probe targets the shipped read function A guard reads the probe's path and token from the backend module, maps the path through the base image's COPY of ./agent_server to the repo file, and asserts that file defines the token as a module-level function (matched on the def with comments stripped). Renaming or moving the read function now fails here instead of on every agent after the next image build. Refs Abilityai/trinity-enterprise#819 * fix(files): the objective join names a linked file in the log The objective join reads template.yaml and objectives over the agent door directly, so the platform-read warning in agent_client.read_file never reached it. A 403 from that read now goes through the same warning, which names the file when the agent server refused it as a link. The read still reports the file as unreadable. Refs Abilityai/trinity-enterprise#819 * fix(base-image): download and preview read at most the size they opened Download reads at most the file's size when it was opened, and the preview stream stops at the Content-Length it declares. A file that grows after the open (an agent log, say) is served at its opened size, so the body never runs past the declared length. Refs Abilityai/trinity-enterprise#819 * test(frontend): Quick Inject shows a refused read's message and writes nothing Quick Inject reads the current .env before merging. A coded 403 on that read now has a mounted case: the server's message is shown and nothing is injected; an uncoded failure keeps the generic message. Refs Abilityai/trinity-enterprise#819 * test(files): the link-creation scan reads a file that does not tokenize A Python file that fails to tokenize, or is not UTF-8, was skipped by the scan for link creation in an agent's home. It is now scanned as raw text, comments included, so it can only add a finding, never hide one. Refs Abilityai/trinity-enterprise#819 * docs(files): the file-route docs state the read rule and the per-verb tier The requirements, architecture and feature-flow lines on the file routes now state the rule: reading credential paths and the secret file classes is owner-tier, the credential, alias and Trinity-copy patterns are on the write deny list, and the per-verb tier follows separately. Refs Abilityai/trinity-enterprise#819 * test(files): register the owner-tier read, no-link read and runtime config test files Registry entries for the four new test files and the two extended ones. Refs Abilityai/trinity-enterprise#819 Refs Abilityai/trinity-enterprise#823 * docs(files): feature flows follow the older-image check and the no-link read The file-browser flow describes the PERSON gate before the owner tier, the older-image check (only a person who is the owner or an admin skips it; only grep's quiet answer is cached), the descriptor-based agent-server reads that stop at the opened size, the three platform readers that name a linked file, and the current line references. The sharing and credential flows match the panel and the restart message. Refs Abilityai/trinity-enterprise#819 * docs(files): the docs state who skips the older-image check and how a teammate gets a restart The architecture bullet and requirements now say a conclusive older-image probe is cached per container and image, and that only a person who is the owner or an admin skips the check (agent keys included in the refusal). Requirements state reads stop at the size the file had when opened. The Files user doc quotes the restart message and lists every owner-tier path; the sharing doc says whom to ask for a restart and that credential management is for the owner or an admin. Refs Abilityai/trinity-enterprise#819 Refs Abilityai/trinity-enterprise#823 * test(files): the real routes forward the checked path, survive an audit failure and probe agent keys Route-level tests over routers.agent_files.router for download and preview: the agent is sent the normalized path, a failing audit store changes no answer (403 for a shared user, 200 for an admin), and the owner's agent-scoped key gets agent_restart_required on an unverified image. Refs Abilityai/trinity-enterprise#819 * merge-train: the owner-tier fixture grants the sibling key reach (interaction with #3346) #3346 (trinity-enterprise#629) bounds an agent key to its own agent, its permission edges and the agents it spawned. The read-tier tests here use a 'sibling' agent key against 'shared-agent' with only the access layer stubbed, so seven of them hit the new reach refusal before the owner-tier gate they test. Stub reach alongside access; reach refusal on the file routes stays pinned by test_ent629_agent_reach.py. Mechanical, per the merge-train note on the PR. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: trinity-ability <309458136+trinity-ability@users.noreply.github.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Summary
An agent-scoped MCP key resolves to its owner with the owner's role (Invariant #8), so every access and owner check said yes for any sibling the owner holds. On an admin-owned install, that's every agent. The
agent_permissionsedge the MCP layer checks (P-02) wasn't enforced one hop down. With its injected key andTRINITY_BACKEND_URL, agent A could, for any sibling B: chat with, task, fan out to, or loop it; read its history and logs; replace its git token. The public guardrails page (#3280) says an agent can call only the agents you allow. Operator ruling 2026-10-06: fix the hole.Narrowed in the shared gate, not per endpoint (the #1890 lesson).
dependencies.agent_may_reach(current_user, target, manage=):get_authorized_agent[_by_name],assert_agent_access, A2A inboundget_owned_agent[_by_name],assert_agent_owneruser/systemkeys and connectors are unchanged.capability_fence,require_person_or_capability, the skills fence) admits a holder only after the grant check andowner_owns_agent(owner_usernameequality, never the admin short-circuit). It records the target on the request's principal (User._capability_reach), andagent_may_reachhonours that record. So aschedules.manageholder reaches a sibling's schedule routes, anagents.manageholder its reconfigure routes, askills.manageholder its skills routes, and none of them reaches anything else or another user's agent. Holding a grant is never reach by itself. The files tab applies the same bound per file class (skills / instructions).can_user_access_agent/can_user_share_agentcall ANDs the reach (AST-guarded).nevermined's admin shortcut no longer skips it. Loops by id check reach before the admin/initiator shortcut. Credential export/import refuse an agent key by name before the owner fence (human_only).{self} ∪ edges ∪ spawned, not every agent's asks.baselined: ENT629row (33 tools) is nowenforce. The edge is decided byGET /api/agents/{caller}/reach/{target}: the edge or the spawn record, from the database, never the container list. So a ghost with no container, a child being recreated, and a Docker read fault no longer read as "not permitted".Behaviour change to know before merging
Any agent that today reaches a sibling without an edge through the raw backend or a now-
enforceMCP tool will get 404 / "Permission denied". That's the purpose of the ruling. Spawned children are unaffected. None of the publicabilitiesskills call the backend raw. Private orchestration skills are not audited: they will need an edge for each sibling they act on.Not in this PR
Fleet-level routes that take no single agent (collection reads such as the agent list, event subscriptions and the activity timeline, plus some fleet-level writes) still answer an agent key with its owner's reach. Some return task text and emails. They're tracked privately as a follow-up (#3346 review);
security.mdsays so.Test Plan
tests/unit/test_ent629_agent_reach.py, 41 tests on a real (temp) schema:human_only, loops by id, the spawned-set query (soft-deleted excluded), the reach endpoint, the chat-history rule;human_only, the non-admin queue bound,deleted_at, the skills owner bound) → red.tscclean.journey-smoke(J10 loop refusal) andregression diff.Fixes abilityai/trinity-enterprise#629
🤖 Generated with Claude Code