Skip to content

feat(access): the platform role on the Access tab; a user assigned to a companion answers its asks (abilityai/trinity-enterprise#837) - #3634

Merged
webmixgamer merged 5 commits into
devfrom
feature/837-access-role
Oct 12, 2026
Merged

webmixgamer merged 5 commits into
devfrom
feature/837-access-role

Conversation

@webmixgamer

Copy link
Copy Markdown
Contributor

Summary

PR 2 of abilityai/trinity-enterprise#837 (AC5 + AC7): the platform role on Agent Detail › Access, and a user assigned to a companion answers its asks in the Workspace Inbox.

  • Role route. PUT /api/users/{username}/role refuses every key (reject_non_interactive_principal — require_admin alone admitted trinity-system's key and an admin's user-scoped key, so either could promote anyone to admin), writes a role_change audit row (from → to; no-ops and refusals write none), and points the person's whitelist row at the new role, so a demotion can't be undone by re-binding the account away and signing in with the old address.
  • Access tab. For an admin whose role the server verified: a role select on every other Active row — every change confirmed; a cancel, Esc or refusal puts it back; one save at a time — and Give a platform account on a Pending row (confirmed: a platform account sees the team view; colleagues, not outside clients). Both sit in one fixed-width slot per row. "never signed in"; the list reloads in place (viewState; loading-gate baseline 1 → 0). Everyone else keeps the read-only label and the pointer to Settings.
  • People this agent serves. Each person's role as a badge and in the pickers (Name · Workspace only). Assigning someone writes the assignment, then shares the agent with them (never the owner, yourself, or an account with no email; 409 = already shared) — an assignment alone isn't access: the Inbox lists asks only from agents on the person's roster. An admin sees "Not shared with …" + Share with them on an assignment the agent isn't shared for.
  • Settings › Email Whitelist. Source reads "Access tab" for rows given from the Access tab and shows any other unlabelled source as stored, never "Manual".

Changes

  • Backend: routers/users.py (gate, audit, whitelist sync), db/email_auth.py + database.py (set_whitelist_default_role)
  • Frontend: AccessPanel.vue, AgentAssignmentsSection.vue, AgentDetail.vue, Settings.vue, new stores/users.js, utils/workspaceOnly.js, utils/emailWhitelist.js, loading-gate-baseline.json
  • Tests: tests/unit/test_837_role_change_audit.py, tests/unit/test_837_assigned_user_inbox.py, accessPanelMembers.mount.spec.js, agentAssignmentsShare.mount.spec.js, emailWhitelist.spec.js; tests/registry.json
  • Docs: requirements/auth.md §2.10, the workspace-only-role / role-model / agent-sharing / email-authentication flows, the API catalog + security area, the upgrade note, user docs, docs/security-reports/cso-diff-2026-10-12-ent837-access-role.*, two learnings fragments

Test Plan

  • New unit tests: test_837_role_change_audit.py (15), test_837_assigned_user_inbox.py (2 — a real user JWT through the Workspace door and the real roster). Every gate, the audit, the whitelist sync and the assign-then-share order are mutation-proven.
  • Related backend batch: 936 passed, 9 skipped; frontend: 5680 passed (the 3 host-only files markdownCodeBlocks, roomComposerChain, roomStopWork fail as on dev).
  • /verify-local --skip-agent: build + import smoke, boot + health, test preflight OK; integration 69 passed, 13 skipped (agent-dependent with --skip-agent, no PostgreSQL), 2 deselected (known false fails). Unit stage: 25 479 passed; the 3 test_3262 system-seed failures appear only under TRINITY_DEFAULT_SYSTEM_MANIFEST=disabled (10/10 without).
  • /cso --diff: 2 pre-existing findings fixed here (machine keys on the role route; a stale whitelist role undoing a demotion); report in docs/security-reports/.
  • Eyeballed on localhost: all 11 recipe checks, including AC7 end to end (a new member given an account, made primary, answered an ask in the Inbox) and the six layout follow-ups (aligned slot, no caption, link-style "Share with them", compact Kind select, full-height Add, phone-width share box, Source "Access tab").

Merge order

The account endpoint behind Give a platform account is a private-module change, abilityai/trinity-enterprise#874. That PR merges first; this PR then gets one more commit bumping the enterprise submodule pointer to its merge commit (b5080b6 -> <merge>). Don't merge this PR before that commit lands. No DB migration.

Fixes abilityai/trinity-enterprise#837

🤖 Generated with Claude Code

webmixgamer and others added 3 commits October 12, 2026 15:52
…to a companion answers its asks (Abilityai/trinity-enterprise#837)

PR 2 of Abilityai/trinity-enterprise#837 (AC5 + AC7).

- Role route: PUT /api/users/{username}/role refuses every key
  (reject_non_interactive_principal; require_admin alone admitted trinity-system's key
  and an admin's user-scoped key, so either could promote anyone to admin), audits
  role_change (from -> to; no-ops and refusals write none), and points the person's
  whitelist row at the new role (db.set_whitelist_default_role), so a demotion cannot be
  undone by re-binding the account away and signing in with the old address.
- Access tab (AccessPanel.vue): for an admin whose role the server verified, a role select
  on every other Active row (every change confirmed; a cancel, Esc or refusal puts the
  select back; one save at a time), "Give a platform account" on a Pending row
  (confirmed: a platform account sees the team view), "never signed in". The list's first
  load goes through viewState and reloads in place (loading-gate baseline 1 -> 0).
  New domain store stores/users.js on the single API client.
- People this agent serves: each person's role as a badge and in the pickers; assigning
  someone writes the assignment, then shares the agent with them (never the owner,
  yourself, or an account with no email; a 409 is "already shared"); a warning and
  "Share with them" on an assignment the agent isn't shared for; who the picker can't
  offer yet. An assignment alone is not access: the Inbox lists asks only from agents on
  the person's roster.
- Tests: test_837_role_change_audit.py, test_837_assigned_user_inbox.py (a real `user`
  JWT through the Workspace door and the real roster), accessPanelMembers.mount.spec.js,
  agentAssignmentsShare.mount.spec.js. Every gate and the assign-then-share order are
  mutation-proven.
- Docs: requirements/auth.md §2.10, the workspace-only-role, role-model, agent-sharing and
  email-authentication flows, the API catalog and security area, the upgrade note, user
  docs, the cso diff report, two learnings fragments.

The account endpoint behind "Give a platform account" is a separate trinity-enterprise
change; the submodule pointer bump follows its merge. No DB migration.

Part of Abilityai/trinity-enterprise#837

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…prise#837)

From the localhost eyeball of PR 2 (all 11 checks passed):

- The role select and "Give a platform account" sit in one fixed-width slot on every
  row (empty on your own), so Proactive and Remove line up.
- No "Platform role · every agent" caption under each select; the header says it and
  the select's accessible name keeps "(every agent)".
- "Share with them" is a link-style (ghost) button, like "Add seat"; the assignment
  row's Kind select is size sm; the add form's Add button matches the fields' height.
- At phone width the share box puts "Add person" under a full-width email field.
- Settings > Email Whitelist > Source: a row given from the Access tab reads "Access
  tab", and any other unlabelled source shows as stored (no source: "—") instead of
  claiming "Manual" (utils/emailWhitelist.js::whitelistSourceLabel).

Tests: the Access spec pins the slot on every row and the caption's absence; the
whitelist spec pins the label rule and the mounted Source column. Both mutation-checked.

Part of Abilityai/trinity-enterprise#837

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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 12, 2026
@webmixgamer
webmixgamer requested a review from dolho October 12, 2026 16:59

@obasilakis obasilakis 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.

Validated at e7350a9. Approving the content.

Checked:

  • Tier 1 is green, plus journey-smoke and e2e.
  • The 17 new backend tests pass, and each fix is mutation-proven: removing the interactive-principal gate fails 3 tests, removing the whitelist sync fails 2, inverting the audit condition fails 4.
  • The two component specs mount the components (no source-text reads).
  • AccessPanel.vue raw-color count is unchanged from dev (3), and the loading-gate baseline drops 1 → 0.
  • No secrets, host paths, or enterprise-design disclosure in the diff.

Two notes for the author:

  1. The whitelist sync runs on promotion as well as demotion. Raising someone to admin sets their address's row to admin, so if that account later re-binds to another address, the old address signs in as a new admin account. The CSO report covers the demotion direction only. Please confirm this is intended.
  2. The role, the audit row and the whitelist row are written in sequence. A failure in the whitelist write returns 500 with the role already changed. Low severity.

Merge order per the PR body still applies: trinity-enterprise#874 merges first, then the submodule-bump commit lands here and Tier 1 re-runs on it. status-in-dev on trinity-enterprise#837 needs setting by hand after merge, since the closing reference is cross-tracker.

webmixgamer and others added 2 commits October 12, 2026 21:10
…tes it first (Abilityai/trinity-enterprise#837)

Review on #3634 (two notes):

1. The whitelist sync also RAISED the row on a promotion. Raised to admin, a person
   who re-binds their account to another address could sign in with the old one as
   a second admin account — one a later demotion of the first would not reach. The
   row is now capped at the new role: lowered, never raised
   (db.cap_whitelist_default_role, one conditional UPDATE, replacing the setter).
2. The role, the audit row and the whitelist row were written in that order, so a
   failed whitelist write answered 500 with the role already changed. The cap now
   runs first; a failure leaves the role unchanged, and the only partial state is a
   row already lowered.

The whitelist module's role copy is now an ordered tuple pinned to ROLE_HIERARCHY by
a test. New tests: a promotion never raises the row (re-bind replay), a row below the
new role stays, a failed whitelist write leaves the role unchanged, the order pin.
Mutation-proven: letting the row rise fails 2; capping after the write fails 1.

Part of Abilityai/trinity-enterprise#837

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…1d3c)

Carries trinity-enterprise#874 (squash f8d1d3c on private main): the account endpoint
behind the Access tab's "Give a platform account" (Abilityai/trinity-enterprise#837).
f8d1d3c has the same tree as the reviewed head 90c6b5f. Enterprise migration graph:
one head (scripts/ci/check_alembic_heads.py).

Part of Abilityai/trinity-enterprise#837

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

Copy link
Copy Markdown
Contributor Author

Thanks for the review. Both notes are addressed in 5606b7dc7; the pointer bump is c39d737ce.

  1. The whitelist sync on promotion. That was not intended. Raising someone to admin set their old address's whitelist row to admin, so after a re-bind that address could sign in as a second admin account. A later demotion of the first account would not reach it. The row is now capped at the new role: lowered, never raised (db.cap_whitelist_default_role, one conditional UPDATE). New tests replay the re-bind in both directions and cover a row that is already below the new role. Letting the row rise fails 2 tests.
  2. Write order. The cap now runs before the role write. A failed whitelist write leaves the role unchanged, so the only partial state is a row that has already been lowered. A new test covers it, and moving the cap after the write fails it.

Pointer bump: b5080b6 -> f8d1d3c. That is trinity-enterprise#874's squash commit on private main, with the same tree as the reviewed 90c6b5f. The enterprise migration graph has one head.

status-in-dev on trinity-enterprise#837 will be set by hand after merge.

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.

2 participants