feat(skills): a Settings panel for the skill runner's operator half (abilityai/trinity-enterprise#242) - #2772
Conversation
Verified on a live instance — the entitled path now exercisedThe draft caveat is discharged. I mounted the enterprise submodule into the running dev stack, confirmed The live { "enabled": false,
"library": { "configured": true, "synced": true, "skill_count": 41 },
"runner_agent": "trinity-skill-runner",
"runner_exists": false, "runner_running": false,
"exposed_skill_count": 0, "grant_count": 0 }Every rule confirmed against that payload
Toggling the feature on and off round-tripped through Two measurement corrections worth recordingI nearly reported two bugs that were not bugs, and both are the same mistake — measuring before asserting I was on the right screen:
Neither reached the PR as a finding, but they would have if the first number had been trusted. One real bug found — pre-existing, not from this PRThe screenshot shows a blue "How it works" box below the panel, describing the Trinity Prompt. It is ungated in Instance restoredEnterprise checkout removed, the three copied frontend files reverted, feature toggled back off, backend restarted — |
Held as draft until v0.9.5 landsDecision (2026-09-14): this PR stays a draft until the 0.9.5 release is cut. Not a quality hold — the work is complete and verified; see the verification comment above. Why. ent#242 is a new operator surface, and 0.9.5 is already at State when parked:
What unblocks it: the 0.9.5 release cut (dev → main). After that, mark ready — no rework is expected. If One caveat for whoever picks it up: the entitled render was verified by temporarily mounting the enterprise submodule into a local stack. That is a real check, not a simulated one, but it is not the same as an instance that ships entitled — worth a look on a genuinely entitled deployment before release. Pre-existing bug found while testing and filed separately: #2774 (the ungated "How it works" box, visible in the screenshot, not from this diff). |
…ent#242) The six admin endpoints (`enabled`, `provision`, `sync`, and the three `access` routes) shipped with no writer outside curl: the consumer tools `run_skill` / `list_runnable_skills` are advertised over MCP but gate on an ACL table that nothing could populate. An entitled operator could activate the feature and grant access only by hand-rolling HTTP. This is that writer — one entitlement-gated Settings tab covering enable -> provision -> sync -> per-(caller, skill) grants. **No backend change.** All six endpoints already exist, are typed, and are `require_human_admin`-gated (admin AND a human principal — widening an allow-list must not be reachable by a prompt-injected agent's own key). That premise was checked before planning rather than assumed, because it decides the PR shape: had anything needed the private submodule, this would have been three PRs in the ent#545 order instead of one. Gated with `requires: 'skill_runner'`, the same declarative seam `sso` and `credential-vault` use, so an unentitled install renders no tab rather than a panel whose every control 404s. That gate is UX, not containment — the server refuses regardless — which is what keeps a stale entitlement list from becoming an escalation. Decidable rules live in `skillRunnerPanel.js`, not the SFC: vitest runs `environment: 'node'` with no mount harness, so a rule inside a component is one no test can reach (the ent#392 precedent). 32 tests cover them. Two details worth naming: * `SkillRunnerStatus` reports each fact independently, and the panel keeps it that way: "no runner yet", "provisioned but not running" and "running" stay three states with three badges, and the library's own unconfigured / never-pulled / unreadable states are three more. None collapse into a spinner, and a blocked operator is told which screen fixes it. * There is no endpoint listing the library for a grant picker — `/available` is agent-facing (ACL ∩ library for ONE caller, and 422s for a user-scoped key with no `caller_agent`). The picker uses the OSS `GET /api/skills/library`. Worth knowing for whoever builds the MCP half. The revoke confirmation names the CALLER, not just the skill: "Revoke summarise" reads harmlessly, while the decision being made is which agent loses the ability to execute it. Loading uses a skeleton keyed on "no data yet", not `ScanlineReveal` — #2540 ruled the beam is chart-loading only, and that primitive's importer allowlist caught this panel reaching for it. Fixed the panel rather than widening the allowlist, and pinned the choice with a test. No feature-flow doc: a public doc describing this module would name enterprise internals under `docs/**`, which is the enterprise-docs guard's scope and the CLAUDE.md standing rule. No API or architecture changed, so tiered docs do not require one. Deferred, recorded on the issue: the MCP admin tools half (Invariant #13's third surface), so an ops agent still cannot enable or grant without curl. Fixes Abilityai/trinity-enterprise#242 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q19uRCksdn4DiRAJ55rfpZ
0441c6d to
be198aa
Compare
/review — post-rebase (onto
|
…#242)
`GET /api/enterprise/skill-runner/access` answers `{ grants: [...] }`
(`service.list_access` wraps `db.list_access`), and the panel read the
payload AS the array — so `grants` was always `[]`, every grant rendered as
"No agent can run any skill yet", `isDuplicateGrant` never fired, and the
only human writer for the ACL had no revoke path. The PR's own "the entitled
render is unexercised" note is exactly where it hid.
`grantsFrom(payload)` is the one unwrap rule, pure so the node-env suite can
pin the envelope shape; a bare list is still accepted, anything else is an
empty list rather than a throw (the status half of the same load must still
render). A source guard pins that the panel reads through it — reverting the
call site turns the spec red.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
/review — re-review after e142f3f (C1 fix)Scope: CLEAN — the fix touches only the three files C1 named. Plan completion: 5 done / 1 deferred-recorded (MCP half); the grants AC is now DONE. C1 closure — verified end to end
CriticalNone — C1 closed. Informational
CleanAuth unchanged ( Ready to merge — base |
…#242) — mechanical, per the merge-train note on the PR blockingAction() emitted navigate-tab with 'skills', an id absent from Settings.vue's ALL_TABS (the Skills Library renders under 'agents'), and the handler assigned it straight to activeTab, bypassing selectTab()'s validTabIds guard — so a fresh entitled install's only blocked-state action blanked the Settings content area. Emit 'agents' and route the event through selectTab so an unknown id is a no-op rather than a blank page, and pin the corrected value in the spec. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
merge-train — mechanical fix pushed to this branch (
|
Fixes abilityai/trinity-enterprise#242
What this closes
The six admin endpoints shipped with no writer outside curl.
run_skill/list_runnable_skillsare advertised over MCP but gate on an ACL table nothing could populate, so an entitled operator could enable the feature and grant access only by hand-rolling HTTP.One entitlement-gated Settings tab now covers enable → provision → sync → per-(caller, skill) grants.
No backend change
All six endpoints already exist, are typed, and are
require_human_admin-gated. I verified that before planning rather than assuming it, because it decides the PR shape: had anything needed the private submodule, this would have been three PRs in the ent#545 order (public first, private draft, then a pointer bump) instead of one.Scope ruling
The issue offered "a Settings/agent-detail panel (or MCP admin tools)" and did not resolve it. Operator ruling: the Settings panel. The MCP half is deferred and recorded on the issue rather than dropped — Invariant #13's third surface stays open, so an ops agent still cannot enable or grant without curl.
Design
{ id: 'skill-runner', adminOnly: true, requires: 'skill_runner' }— the same declarative seamssoandcredential-vaultuseskillRunnerPanel.js, pure — vitest runsenvironment: 'node'with no mount harness, so a rule inside an SFC is one no test can reach (ent#392 precedent)GET /api/agents+ OSSGET /api/skills/libraryThe frontend gate is UX, not containment. The server refuses regardless, which is what keeps a stale entitlement list from becoming an escalation.
Every state stays distinguishable.
SkillRunnerStatusreports each fact independently and the panel preserves that: "no runner yet" / "provisioned but not running" / "running" are three states with three badges, and unconfigured / never-pulled / unreadable library are three more. None collapse into a spinner, and a blocked operator is told which screen fixes it.The revoke confirm names the CALLER, not just the skill — "Revoke summarise" reads harmlessly, while the decision is which agent loses the ability to execute it.
Two things found while building
There is no endpoint listing the library for a grant picker.
/availableis agent-facing — ACL ∩ library for one caller, and it 422s for a user-scoped key with nocaller_agent. The picker uses the OSSGET /api/skills/library. Whoever builds the MCP half needs the same.The suite caught a real design violation. I reached for
ScanlineReveal;portalLoadingTreatment.spec.jsfailed because that primitive has an importer allowlist — #2540 ruled the beam is chart-loading only and panels load with a skeleton keyed on "no data yet". Fixed the panel rather than widening the allowlist, and pinned the choice with a test so it isn't repeated.Docs
No feature-flow doc, deliberately. A public doc describing this module would name enterprise internals under
docs/**— the enterprise-docs guard's scope and the CLAUDE.md standing rule. No API or architecture changed, so tiered docs don't require one. Verified my staged files carry no token the guard flags (the onepermissions_matrixhit inSettings.vueis pre-existing ondev, outside this diff, andsrc/frontend/**isn't in the guard's scope anyway).Verification
npm run buildview.state, never a bareloadingNot verified: the local instance reports
enterprise_features: []and the endpoints 404, so the entitled render is unexercised. The unentitled path — tab absent, no dead panel — is what the gate actually decides and is covered by source guards onrequires:+adminOnly. Draft until the entitled path is seen on a real instance.🤖 Generated with Claude Code
https://claude.ai/code/session_01Q19uRCksdn4DiRAJ55rfpZ