Skip to content

fix(auth): the agent-key self-gate is one helper the route census recognises (abilityai/trinity-enterprise#840) - #3929

Merged
webmixgamer merged 8 commits into
devfrom
feature/840-agent-self-gate
Oct 26, 2026
Merged

webmixgamer merged 8 commits into
devfrom
feature/840-agent-self-gate

Conversation

@webmixgamer

@webmixgamer webmixgamer commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Summary

An agent-scoped MCP key resolves to its owner, carrying the owner's role. So AuthorizedAgent and assert_agent_access answer "may this key reach that agent", which since ent#629 means itself, its permission-edge targets and the agents it spawned. They never answer "is this key acting on its own agent". Ten modules copied that second check by hand, invisible to CI (the tenth, the Nevermined delegation read, arrived from dev during review), and seat_decisions shipped without it (ent#839).

  • One helper. dependencies.assert_own_agent(current_user, agent_name, *, detail, refusal=None) returns 403 with the route's own message when an agent key names another agent. It is a no-op for every other principal, exactly like the copies it replaces. refusal= builds the exception instead, for a router whose access dependency answers a uniform 404.
  • 19 routes converted, behaviour-preserving. Every route keeps its status and message: reminders ×3, seat decisions ×2, user memory, metric points, reports, canvas writes and deletes ×4, canon roles, objectives, shared files, voice reply, the A2A call and poll, and the Nevermined delegation read (merged from dev; it keeps its uniform 404 through refusal=).
  • The census splits agent_callable.
    • agent_callable_self is recognised AND listed, the admin_widened shape. The handler's first statement is assert_own_agent(<get_current_user principal>, <the path's name|agent_name>), or the signature takes Depends(get_self_acting_agent). Each route also needs an AGENT_CALLABLE_SELF entry saying why it is a use, not a grant. The gate alone never classifies a route, so every self-gated route reaches review with a reason; that reviewed reason, not a code check, keeps a self-scoped grant out, as it did for the old AGENT_CALLABLE.
    • agent_callable_any entries now answer, in their own field, why an agent other than the one in the path may reach the route.
    • The baseline shrinks from 347 to 330.
  • Hand copies fail CI. test_1310_auth_wiring.py refuses an inline self-gate anywhere under src/backend/. It reads the principal's agent name as an attribute, a getattr, agent_principal_name or a local bound from one (also inside or / and / … if … else …), on a parameter annotated User/CurrentUser (any form), a Depends default or an unannotated current_user. It catches !=, not in, a negated == and == … else: raise, guarding any raise or a returned 401/403/404, in handlers and helpers alike. The stated limit: the comparison must be against name/agent_name or an alias of one.
  • MCP. The agent_name parameter of write_user_memory, the three reminders tools and send_voice_reply is now described as for a user-scoped key only, the decisions.ts wording. It is no longer advertised as an override a prompt-injected agent may take. get_a2a_task now says, like call_a2a_agent, that an agent key may only pass its own name.

Behaviour changes (all for a caller who is refused either way)

  1. POST /api/agents/{name}/user-memory. The self-gate now runs before assert_agent_access. An agent key naming an agent outside its reach, or one that doesn't exist, gets 403 "Agent-scoped key may only write its own agent's user memory" instead of 403 "Not authorized". The status is the same, and the reach/existence differential is gone.
  2. POST /api/agents/{name}/a2a/call and /a2a/task. The kill switch is now a route dependency, the aauth_person.require_live shape. With the switch off, a schema-invalid or missing body answers 404 instead of 422, so a disabled route no longer reveals itself that way. JSON that doesn't parse is still 422, because FastAPI decodes the body before solving dependencies. Valid bodies are unchanged.
  3. Test doubles. The predicate is agent_principal_name, the one agent_may_reach uses. A principal whose agent_name is not a string, such as a bare MagicMock, or that has no agent_name at all, is no longer treated as an agent key. A real User always declares the field, as a string or None.

