DO NOT MERGE — merge train: 3122,3119,3120,3085 - #3126
Closed
trinity-ability wants to merge 32 commits into
Closed
trinity-ability wants to merge 32 commits into
trinity-ability wants to merge 32 commits into
Conversation
Abilityai/trinity-enterprise#720) Every sign-in path resolves the account by email alone, so whoever holds an address on a users row holds that identity. These are the residual doors after #711: 1. Mailbox proof to bind. POST /api/users/me/email/code sends a 6-digit code to the NEW address (3 per 10 min). PUT /api/users/me/email requires {email, code}. Codes carry a purpose (email_login_codes.purpose): `email_bind:<user id>` completes only that account's bind; a bind code never signs in and a sign-in code never binds. Both routes are interactive-only. The one no-proof bind is the #82 transition on an install that cannot deliver mail (provider `console`): an interactive admin, audited as email_bind_unverified. Anyone else there gets 409 email_verification_unavailable. The onboarding step and the Settings card gain the code step (auth store `bindOwnEmail`). 2. Unique. idx_users_email_unique ON users(lower(email)) WHERE email IS NOT NULL, on both tracks (SQLite `ent720_email_identity`, Alembic `0081_ent720_email_identity`). Existing duplicates are resolved first: blank becomes NULL, and per address the earliest-created account keeps it while the rest become NULL, logged by username only. 3. One writer. create_user, update_user, the password upsert and email sign-in creation all write through `_insert_user` / `_update_user_row`. These refuse a held address (EmailInUseError → 409 email_in_use) and map a lost race on the index to the same refusal. A writer-census test enumerates them. get_user_by_email is case-insensitive. 4. A reclaimed username is not a 500. Email sign-in whose `username = email` is taken creates a suffixed username. Auth0 sign-in resolves the HOLDER of the address first and never hands it back to an account that re-bound away. 5. Redeemers honour suspension. Telegram and WhatsApp redemption, the MCP inline redeemer, email_has_agent_access and the per-message channel gate (open_access included) refuse a suspended account, through db.is_email_account_suspended. Second factor on the channel redeemers is out of scope: they cannot present a challenge. It is recorded in FR-4 as a follow-up. Tests: - test_ent720_email_binding (28 tests); - suspended cases in the Telegram, WhatsApp and router suites; - emailBindProof.spec.js (mounted). Mutations: bind-without-proof, no unique index, no username suffix, and suspension ignored each turn tests red. The related backend suites (67 files) pass on seeds 12345 and 99999 (1571 passed). Frontend: 181 files and 3728 tests pass, and the build is OK. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…viction-proof tests The duplicate-email sweep read username/created_at unconditionally, which crashed the #1160 concurrent-boot fixture's minimal users table. Read those columns only when present. The unit tests now resolve EmailInUseError and UserOperations from the module the live db singleton uses (another test may re-import db.users), and stub alembic.op via monkeypatch.setitem so the sys.modules lint ratchet stays at zero. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…est helper
PUT /api/users/me/email verified the bind code without counting failures,
so a caller could mint 3 live codes per window and guess without limit.
Reuse the sign-in OTP limiter (OTP_MAX_ATTEMPTS=5 per 10 min) under a
bind-scoped key `otp_attempts:bind:{user_id}:{email}` — never the bare
address, which would let wrong bind guesses lock the owner out of email
sign-in. Past the cap even the right code is refused (429
too_many_attempts); a successful bind clears the counter, as sign-in does.
_users_mod now reads the raising function's own globals, so the tests hold
after another suite evicts and re-imports db.users.
Refs Abilityai/trinity-enterprise#720
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…on to 0082 dev added 0081_portal_messages_unread_idx (#3076) off the same parent. Rename the Alembic revision to 0082_ent720_email_identity, parented on 0081_portal_messages_unread_idx, and order the SQLite entry after dev's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A sibling suite (test_telegram_login_gate) parks a bare `routers.auth` stub in sys.modules at collection time. The bind route now reaches the OTP limiter through that module, so the `api` fixture re-imports the real one when a stub holds the slot and always backs its counter with an in-memory Redis. Refs Abilityai/trinity-enterprise#720 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
POST /api/users/me/email/code and PUT /api/users/me/email: code purpose, console-provider admin bypass, the bind-scoped attempt cap, refusal codes, and what the duplicate-resolving migration does. Refs Abilityai/trinity-enterprise#720 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sion to 0083 dev gained 0082_agent_sync_state_divergence (#3035), forking the Alembic head. Chain 0083_ent720_email_identity off it and order the SQLite entry after agent_sync_state_divergence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…(ent#661) An edition-agnostic provider registry (services/turn_context.py) that lets a module add a line to a Workspace turn. Both composers call it: the 1:1 chat on both the resumed and cold arms, the room ahead of the file manifest. The context is resolved by the platform (chat row, room membership), including internal_audience, so an internal-only line never reaches a turn an outside client can read. No provider in an OSS build: turns are byte-for-byte unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
raise_ask(addressee=...) bypasses role resolution for a platform raise, so a consent decision (an agent owner approving a project link) reaches that owner rather than whoever an assignment provider maps primary to. An agent's raise that names an addressee is a programming error: an agent never picks who is asked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…w seam (ent#661) PortalRoster.projects_available is true only when the module is entitled and the principal is a platform user, so an outside client is never told the capability exists. Defaults False (fails closed on an older backend). The roster stays the Workspace's only capability channel (#2128). The enterprise-docs guard now scans services/turn_context.py like the other open-core seam files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ent#720 requires the mailed code for PUT /me/email, except an admin on a console-provider install. #2996's session-path test binds without a code, so it now states that exception explicitly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-facing reads over the Workspace Projects agent routes: the projects the calling agent is active on (its owner consented), goal/status/steward/links, and linked chat/room counts only — never other people's chats. License-blind like credential_vault.ts: 404 = OSS build, string 403 = unlicensed, code 403 = agent-key gate; never throws. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…trols (ent#661) Gated on the roster's projects_available (strict === true), so an outside client and a build without the module never see it. - /workspace/projects: the person's list across agents (all / member / company, archived toggle, search); /workspace/projects/:id: goal, status, steward, Work on it (reopen my newest chat, or start one and link it), my chats, others' chats as a count only, agents with consent state, members, rooms. Creator: status, archive/restore, Members & access. - Rail Projects tab (platform door + capability, checked fail-closed). - Chat header: the project badge + Detach, or one Project button that adds the chat to a project or makes a new one from it. Never in Main. - stores/projects.js on portalHttp; four honest states via viewState. - Mount tests for every destructive or store-writing predicate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t view (ent#661 v3)' Requirements 48.4 and the architecture entries for the turn-context seam, the addressed gate ask, the roster capability and the gated Workspace UI. Describes the seam only; the module design stays private. Renames the objective-join references from the retired 'project hub' to 'the project view (ent#661 v3)' as asked on the issue. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…uests, import, Wrap up (ent#661) Backend (open-core seams): - services/portal_capabilities.py: a per-person capability seam (fails closed); the roster's projects_available now also reaches an outside client invited to a project. Guarded by enterprise-docs-guard. Workspace UI (gated on the roster capability): - Project page tabs: Overview / Tasks (ent#673 fields, grouped open / awaiting verification / done; a done task only reopens) / Log (append- only timeline + add) / Files & reports (add from what you can see, share with guests, ReportRenderer view). - Guests: invite in Members & access; a narrow page, list and rail for an invited outside client; no Project button or Wrap up for them. - Import a folder project from an agent (one-shot). - Wrap up in a linked chat: a visible message asking the agent to record the chat's outcomes with its project tools. - v1 gaps: Edit (name, goal, person/agent steward, tracker, visibility); link a room from its header. MCP: list_project_tasks, get_project_log, create_project_task, update_project_task (assignee -> agent), add_project_task_note, add_project_log_entry, link_to_project. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every Workspace-project call now carries the request's X-Trinity-Execution-Id (#2392), taken from the auth context and never from a tool parameter, so the backend can decide who the turn is for. A refusal for an outside audience, or for a turn it can't identify, comes back as words the agent can act on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s kind (ent#661) At 390px the Overview grid's implicit column and the tab strip sized to their content and pushed the cards past the viewport. Explicit single-column grids, plus a bounded box around OverflowTabs so "More" collapses the extra tabs. The task log also shows each entry's kind (Created, Done claim, Reopened…). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ask_operator forwards the request's X-Trinity-Execution-Id (#2392). When that id is the raising agent's own execution, raise_ask stamps it as the ask's execution, winning over an agent-written context.execution_id. `manual`, an unknown id or another agent's id changes nothing. This is how an ask raised in a project's chat is found on the project; before this, the row carried only what the agent chose to write. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
get_steward_digest and set_project_health for a steward agent, and link_to_project now accepts an ask the agent raised. Both forward the turn id like the other project tools, and have policy rows in access.ts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eeds you, I steward (ent#661) Overview gains the hub: - the steward's health (set in a dialog); - tasks by status, with what needs attention (opening Tasks); - linked agent metrics, each with its value, trend and stale state; - Needs you: the viewer's own asks on the project, answered in place with PortalAsks, narrowed by the new askIds prop, which can only narrow. The list shows health and an attention count per row, and gains an "I steward" tab. A health update reads as Status in the log. Mount tests cover every write gate (#2918); FR-5, FR-7 and the ask-turn note are updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…triggers, session turns and events (#2973) Follow-up to #2806. Loop starts, manual schedule triggers, chat-session turns and agent event emits by an agent principal now inherit the caller's chain depth and are refused past inter_agent_max_chain_depth with the named 403 inter_agent_depth_exceeded, instead of starting a fresh depth-0 root. - Loops: depth stored on agent_loops.chain_depth (SQLite migration + Alembic 0084) and stamped on every iteration row. - Schedule trigger: depth forwarded to the scheduler; retries keep it. - Session turns: depth passed through run_resumable_turn to execute_task. - Events: depth signed into the EVT-001 loopback JWT (chain_depth claim), minted whether or not the source is vouched; task-completion events carry the finished row's depth + 1 (the max when the row is unreadable); _chain_caller falls back to vouched_source_agent. - Per source->subscriber hourly event-dispatch budget in trigger_subscription (ops setting event_dispatch_max_fires_per_hour, default 120, one alert per window, fail-open on Redis errors). - App-level handler maps InterAgentDepthExceeded to the #2806 403 body. - MCP run_agent_loop, trigger_agent_schedule and emit_event render the refusal as a non-retryable result. Mutation-checked: reverting each fix turns its test red in tests/unit/test_2973_depth_new_roots.py, test_2973_event_dispatch_budget.py and src/mcp-server/src/tools/depth-refusal.test.ts. Webhook tokens, agent-created cron schedules and self-reminders remain roots; tracked in #3116. Fixes #2973 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ons, budget alert write (#2973) Review follow-ups on #3120: the cold retry in run_resumable_turn keeps chain_depth (mutation-checked), a missing session answers 404 before the depth 403, and the budget alert writes a high-priority notification on the subscriber. Doc counts updated for the four covered paths. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An ask's question rendered as markdown on the operator queue card but as raw text in the Workspace asks and on /m, and titles, option labels and the given answer were plain text everywhere. The same ask looked different depending on where it was answered. One component, operator/AskMarkdown.vue, now renders every ask field through the app's one sanitiser: - the question as block markdown (`renderMarkdown`, the chat/report policy), with the chat bubble's prose and code-pill classes, so it reads the same on every surface and in the agent's chat; - the title, option labels and the given answer inline-only (`renderInlineMarkdown`, the #2771 cell policy: no block element, no class). Wired into QueueCard, ResolvedCard, PortalAsks and /m. Option values and the answer sent are unchanged raw strings. QueueItemDetail.vue is left alone: nothing mounts it, and the store fields it reads no longer exist. Fixes #3115 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…il_identity (#3085) dev gained 0083_execution_conversation_key (#3110) off the same parent as this PR's 0083_ent720_email_identity — a two-head fork (#2068). The two touch disjoint tables, so the unmerged revision is re-parented: renamed to 0084_ent720_email_identity with down_revision 0083_execution_conversation_key. SQLite list keeps both entries, dev's first. Test path + down_revision pin, migrations.py docstring and the feature flow follow the rename. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
schema.py and both migration tracks create the index; tables.py MetaData did not, so #746 autogenerate would propose dropping it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ance (#3085) count_recent_code_requests had no purpose filter, so sign-in (routers/auth.py) and the MCP code request counted bind rows: any signed-in user could request bind codes for someone else's address three times per 10 minutes and keep the owner's sign-in codes suppressed while flooding their inbox. - Sign-in counters count sign-in codes only (purpose IS NULL), the same exact-match rule verify_login_code applies. - The bind route limits its CALLER across every address (count_recent_codes_for_purpose('email_bind:<id>')), 3 per 10 minutes, so no account can spend another's allowance. Tests drive the real route: another account's bind requests leave the sign-in counter at 0; the cap is per caller across addresses; one account cannot spend another's; sign-in codes do not spend a bind allowance. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Sep 30, 2026
fix(asks): render agent markdown in asks, and show only waiting asks in the chat strip (#3115)
#3122
Merged
vybe
pushed a commit
that referenced
this pull request
Sep 30, 2026
…5_ent720_email_identity (#3085) #3120 landed 0084_agent_loops_chain_depth off the same parent as this PR's 0084_ent720_email_identity. Disjoint tables, so the revision is re-parented: 0085_ent720_email_identity <- 0084_agent_loops_chain_depth. SQLite list keeps both entries, dev's first — the same resolution train #3126 was gated on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Integration surface for #3122, #3119, #3120, #3085. Never merged; members merge individually once green.
The #3085 merge commit resolves its sibling collision with #3120 on the train only (SQLite list: keep both; Alembic: ent720 re-parented to
0085on #3120's0084_agent_loops_chain_depth). The same change lands on #3085's own branch after #3120 merges.🤖 Generated with Claude Code