Skip to content

DO NOT MERGE — merge train: 3635,3304,3346 - #3639

Closed
trinity-ability wants to merge 41 commits into
devfrom
train/20261012-2333
Closed

trinity-ability wants to merge 41 commits into
devfrom
train/20261012-2333

Conversation

@trinity-ability

Copy link
Copy Markdown
Contributor

Integration surface for #3635, #3304, #3346. Never merged; members merge individually once green.

One interaction found in assembly: #3346's agent-key reach refusal trips 7 of #3304's owner-tier tests, whose sibling agent key has no permission edge. The train carries a 5-line fixture stub. It will be pushed to #3304's branch after #3346 lands, together with the one-line import union in services/agent_service/files.py.

🤖 Generated with Claude Code

AndriiPasternak31 and others added 30 commits October 6, 2026 22:22
…low 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
…review

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
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
~/.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
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
… 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
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
…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
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
…ads, 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
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
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
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
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
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
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
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
…s 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
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
… 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
…onfig 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
…nk 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
… 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
…it 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
… 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>
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>
…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>
Conflicts (docs and test registry only, both sides kept):
- docs/memory/feature-flows.md: dev's new index rows above this branch's ent#819/ent#823 row
- docs/user-docs/agents/agent-runtimes.md: the ~/.tmp/codex note, then dev's Gemini and failure-reporting sections
- tests/registry.json: this branch's four ent#819/ent#823 entries and every dev entry
# Conflicts:
#	docs/memory/architecture.md
#	src/backend/routers/agents.py
#	src/mcp-server/src/access.ts
dolho and others added 11 commits October 12, 2026 10:49
…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>
…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>
… 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>
…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>
…s nothing to name (#3533)

An opener with no topic ("yo", "Run sleep 40, then say done.") made the
title model describe its own inability, and that sentence was stored as
the chat title ("Unable to generate title", "Unclear Initial Contact").

Both title prompts now define the reply for "nothing to title": exactly
NONE. `_is_no_title` treats the sentinel and a closed set of failure
phrasings as no title; `_generate_thread_title` then returns None, so
nothing is stored and the derived first-line title stands. The outcome
is recorded as ok, because the generator answered and three greetings in
a row must leave the operator health record quiet. `title_source` stays
unset, so the existing one retry on the next exchange still runs.

Fixes #3533

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	src/backend/services/agent_service/files.py
…action with #3346)

#3346 (trinity-enterprise#629) bounds an agent key to its own agent, its
permission edges and the agents it spawned. #3304's read-tier tests 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.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ui PR touches the frontend UI — triggers Playwright e2e tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants