Skip to content

DO NOT MERGE — merge train: 3107,3108,3045,3113,3112,3111 - #3118

Closed
trinity-ability wants to merge 40 commits into
devfrom
train/20260930-1240
Closed

trinity-ability wants to merge 40 commits into
devfrom
train/20260930-1240

Conversation

@trinity-ability

Copy link
Copy Markdown
Contributor

Integration surface for #3107, #3108, #3045, #3113, #3112, #3111. Never merged; members merge individually once green.

🤖 Generated with Claude Code

AndriiPasternak31 and others added 30 commits September 28, 2026 01:14
Records the rule in security.md §20.10 ahead of the change: POST
/api/mcp/keys and POST /api/mcp/keys/ensure-default take a signed-in
(JWT) session, and ensure-default writes the same key_create audit row
as the create route.
…person, require_interactive

Two allowlist rules over mcp_scope, both fail-closed on a principal
without one: PERSON (a JWT session or the person's own user-scoped key,
via is_person_principal) and INTERACTIVE (a JWT session only). The
Depends forms make a route's rule a one-line signature change an AST
guard can see. Both also refuse a principal carrying
vouched_source_agent. The ent#611 ask-endings detail and predicate are
unchanged, and pinned so.
POST /api/mcp/keys (every scope) and POST /api/mcp/keys/ensure-default
now take Depends(require_interactive): a credential minter is at least
as strict as the principal it produces. The existing per-scope checks
stay as they were.

ensure-default also writes the same key_create audit row as the create
route, so every created key is attributable.

Tests go through the real get_current_user with seeded key rows and
assert the mcp_api_keys row count is unchanged on every refusal.
…t registry

mcp-api-keys.md: POST /keys and ensure-default are signed-in-session
only; ensure-default is audited as key_create. Adds the feature-flows
index row, a learnings fragment and the two new test files to
tests/registry.json.
…he route census

requirements/auth.md §2.8 (and the §2.7 helper table): the PERSON and
INTERACTIVE rules, which routes take which, the primitives, and the
route census with its shrink-only baseline. architecture/security.md §5
and §6, and one sentence on Invariant #8.

Refs #2996, trinity-enterprise#711
PUT /api/agents/{name}/autonomy takes Depends(require_person): a JWT
session or the person's own user-scoped key. Agent-scoped keys (on
their own agent or any other), the system key and every other scope get
a 403 with the human-only detail, before the owner check, so the
refusal discloses nothing about whether the agent exists.

Autonomy decides whether an agent's cron schedules fire unattended; it
is a grant, so the agent's own key must not be able to switch it on.

Tests go through the real get_current_user with seeded key rows and
assert the stored autonomy flag before and after.

Refs #2996
…2996)

PUT api-key-setting, read-only, resources, capabilities, capacity,
timeout, public-channel-model and guardrails take Depends(require_person),
the same rule as autonomy: the same file, the same owner configuration
surface, and no agent or system caller.

Each write is tested from its own non-ambient stored value: machine keys
through the real get_current_user leave it unchanged, a person changes
it. A router-level check pins that these and autonomy are every PUT in
the file.

Refs #2996
…a signed-in session

PUT /api/users/me/email and PUT/DELETE /api/users/me/github-pat take
Depends(require_interactive). The email is the account's sign-in
identity and the PAT is the credential future agent creations inherit;
both are credential/identity bindings, so no MCP key may change them,
the person's own user key included.

Tests: every key kind through the real get_current_user, with the stored
email and encrypted PAT checked before and after; the session path
(400 on a bad shape, 409 on a taken address, set and clear) unchanged;
and a record that email sign-in resolves the account by the email
column alone.

Refs trinity-enterprise#711
…2996)

Every OSS backend route, every method, must resolve to exactly one
class: gated in code (interactive / person / admin tier / widened admin
/ portal), listed with a reason (agent-callable / own-auth / delegated),
or in the frozen baseline, which only shrinks. A new route that is
neither gated nor classified fails the build, so the next human-only
route inherits the rule instead of relying on someone remembering it.