Findings (not fixed here; ruling: behaviour-preserving)

  • System key and event loopback. A system-scoped key and the event loopback carry no agent name, so they pass every self-gate while resolving to the owner's role. This is for trinity-enterprise#719 to rule on.
  • Reads without a self-gate. These admit an agent inside its reach and stay in the baseline until ruled: canvas reads (list_canvases, get_canvas, canvas_context), GET …/reports and the metrics cross-read (that one is ent#727's deliberate edge rule).
  • share_agent_file. It mints a public download URL and is agent-callable for the agent's own publish dir (FILES-001), listed self with that reason. It belongs to ent#719's sharing cluster.
  • event_subscriptions.emit_event_for_agent. A sibling can emit events as another agent; only the attribution is narrowed.
  • Admin short-circuit (the issue's "also worth ruling" item 1) is already settled by ent#629 and ent#872.

Changes

  • src/backend/dependencies.py: assert_own_agent, beside the refactor(auth): consolidate duplicated auth/dependency patterns behind factories (INV-8, split from #654) #1310 imperative family.
  • src/backend/routers/{reminders,seat_decisions,public_memory,metric_points,reports,canvas,agent_files,agents,a2a,nevermined_delegation}.py: the conversions.
    • The canvas helpers keep their full rule, so neither is weaker than its name when called alone.
    • The a2a kill switch is now a dependency.
  • tests/unit/_route_census.py:
    • recognition (SelfGate, _self_gate, _current_user_params);
    • the AGENT_CALLABLE_SELF and AGENT_CALLABLE_ANY tables;
    • classify rule 8;
    • hand_copied_self_gates.
  • tests/unit/fixtures/human_only_route_baseline.json: 17 lines deleted, nothing else (FROZEN_BASELINE_COUNT 347 → 330 against today's dev).
  • tests/unit/test_ent966_delegation_routes.py (from dev): reads AGENT_CALLABLE_SELF and asserts the recognised gate carries refusal=.
  • src/mcp-server/src/tools/{memory,reminders,voice,a2a_call,nevermined}.ts, src/mcp-server/src/access{,.test}.ts: wording, and the owner string that named the deleted _self_gate. New src/mcp-server/src/tools/self-fenced-overrides.test.ts pins the wording on the seven tools that take agent_name.
  • Docs:

Test Plan

  • cd tests && pytest unit/test_ent840_agent_self_gate.py unit/test_2996_human_only_routes.py unit/test_1310_auth_wiring.py -v:
    • the helper's truth table;
    • one sibling refusal per AGENT_CALLABLE_SELF route (22 cases), driven off the census, with the caller's own name in every other argument so a gate that compares the wrong parameter fails;
    • the converted routes keep their messages;
    • recognition self-tests;
    • hand-copy spellings.
  • unit/test_736_a2a_outbound_call.py::test_with_the_switch_off_a_malformed_body_is_the_same_404.
  • After merging dev (bbd0d4095): unit/test_ent966_delegation_routes.py passes on the merged tree, including its in-reach sibling 404 over HTTP; test_2996_route_census_runtime.py 3/3 against the real app.
  • cd src/mcp-server && node --import tsx --test src/*.test.ts src/tools/*.test.ts: 984 pass on the merged tree, tsc --noEmit clean.
  • 72-file affected subset: base 66a860094 and head fail identically (21 pre-existing on this host: import main needs opentelemetry.exporter, plus the known conftest subset errors); head adds 69 passing tests.
  • test_2996_route_census_runtime.py: 3/3 in the /verify-local venv (real import main). Every stored METHOD /path matches the live app, the two new tables included.
  • /verify-local --skip-agent: PASS.
    • Unit: 28,563 passed. The only reds are the 3 test_3262 cases caused by TRINITY_DEFAULT_SYSTEM_MANIFEST=disabled, which pass 10/10 without it.
    • Build + import main in the image: OK. Boot: OK.
    • Integration: 69 passed, 13 gated skips, 2 known false-fails deselected.

Eyeball (local stack, on 314cfe527)

A is an agent whose own key makes the calls. B is a second agent inside A's reach (a temporary A→B permission edge, removed afterwards). C is an agent outside A's reach.

  • Sibling refused, messages unchanged. A's key on B's reminders → 403 "Agent-scoped key may only manage its own reminders". On B's seat decisions → 403 "Agent-scoped key may only record and read its own agent's seat decisions".
  • Own agent and a person unchanged. A's key on A's reminders → 200; an admin session on B's reminders → 200.
  • Behaviour change 1. A's key on C's user memory → 403 "Agent-scoped key may only write its own agent's user memory", and the same 403 for a name that doesn't exist. On dev both were 403 "Not authorized".
  • Behaviour change 2. POST …/a2a/call with {} and outbound off → 404 "Not found"; on dev it was 422 "Field required". Unparseable JSON is still 422 json_invalid.
  • MCP wording not eyeballed. It needs an mcp-server rebuild; self-fenced-overrides.test.ts covers it.

Mutation: 30 call-site mutants, all killed (18 before the code-review round, 9 after it, and 3 in the review round). Each was applied to the call site, then reverted with a byte-identical restore:

  • deleting, nesting or misplacing a gate, or aiming it at the wrong parameter, turns census rule 8 and the route's sibling case red;
  • changing a message turns its message pin red;
  • dropping the a2a kill-switch dependency turns test_736::test_with_the_switch_off_a_malformed_body_is_the_same_404 red;
  • reverting the helper's predicate turns the truth table red;
  • reverting the census recognition, a both-parameter path or the add_api_route merge reset turns the recognition self-tests red;
  • dropping a ratchet branch (not in, the refusing else, unannotated principals, any raise) turns the spelling tests red;
  • a hand copy in a canvas helper, or reverting the canvas branch predicate, turns test_1310 or test_ent840 red;
  • restoring an override wording turns self-fenced-overrides.test.ts red;
  • swapping the delegation read's refusal= for detail= turns test_ent966's in-reach sibling 404 red, and deleting its gate also turns census rule 8 and its sibling case red;
  • dropping the matcher's or / and / ternary branch turns the two new spellings red.

A private submodule PR, abilityai/trinity-enterprise#970 (draft), follows this one. It moves the private tree's one hand-copied self-gate onto the helper and adds a private twin of these guards. Order: this PR, then the private PR, then a gitlink bump.

Fixes abilityai/trinity-enterprise#840

🤖 Generated with Claude Code

webmixgamer and others added 2 commits October 23, 2026 19:01
…ognises (Abilityai/trinity-enterprise#840)

An agent-scoped key resolves to its owner carrying the owner's role, so the
access check answers "may this key reach that agent" (its ent#629 reach),
never "is it acting on its own agent". Nine modules hand-copied that second
check, invisible to CI; seat_decisions shipped without it (ent#839).

- dependencies.assert_own_agent(current_user, agent_name, *, detail,
  refusal=None): 403 with the route's own message when an agent key names
  another agent; a no-op for every other principal, like the copies.
- 18 routes converted, each keeping its status and message: reminders x3,
  seat decisions x2, user memory, metric points, reports, canvas writes and
  deletes x4, canon roles, objectives, shared files, voice reply, outbound
  A2A call and poll. Canvas helpers keep their full rule.
- Route census: agent_callable splits into agent_callable_self (the gate as
  the first statement, or Depends(get_self_acting_agent), recognised AND
  listed with why it is a use, not a grant) and agent_callable_any (listed
  with why a sibling may reach it). Baseline 348 -> 331.
- test_1310 refuses a hand-copied gate anywhere under src/backend/.
- MCP: agent_name on write_user_memory, the reminders tools and
  send_voice_reply is described as user-key-only; get_a2a_task matches
  call_a2a_agent.

Behaviour changes, each for a caller refused either way: user memory refuses
an out-of-reach agent key with its self message (was "Not authorized", both
403); the A2A kill switch is a dependency, so with it off a schema-invalid or
missing body is 404 (was 422); a principal whose agent_name is not a string,
or absent, is not treated as an agent key (test doubles only).

Mutation: 27 call-site mutants killed. Removing, nesting, misplacing or
misaiming a gate reds census rule 8 and that route's sibling case in
test_ent840_agent_self_gate.py; a changed message reds its pin; dropping the
A2A switch dependency reds test_736's malformed-body test; reverting the
helper predicate, the census recognition or a ratchet branch reds the
matching self-tests; restoring an override wording reds
self-fenced-overrides.test.ts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Abilityai/trinity-enterprise#840)

/review on #3929: architecture/security.md (Container Security) still named
the AGENT_CALLABLE table that this change splits into AGENT_CALLABLE_SELF /
AGENT_CALLABLE_ANY. POST /api/skill-gate/check takes the agent from the key,
so it is an ANY entry with no sibling to name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@webmixgamer webmixgamer added the ui PR touches the frontend UI — triggers Playwright e2e tests label Oct 23, 2026
@webmixgamer

Copy link
Copy Markdown
Contributor Author

@AndriiPasternak31, could you review this? It's the route-census class for the agent-key self-gate (abilityai/trinity-enterprise#840, under epic abilityai/trinity-enterprise#318), and it preserves behaviour. One shared assert_own_agent replaces the hand copies on 18 routes and keeps their messages. The census splits agent_callable into agent_callable_self (recognised AND listed) and agent_callable_any (each entry answers "why may a sibling reach this"), and a ratchet refuses new hand copies. The description lists the three intended behaviour changes and the findings left for abilityai/trinity-enterprise#719. The private half is abilityai/trinity-enterprise#970, a draft that merges after this one.

@github-actions

Copy link
Copy Markdown

⚠️ Nightly unit-suite check skipped — merge conflict against dev.

Resolve by running git merge dev locally and pushing the result. The next nightly run will re-test once the conflict is gone.

@github-actions

Copy link
Copy Markdown

⚠️ Live-instance suite skipped — merge conflict against dev.

Resolve by merging dev locally and pushing the result; the next nightly re-tests.

@AndriiPasternak31 AndriiPasternak31 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at 86e97f7. The conversion is right and the census has teeth. One blocker, and it comes from the merge, not the diff.

I ran the 18 routes over HTTP on the merge base (66a8600) and on this head. It's a real FastAPI app; only get_current_user and the reach lookups are faked. Each route got 14 principals: own key, a sibling inside reach, a sibling outside reach, an agent key on a missing agent, JWT owner/shared/stranger, a user key, the system key, a connector bound to the agent, a connector bound to another agent, and the event loopback. 298 of 308 cases match, status and message. The other 10 are the two changes the body lists and nothing else:

  • POST …/user-memory: an out-of-reach sibling and a missing agent get 403 with the self message instead of 403 "Not authorized" (public_memory.py:71).
  • POST …/a2a/call with {} and outbound off: 422 becomes 404 for every principal that passes access, not just agent keys (a2a.py:1964).

Mutation check over test_ent840, test_2996_human_only_routes, test_1310, test_2996_route_census_runtime and test_736: red in 10 of 13. All of these go red:

  • deleting the gate, moving it off the first line, or aiming it at reminder_id;
  • an aliased import;
  • a new unlisted route, gated or not;
  • a hand copy listed as ANY, or one in a helper;
  • moving a self route to ANY;
  • dropping the a2a switch dependency.

The three greens are under non-blocking below.

Blocking:

  1. The conflict isn't registry-only. dev added nevermined_delegation.py::get_agent_delegation (#3914), and its self-gate is a hand copy (nevermined_delegation.py:161-163, in the old AGENT_CALLABLE). I merged dev into this head locally:

    • Taking this side of _route_census.py fails rule 1 and test_no_hand_copied_agent_self_gate.
    • Moving the entry to ANY still fails the ratchet.
    • What passes (166/166 across the three census files) is making assert_own_agent(current_user, agent_name, refusal=lambda: HTTPException(status_code=404, detail="Agent not found")) the first statement, plus an AGENT_CALLABLE_SELF entry.

    Please keep refusal= there: with detail=, a sibling inside reach gets 403 where dev answers 404. The agent_principal_name import in that file goes unused after the change. tests/registry.json is a plain take-both. Once it's pushed I'll re-check that one route.

Non-blocking:

  • "A self-scoped grant cannot classify itself" only holds until it's listed. A new route with assert_own_agent first and an AGENT_CALLABLE_SELF entry passes everything (249/249). That matches the old AGENT_CALLABLE, where the reason in review was the guard, so it's fine. I'd word it that way in security.md §6 and the census docstring so nobody reads it as a code check.
  • Ratchet gap outside the stated limit: if (current_user.agent_name or name) != name: raise isn't caught, because _agent_name_of (_route_census.py:520) doesn't look inside a BoolOp. The {agent}-parameter miss is the stated limit, so that one's expected.
  • ui label: no src/frontend changes, so the design-system contract doesn't apply. It's the e2e opt-in, and e2e is green.

I didn't run the MCP tests locally. CI's typecheck + unit run is green.

webmixgamer and others added 2 commits October 26, 2026 12:53
Conflicts:
- tests/registry.json: both sides appended at the tail; kept both (text edit,
  every entry three-way checked).
- tests/unit/_route_census.py: dev (#3914) added
  routers/nevermined_delegation.py::get_agent_delegation to AGENT_CALLABLE,
  which this branch splits; it is now an AGENT_CALLABLE_SELF entry.

Semantic, from #3914 (merged cleanly, would fail the census):
- routers/nevermined_delegation.py: the read's hand-copied agent-key check is
  now assert_own_agent(..., refusal=...) as its first statement, building the
  access dependency's uniform 404, so a sibling inside the caller's reach still
  answers exactly like a missing agent; the unused agent_principal_name import
  is gone.
- tests/unit/test_ent966_delegation_routes.py: reads AGENT_CALLABLE_SELF and
  asserts the recognised gate carries refusal=.
- tests/unit/test_ent840_agent_self_gate.py: pins the route's message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nary, the census wording names its real guard (Abilityai/trinity-enterprise#840)

From the #3929 review:
- The ratchet missed a principal's agent name inside an expression that can
  evaluate to it: `(current_user.agent_name or name) != name`, and the
  `… if … else …` form. `_agent_name_of` now reads BoolOp operands and IfExp
  branches; two new spellings in test_1310 (red with the branch removed). All
  19 earlier spellings are still found, the 9 benign shapes still spared, and
  the live tree has no new hit.
- "A self-scoped grant cannot classify itself" held only until someone lists
  it. The census, security.md §6 and auth.md §2.11 now say what is true: the
  gate alone never classifies a route, so every self-gated route reaches review
  with a reason, and the reviewed reason is the guard, as it was for the old
  AGENT_CALLABLE table.
- Counts after the dev merge: ten modules carried a hand copy; the Nevermined
  delegation read joins the converted list.
- MCP get_agent_spending: its agent_name says an agent-scoped key may only pass
  its own name, like the a2a tools.

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

Copy link
Copy Markdown
Contributor Author

Thanks, all addressed in 1769ee7e2 (merge of dev) and 3ca51df53:

  • Blocking: get_agent_delegation now opens with assert_own_agent(current_user, agent_name, refusal=lambda: HTTPException(status_code=404, detail="Agent not found")), the form you checked, and has an AGENT_CALLABLE_SELF entry. The unused agent_principal_name import is gone. The merge broke one more thing: test_ent966_delegation_routes.py read census.AGENT_CALLABLE, so it now reads AGENT_CALLABLE_SELF and asserts that the gate carries refusal=. Swapping refusal= for detail= turns ent966's in-reach sibling 404 test red.
  • Wording: the census docstring, security.md §6 and auth.md §2.11 now say the gate alone never classifies a route, and the reviewed reason is the guard, as it was for the old table.
  • Ratchet: _agent_name_of now reads or / and operands and … if … else … branches. Your spelling and the ternary form are in test_1310. All 19 earlier spellings are still found, the 9 benign shapes are still spared, and the live tree has no new hit.
  • Also: the agent_name parameter of get_agent_spending now says an agent-scoped key may only pass its own name, like the a2a tools.

CI is green on 3ca51df53. Could you re-check the delegation route?

@dolho dolho left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at 3ca51df53. All three items from the previous review are addressed:

  • Delegation route: get_agent_delegation now opens with assert_own_agent(..., refusal=lambda: HTTPException(404, "Agent not found")) and has an AGENT_CALLABLE_SELF entry. I swapped refusal= for detail= locally, and test_ent966 went red on both the in-reach sibling 404 and the census pin, so the uniform 404 is protected.
  • Wording: security.md §6, auth.md §2.11 and the census docstring now say the reviewed reason is the guard, not the gate.
  • Ratchet: I planted if (current_user.agent_name or name) != name: raise and a getattr-alias copy that returns a 403. test_no_hand_copied_agent_self_gate caught both.

The 10 affected unit files pass locally (526 tests), and CI is green.

One nit you can take or leave: docs/security-reports/cso-diff-2026-10-23-840-agent-self-gate.md line 12 still says "A self-scoped grant cannot classify itself" and counts 18 routes and 6 tools. You could update it or add a short note that it predates the review round.

Approving. Before merging, note that dev-ci on dev is currently red on frontend-e2e.

Conflict: tests/registry.json, both sides appended at the tail (dev:
test_3940_cron_dst_fold); kept both, every entry three-way checked. The two dev
commits are scheduler-only (#3940), so nothing else interacts with the census.

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

@AndriiPasternak31 AndriiPasternak31 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at f4d8ba3f6. All three points are fixed, so I'm approving. GitHub shows a new conflict with dev, and it's bookkeeping only, see below.

Delegation route. get_agent_delegation now opens with assert_own_agent(..., refusal=lambda: HTTPException(status_code=404, detail="Agent not found")) and has an AGENT_CALLABLE_SELF entry. I ran a mutation check over test_ent840, test_2996_human_only_routes, test_1310, test_2996_route_census_runtime and test_ent966. The control run passes (275 passed). Every planted change goes red, 12 of 12:

  • dropping refusal=, which gives a 403;
  • a refusal that returns 403, or 404 with a different detail;
  • deleting the gate, or moving it below the rate limit;
  • going back to dev's hand copy;
  • hand copies spelled with or, a ternary, getattr inside or, and and;
  • deleting the census entry;
  • reverting the new BoolOp / IfExp branches in _agent_name_of.

Wording. The census docstring, security.md §6 and auth.md §2.11 now say the gate alone never classifies a route, and that the reviewed reason is the guard. That's right.

At this head, the 13 auth and census unit files pass (642 passed).

The new conflict. dev moved after your merge. #3935 (ent#717, the email-whitelist gate) took one route out of the frozen baseline, 348 → 347, and edited the same security.md §6 paragraph and the feature-flows.md table. I resolved it locally like this:

  • _route_census.py: FROZEN_BASELINE_COUNT = 330. The merged human_only_route_baseline.json has 330 entries: your 331 minus the whitelist route. With 331 or 347, test_rule4_the_baseline_count_is_frozen goes red, so the count checks itself.
  • security.md §6: take dev's "Two human-only rules" paragraph, change its agent_callable to agent_callable_self / agent_callable_any, then add your new ent#840 paragraph. The two sides changed different words.
  • feature-flows.md: keep both rows.

On that merge, the same 13 files plus test_ent717_whitelist_session_only.py pass (728 passed).

Optional: dolho's nit still applies. docs/security-reports/cso-diff-2026-10-23-840-agent-self-gate.md:12 says "A self-scoped grant cannot classify itself".

I didn't run the MCP tests locally. CI's typecheck and unit run is green.

webmixgamer and others added 3 commits October 26, 2026 17:18
Conflicts:
- docs/memory/architecture/security.md: dev (#3935) extended the "two human-only
  rules" paragraph; kept dev's paragraph with this branch's class rename
  (agent_callable -> agent_callable_self / agent_callable_any), then the
  self-gate paragraph.
- docs/memory/feature-flows.md: both sides added an index row; kept both,
  newest first.
- tests/unit/_route_census.py: both sides removed baseline rows (17 here, the
  whitelist add in #3935); FROZEN_BASELINE_COUNT = 330, the merged baseline's
  exact size.

Checked: #3913's routers/a2a.py change is inbound-path attribution; both outbound
handlers still open with assert_own_agent and keep the switch dependency, and no
new hand-copied self-gate arrived.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts (both sides appended): docs/memory/feature-flows.md (kept both index
rows, newest first) and tests/registry.json (kept both; every entry three-way
checked). #3936 and #3946 add no route this census sees as a hand-copied
self-gate and do not reference the old AGENT_CALLABLE table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eally is, and what changed after the audit

Review nit (both reviewers): "A self-scoped grant cannot classify itself" was an
overclaim; the gate alone never classifies a route, and the reviewed reason is
the guard. A dated note records the review round's additions (the 19th route,
the seventh MCP description, the or/and/ternary ratchet); the audited counts stay
as they were at 66a8600.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@webmixgamer
webmixgamer merged commit 40b3295 into dev Oct 26, 2026
27 checks passed
webmixgamer added a commit that referenced this pull request Oct 27, 2026
…ec40) (#3995)

Three enterprise commits, in order:
- Abilityai/trinity-enterprise#963: a retention fix (#3089).
- Abilityai/trinity-enterprise#786: a CI workflow change; no product code.
- Abilityai/trinity-enterprise#970: the private half of
  Abilityai/trinity-enterprise#840 — the private tree's one hand-copied
  agent-key self-gate now calls dependencies.assert_own_agent (#3929), with
  a private twin of the census guards.

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.

3 participants