Repository navigation
feat(access): who an agent serves, its seat, and what reads it (abilityai/trinity-enterprise#810–#814, #817) - #3271
Conversation
…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>
…, 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>
…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>
…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>
…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>
…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>
…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>
…bilityai/trinity-enterprise#810) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
|
@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 |
…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>
|
The three "Still open" items from the 2026-10-06 live check are fixed in 1. The Workspace role card now reads the seat on record, not
2. "Your relationship" now shows the viewer's assignment kind.
3. "Add operator" no longer overflows on phones. The email input gets Tests
Docs:
🤖 Generated with Claude Code |
|
Resolve by running |
|
Resolve by merging |
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>
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>
|
Merged Heads-up: #2984 now chains Related: trinity-enterprise#818 now leaves 🤖 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>
…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)
…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)
…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)
…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>
|
Validated (
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 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); PR description: 🤖 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>
|
One more after CI on 🤖 Generated with Claude Code |
vybe
left a comment
There was a problem hiding this comment.
Validated with /validate-pr (Lane B+schema); merge, fixes and CI are recorded in the comments above.
…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>
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:
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)
LoadFailedwith retry; a failed refresh keeps the data and shows a stale banner.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 optionalseat_forthroughgetattr.<id>" when the agent holds it itself (this wins when both apply);<id>()";nullclears).ent#812: owned objectives come from the seat on record
resolve_seat, notx-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-canonalone enables the join;x-roleis neither required nor read. Behaviour change: an agent withx-rolebut nox-canonno longer joins, as the issue specifies.no_seatfinding says why and what to do (assign the primary with their seat on the Access tab). It replacesrole_id_invalid, and supporting objectives still show. The Workspace role card renders that sentence.supporting_agentsis read as before, documented as frozen.rolenow carriescaseandseats(model updated, soresponse_modeldoesn't drop them).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.requirements/lifecycle-observability.md§50) are updated.ent#813: the readiness gate asks the seat, not the template
resolve_seat. A companion is an agent whose primary holds a seat (serves).core-agent.md§5.36) say so.ent#817: canon seats, three-state consent, drift on read
GET /api/agents/{name}/canon/roles(services/canon_roles_service.py) lists<canon>/roles/*.yamlwith id,titleandupdated.roles/, unreadable, timeout.agent_sharing.allow_proactiveNULL = not asked, 0 = declined, 1 = consented.0098_proactive_consent_not_asked, re-chained onto0097_operator_queue_replaceat the 2026-10-11 merge ofdev) moves the legacy DEFAULT-0 rows to NULL. Data only; the DDL is untouched, so there's no table rebuild.proactive_consent.updatedstamp;requirements/infrastructure.md.ent#814: a shared companion serves each person's own seat
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 OPTIONALserved_seat_forfalls back toseat_for.decided_by_roleis used only when nobody's seat is on record, because the old prompt named the primary's seat on every run.Primary humanline is unchanged. ASeat this run servesline 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.primaryaddress.Role card (closed 2026-10-07,
797a6c3af)The Workspace role card reads the seat on record through
resolve_served_seat(agent, viewer), notx-role:seat_sourcesays where the seat came from in words and never names a person;x-roleis read only for its readinessstatus, so a legacy companion keeps its readiness control. "Your relationship" reads the provider'skinds_forwhen one is registered (trinity-enterprise#818 leaves it unregistered untilreadable_seatsis 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 optionalseat_formethod andresolve_seat()src/backend/services/objective_join_service.py,models.py(ObjectiveRoleRead),client_portal'sPortalAgentRole.vuecopy (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 cataloguedsrc/frontend/src/components/AgentAssignmentsSection.vue(new), mounted inAccessPanel.vuesrc/frontend/src/stores/assignments.js(new, through the singleapiclient)src/frontend/src/utils/assignments.js(new, pure: seat line, replace-primary steps + rollback, consent/drift labels, error text)tests/unit/test_ent811_resolve_seat.py,tests/unit/test_ent812_objective_join_seat.py,src/frontend/tests/unit/agentAssignmentsSection.mount.spec.jsTest 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 realread_objective_joinwith the ent#666 fakes. Covers:x-roleis not read;x-canonalone enables the join;no_seat, and still shows supporting work.Five mutations each turn tests red:
x-rolestill enabling the join;no_seatfinding;tests/unit/test_ent689_readiness_gate.py(TestBriefReadinessrewritten for seats): 32 passed. Covers:Four mutations each turn tests red:
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) andtest_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
devcheckout, caused by local venv package versions (e.g.1.12.0vs the1.18.0pin); the other 6 are order-flaky in those same files.Live on a local instance:
Seat this run serves: project-lead, with no email in the prompt;project-lead, though the agent claimedcfo;agentAssignmentsSection.mount.spec.js: 32 passed (mounted, jsdom). These mutations each turn tests red:Full frontend unit suite: 274 files, 4,769 tests passed, including the raw-colour, loading-gate and source-text ratchets.
check:tokensOK.Live on a local instance with the assignments module enabled (this branch + ent#818), light and dark:
The live run also caught that the routes are mounted at
/api/enterprise/assignments, not/api/assignmentsas 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:
The run found seven defects the unit suites missed. All are fixed here, each with a test that fails when the fix is reverted:
no_seatsentence. Before, it was mapped as a role error and never appeared.During actions (add, edit, replace, seat changes) the section causes no layout shift.
Merge of
dev(2026-10-11)Merged
devatd127132b4(c108addf8): keep-both onmigrations.py, theagent_files.pyimports,platform_prompt_service.py(#3247's pending asks beside ent#814's served seat) andtests/registry.json,dev's entries first. The Alembic revision is0098_proactive_consent_not_askedon0097_operator_queue_replace, one head.dev's ent#839 self-gate on the seat-decision router readscurrent_user.agent_name, so the ent#814 test's user stub now carries it as the realUserdoes (8eda88239).After merge
Trinity
devneeds 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