Repository navigation
feat(auth): an agent changes its own shape only with a grant (ent#164) - #3236
Conversation
Re-scoped 2026-10-02: grants, not per-action approval. Three capabilities join skills.manage (ent#596) in the closed set: - schedules.manage: schedule create/update/delete/enable/disable and the webhook routes on ANOTHER agent. An agent's own schedules stay free (#2996); trigger is not fenced. - instructions.manage: CLAUDE.md, AGENTS.md and .claude/** except skills through the file routes, plus git reset-to-main-preserve-state. Not grantable to a calibrating companion (ent#663). - agents.manage: durable create, delete, deploy-local, systems/deploy, the chat model, and the read-only/resources/timeout/ public-channel-model/guardrails writes (now person-or-grant). Ghost spawn and discard are exempt (ent#69). Without the grant, an agent gets a named 403 (*_management_not_permitted). The 403 tells the agent to raise a permission-request ask and says an admin grants the permission in Settings. Every refusal is audited. Autonomy, api-key-setting, capabilities, capacity and rename stay person-only. New: GET/PUT /api/agents/{name}/capability-grants[/{capability}], with PUT limited to an interactive admin and audited; this is the backend for ent#756. The route census gains a person_or_grant class. Related to Abilityai/trinity-enterprise#164 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
merge-train (2026-10-05, evening run): not on this train. It rides the next one once fixed. CI is green at Blocking
Needs a decision before it lands
Mechanical, for the same push
What held up: the grant route is human-only against every key type tried, the own-agent and ghost exemptions are not spoofable, the table already exists on both migration tracks, and |
AndriiPasternak31
left a comment
There was a problem hiding this comment.
Review from my side (lane C). Re-checked vybe's train notes against 02a21145 and they all still hold; adding three things that aren't in that comment.
Ran locally: 385 passed across the PR's tests plus the adjacent auth guards (test_293 / test_186 / test_2094 / test_1310 / the #2996 census + runtime). Mutating require_person_or_capability to skip the capability call turns 6 tests red, so that path is pinned.
Blocking
-
abilityai/trinity-enterprise#792 is still open, and it defeats
instructions.manageon the file routes (andskills.manage). I reproduced it against this branch's_touches_instructions. This needs to land after #792 or carry the fix, with the classification parametrize intest_ent164extended to cover it. -
agents.manageon the five reconfigure routes reaches further than the PR says. Those handlers authorise throughassert_agent_owner→can_user_share_agent, which returns True for anyrole == "admin"(db/agent_settings/sharing.py:286). An agent key carries its owner's role. So on a default admin-owned install, one grant lets that agent change read-only / resources / timeout / public-channel-model / guardrails on every agent on the instance, other users' included.security.md§5a currently says "a holder gains no reach beyond its owner's agents". Separately, nothing stops a holder targeting itself: it can switch off its own read-only mode and clear the owner'sextra_bash_deny/extra_path_deny/disallowed_tools. These five were PERSON-only under #2996, so this is the one place the PR loosens rather than narrows. Suggest: (a) bound the holder to agents its owner actually owns (owner_usernameequality, not the admin short-circuit), and (b) keep read-only-off and guardrail-relaxing PERSON-only, at least when target == caller. If the team wants the fleet-wide reach, thensecurity.mdand the ent#756 toggle copy need to say so. -
Open-core ruling. The only statement on ent#164 is the 09-11 "recommendation … pending operator confirmation". It needs an explicit OSS-core line from the operator before merge (CLAUDE.md: enterprise-tracker features are gated by default).
Same push, non-blocking
- The 403's
_ASK_FOR_IT(dependencies.py:1795) tells the agent to raise an ask "with ask_class 'permission-request'". The ask surface has no such field.ask_service._normalizereadstype ∈ {approval, question, alert}and friends, and MCPask_operator's zod schema has noask_class, so it is silently dropped.ask_classonly exists on seat decisions. Suggesttype: "question"with the permission named in the title. The same wording is inrequirements/auth.md:110and the flow doc. - The 403 also says "an admin grants it in the agent's Settings". That's ent#756, which isn't built. With the upgrade-time 403s, the only remedy after deploy is an admin JWT
PUTby hand. Either say how to grant via the API until #756 ships, or merge them together. - There are now two doors that grant
skills.managewith different audit verbs:PUT /api/skills/agents/{name}/skill-manager→skill_manager_grant, and the new generic route →capability_grant. Either refuseskills.manageon the generic route or unify the verb. - The capability table says
agents.managecovers "delete". In practiceenforce_agent_spawn_scope(agents.py:739) still limits an agent to agents it spawned, which is good. Worth stating so nobody reads the grant as "delete any agent". - MCP descriptions: #3238 just trimmed
deploy_local_agentto Claude Code's 2,048-char cap, so the grant sentence vybe asked for has to fit inside that. - Lane C:
frontend-e2eskipped on the PR (nouilabel). Branch is 4 behinddev.
What looks solid: the grant route is admin + interactive (no require_role("admin")), the fence refuses on the principal before any target lookup (no #186 differential), and the own-agent and ghost exemptions can't be spoofed (the ghost path is entitlement-gated and ghosts can't hold grants). The grants table is already CASCADE in agent_cleanup.
…al copy that works (ent#164 review) - require_person_or_capability: after the capability, the target must be owned by the holder's owner (owner_username equality, 404 otherwise). The reconfigure handlers authorise through can_user_share_agent, which admits any admin, so one grant on an admin-owned install reached every agent. - read-only and guardrails stay person-only when the holder targets itself. - The 403 tells the agent to raise an ask of type 'question' naming the permission (ask_class does not exist on the ask surface) and names the grant route, since the Settings toggles (ent#756) are not built. - skills.manage granted through the generic route audits with the skill-manager verb, so both doors share one. - instructions.manage copy names reset-to-main-preserve-state instead of claiming every git reset; the flow doc lists force_reset pull, sync pull_first, the instruction writes outside the path list, the other owner-tier writes and the grant-time calibration check as out of scope. - Leading-slash paths (ent#792, fixed on dev by #3276) added to the instruction-path classification parametrize. - Grant route tested against an admin-owned agent key and an admin user-scoped key; reach and self bounds tested through real keys. - MCP descriptions for the schedule, agent, system and reset tools name the grant (budget test passes). - security.md §5a/§6, auth.md and the flow doc updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Addressed the 10-05 train notes (vybe) and @AndriiPasternak31's review in Blocking
Same push
Not done: pinning the schedule route lists against the router ( 🤖 Generated with Claude Code |
…ry fenced route (ent#164 validation) The owner bound landed on the five reconfigure routes only. Every `capability_fence` route (schedule writes, PUT /model, delete, git reset) still authorised through `can_user_access_agent`, which admits any admin, so an admin-owned holder of `schedules.manage` wrote schedules on every agent on the instance and the agents.manage refusal text overclaimed. One helper, `_refuse_unless_owners_agent`, now runs inside the fence after the capability check, so a non-holder's 403 still comes first (#186). Pinned at the unit level across four fences and through real keys and the real grant table; the schedule write set is now derived from the router, not only a literal. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Held up: 373 passed locally across the PR's tests + Fixed in Open-core ruling: posted on ent#164 (OSS-core), which closes @AndriiPasternak31's item 3; items 1–2 were addressed in Still wiring-only (author's "not done"): behaviour tests for the eight remaining schedule routes, 🤖 Generated with Claude Code |
|
Heads-up from #3347 (the skill gate map, abilityai/trinity-enterprise#753): it carries a verbatim copy of this PR's
|
#3236 landed the shared `require_person_or_capability` / `_path_agent` this branch carried a verbatim copy of. Resolved `src/backend/dependencies.py` by keeping dev's version (with `_refuse_unless_owners_agent` and `capability_fence`) and dropping the duplicate; `get_skill_gate_readable_agent_by_name` stays. The gate routes' census entries still classify (test_2996 green); the flow doc no longer calls the helper a copy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…terprise#753) (#3347) * feat(skill-gates): the per-agent skill gate map (trinity-enterprise#753) Stores, per agent, which skills need approval before they run and which kind of person approves. The dispatch check (ent#751) and the in-container hook (ent#752) read it through skill_gate_service.list_skill_gates, which stops returning {} and reads the table — the gate is live from here. - Storage: agent_skill_gates (agent x skill -> approver primary|approver, deadline 1-168h, origin set|library_default|cleared), both tracks (SQLite entry + Alembic 0093 on 0092), CASCADE AgentRef. A failed read raises, so the check refuses (gate_unavailable) instead of reading "nothing is gated". - REST GET/PUT/DELETE /api/agents/{agent_name}/skill-gates[/{skill_name}] and MCP list_skill_gates / set_skill_gate / clear_skill_gate. Writes: a person who owns the agent or is an admin, or a skills.manage holder on an agent its owner owns, never on itself (require_person_or_capability, copied verbatim from #3236). An agent key reads its own gates; set_by is withheld from machine keys. Named 422/409 refusals; OSS offers only `primary`. - Library `approval: recommended` defaults follow assignment state: applied before a package is delivered (existing holders backfilled at start/sync), removed once the skill is unassigned and its package has gone; a cleared default leaves a tombstone. The library can tighten, never loosen. - Removal: an explicit gate goes only when a person unassigns the skill and its package removal completed. An agent's unassign, the system key, a deferred removal, Sync, start and the sweep keep it (gates_kept), so unassign-then-reassign cannot launder a gate away. Own-skill gates are sticky until cleared. This departs from the issue's "removing a skill removes its gate" for own skills and non-person removals, by the recorded scope decisions. - Marker ordering (#752): the fail-closed marker is written before an agent's first gate (decided under the per-agent lock) and removed after its last; stopped agents are never exec'd. - Audit on every change (who; via ui/api/orchestrator/system; trigger). Tests: tests/unit/test_ent753_*.py, src/mcp-server/src/tools/skill-gates.test.ts; every write path, auth bound and ordering line mutation-checked (each red). Security: docs/security-reports/cso-diff-2026-10-07-ent753-skill-gate-map.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(skill-gates): partial writes merge from a mapping, not a sentinel (trinity-enterprise#753) verify-local's unit stage (random order) found two things the per-file runs could not: - `write_skill_gate` marked an unsent field with a module-level `KEEP = object()` compared by identity. After another test re-imported `db.*`, the caller's KEEP and the DB module's were different objects, so the sentinel itself reached SQL ("type 'object' is not supported"). The write now takes `changes` — only the fields to set — so there is nothing to compare by identity and a reload cannot turn "keep" into a value. - The service and route fixtures patched `import services.x as X`, which reads the package attribute; after another test's re-import that can be a stale copy while the code under test imports from sys.modules (learning 2026-10-05). The fake Docker exec then missed, the real exec ran, and the marker tests saw no marker events. The fixtures now patch `importlib.import_module(...)`; reproduced with a stale package binding (red), then green. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(skill-gates): architecture catalog rows for the gated-skill check (#3344 item 2) `skill_gate_requests` in database.md, `skill_gate_service.py` and `skill_gate_map_service.py` in backend.md's service catalog, the MCP gate result handling in mcp-server.md, and the dispatch gate's person-only endings in security.md — each two lines or fewer, beside the gate map rows this branch already added. Item 2 of #3344; items 1 and 3 stay open there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(skill-gates): the PUT response says whether the approver is reachable, as GET does (trinity-enterprise#753) Found smoke-testing the routes on the verify-local sibling stack: GET reported approver_reachable=false for an owner with no email while the PUT response's gate carried null (only its warnings said approver_unassigned). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: trinity-ability <309458136+trinity-ability@users.noreply.github.com>
….manage + create_project / import_project (trinity-enterprise#588) Re-scope of ent#588 (ruling 2026-09-29, ent#661): on Trinity, /project-init creates the project through the platform. ent#661 planned the agent right as "a new capability beside skills.manage; changing members or visibility stays person-only" — this is that capability, on the #3236 seam: - db/capability_grants: `projects.manage` joins the closed set — start a Workspace project ON BEHALF OF the agent's owner (owner = creator and first member, agent = steward, members-only) and import one of the agent's OWN folder projects. People and visibility are never grantable. - dependencies: its named refusal (`project_management_not_permitted`) with the ask instruction. - Settings → Permissions to change itself: a fifth toggle, "Start projects". - MCP: `create_project` and `import_project` (license-blind, like the rest of projects.ts), each saying the grant is needed and what the owner keeps. The enforcement and the create/import themselves are in the enterprise projects module (abilityai/trinity-enterprise, same issue), which checks projects.manage before any read. Until this lands on dev the enterprise routes refuse every agent — the capability is not in the closed set — so the merge order is this PR, then the enterprise PR. 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>
….manage + create_project / import_project (trinity-enterprise#588) Re-scope of ent#588 (ruling 2026-09-29, ent#661): on Trinity, /project-init creates the project through the platform. ent#661 planned the agent right as "a new capability beside skills.manage; changing members or visibility stays person-only" — this is that capability, on the #3236 seam: - db/capability_grants: `projects.manage` joins the closed set — start a Workspace project ON BEHALF OF the agent's owner (owner = creator and first member, agent = steward, members-only) and import one of the agent's OWN folder projects. People and visibility are never grantable. - dependencies: its named refusal (`project_management_not_permitted`) with the ask instruction. - Settings → Permissions to change itself: a fifth toggle, "Start projects". - MCP: `create_project` and `import_project` (license-blind, like the rest of projects.ts), each saying the grant is needed and what the owner keeps. The enforcement and the create/import themselves are in the enterprise projects module (abilityai/trinity-enterprise, same issue), which checks projects.manage before any read. Until this lands on dev the enterprise routes refuse every agent — the capability is not in the closed set — so the merge order is this PR, then the enterprise PR. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
….manage + create_project / import_project (Abilityai/trinity-enterprise#588) (#3339) * feat(projects): an agent can start a project for its owner — projects.manage + create_project / import_project (trinity-enterprise#588) Re-scope of ent#588 (ruling 2026-09-29, ent#661): on Trinity, /project-init creates the project through the platform. ent#661 planned the agent right as "a new capability beside skills.manage; changing members or visibility stays person-only" — this is that capability, on the #3236 seam: - db/capability_grants: `projects.manage` joins the closed set — start a Workspace project ON BEHALF OF the agent's owner (owner = creator and first member, agent = steward, members-only) and import one of the agent's OWN folder projects. People and visibility are never grantable. - dependencies: its named refusal (`project_management_not_permitted`) with the ask instruction. - Settings → Permissions to change itself: a fifth toggle, "Start projects". - MCP: `create_project` and `import_project` (license-blind, like the rest of projects.ts), each saying the grant is needed and what the owner keeps. The enforcement and the create/import themselves are in the enterprise projects module (abilityai/trinity-enterprise, same issue), which checks projects.manage before any read. Until this lands on dev the enterprise routes refuse every agent — the capability is not in the closed set — so the merge order is this PR, then the enterprise PR. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * merge-train: five capabilities, not four — stale wording after projects.manage (ent#588) — mechanical, per the merge-train note on the PR --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Eugene Vyborov <1073874+vybe@users.noreply.github.com>
Summary
Implements the re-scoped abilityai/trinity-enterprise#164 (2026-10-02): grants, not per-action approval. An agent key resolves to its owner (Invariant #8), so until now any agent could reshape any agent of the same owner. ent#596 closed this for skills. This PR extends the same capability-grant seam to the rest of an agent's shape.
schedules.managetriggerinstructions.manageCLAUDE.md,AGENTS.mdand.claude/**except skills, through the file routes;git/reset-to-main-preserve-stateagents.managedeploy-local,systems/deploy,PUT /model, andread-only/resources/timeout/public-channel-model/guardrailsHow it behaves:
*_management_not_permitted).ask_class: permission-request. Approving that ask does not grant anything; an admin grants the permission in Settings.capability_refused.trinity-systemare never fenced.instructions.manage(422calibrating_agent, ent#663).New routes (the backend for ent#756's Settings toggles):
GET /api/agents/{name}/capability-grants: owner-level. Returns all four capabilities, held or not, with who granted each and when.PUT /api/agents/{name}/capability-grants/{capability}{granted}: admin and interactive, idempotent, audited.After deploy, no agent holds the three new capabilities. An orchestrating agent that does any of the following gets a 403 until an admin grants the permission:
Self-scheduling and ghost spawning keep working.
Decisions taken (2026-10-05)
agents.manage.agents.managecovers every reconfigure route. bug(auth): agent-scoped keys can toggle their own agent's autonomy — PUT /api/agents/{name}/autonomy lacks reject_agent_principal #2996's PERSON rule becomes PERSON-or-grant for those five routes; autonomy stays strictly PERSON.schedules.manage..claude/skills/**staysskills.manageonly.Tests
tests/unit/test_ent164_self_change_grants.py(40 tests):test_2996_owner_config_person_only.py: real keys through the realget_current_userand grant table.test_2996_human_only_routes.py: new census classperson_or_grant; the GET route is listedagent_callablewith a reason.test_ent679_*. Those fail identically with this change stashed (ModuleNotFoundError: payments_py.a2a.inband, local venv only).Not in this PR
Fixes abilityai/trinity-enterprise#164
🤖 Generated with Claude Code