The AST side is import-free (rglob over src/backend, minus the private
submodule). The runtime side imports main in a subprocess and checks
every stored METHOD /path against the live table; it fails, never skips.
The human-only rule covers the autonomy grant, not the schedule tools it
bounds. Pins create/enable/disable with the agent's own key through the
real principal resolution, asserting the stored state moves.
Autonomy, read-only, resources, capabilities, capacity, guardrails,
public-channel model and the API-key setting flows note the person-only
rule on their writes; email-authentication, first-time-setup and
github-sync note the session-only rule on the sign-in email and the
personal GitHub PAT. Index rows, a learnings fragment, and the schedule
regression test in auth.md §2.8.

Refs #2996, trinity-enterprise#711
…tes call (#2996)

The read-only route imports its logic lazily from sys.modules, and a
sibling unit file evicts that entry at import, so under some orders the
route ran an unpatched copy and 404'd. The fixture now resolves and pins
the real module, and patches the bound logic through the globals of the
functions the router holds.
Drop the sign-in resolution test class and its registry note: they pinned
behaviour outside this change. Reword the census and MCP-boundary notes
neutrally; the baseline is a to-do marker, not a judgement.
… only (#2996)

An inherited PYTHONPATH (the verify-local unit stage sets one) let the
subprocess resolve `main` from another tree, so the fail-loud check on a
missing app could not fail.
GET /api/mcp/keys now takes the interactive rule: only a JWT session may
list keys. The Settings tab is unchanged; a signed-in admin still sees
every key with its owner's email, and a signed-in user sees their own.

Refs trinity-enterprise#712
The real connector key is refused at the entry point, so add a principal
that reaches the route. Route labels no longer depend on other changes.

Refs trinity-enterprise#712
Switch GET /api/mcp/keys to Depends(require_interactive), like its POST
siblings, and take it out of the route-census baseline (count 360) now
that it is classified.

Refs trinity-enterprise#712
…sus (#2996)

Routes that landed on dev after the branch was cut were unclassified.
Skill-set assign/unassign are the USE of the ent#596 skills-manage
capability (fenced by get_skill_managed_agent_by_name; the grant is the
admin+interactive /skill-manager route), the two skill-set reads are
open reads, and raise_my_ask is MCP ask_operator, self-only via
get_self_acting_agent. All five go in AGENT_CALLABLE with a reason.
An agent key can no longer PUT /api/agents/{name}/timeout, so the cap
refusal on schedule and loop timeouts now says the owner sets it.
…12-on-2996

# Conflicts:
#	docs/memory/feature-flows.md
#	docs/memory/requirements/auth.md
#	tests/registry.json
#	tests/unit/_route_census.py
#	tests/unit/fixtures/human_only_route_baseline.json
#	tests/unit/test_2996_human_only_routes.py
…etry; pin its deps (#3106)

startup.sh upgraded the agent-server dependencies UNPINNED on every boot, so
each container start took PyPI's latest. FastAPI 0.142 auto-configures
OpenTelemetry from the standard OTEL_* env at lifespan startup and refuses
the grpc protocol Trinity injects for Claude Code: every OTEL-enabled
agent's server exited on its next boot while the container stayed Up.

Three layers:
- agent server opts out of FastAPI auto-telemetry (the OTEL_* env is Claude
  Code's), passed only when the installed FastAPI has the parameter;
- agent-server deps exact-pinned in agent-server-requirements.txt, used by
  the image build and by the boot-time repair (no unpinned --upgrade);
- backend injects OTEL_SDK_DISABLED=true beside the OTEL env at create,
  recovery and system-agent create, rescuing agents on older images on
  recreate. Claude Code does not read it, so its metrics keep flowing.

Verified against FastAPI 0.142.1 with the grpc env: lifespan startup fails
without the opt-out and completes with it (and with the env flag alone);
a freshly built image boots the server on 0.141.1, still boots when a
template installs 0.142.1, and the boot repair restores the pin.

Fixes #3106

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eleases unreviewed (#3106)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…endabot for the pins (#3106)

- Recreate replays the old Config.Env, so existing agents never gained
  OTEL_SDK_DISABLED on restart / drift self-heal / rebuild. The recreate
  path now sets it (setdefault) wherever Claude Code's OTEL env is present.
  Behavioural tests through the real lifecycle seam.
- Split the pins: the image build installs exact pins
  (agent-image-requirements.txt); the per-boot repair installs floors for
  the server's own packages only (agent-server-requirements.txt), so it
  never downgrades a newer version a template brought and no longer
  touches rich/cryptography. Floors are tested to sit at or below the pins.
- Dependabot pip entry for /docker/base-image, the pins' bump channel.

Verified on a rebuilt image with the grpc OTEL env: fresh boot serves on
0.141.1; a template's 0.142.1 is kept and serves; a template's 0.115.6 is
raised past the floor and serves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…g Host headers (#3102)

Starlette rebuilds request.url from the Host header, so a Host carrying
"/", "?" or "#" made request.url.path name an allowlisted route while the
router dispatched a forbidden one. The event-loopback, portal_delegate,
connector, ephemeral and ops fences in get_current_user now compare
request.scope["path"], and every audit endpoint= label records it too.

Defense in depth: utils/host_header.HostHeaderGuard, a pure-ASGI middleware
registered outermost, rejects a Host containing / ? # \ @, whitespace or
control characters (HTTP 400, WebSocket close 1008).

test_3102_host_header_fences drives each fence with a real Starlette
request and a crafted Host, covers the guard over HTTP and WebSocket, and
fails on any .url.path read under src/backend. Unit fakes that modelled
request.url.path now model scope["path"].

Fixes #3102

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
obasilakis and others added 10 commits September 30, 2026 12:57
…cts (#3103)

backend-unit-nightly merged and ran every open PR, forks included, in a
scheduled main-scoped job: no first-contributor approval gate, a pip cache
saved after PR code ran into the scope required checks restore from, and a
write-token comment job that took PR number, seed and head SHA from JSON
the PR code could forge.

- Both nightlies: forks filtered with a visible warning (integration-nightly's
  single-PR dispatch path now requests isCrossRepository too); no pip cache
  in jobs that run PR code.
- Both comment jobs: leg identity (PR, seed, head SHA) comes from the
  discover matrix via legsFromMatrix; artifacts supply only the verdict
  booleans and the diff, and are extracted into per-artifact directories
  outside the workspace root, where the trusted verdict module lives.
- backend-unit-nightly: dispatch input through env; the empty-sweep failure
  counts accepted legs.

Residual: a same-repo writer's PR can still forge another PR's artifact
from its own test leg. Forks cannot.

Fixes #3103

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
actions/download-artifact extracts into <path>/<name>/ only when the
pattern matched more than one artifact; a single match lands in <path>
itself. With one open PR or a scoped dispatch, the comment jobs looked
in a directory that never exists and posted nothing. Fall back to the
root when the named directory is absent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r prompts (#3104)

Event payloads come from whoever emitted the event, but were interpolated
into the subscriber's task message raw, uncapped and unframed.

- Each substituted value is credential-sanitized, clamped to
  CONTEXT_MAX_CHARS (4000, with a truncation marker) and wrapped in ⟦ ⟧;
  marker characters are stripped from the value so it cannot close early.
- When any value was substituted, the message carries a header line
  saying the marked text is event payload data, not instructions. The
  owner-authored template text stays unframed.
- EmitEventRequest.payload is bounded at 64 KiB of UTF-8 JSON (422).

Fixes #3104

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lth re-checks it per request

bash_deny gains two patterns: any sudo command naming /etc/claude-code,
/opt/trinity or /etc/sudoers (plus visudo), and root shells (sudo -i/-s/
--login/--shell, sudo su, sudo <shell> with no script, bare su). Each sudo
option token has one match path so matching stays linear; a hook that
outlasts its timeout does not deny.

Agent /health reports guardrails_registration (ok/missing/not_root_owned/
writable), stat'd on every request. Docs state the registration cannot be
rewritten without sudo, and that the deny patterns are a speed bump.

Fixes #3105

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vybe

vybe commented Sep 30, 2026

Copy link
Copy Markdown
Contributor

Train complete: all six members squash-merged individually.

@vybe vybe closed this Sep 30, 2026
@vybe
vybe deleted the train/20260930-1240 branch September 30, 2026 13:24
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