Repository navigation
feat(agents): the self-change permissions as four admin-only toggles in Agent Settings (abilityai/trinity-enterprise#756) - #3330
Conversation
…witch (#3330 review) setGranted read the store's current agent before the PUT and reloaded it after; switching agents mid-PUT reset the store to the OLD agent, so the next toggle on screen granted on the agent no longer shown. The agent now comes from the caller and every post-await step runs only while it is still current. Also: busy is per capability (one write finishing no longer re-enables another still in flight), and a failed refresh keeps the toggles on screen with a stale banner. Three mounted regression tests, each red with the fix reverted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
/review ReportBranch:
Execution coverage (Step 2.5)
No source-text assertions were added. The new spec mounts the component. Critical Findings[C1] Race: a grant write could land on the wrong agent after an agent switch (Confidence: 9/10). Fixed in async function setGranted (capability, granted) {
const name = agentName.value
...
await api.put(`/api/agents/${encodeURIComponent(name)}/capability-grants/…`, { granted })
await load(name) // load() resets the store when name !== agentNameIssue: if the panel switches from A to B while the PUT is in flight, Informational Findings[I1] A finished write re-enabled another toggle still in flight (Confidence: 8/10). Fixed in [I2] A failed refresh after a successful toggle was silent (Confidence: 8/10). Fixed in [I3] The permission-request link is generic (Confidence: 7/10, open). Each request links to [I4] The permission-request match depends on what the agent writes (Confidence: 6/10, by design). An agent that ignores the 403's instruction won't be listed. Any of the agent's own pending asks whose title contains a permission id will be listed. That is bounded to this agent's own asks, rendered as interpolated text (no Clean Categories
Summary
🤖 Generated with Claude Code |
/review Report: re-review at
|
| Finding | Status | Verified by |
|---|---|---|
| C1 a grant write re-targets the panel after an agent switch | ✅ resolved | setGranted(name, …) takes the agent from the caller. Reload, error and busy run only while name === agentName. a PUT that lands after the panel switched agents never retargets the panel passes now, and fails when the store and panel from acd8376ba are restored. |
I1 one write's finally re-enabled another in-flight toggle |
✅ resolved | busy is a per-capability map. The test fails on revert. |
| I2 a failed refresh was silent | ✅ resolved | view.stale → self-change-stale banner, retryable, with the toggles kept on screen. The test fails on revert. |
I3 permission-request link goes to /operations, not the item |
open | Operations has no per-item route. This is a follow-up. |
| I4 request matching depends on the agent following the 403 copy | open, by design | A structured field on asks is the durable fix. |
Execution coverage (Step 2.5)
| changed symbol | executed by | live consumer | verdict |
|---|---|---|---|
capabilityGrants.setGranted(name, capability, granted) |
mounted spec: grant, refusal, switch-mid-PUT, two in flight, failed refresh | SelfChangePermissionsPanel.vue (toggle and InlineError retry, both pass agentName) |
✅ executed |
busy map |
one in-flight toggle does not re-enable another… |
:disabled="!canEdit || !!store.busy[cap.id]" |
✅ executed |
| stale banner | a failed refresh keeps the toggles on screen and says so |
panel | ✅ executed |
Mutation check: with the store and panel reverted to acd8376ba, all 3 new tests fail. With the fix, 12/12 pass. The raw-colour and loading-gate ratchets pass.
Critical Findings
None.
Informational Findings
[I5] Switching A→B→A while a save is in flight re-enables that toggle early (Confidence: 7/10, low impact)
File: src/frontend/src/stores/capabilityGrants.js::reset
Evidence: reset() does busy.value = {}, and load(A) after B calls reset(A), so A's in-flight flag is lost. A probe confirms it: after A→B→A the toggle is not disabled while A's PUT is still pending.
Impact: low. The reload shows the server state from before the PUT landed, so a second click re-sends the same granted value. The PUT is idempotent (changed: false) and both writes are audited only on change. No wrong-target write is possible; C1's guard still holds.
Suggestion: key busy by `${name}:${capability}` and leave it out of reset().
[I6] The stale banner can repeat itself (Confidence: 6/10, nit)
The banner text is "Couldn't refresh the permissions — showing the last ones that loaded." followed by store.loadError. When the server sends no detail, that error is the fallback "Could not load the permissions.", so the banner says nearly the same thing twice. Suggestion: append the detail only when the server sent one.
Clean Categories
- Auth boundary: unchanged. No new endpoints. The PUT gate is on the server, so
canEditis cosmetic only. - Race and concurrency: the write path is now keyed by its own agent. Reads keep the generation guard. Two concurrent loads (the switch watch plus a post-write reload) are serialised by
generation. - Credentials, SQL, enterprise disclosure: no new surface. The learnings fragment carries no private-module detail.
- Frontend states: loading, failed, ready and stale all exist and share one footprint. No
v-html. - CI (base is a feature branch, so this is a subset): guard ×3, schema-parity, gitleaks, verify-* pass.
build,e2eandprod-image-smokewere pending at the time of this review.
Summary
- Critical: 0. C1 is verified fixed.
- Informational: 2 new (I5 low-impact, I6 nit) and 2 carried open (I3, I4). None block merge.
- Still needed: a human visual pass in light and dark. The PR stays stacked until feat(auth): an agent changes its own shape only with a grant (ent#164) #3236 lands.
🤖 Generated with Claude Code
…in Agent Settings (trinity-enterprise#756) Agent Settings gets a "Permissions to change itself" card: skills, schedules, instructions and other agents, each with what it allows, what is refused without it, and who granted it when. Owners see the state; only an instance admin can flip it (the PUT stays the gate). A fresh agent shows all four off with a line saying so; a non-owner's read 404s and the card does not render. Autonomy is shown beside them as a person's switch, never a grant. The agent's open permission requests are listed with a link to Operations. The ask surface has no field for "this is a permission request", so the 403 now tells the agent to put the permission id in its ask title verbatim, and the card matches on that id (pure utils/capabilityGrants.js). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…witch (#3330 review) setGranted read the store's current agent before the PUT and reloaded it after; switching agents mid-PUT reset the store to the OLD agent, so the next toggle on screen granted on the agent no longer shown. The agent now comes from the caller and every post-await step runs only while it is still current. Also: busy is per capability (one write finishing no longer re-enables another still in flight), and a failed refresh keeps the toggles on screen with a stale banner. Three mounted regression tests, each red with the fix reverted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
73ea1c0 to
4785b99
Compare
Summary
GET/PUT /api/agents/{agent}/capability-grants/{cap}. Each toggle has a plain line saying what the agent can do with it and what is refused without it. Each also shows granted by · relative time, with the absolute time on hover.LoadFailedwith retry.InlineErrorbeside that toggle, in the backend's own words.title includes 'schedules.manage'), and the card matches on that id.Changes
src/frontend/src/components/settings/SelfChangePermissionsPanel.vue(new), mounted inSettingsPanel.vuesrc/frontend/src/stores/capabilityGrants.js(new, agent-scoped, generation-guarded)src/frontend/src/utils/capabilityGrants.js(new, pure: copy, display order, the title match)src/backend/dependencies.py:_ask_for_it(capability)replaces the shared_ASK_FOR_ITconstant, so the refusal names the exact id to put in the titlerequirements/auth.md§2.9,feature-flows/self-change-grants.md,architecture/frontend.mdOpen-core
Ungated (OSS-core). This is the UI for #3236's grants and follows whatever #3236's open-core ruling is. That ruling is still pending on the issue.
Test Plan
src/frontend/tests/unit/selfChangePermissionsPanel.spec.jsis a mounted spec with 9 tests:LoadFailedcanEditforced true, the owner test goes red.tests/unit/test_ent164_self_change_grants.py: 46 passed. The refusal namestitle includes '<cap>'.check:tokensis OK.Fixes abilityai/trinity-enterprise#756
🤖 Generated with Claude Code