Skip to content

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

Merged
vybe merged 17 commits into
devfrom
feature/ent810-assignments-admin-ui
Oct 11, 2026
Merged

vybe merged 17 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 0098_proactive_consent_not_asked, re-chained onto 0097_operator_queue_replace at the 2026-10-11 merge of dev) 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.

Role card (closed 2026-10-07, 797a6c3af)

The Workspace role card reads the seat on record through resolve_served_seat(agent, viewer), not x-role: seat_source says where the seat came from in words and never names a person; x-role is read only for its readiness status, so a legacy companion keeps its readiness control. "Your relationship" reads the provider's kinds_for when one is registered (trinity-enterprise#818 leaves it unregistered until readable_seats is narrowed to the reader's own seat, so it reads "no assignment recorded" until then).

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/0098_proactive_consent_not_asked.py (ent#817)
  • docs/memory/architecture/{api-endpoints,backend,database}.md: the new read, the seat resolvers and the three-state column are catalogued
  • 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.

Merge of dev (2026-10-11)

Merged dev at d127132b4 (c108addf8): keep-both on migrations.py, the agent_files.py imports, platform_prompt_service.py (#3247's pending asks beside ent#814's served seat) and tests/registry.json, dev's entries first. The Alembic revision is 0098_proactive_consent_not_asked on 0097_operator_queue_replace, one head. dev's ent#839 self-gate on the seat-decision router reads current_user.agent_name, so the ent#814 test's user stub now carries it as the real User does (8eda88239).

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 99 revision(s); exactly 1 is required.
  • 0095_proactive_consent_not_asked  (0095_proactive_consent_not_asked.py)
  • 0097_operator_queue_replace  (0097_operator_queue_replace.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 4 unrelated file(s), which do not change this verdict but must be resolved before merge:

src/backend/db/migrations.py
src/backend/routers/agent_files.py
src/backend/services/platform_prompt_service.py
tests/registry.json

Fix: rechain this PR's revision off 0097_operator_queue_replace (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 0097_operator_queue_replace), 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>
vybe added a commit that referenced this pull request Oct 9, 2026
…orable secret value is a named 422 (#3325) (#3417)

Fixes #3325 — independent fix, one of ten small bug fixes in the trinity-pm `chain-easy-1008` run. Base is `dev`. **Draft until the operator merges.**

## What
- `src/backend/services/credential_encryption.py::decrypt`: every malformed envelope shape raises `ValueError`, as the function promises — JSON that is not an object (`[]`, `null`), and a non-string nonce or ciphertext. Importing such a `.credentials.enc` now returns **400** instead of a 500 with a stack trace in the log. The envelope format and every successful decrypt are unchanged.
- `src/backend/services/secret_settings.py::encrypt_secret_setting`: the key is checked first, in its own `try`, and only that check can raise `MissingEncryptionKeyError`. A value that cannot be encoded (for example a lone surrogate) raises the new `SecretSettingValueError(ValueError)`. Its message names the setting, never the value.
- `src/backend/error_handlers.py` + `src/backend/main.py`: one app-level handler maps `SecretSettingValueError` to **422**. `src/backend/routers/settings/credentials.py`: three pass-through lines in the routes whose catch-all would have turned it into a 500 (Anthropic, GitHub PAT, `PUT /slack`).
- Tests: the three strict-xfail markers in `test_ec_credential_crypto_edges.py` are removed; new `tests/unit/test_3325_credential_error_types.py` (registered in `tests/registry.json`) asserts no value in the message or the exception chain, a missing key reported before a bad value, `PUT /api/settings/api-keys/anthropic` → 422, and that `main.py` registers the handler.

## Rulings carried (orchestrator, on the operator's behalf — plan file)
- TD-1: one app-level handler plus the three pass-through lines — as recommended.
- TD-2: a `.credentials.enc` that decrypts correctly but whose contents are not an object is deferred — as recommended; forging one needs the platform key.
- TD-3: 422, matching the existing 422 for refused cleartext writes — as recommended.

## Review + security
`/review` reading pass (claude-fable-5-1, report-only): **MERGEABLE**, no critical findings. The secret cannot leave through the new error: it is raised after the `except` block, so `__context__` is cleared, not merely hidden; the handler returns only the message; nothing logs it; audit rows are written only after a successful write. The only residual carrier is traceback frame locals, which nothing in the backend renders. The handler is keyed on the subclass, so no other `ValueError` is swallowed or relabelled. `/cso --diff`: no findings.

Evidence sweep (claude-opus-5-5): **GREEN**. 23 unit files import the touched modules; the 16 most direct ran shuffled (seed 12345), one file per process, all passed. All 7 tests in the new file ran, 0 skipped. With both service files taken from `dev`, exactly the three formerly-xfail tests fail (8 cases). Seven less direct files are left to CI and named in the review file. `git merge-tree` against `dev` f6bedcd is clean.

## Tests
Targeted counts above. Not run here: the full unit island (CI).

## Before merge
- Startup auto-import (`lifecycle.py`) on a malformed envelope now lands in its `ValueError` retry arm: it retries without a warning per attempt and ends in the same "failed" result, with the existing error log after the last attempt.
- Enterprise callers of `decrypt` get `ValueError` where they got `AttributeError` / `TypeError` on a malformed envelope. Two catch broadly, three propagate as before; no change needed there.
- The new test file has a module-level `importorskip` for `sqlalchemy` and `cryptography`: it skips silently where those are missing. They are present on the engineer and in CI.
- Ten open PRs share `tests/registry.json` or `src/backend/main.py` with this branch (#3412, #3411, #3407, #3304, #3271, #3256, #2984, #2956, #2713, #2709). A test-merge of this head against each adds no conflict beyond what that PR already has with `dev`.

## Handoffs (not in this diff)
- TD-2 above: the decrypted-but-not-an-object case can still 500 on import.
- `PUT /api/settings/slack` saves the client ID before a later secret write fails (partial update, pre-existing).
- The GitHub PAT, Resend and Gemini routes are covered by the app-level handler but not exercised by the new route test.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
vybe added a commit that referenced this pull request Oct 9, 2026
…L is refused, not stored (#3323) (#3420)

Fixes #3323 — independent fix, one of ten small bug fixes in the trinity-pm `chain-easy-1008` run. Base is `dev`. **Draft until the operator merges.**

## What
- `src/backend/utils/url_validation.py::reject_embedded_credentials`: strips its input and treats a leading `//` as already having a host, the same rule `strip_url_credentials` uses. `//<token>@github.com/o/r` is now refused like the `https://` form, including with leading whitespace or mixed case. All three write paths that store a skills-library URL go through this one function (`routers/skills.py` create and update, legacy adoption in `services/skill_service.py`), so the token is no longer saved in plaintext to `skill_sources.url` or the audit details.
- If `urlparse` refuses an input that starts with `//`, the function falls back to the module's existing userinfo regex instead of raising, so this fix adds no new 500.
- Tests: the D3 strict-xfail marker is removed; four more refused shapes and a four-case "must not raise / must not over-reach" test are added.

## Rulings carried (orchestrator, on the operator's behalf — plan file)
- TD-1: the fallback applies only to inputs starting with `//`; the bare `ValueError` for every other malformed input is #3322's and is unchanged (D4 stays a strict xfail) — as recommended.
- TD-2: keep the username/password check; `//@host` carries no secret and is not refused — as recommended.
- TD-3, TD-4: deferred, see Handoffs — as recommended.

## Review + security
`/review` reading pass (claude-fable-5-1, report-only): **MERGEABLE**, zero critical, five informational. The pre-fix and post-fix modules were executed side by side over forty inputs: nothing that was refused before now passes, credential-free inputs are unchanged, the fallback regex is anchored and linear. `/cso --diff`: no findings.

Fixed after review:
- `546f329a` — the `tests/registry.json` descriptions no longer call #3323 a strict xfail.
- `a75a5e04` — the reject stub in `test_ent183_skill_packages.py` mirrors the fixed `//` rule. It is deliberately stricter than production on a malformed bracket (`//[oops/x` raises).

Evidence sweep after the fix (claude-opus-5-5): **GREEN**. 28 unit files import the module; the 14 most direct ran shuffled (seed 12345), one file per process. Three did not come back clean, none because of this branch:
- `test_ent236_skills_lifecycle.py::TestNonRepoDirectoryRecovers::test_real_repo_still_pulls` and `test_skill_service_user_agent.py` (collection error) fail the same way on `dev` f6bedcd in the engineer's environment.
- `test_ent237_skill_sources.py` hit the 240 s per-file limit at about 72 % with no failure printed — left to CI.

`git merge-tree` against `dev` f6bedcd is clean.

## Tests
895 passed across the 11 files that completed cleanly. Not run here: the 14 less direct importer files, `test_ent237_skill_sources.py` to completion, and the full unit island (CI).

## Before merge
- Ten open PRs share `tests/registry.json` with this branch (#3417, #3412, #3411, #3407, #3304, #3271, #3256, #2984, #2956, #2709). A test-merge of this head against each adds no conflict beyond what that PR already has with `dev`.
- One stale docstring remains at `tests/unit/test_ec_url_validation_properties.py:36` (still calls D3 a strict xfail).

## Handoffs (not in this diff)
- TD-3 (P3): shapes that still pass both reject and strip — `/\t/tok@`, `/\n/tok@`, `///tok@`, `https:/tok@` (`urlparse` removes tab and newline after the check). Fixing it needs the same change in `strip_url_credentials` and cases in both test families.
- TD-4: a property test that reject and strip always agree over the shared `test_2052` corpus; it needs exceptions for empty userinfo.
- #3322 (bare `ValueError` / `UnicodeError` → 500) is the sibling issue in the same file and is untouched.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
vybe added a commit that referenced this pull request Oct 9, 2026
…t the database (#3385) (#3421)

Fixes #3385 — one of ten small bug fixes in the trinity-pm `chain-easy-1008` run. **Stacked on #3414** (the fix for #3314): base is `feature/3314-queue-fingerprint-falsy`, because both issues edit `_clamp_ingested_item` and the fingerprint helpers. Merge #3414 first, then retarget this to `dev`. **Draft until the operator merges.**

## What
- `src/backend/services/operator_queue_service.py`: new `OPERATOR_QUEUE_TYPE_MAX = 64` and `_bounded_type`. An agent-written `type` longer than 64 characters is shortened with the existing `…[truncated]` marker at ingest (`_clamp_ingested_item`); the ask is never refused or lost. `_comparable_type` applies the same bound, so a shortened row does not read as a rewrite (#2915) and a row stored before this fix at full length still compares equal.
- `src/backend/db/operator_queue.py::_insert_values`: `_DB_BELT_TYPE_MAX_BYTES = 1024`; above it the insert raises `ValueError`, like the id / title / question / context belts. All three create paths go through it.
- Docs: five lines in `architecture/api-endpoints.md`, `feature-flows/operating-room.md` and `requirements/security.md` that list the clamp and belt fields now name `type`.
- Tests in `tests/unit/test_1632_operator_queue_caps.py`: an oversized type is shortened with the marker; every platform-emitted type and every type `ask_operator` accepts is unchanged; the belt rejects an oversized type and passes 64 four-byte characters; `test_3385_clamped_type_is_not_a_rewrite` (clamped row vs raw entry, legacy full-length row, a real rewrite inside the first 52 characters).

## Rulings carried (orchestrator, on the operator's behalf — plan file)
- T1: a fixed constant, no environment variable — as recommended.
- T2: 64 characters — as recommended. The longest type the platform emits is `workspace_problem_report`, 24.
- T3: the DB belt raises, like its siblings — as recommended.
- Orchestrator: stacked on #3314 instead of branching from `dev`, so the two fixes do not hand the operator a conflict in the same two functions.

## Review + security
`/review` reading pass (claude-fable-5-1, report-only, against the stacked base): **MERGEABLE**, zero critical. Census of every literal `type` the platform and the enterprise submodule pass to the insert paths: longest is 24 characters, none is altered. Every truncated value ends in the marker, which no set member can match, so truncation cannot move a value into or out of a set that decides control flow (`_BUDGETED_ALERT_TYPES` and neighbours). The new `ValueError` is reachable only from the file sync loop, which quarantines it under its existing handler. `/cso --diff`: no findings; the error text carries the constant, never the value.

Fixed after review: `eab91c10` — I1, the five doc lines. Noted, not changed: I2, a non-string `type` over 1 KiB now raises the named `ValueError` where the base failed at the bind — same quarantine outcome.

Evidence sweep after the fix (claude-opus-5-5): **GREEN**. 13 operator-queue files ran shuffled (seed 12345), one file per process: 836 passed, 7 xfailed. With both production files taken from the #3314 branch, the two `TestClampType` cases, the belt test and the clamped-row fingerprint case go red. `git merge-tree` against `dev` f6bedcd is clean; the base branch has not moved (`923b1edc`).

## Tests
Counts above. Not run here: `test_ent815_queue_walk`, `test_ent815_broad_list_agent_scope`, `test_ent751_gate_entries` (2–4 minutes each; they ran green on the #3314 base in that PR's sweep) and the full unit island.

## Before merge
- **This PR's checks are weaker than a dev-based PR's:** a PR whose base is a feature branch runs a reduced CI set and `backend-unit-test` does not run. The first full run happens when it retargets to `dev` after #3414 merges — read that run before merging.
- Accepted trade-off: an agent that rewrites an oversized type only after character 52 is not detected as a change. Titles and questions already behave this way.
- Thirteen open PRs share a file with this branch (mostly `tests/registry.json` and `docs/memory/architecture/api-endpoints.md`; #3256 also `db/operator_queue.py` and the three docs). A test-merge of this head against each of #3420, #3417, #3412, #3411, #3407, #3304, #3271, #3256, #2984, #2956, #2713 and #2709 adds no conflict beyond what that PR already has with `dev`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
vybe and others added 4 commits October 11, 2026 16:13
…roactive_consent_not_asked onto 0097_operator_queue_replace

Keep-both on migrations.py, agent_files.py imports, platform_prompt_service.py
(#3247 pending asks beside ent#814 served seat) and tests/registry.json, dev's
entries first. The Alembic revision moves 0095 -> 0098 so the version line
keeps one head (#2068).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…real User does

dev's ent#839 self-gate reads current_user.agent_name before any lookup; a bare
SimpleNamespace(id=1) raised AttributeError after the merge.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…and the three-state consent (trinity-enterprise#811/#814/#817)

api-endpoints.md gains GET /api/agents/{name}/canon/roles; backend.md gains
canon_roles_service and the resolve_seat / resolve_served_seat contract on the
assignment seam; database.md marks allow_proactive as NULL/0/1 with both
migration names.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… dots included (trinity-enterprise#814)

Since ent#814 the served seat wins over the agent's decided_by_role, but the
record's check was the ask_class slug (no dots) while canon role ids follow
objective_join_service._ID_RE (roles/head.of.sales.yaml is a valid seat). A
dotted seat on record refused every decision with 422 and the agent could not
work around it. decided_by_role now takes the lowercased canon id shape;
ask_class keeps the strict slug. Regression tests in test_ent814_served_seat.py.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@vybe

vybe commented Oct 11, 2026

Copy link
Copy Markdown
Contributor

Validated (/validate-pr, Lane B+schema) and brought up to date with dev — four commits pushed.

  1. c108addf8 Merge origin/dev (d127132b4). Keep-both on migrations.py, the agent_files.py imports, platform_prompt_service.py (Scheduled runs re-ask what is already pending — show an agent its open asks and let it replace one #3247's pending asks beside ent#814's served seat) and tests/registry.json, dev's entries first. The Alembic revision is re-chained 0095 → 0098_proactive_consent_not_asked on 0097_operator_queue_replace; check_alembic_heads.py: 99 revisions, 1 head.
  2. 8eda88239 Test stub after the merge. dev's ent#839 self-gate on the seat-decision router reads current_user.agent_name before any lookup, so test_ent814_served_seat.py's SimpleNamespace(id=1) raised AttributeError; the stub now carries agent_name=None as the real User does.
  3. dce477a95 Architecture catalogs. api-endpoints.md gains GET /api/agents/{name}/canon/roles; backend.md gains canon_roles_service and the resolve_seat / resolve_served_seat contract on the assignment seam; database.md marks allow_proactive as NULL/0/1 with both migration names. None of the three mentioned the new surfaces.
  4. eeccbb80c A defect the structural review found. Since ent#814 the seat on record wins over the agent's decided_by_role, but seat_decision_service.validate_record checked it with the ask_class slug ([a-z0-9_-], no dots) while canon role ids follow objective_join_service._ID_RE (roles/head.of.sales.yaml is a valid seat). A dotted seat on record refused every decision for that agent with 422, and the agent could not work around it. decided_by_role now takes the lowercased canon id shape; ask_class keeps the strict slug. Regression tests (dotted id validates; traversal-shaped, spaced, leading-dot and over-long ids still refused; ask_class unchanged) go red with the fix reverted.

Verified locally: the 60 unit files touching the join, role card, provider seam, seat decisions, readiness gate, sharing, consent, migrations and the route census — 1,382 passed on seed 12345 (+3 new); frontend vitest run 345 files / 5,630 tests including the raw-colour and loading-gate ratchets against the committed baselines; the enterprise-docs guard pattern is clean over the edited docs and assignment_provider.py.

Reviewed and left as designed (documented in the requirements or the PR body): the 0→NULL data migration cannot tell a legacy default from an explicit decline (operator ruling 2026-10-06); an unstamped companion with no seat provider now fires its brief (ent#813 fails open); schema.py keeps DEFAULT 0 on allow_proactive while the code writes NULL — nothing omits the column today, but a future writer that does would record a decline nobody made, worth a follow-up when the column is next rebuilt.

PR description: 0090 → 0098 and the "Still open" block folded into the 2026-10-07 role-card fix.

🤖 Generated with Claude Code

… its ent#666 siblings

dev's ent#731 suite reuses the ent#666 fakes but not the autouse _seat_on_record
fixture ent#812 added there, so after the merge read_objective_join owned nothing
(x-role is no longer read) and the test indexed an empty list. Importing the
fixture registers it for this module.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@vybe

vybe commented Oct 11, 2026

Copy link
Copy Markdown
Contributor

One more after CI on eeccbb80c: the regression diff caught test_ent731_guard_metrics::test_a_guard_is_fetched_through_the_whole_read (new on dev). The ent#731 suite reuses the ent#666 fakes but not the autouse _seat_on_record fixture ent#812 added there, so after the merge the whole read owned nothing (x-role is no longer read) and indexed an empty list. e6271b15a imports the fixture, which registers it for that module — ent#731 + ent#666 + ent#812: 122 passed on seed 12345. No product code changed.

🤖 Generated with Claude Code

@vybe
vybe marked this pull request as ready for review October 11, 2026 16:09

@vybe vybe 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 with /validate-pr (Lane B+schema); merge, fixes and CI are recorded in the comments above.

@vybe
vybe merged commit 502921d into dev Oct 11, 2026
28 checks passed
obasilakis pushed a commit that referenced this pull request Oct 12, 2026
…80b6) (#3607)

Carries Abilityai/trinity-enterprise#818: seats (#811), per-person seats
(#814), drift on read and three-state consent (#817). It is the private pair
of #3271, whose public seam reads the provider's optional `seat_for` /
`served_seat_for`. The adapter at 8331df5 implements neither, so on dev every
seat holder's role card and objective join read `no_seat` (#3534).

Migration graph checked before advancing (docs/ENTERPRISE.md):
scripts/ci/check_alembic_heads.py gives 21 revisions, 1 head
(0020_seat_holders), PASS.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

3 participants