Skip to content

feat(access): who an agent serves, its seat, and what reads it (abilityai/trinity-enterprise#810–#814, #817) - #3271

Draft
dolho wants to merge 12 commits into
devfrom
feature/ent810-assignments-admin-ui
Draft

dolho wants to merge 12 commits into
devfrom
feature/ent810-assignments-admin-ui

Conversation

@dolho

@dolho dolho commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Who an agent serves, which seat it holds, and what it owns because of it: one feature, managed from Agent Detail › Access. This PR is the public half of six private-tracker tickets:

  • ent#810: the admin UI;
  • ent#811: seats;
  • ent#812: the objective join reads the seat;
  • ent#813: the readiness gate reads the seat;
  • ent#817: canon seats, three-state consent, and drift checked on read;
  • ent#814: a shared companion serves the seat of the person each run is for.

Its private pair is Abilityai/trinity-enterprise#818, the assignments module changes for ent#811. Neither PR depends on the other to build or pass tests.

ent#810: "People this agent serves" (above the operator list)

  • Admin view: the primary shown apart from the stakeholders, each row with its kind, proactive-brief consent and role-check state.
  • Add a person: picked by name from the instance's users (never a typed email; suspended users are not offered). Change a kind in one action on the row, or remove the row.
  • Replace primary is one flow. The old primary is re-kinded, or deleted if they already hold that kind or are being removed, before the new one is written, so the agent never has two. If the new one can't be written, the old one is put back.
  • Errors and loading: every refused write shows the backend's own reason. A failed first load gets LoadFailed with retry; a failed refresh keeps the data and shows a stale banner.
  • Empty and missing states: the empty state names the next action, and the add form starts on Primary. With no primary, a warning says briefs and approvals have nowhere to go.
  • Non-admins see the section read-only, controls hidden. The section is absent when the instance isn't entitled to the module. Admin controls are gated on the server-verified profile (bug: Agent Detail and Workspace over-fetch on load (21 redundant requests, N+1 roster) #2198).

ent#811: seats (operator ruling 2026-10-06, R50 + R50a)

  • services/assignment_provider.resolve_seat(agent) answers which seat an agent holds itself or serves through its primary, with no triggering user. It's for the readiness gate (ent#813), the objective join (ent#812) and the role card. It always returns a dict and never raises: every degraded case reads as "no seat" with a named reason. It reads the provider's optional seat_for through getattr.
  • Header:
    • "Holds the seat: <id>" when the agent holds it itself (this wins when both apply);
    • "Serves the seat of its primary: <id> ()";
    • "No seat yet".
  • Admins can record, change or clear the seat the agent holds. A refusal names the agent that already holds it; a person's row naming the same seat does not block it.
  • The seat on a row is optional when adding or replacing the primary, and can be added, changed or cleared per row (an explicit null clears).

ent#812: owned objectives come from the seat on record

  • The objective join resolves "owned" through resolve_seat, not x-role.role. That means the seat the agent holds (that seat only), else the seats its assigned people hold, primary first; a shared companion owns the union.
  • x-canon alone enables the join; x-role is neither required nor read. Behaviour change: an agent with x-role but no x-canon no longer joins, as the issue specifies.
  • No seat, no owned objectives. A no_seat finding says why and what to do (assign the primary with their seat on the Access tab). It replaces role_id_invalid, and supporting objectives still show. The Workspace role card renders that sentence.
  • supporting_agents is read as before, documented as frozen.
  • Response: the role now carries case and seats (model updated, so response_model doesn't drop them).
  • Re-pinned suites: ent#527, ent#666, ent#676, ent#727 and the route model test declared their seat through x-role. They now record it through a registered provider, which is what an assigned primary does. Assertions about objectives, numbers and findings are unchanged.
  • The objective-join requirements (requirements/lifecycle-observability.md §50) are updated.

ent#813: the readiness gate asks the seat, not the template

  • Who counts as a companion: for an unstamped agent, "is this a companion?" is now answered by resolve_seat. A companion is an agent whose primary holds a seat (serves).
  • What stays the same: the owner's stamp still decides first, and a stamped agent's seat isn't looked up. The ent#689 rollout seed and held-schedule behaviour are unchanged; a stamped "calibrating" companion with a primary is still held.
  • The template is no longer read: one container read less per brief, and an agent can't take itself out of scope by editing its own files.
  • Behaviour change: a companion template with no assigned primary now sends its brief while calibrating, until its primary is assigned. The requirements (core-agent.md §5.36) say so.

ent#817: canon seats, three-state consent, drift on read

  • Canon seats (OSS-core, operator ruling 2026-10-06): GET /api/agents/{name}/canon/roles (services/canon_roles_service.py) lists <canon>/roles/*.yaml with id, title and updated.
  • Three-state consent: agent_sharing.allow_proactive NULL = not asked, 0 = declined, 1 = consented.
    • New shares are written NULL.
    • A data migration on both tracks (SQLite + Alembic 0090_proactive_consent_not_asked) moves the legacy DEFAULT-0 rows to NULL. Data only; the DDL is untouched, so there's no table rebuild.
    • Who may be messaged is unchanged: only 1 allows it.
    • Shares and the Access list carry proactive_consent.
  • UI:
    • seat fields suggest the canon's seats through a datalist, and a typed id still works;
    • the header and rows show the role's title, and the header shows the file's updated stamp;
    • drift reads current / changed / missing / unknown with its reason, and nothing for a row with no seat;
    • consent reads in three states;
    • when there's nothing to suggest, admins are told why.
  • Requirements: updated in requirements/infrastructure.md.

ent#814: a shared companion serves each person's own seat

  • Seam: assignment_provider.resolve_served_seat(agent, email) returns the person's own seat. The agent's own seat wins when it holds one (holds wins), and the primary's is the fallback. It never raises. A provider without the OPTIONAL served_seat_for falls back to seat_for.
  • Seat decisions (ent#638) are stamped with the served seat. The agent's decided_by_role is used only when nobody's seat is on record, because the old prompt named the primary's seat on every run.
  • Prompt: the Primary human line is unchanged. A Seat this run serves line is added only when the run serves a different seat: a chat user, or the Workspace a scheduled brief is delivered to. That address is read off the execution row and never rendered. Suppressed audiences still render nothing.
  • UI: the add form no longer pre-fills the primary's seat for every new person.
  • Unchanged: the one-primary rule, the primary display name and the primary address.

Still open

The Workspace role card still decides whether to show from x-role and takes its role title from there. ent#812 only moved its objectives line, so switching the card is a follow-up.

The drift loop is replaced by the check on read (ent#817).

Changes

  • src/backend/services/assignment_provider.py: the optional seat_for method and resolve_seat()
  • src/backend/services/objective_join_service.py, models.py (ObjectiveRoleRead), client_portal's PortalAgentRole.vue copy (ent#812)
  • src/backend/services/role_readiness_gate.py (ent#813)
  • services/assignment_provider.py, routers/seat_decisions.py, services/platform_prompt_service.py, services/task_execution_service.py (ent#814)
  • src/backend/services/canon_roles_service.py (new), routers/agent_files.py, db/agent_settings/sharing.py, db_models.py, db/migrations.py, migrations/versions/0090_proactive_consent_not_asked.py (ent#817)
  • src/frontend/src/components/AgentAssignmentsSection.vue (new), mounted in AccessPanel.vue
  • src/frontend/src/stores/assignments.js (new, through the single api client)
  • src/frontend/src/utils/assignments.js (new, pure: seat line, replace-primary steps + rollback, consent/drift labels, error text)
  • Tests: tests/unit/test_ent811_resolve_seat.py, tests/unit/test_ent812_objective_join_seat.py, src/frontend/tests/unit/agentAssignmentsSection.mount.spec.js

Test Plan

  • tests/unit/test_ent811_resolve_seat.py: 17 passed. Each answer passes through; no provider, no agent, unsupported provider, a raise, no answer and eight malformed shapes all read "no seat". The neighbouring ent#500 provider suites: 72 passed together.

  • tests/unit/test_ent812_objective_join_seat.py: 12 passed, through the real read_objective_join with the ent#666 fakes. Covers:

    • a held seat owns only itself;
    • a companion owns the union, primary first;
    • a lying x-role is not read;
    • x-canon alone enables the join;
    • no seat (provider says none, answers nothing, or no provider) owns nothing, gives no_seat, and still shows supporting work.

    Five mutations each turn tests red:

    • holds owning the union;
    • a companion owning only the primary's seat;
    • x-role still enabling the join;
    • no no_seat finding;
    • the owned flag matching only the first seat.
  • tests/unit/test_ent689_readiness_gate.py (TestBriefReadiness rewritten for seats): 32 passed. Covers:

    • an unstamped companion is held;
    • a stamped "calibrating" companion with a primary is still held;
    • a stamped agent never asks for its seat;
    • holds and none fire with their reason;
    • no provider, a lookup error, no answer and a malformed answer fire;
    • an unreadable stamp fails open;
    • the template is never read;
    • the decision is logged.

    Four mutations each turn tests red:

    • holds counted as a companion;
    • an unknown seat holding the brief;
    • the seat consulted for a stamped agent (caught after I strengthened the test);
    • the decision not logged.
  • The 24 unit files touching the join, role card, provider interface, seat decisions and readiness gate: 847 passed on seeds 12345 and 106.

  • tests/unit/test_ent817_proactive_consent.py (8) and test_ent817_canon_roles.py (14) pass, on real SQLite and the agent-door fakes. The 42 consent and sharing files, including schema parity, pass: 1,139. The bug(auth): agent-scoped keys can toggle their own agent's autonomy — PUT /api/agents/{name}/autonomy lacks reject_agent_principal #2996 route census passes.

  • tests/unit/test_ent814_served_seat.py: 22 passed. A mutation check reverting each half (the decision stamp, the prompt line, the seat-run address) turns it red.

  • Full public unit suite: 21,973 passed. The 42 failures (payments pin parity, a2a, ssrf, mcp_validator, ipv6) fail identically on a clean dev checkout, caused by local venv package versions (e.g. 1.12.0 vs the 1.18.0 pin); the other 6 are order-flaky in those same files.

  • Live on a local instance:

    • a chat with the non-primary person gets Seat this run serves: project-lead, with no email in the prompt;
    • a decision on a brief delivered to them is stored as project-lead, though the agent claimed cfo;
    • an agent that holds a seat serves it for everyone.
  • agentAssignmentsSection.mount.spec.js: 32 passed (mounted, jsdom). These mutations each turn tests red:

    • admin gate always true;
    • entitlement gate removed;
    • no rollback;
    • ignoring an existing kind in the replace plan;
    • no drift warning;
    • suspended users offered;
    • no seat line.
  • Full frontend unit suite: 274 files, 4,769 tests passed, including the raw-colour, loading-gate and source-text ratchets. check:tokens OK.

  • Live on a local instance with the assignments module enabled (this branch + ent#818), light and dark:

    • empty state;
    • add a primary and an approver;
    • a duplicate add refused with the backend's message;
    • a real primary replacement;
    • the agent holding a seat;
    • a second agent refused, naming the holder;
    • a viewer added without a seat, then given one through the row editor.

    The live run also caught that the routes are mounted at /api/enterprise/assignments, not /api/assignments as the issue's notes say. The spec pins the real paths.

Live walkthrough (2026-10-06)

Every claim above was driven end to end on a local instance:

  • Setup: seeded people (one of them suspended), canon roles and objectives, three agents, a non-admin owner.
  • Browser: Playwright, recording layout shift at every step. 64 of 65 scripted checks pass across 24 steps. The miss is a phone overflow from the existing "Add operator" button, which predates this PR.
  • Backend: prompts, decisions and the readiness gate were checked in the running backend.

The run found seven defects the unit suites missed. All are fixed here, each with a test that fails when the fix is reverted:

  • Replacing the primary with someone already on the agent now promotes their row, instead of listing them twice with two seats.
  • The add form clears the seat after an add. Before, the next person added silently got it.
  • A person on several rows serves the primary row's seat first (fix(security): sandbox voice panel HTML in iframe (CRITICAL XSS) #818). Before, the answer depended on read order.
  • The first load keeps the loaded shape and doesn't say "No seat yet" while loading. On a slow network the list below used to jump 133px; it now moves 5–24px.
  • The Workspace role card shows the no_seat sentence. Before, it was mapped as a role error and never appeared.
  • On a phone each row stacks, so the kind dropdown no longer covers the person's name.
  • A failed replacement says who the primary is now, from a fresh read.

During actions (add, edit, replace, seat changes) the section causes no layout shift.

After merge

Trinity dev needs an enterprise pointer bump once ent#818 lands.

Related to abilityai/trinity-enterprise#810, #811, #812, #813, #814 and #817 (cross-tracker: closed by hand at release)

🤖 Generated with Claude Code

…ai/trinity-enterprise#810)

Assignments (primary, approver, collaborator, viewer) had no UI: setting
an agent's primary human or its approvers took an API call. Agent Detail
› Access now has a "People this agent serves" section above the
operator list, over the existing ent#500 endpoints.

An instance admin sees the primary apart from the stakeholders, with
the seat it serves, each row's proactive-brief consent and role-check
state; adds a person picked by name, changes a kind in one action on the
row, removes a row, and replaces the primary as one flow that demotes
the old primary first and puts it back if the new one cannot be written.
Every refused write shows the backend's own reason. Non-admins see the
same section read-only. The section is absent without the assignments
module.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dolho dolho added the ui PR touches the frontend UI — triggers Playwright e2e tests label Oct 6, 2026
…, optional and clearable roles (Abilityai/trinity-enterprise#811)

The public half of ent#811, on top of the ent#810 section.

- services/assignment_provider: the optional provider method seat_for
  and resolve_seat(agent), which answers which seat an agent holds
  itself or serves through its primary with no triggering user. Always
  a dict, never a raise: no provider, a provider without seat_for, no
  answer, a raise or a malformed shape all read as "no seat" with a
  named reason, the failure direction the readiness gate needs.
- The section's header has both forms: "Holds the seat: <id>" (the
  agent itself, which wins) or "Serves the seat of its primary". An
  admin can record, change or clear the seat the agent holds; a refusal
  names the agent that already holds it.
- The seat on a row is optional when adding or replacing the primary,
  and a row's seat can be added, changed or cleared (explicit null).
- With no primary yet, the add form starts on Primary.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dolho dolho changed the title feat(access): manage who an agent serves from its Access tab (abilityai/trinity-enterprise#810) feat(access): who an agent serves and which seat it holds — the Access-tab section (abilityai/trinity-enterprise#810, #811) Oct 6, 2026
@dolho dolho changed the title feat(access): who an agent serves and which seat it holds — the Access-tab section (abilityai/trinity-enterprise#810, #811) draft: feat(access): who an agent serves and which seat it holds — the Access-tab section (abilityai/trinity-enterprise#810, #811) Oct 6, 2026
@dolho
dolho marked this pull request as draft October 6, 2026 12:42
…x-role (Abilityai/trinity-enterprise#812)

Under the 2026-10-06 ruling (R50 + R50a) the seat is recorded in
Trinity, held by a person or by an agent; x-role.role in template.yaml
stops being truth. The objective join was the one surface that relied
on it.

- "Owned" resolves through assignment_provider.resolve_seat: the seat
  the agent holds (that seat only), else the seats its assigned people
  hold, primary first; a shared companion owns the union.
- The canon declaration alone enables the join; x-role is neither
  required nor read.
- An agent with no seat owns nothing and a no_seat finding says why and
  what to do (it replaces role_id_invalid); supporting objectives still
  show. The role card renders the sentence.
- supporting_agents is read as before, documented as frozen.
- The response's role carries case and seats.

Existing suites that declared their seat through x-role now record it
through a registered provider, which is what an assigned primary does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dolho dolho changed the title draft: feat(access): who an agent serves and which seat it holds — the Access-tab section (abilityai/trinity-enterprise#810, #811) feat(access): who an agent serves, which seat it holds, and the objectives it owns (abilityai/trinity-enterprise#810, #811, #812) Oct 6, 2026
…not one whose template has x-role (Abilityai/trinity-enterprise#813)

The readiness gate (ent#689) held an unstamped agent's proactive brief
when its template declared x-role. Under the 2026-10-06 ruling the seat
is recorded in Trinity, so "is this a companion" is now answered by
assignment_provider.resolve_seat: a primary that holds a seat (serves).

- An agent holding a seat itself, no primary with a seat, no seat
  lookup or a lookup error all read as "not a companion", so the brief
  fires as before (fails open, #1638).
- The owner's stamp still decides first and the seat is not looked up
  for a stamped agent; the rollout seed and held-schedule behaviour of
  ent#689 are unchanged.
- The template is no longer read: one container read less per brief,
  and an agent can no longer take itself out of scope by editing its own
  files.
- Each decision is logged with its reason.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dolho dolho changed the title feat(access): who an agent serves, which seat it holds, and the objectives it owns (abilityai/trinity-enterprise#810, #811, #812) feat(access): who an agent serves, the seat it holds, and what reads that seat (abilityai/trinity-enterprise#810, #811, #812, #813) Oct 6, 2026
…ilityai/trinity-enterprise#817)

The public half of ent#817, completing ent#810's remaining criteria.

- Canon seats (OSS-core, operator ruling 2026-10-06): GET
  /api/agents/{name}/canon/roles lists <canon>/roles/*.yaml with id,
  title and updated through the agent door the objective join uses, on
  the same per-agent read budget, self-gated for agent keys. Every empty
  answer is named (stopped, no canon, no roles/, unreadable, timeout).
- Proactive consent in three states: agent_sharing.allow_proactive NULL
  = not asked, 0 = declined, 1 = consented. New shares are written NULL;
  a data migration on both tracks (SQLite + Alembic 0090) moves the
  legacy DEFAULT-0 rows to NULL. Who may be messaged is unchanged: only
  1 allows it. Shares and the Access list carry proactive_consent.
- The assignments section: seat fields suggest the canon's seats (a
  typed id still works), the header and rows show the role's title and
  the file's updated stamp, drift reads current / changed / missing /
  unknown with its reason, and consent reads in three states.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dolho dolho changed the title feat(access): who an agent serves, the seat it holds, and what reads that seat (abilityai/trinity-enterprise#810, #811, #812, #813) feat(access): who an agent serves, its seat, and what reads it — UI, seats, objectives, readiness, canon roles (abilityai/trinity-enterprise#810–#813, #817) Oct 6, 2026
…un is for (Abilityai/trinity-enterprise#814)

- assignment_provider.resolve_served_seat(agent, email): the person's
  own seat, the agent's own when it holds one (holds wins), the
  primary's as the fallback; never raises. A provider that predates
  the OPTIONAL served_seat_for falls back to seat_for.
- Seat decisions (ent#638) are stamped with the served seat; the
  agent's decided_by_role is only the fallback when nobody's seat is on
  record (its prompt used to name the primary's seat on every run).
- The prompt keeps its Primary human line and adds "Seat this run
  serves" only when the run serves a different seat: a chat user, or
  the Workspace a scheduled brief is delivered to (read off the
  execution row, never rendered). Suppressed audiences render nothing.
- The add form no longer pre-fills the primary's seat for every new
  person.

The one-primary rule, the primary display name and the primary address
are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dolho dolho changed the title feat(access): who an agent serves, its seat, and what reads it — UI, seats, objectives, readiness, canon roles (abilityai/trinity-enterprise#810–#813, #817) feat(access): who an agent serves, its seat, and what reads it (abilityai/trinity-enterprise#810–#814, #817) Oct 6, 2026
dolho and others added 2 commits October 6, 2026 18:43
…d up (Abilityai/trinity-enterprise#810, #812, #814)

Found by driving the section end to end on a local instance (seeded
people, canon, three agents) with layout-shift capture on every step.

- Replacing the primary with someone already on the agent promotes
  their existing row instead of adding a second one. Before, the person
  was listed twice (primary and approver) and, under ent#814, held two
  seats to choose between. The old primary still steps down first.
- The add form clears the seat after a successful add. Before, the seat
  typed for one person was silently given to the next one added.
- First load keeps the loaded section's shape (seat bar, a two-person
  list card, the add form) and no longer flashes "No seat yet" for an
  agent that has one. On a slow network the operator list below moved
  133px on arrival; it now moves 5-24px for agents with up to two
  people.
- A failed replacement says where the agent stands, from the re-read
  roster ("Leo Park is still the primary."), not only the error.
- On a phone each row stacks its details above its controls; the kind
  select no longer covered the person's name.
- The Workspace role card renders the no_seat finding. ent#812 had
  mapped it as a role error, which the backend never sends, so the
  sentence could not appear.

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

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

⚠️ Alembic head fork if this PR is merged into dev.

dev has advanced since this PR's checks last ran. GitHub recomputes the merge ref when the base moves but does not re-trigger workflows, so a green schema-parity here can describe a base that no longer exists (#2533).

scripts/ci/check_alembic_heads.py against dev + this PR, merged in memory
alembic-heads: FAIL — src/backend/migrations/versions resolves to 2 heads across 98 revision(s); exactly 1 is required.
  • 0095_proactive_consent_not_asked  (0095_proactive_consent_not_asked.py)
  • 0096_a2a_internal_scope  (0096_a2a_internal_scope.py)

They fork at: 0094_agent_skill_gates  (0094_agent_skill_gates.py)

`alembic upgrade head` is singular and resolves its target BEFORE applying anything,
so this graph applies ZERO revisions — every revision since the fork stops arriving,
not only the one that forked. Fix by chaining the newer revision off the real head,
or — if the forked revision may already be applied somewhere — by adding a merge
revision (`alembic merge -m "…" <head-a> <head-b>`), whose tuple `down_revision`
converges the line from any starting state. See Architectural Invariant #3.
alembic-heads: src/backend/enterprise/backend/migrations/versions — version directory absent (submodule not initialised) — skipped.

Evaluated on the version line only — this PR also conflicts with dev in 2 unrelated file(s), which do not change this verdict but must be resolved before merge:

src/backend/db/migrations.py
tests/registry.json

Fix: rechain this PR's revision off 0096_a2a_internal_scope (the current dev head), or — if the forked revision may already be applied somewhere — add a merge revision (alembic merge -m "…" 0095_proactive_consent_not_asked 0096_a2a_internal_scope), whose tuple down_revision converges the line from any starting state. See Architectural Invariant #3.

Push the fix and schema-parity re-checks it against the merge ref immediately; this comment clears on the next push to dev touching src/backend/migrations/versions/**.

Advisory — this check does not block merge. · head_sha: 96673aae6ef1c87128af891dea1d32ec2d05d458 · run

@AndriiPasternak31

Copy link
Copy Markdown
Contributor

@dolho a heads-up from #3304 (draft): with it, agent file download and preview no longer open a path that is a link, for every caller, the platform's own reads included. A template.yaml that a template ships as a link becomes unreadable to role_readiness_gate and the objective join, which treat it like any other unreadable template; the backend logs a warning naming the file. Templates should ship template.yaml as a regular file. Nothing to change in this PR unless you want the gate to handle that case differently.

…n_claude_id

Conflicts: SQLite MIGRATIONS list (dev's platform_alert_subjects and
chat_session_claude_id first, then proactive_consent_not_asked) and
tests/registry.json (union of both sides).

Alembic: 0090_proactive_consent_not_asked forked off 0089 alongside dev's
0090_platform_alert_subjects; renamed to 0092_proactive_consent_not_asked
with down_revision = 0091_chat_session_claude_id. One head.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…'s assignment, not x-role (live-check follow-up)

The 2026-10-06 live check of this PR and ent#818 left three open items:

- The Workspace role card took its role from x-role.role and its "Seat ·"
  line from x-role.seat (an email), so a shared companion's card said
  "sales-lead" while the Access tab said Head of Sales. It now reads the
  seat this agent serves for the viewer (resolve_served_seat: held, else
  the viewer's own, else the primary's) — the answer the agent's prompt
  gets — and says where it came from (holds / your seat / its primary's),
  never a person. No seat → role.error no_seat, said once, with the
  supported objectives still listed. x-role remains only as the readiness
  claim and as a reason to show the card.
- "Your relationship" was never wired: the agent's own primary read "no
  assignment recorded". It is now the viewer's assignment kind via the
  provider's kinds_for (Abilityai/trinity-enterprise#818), answered for a
  stopped agent too.
- "Add operator" pushed the page 47px wider than a 390px screen: the
  email input now gives way (min-w-0) and the button does not wrap.

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

dolho commented Oct 7, 2026

Copy link
Copy Markdown
Contributor Author

The three "Still open" items from the 2026-10-06 live check are fixed in 797a6c3af. The enterprise half is abilityai/trinity-enterprise#818 a043f6770.

1. The Workspace role card now reads the seat on record, not x-role.

  • Before, the role came from x-role.role and the "Seat ·" line from x-role.seat, which is an email. So Maya's card said "sales-lead · role file not in the canon · Seat · admin@local.test" while the Access tab said Head of Sales.
  • build_role_card now asks resolve_served_seat(agent, viewer). That is the same answer the agent's prompt gets: the seat it holds itself, else the viewer's own seat, else its primary's. It reads that seat's role file.
  • The new seat_source field says where the seat came from, in words: "The agent holds this seat itself", "Your seat on this agent" or "Its primary's seat". It never names a person. The seat field is gone.
  • When the card shows: the agent serves a seat, has a canon, or still declares x-role. The last condition stays only so a legacy companion keeps its readiness control. x-role.status is still the template's readiness claim. x-role.role and x-role.seat are no longer read.
  • No seat: the card says role.error = no_seat once, on the role, and the objectives block no longer repeats it. Supporting objectives still list.

2. "Your relationship" now shows the viewer's assignment kind.

  • It comes from the provider's kinds_for (added in fix(security): sandbox voice panel HTML in iframe (CRITICAL XSS) #818), via seat_decision_service.reader_kind. The values are primary, approver, collaborator or viewer. "No assignment recorded" now appears only when there really is none.
  • It's answered for a stopped agent too, because the record isn't in the container.

3. "Add operator" no longer overflows on phones. The email input gets min-w-0 and the button shrink-0 whitespace-nowrap. Colour classes are untouched, so the colour ratchet is unchanged. It needs a 390px look in the browser; jsdom has no layout to test it.

Tests

  • test_ent527_role_card.py is rewritten to the seat rule, with seven new or re-pinned tests, all red with role_card.py reverted. They cover:
    • a card with a seat even without x-role;
    • a lying x-role not being read;
    • each viewer seeing their own served seat (Maya → CFO);
    • no seat → no_seat with supported objectives;
    • the relationship from kinds_for, including a stopped agent;
    • a traversal-shaped seat id refused.
  • portalAgentRole.spec.js: the seat-source words, the relationship text (an unknown kind falls back to "no assignment recorded"), and the no-seat sentence shown once.
  • 108 backend tests (ent527, ent676, readiness-on-lists) and 93 frontend specs, plus the ratchets, pass locally. Full suites are left to CI.

Docs: feature-flows/workspace-role-card.md and requirements/core-agent.md §5.36.

⚠️ Behaviour that #818's kinds_for switches on (needs your attention): ent#638's reader gate readable_seats already gave "a provider-declared stakeholder" read-only access to every seat's decisions. No provider answered until now. With kinds_for live, anyone with a row on the agent can read every seat's decision records. That is the documented per-agent v1 rule (DEBT_INBOX 2026-09-22), not new policy, but it is live now.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Oct 7, 2026

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

github-actions Bot commented Oct 7, 2026

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.

Re-chain the proactive-consent revision: 0092_proactive_consent_not_asked
forked with dev's 0092_portal_messages_attachments off 0091; it is now
0095_proactive_consent_not_asked off 0094_agent_skill_gates (1 head).
migrations.py and tests/registry.json: keep both, dev's entries first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dolho added a commit that referenced this pull request Oct 8, 2026
Re-chain seat_ask_class_state: 0093 forked with dev's
0093_platform_alert_responded_heal; it is now 0096_seat_ask_class_state
off 0094_agent_skill_gates (1 head). 0096, not 0095, because #3271 takes
0095_proactive_consent_not_asked off the same parent — whichever of the
two lands second re-chains onto the other.

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

dolho commented Oct 8, 2026

Copy link
Copy Markdown
Contributor Author

Merged dev (0f97d1171). Fixes the alembic-head-watch fork: 0092_proactive_consent_not_asked is now 0095_proactive_consent_not_asked, with down_revision = "0094_agent_skill_gates" (96 revisions, 1 head). The migrations.py and tests/registry.json conflicts are resolved by keeping both, dev's entries first. test_ent817_proactive_consent.py: 8 passed.

Heads-up: #2984 now chains 0096_seat_ask_class_state off the same 0094, so whichever of the two merges second has to re-chain onto the other.

Related: trinity-enterprise#818 now leaves kinds_for unregistered until readable_seats here is narrowed to the reader's own seat. Until then, "Your relationship" reads "no assignment recorded".

🤖 Generated with Claude Code

… the ent#817 migration on the engine's connection

- test_3274's free-text guard (from dev) requires every Optional[str]
  ExecutionContext field to be scanned or named platform-controlled:
  served_role_id is a sanitized seat id, served_person_email is never
  rendered.
- The ent#817 migration test wrote through get_engine() but migrated
  through get_db_connection(); under seed 12345 the two pointed at
  different files ('no such table: agent_sharing'). It now migrates on the
  engine's own DBAPI connection.

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

This branch has not been deployed

No deployments
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