Skip to content

DO NOT MERGE — merge train: 3122,3119,3120,3085 - #3126

Closed
trinity-ability wants to merge 32 commits into
devfrom
train/20260930-1429
Closed

trinity-ability wants to merge 32 commits into
devfrom
train/20260930-1429

Conversation

@trinity-ability

Copy link
Copy Markdown
Contributor

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 0085 on #3120's 0084_agent_loops_chain_depth). The same change lands on #3085's own branch after #3120 merges.

🤖 Generated with Claude Code

dolho and others added 30 commits September 29, 2026 13:45
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>
 single-flight scan

The dispatch-budget alert flag is a once-guard (no release, no token, TTL
only), the same non-lock nx shape as the heartbeat and voip entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…url.path (#3085)

test_3102_host_header_fences (#3108, now on dev) forbids request.url.path in
backend code: request.url is rebuilt from the Host header. The two ent#720
audit calls in routers/users.py predate the guard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vybe vybe added the ui PR touches the frontend UI — triggers Playwright e2e tests label Sep 30, 2026
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>
@vybe

vybe commented Sep 30, 2026

Copy link
Copy Markdown
Contributor

Train done: #3122, #3119, #3120, #3085 merged individually.

@vybe vybe closed this Sep 30, 2026
@vybe
vybe deleted the train/20260930-1429 branch September 30, 2026 15:19
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.

4 participants