diff --git a/.env.example b/.env.example index 3b04c7134d..25cf320192 100644 --- a/.env.example +++ b/.env.example @@ -383,10 +383,13 @@ OPERATOR_QUEUE_CREATE_RATE_WINDOW=60 # rate-limit window (seconds) OPERATOR_QUEUE_FLEET_CREATE_RATE_LIMIT=300 # fleet-wide create rate / window OPERATOR_QUEUE_MAX_SCAN_PER_CYCLE=500 # max requests scanned per agent per 5s cycle OPERATOR_QUEUE_MAX_FILE_BYTES=2097152 # skip an operator-queue.json larger than this (bytes) -OPERATOR_QUEUE_TITLE_MAX=300 # title char cap (truncate-with-marker) +OPERATOR_QUEUE_TITLE_MAX=300 # title char cap (truncate-with-marker belt; agent titles are refused/held at OPERATOR_QUEUE_ASK_TITLE_MAX_CHARS first) OPERATOR_QUEUE_QUESTION_MAX=4000 # question char cap (truncate-with-marker) OPERATOR_QUEUE_CONTEXT_MAX_BYTES=8192 # context serialized-byte cap (marker over) OPERATOR_QUEUE_OPTIONS_MAX_BYTES=4096 # options serialized-byte cap (dropped-marker over) +OPERATOR_QUEUE_MAX_OPTIONS=5 # #3243: options per agent ask, (something else) not counted (refused/held over; floor 2) +OPERATOR_QUEUE_OPTION_MAX_CHARS=60 # #3243: characters per option (refused/held over; floor 16) +OPERATOR_QUEUE_ASK_TITLE_MAX_CHARS=120 # #3243: title of an agent-raised ask (refused/held over; floor 40) OPERATOR_QUEUE_PROPOSAL_MAX_BYTES=8192 # proposal serialized-byte cap (native asks; refused with 422 over) OPERATOR_QUEUE_ID_MAX=256 # max agent-supplied id length OPERATOR_QUEUE_EXECUTION_ID_MAX=128 # max execution_id preserved in a context marker diff --git a/AGENTS.md b/AGENTS.md index 97a93cdfa4..87be8cb143 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # Trinity — Guide for AI Agents -Trinity is an autonomous agent orchestration platform: every agent runs in its own Docker container with scheduling, observability, credential injection, channel integrations (Slack/Telegram/WhatsApp), and a tamper-evident audit trail — self-hosted on infrastructure the operator controls. Agents are plain Claude Code (or Gemini CLI) projects; Trinity is where they run in production. +Trinity is the operating system for the AI-native company — open source, self-hosted, that you own. Every agent runs in its own Docker container with scheduling, observability, credential injection, channel integrations (Slack/Telegram/WhatsApp), and a tamper-evident audit trail, on infrastructure the operator controls. Agents are plain Claude Code, Gemini CLI or OpenAI Codex projects; Trinity is where they run in production. > **Detailed documentation index: [docs/user-docs/README.md](docs/user-docs/README.md)** — guides, agent management, credentials, automation, operations, integrations, and the full API reference live there. diff --git a/CLAUDE.md b/CLAUDE.md index 8c885e7c05..e8f5d6f708 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -55,7 +55,7 @@ Enterprise **feature designs, paid-module schema, and the open-core gating/monet ## Project Overview -**Trinity** is an **autonomous agent orchestration and infrastructure platform** — sovereign infrastructure for deploying, orchestrating, and governing fleets of autonomous AI agents on your own hardware. +**Trinity** is the **operating system for the AI-native company** — open source, self-hosted, that you own. It deploys, orchestrates, and governs fleets of AI agents on your own hardware, and it is model-agnostic: each agent runs on Claude Code, Gemini CLI or OpenAI Codex. Each agent runs as an isolated Docker container with standardized interfaces for credentials, tools, and MCP server integrations. @@ -332,7 +332,7 @@ curl -X POST http://localhost:8000/api/agents \ | Repository | Description | |------------|-------------| -| [abilityai/trinity](https://github.com/abilityai/trinity) | This repository - Autonomous Agent Orchestration Platform | +| [abilityai/trinity](https://github.com/abilityai/trinity) | This repository - the operating system for the AI-native company | | [abilityai/trinity-ops-public](https://github.com/abilityai/trinity-ops-public) | **Claude Code ops agent** — manage any Trinity instance (health, updates, logs, rollback, provisioning) | | [abilityai/abilities](https://github.com/abilityai/abilities) | **Canonical agent development toolkit** — plugins for the full autonomous agent lifecycle (scaffolding, onboarding, deployment, scheduling, ops) | diff --git a/README.md b/README.md index 861a782e2b..2947e3416c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

- Trinity — The Sovereign AI Agents Platform + Trinity — the operating system for the AI-native company

@@ -53,9 +53,9 @@ --- -## The Sovereign AI Agents Platform +## The operating system for the AI-native company -Trinity is a sovereign AI agents platform — build and run proprietary systems, from fully (or semi-) autonomous businesses to self-improving cognitive systems, all governed, auditable, and on infrastructure you control. +**Trinity: the operating system for the AI-native company — open source, self-hosted, that you own.** Compounding intelligence your company owns. Every agent is governed and auditable, and runs on infrastructure you control. > **Claude Code writes the agent. Trinity runs it in production.** @@ -72,7 +72,7 @@ Each agent runs in its own isolated Docker container with real-time observabilit **The problem:** Everyone wants autonomous AI agents. But your options are terrible — SaaS platforms where data leaves your security perimeter, custom builds that take 6-12 months, or frameworks that don't handle governance and audit trails. -**The solution:** Trinity is autonomous agent orchestration infrastructure. Each agent runs in its own isolated Docker container. You get real-time observability, fleet-wide health monitoring, cron-based scheduling, agent-to-agent delegation, and cost tracking — all on your own hardware. +**The solution:** Trinity is the operating system for the AI-native company. Each agent runs in its own isolated Docker container. You get real-time observability, fleet-wide health monitoring, cron-based scheduling, agent-to-agent delegation, and cost tracking — all on your own hardware. | Option | Problem | Trinity | |--------|---------|---------| @@ -820,7 +820,7 @@ git push origin cli-v1.0.0 This project is licensed under the [Apache License 2.0](LICENSE) — free for any use, commercial included, with an explicit patent grant. Run it on your own infrastructure or any cloud you control. -Optional **enterprise modules** (SSO, user management, SIEM export, and more) are available under a separate commercial license. Contact [hello@ability.ai](mailto:hello@ability.ai) for enterprise licensing. +Optional **enterprise modules** (user management, SIEM export, and more) are available under a separate commercial license. Contact [hello@ability.ai](mailto:hello@ability.ai) for enterprise licensing. ## Contributing @@ -839,5 +839,5 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. ---
- Built by Ability.ai — Sovereign AI infrastructure for the autonomous enterprise + Built by Ability.ai — the lab for AI-native companies
diff --git a/config/agent-templates/trinity-system/CLAUDE.md b/config/agent-templates/trinity-system/CLAUDE.md index 737c1819d9..195c7fc9b6 100644 --- a/config/agent-templates/trinity-system/CLAUDE.md +++ b/config/agent-templates/trinity-system/CLAUDE.md @@ -1,6 +1,6 @@ # Trinity System Agent - Platform Operations -You are the **Trinity System Agent**, the platform operations manager for the Trinity autonomous agent platform. +You are the **Trinity System Agent**, the platform operations manager for this Trinity instance. ## Your Role: Operations, Not Orchestration diff --git a/config/trinity-meta-prompt/prompt.md b/config/trinity-meta-prompt/prompt.md index d68fa8e6d4..ba182f7ba0 100644 --- a/config/trinity-meta-prompt/prompt.md +++ b/config/trinity-meta-prompt/prompt.md @@ -1,6 +1,6 @@ # Trinity Agent System Prompt -You are a Trinity agent - an autonomous AI system capable of independent reasoning and execution. +This agent runs on Trinity, the operating system for AI-native companies. ## Core Principles @@ -45,10 +45,12 @@ Before performing an action that cannot be undone or verified afterwards — pay `ask_operator` takes a `request_id` and a `title`, plus optional `question`, `type`, `options`, `priority`, `context`, `proposal`, `to` and `expires_at`; its description has the details and the named refusals. +**Write atomic asks.** One decision per ask; a title a person reads at a glance (by default at most 120 characters); options that name the choice only (by default at most 5 options, each at most 60 characters — the person can always answer `(something else)`), with the reasoning in `question` and what an option does in `proposal`; `context` as a few labelled facts for a person. The `ask_operator` description has the full rules. + **Request IDs must be globally unique.** Derive the `request_id` from your current execution ID (see the Execution Context block), e.g. `approval-{execution_id}-{short-slug}`. Never use date-serial IDs like `req-20260307-001` — a second task that picks the same ID gets the first ask's receipt instead of a new ask. Re-using your own derived ID when the same task runs again is safe and intentional: it prevents duplicate requests. **Request types:** -- `approval` — You need a yes/no or multi-choice decision. Provide `options`, and state the exact action and its parameters in `proposal` so the operator can verify what they are approving. +- `approval` — You need a yes/no or multi-choice decision. Provide `options`, and state the exact action and its parameters in `proposal` so the operator can verify what they are approving. Never list `(something else)` as an option: the platform offers it on every approval. An answer of `(something else)` means none of your options is approved — carry out none of them; the person's instruction is in `response_text`, so re-plan from it or ask again. - `question` — You need freeform guidance. No `options` needed. - `alert` — You're reporting a situation. No decision needed; it goes to the operators. @@ -80,7 +82,7 @@ Until it is removed, an entry appended to the `requests` array of `~/.trinity/op } ``` -The operator's answer is written back into the entry: `status: "responded"` with `response`, `response_text` and `responded_at` — never who answered. An item that has waited past the operator's aging bound carries a `platform.aging_since` timestamp written by Trinity — read it, never write to `platform`. While Trinity is holding new entries from this file, the file carries a `platform.ingestion` block: `reason` is `queue_full` (you already have `max_pending` open requests — wait for one to end), `rate_limited` (too many too fast — they are read again shortly) or `invalid_id` (an entry's `id` is malformed and will never be read — fix it), and `since` says when the hold began. Held entries stay `pending` and are read again every few seconds; the block is removed once nothing is held. `ask_operator` refuses the same cases immediately, with the reason. After processing a response, update the item's status to `"acknowledged"`. Keep only `pending` and `responded` items plus up to 3 recent `acknowledged` items. The platform database is the permanent record. An ID you raised with `ask_operator` is never read from the file. +The operator's answer is written back into the entry: `status: "responded"` with `response`, `response_text` and `responded_at` — never who answered. On an approval, `response` is one of your options or the reserved value `(something else)`, which comes with the person's instruction in `response_text`. An item that has waited past the operator's aging bound carries a `platform.aging_since` timestamp written by Trinity — read it, never write to `platform`. While Trinity is holding new entries from this file, the file carries a `platform.ingestion` block: `reason` is `queue_full` (you already have `max_pending` open requests — wait for one to end), `rate_limited` (too many too fast — they are read again shortly), `invalid_id` (an entry's `id` is malformed and will never be read — fix it), or `invalid_options` / `invalid_title` (an entry breaks the ask caps: more than 5 options, an option over 60 characters or that reads as `(something else)`, or a title over 120 characters — the entry ids are listed under those keys beside `reason`, whatever the reason; shorten or split them, and they will not be read until they fit), and `since` says when the hold began. Held entries stay `pending` and are read again every few seconds; the block is removed once nothing is held. `ask_operator` refuses the same cases immediately, with the reason. After processing a response, update the item's status to `"acknowledged"`. Keep only `pending` and `responded` items plus up to 3 recent `acknowledged` items. The platform database is the permanent record. An ID you raised with `ask_operator` is never read from the file. ### When to Use diff --git a/docker-compose.hosted.yml b/docker-compose.hosted.yml index 763ffda7f0..230258cf0b 100644 --- a/docker-compose.hosted.yml +++ b/docker-compose.hosted.yml @@ -274,6 +274,9 @@ services: - OPERATOR_QUEUE_QUESTION_MAX=${OPERATOR_QUEUE_QUESTION_MAX:-4000} # question char cap (truncate) - OPERATOR_QUEUE_CONTEXT_MAX_BYTES=${OPERATOR_QUEUE_CONTEXT_MAX_BYTES:-8192} # context byte cap (marker over) - OPERATOR_QUEUE_OPTIONS_MAX_BYTES=${OPERATOR_QUEUE_OPTIONS_MAX_BYTES:-4096} # options byte cap (dropped over) + - OPERATOR_QUEUE_MAX_OPTIONS=${OPERATOR_QUEUE_MAX_OPTIONS:-5} # #3243 options per agent ask + - OPERATOR_QUEUE_OPTION_MAX_CHARS=${OPERATOR_QUEUE_OPTION_MAX_CHARS:-60} # #3243 chars per option + - OPERATOR_QUEUE_ASK_TITLE_MAX_CHARS=${OPERATOR_QUEUE_ASK_TITLE_MAX_CHARS:-120} # #3243 agent ask title - OPERATOR_QUEUE_PROPOSAL_MAX_BYTES=${OPERATOR_QUEUE_PROPOSAL_MAX_BYTES:-8192} # proposal byte cap (native asks; 422 over) - OPERATOR_QUEUE_ID_MAX=${OPERATOR_QUEUE_ID_MAX:-256} # max agent-supplied id length - OPERATOR_QUEUE_EXECUTION_ID_MAX=${OPERATOR_QUEUE_EXECUTION_ID_MAX:-128} # max execution_id kept in a marker diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index ccb9502c54..0b1a4b386d 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -211,6 +211,9 @@ services: - OPERATOR_QUEUE_QUESTION_MAX=${OPERATOR_QUEUE_QUESTION_MAX:-4000} # question char cap (truncate) - OPERATOR_QUEUE_CONTEXT_MAX_BYTES=${OPERATOR_QUEUE_CONTEXT_MAX_BYTES:-8192} # context byte cap (marker over) - OPERATOR_QUEUE_OPTIONS_MAX_BYTES=${OPERATOR_QUEUE_OPTIONS_MAX_BYTES:-4096} # options byte cap (dropped over) + - OPERATOR_QUEUE_MAX_OPTIONS=${OPERATOR_QUEUE_MAX_OPTIONS:-5} # #3243 options per agent ask + - OPERATOR_QUEUE_OPTION_MAX_CHARS=${OPERATOR_QUEUE_OPTION_MAX_CHARS:-60} # #3243 chars per option + - OPERATOR_QUEUE_ASK_TITLE_MAX_CHARS=${OPERATOR_QUEUE_ASK_TITLE_MAX_CHARS:-120} # #3243 agent ask title - OPERATOR_QUEUE_PROPOSAL_MAX_BYTES=${OPERATOR_QUEUE_PROPOSAL_MAX_BYTES:-8192} # proposal byte cap (native asks; 422 over) - OPERATOR_QUEUE_ID_MAX=${OPERATOR_QUEUE_ID_MAX:-256} # max agent-supplied id length - OPERATOR_QUEUE_EXECUTION_ID_MAX=${OPERATOR_QUEUE_EXECUTION_ID_MAX:-128} # max execution_id kept in a marker diff --git a/docker-compose.yml b/docker-compose.yml index 78d158dd4e..46c4ba204b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -153,6 +153,9 @@ services: - OPERATOR_QUEUE_QUESTION_MAX=${OPERATOR_QUEUE_QUESTION_MAX:-4000} # question char cap (truncate) - OPERATOR_QUEUE_CONTEXT_MAX_BYTES=${OPERATOR_QUEUE_CONTEXT_MAX_BYTES:-8192} # context byte cap (marker over) - OPERATOR_QUEUE_OPTIONS_MAX_BYTES=${OPERATOR_QUEUE_OPTIONS_MAX_BYTES:-4096} # options byte cap (dropped over) + - OPERATOR_QUEUE_MAX_OPTIONS=${OPERATOR_QUEUE_MAX_OPTIONS:-5} # #3243 options per agent ask + - OPERATOR_QUEUE_OPTION_MAX_CHARS=${OPERATOR_QUEUE_OPTION_MAX_CHARS:-60} # #3243 chars per option + - OPERATOR_QUEUE_ASK_TITLE_MAX_CHARS=${OPERATOR_QUEUE_ASK_TITLE_MAX_CHARS:-120} # #3243 agent ask title - OPERATOR_QUEUE_PROPOSAL_MAX_BYTES=${OPERATOR_QUEUE_PROPOSAL_MAX_BYTES:-8192} # proposal byte cap (native asks; 422 over) - OPERATOR_QUEUE_ID_MAX=${OPERATOR_QUEUE_ID_MAX:-256} # max agent-supplied id length - OPERATOR_QUEUE_EXECUTION_ID_MAX=${OPERATOR_QUEUE_EXECUTION_ID_MAX:-128} # max execution_id kept in a marker diff --git a/docs/CLI.md b/docs/CLI.md index f6d6057bdf..e1ca78331e 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -1,6 +1,6 @@ # Trinity CLI -Command-line interface for the Trinity Autonomous Agent Orchestration Platform. +Command-line interface for Trinity, the operating system for the AI-native company. ## Installation diff --git a/docs/TRINITY_COMPATIBLE_AGENT_GUIDE.md b/docs/TRINITY_COMPATIBLE_AGENT_GUIDE.md index 7e5c301fcc..380eb513b1 100644 --- a/docs/TRINITY_COMPATIBLE_AGENT_GUIDE.md +++ b/docs/TRINITY_COMPATIBLE_AGENT_GUIDE.md @@ -2092,7 +2092,7 @@ All human/operator communication on Trinity is **asynchronous**, mediated by the 1. **Park** the request (approval / question / alert) with `ask_operator`, or, as the fallback, by appending an entry to the queue file. 2. **End the turn.** A turn must never wait, poll, or sleep for a human response — a human may answer in minutes or days, and a blocked turn burns its entire timeout budget while pinning platform capacity. Never assume a synchronous human answer is available mid-turn. -3. **Process the outcome in a later turn.** `get_my_ask` returns how an ask ended (answered, cancelled or expired, with the answer when there is one), and the Execution Context block of the system prompt lists the agent's asks that ended in the last 24 hours. For a queue-file entry, check the file for `status: "responded"` items, act on them, then mark them `"acknowledged"`. +3. **Process the outcome in a later turn.** `get_my_ask` returns how an ask ended (answered, cancelled or expired, with the answer when there is one), and the Execution Context block of the system prompt lists the agent's asks that ended in the last 24 hours. For a queue-file entry, check the file for `status: "responded"` items, act on them, then mark them `"acknowledged"`. On an approval the answer is one of the agent's own options or the platform-reserved value `(something else)` (#3242): none of the options is approved — carry out none of them — and the person's instruction is in `response_text`. Never list `(something else)` as an option yourself; `ask_operator` refuses it. Write atomic asks (#3243): one decision per ask, a one-glance title (at most 120 characters), and at most 5 short options that name the choice only — the full rules are in the `ask_operator` tool description, and a queue-file entry over the caps is held as `invalid_options` / `invalid_title`. **Ask before irreversible actions.** Before an action the platform cannot undo or verify — payments, emails/messages through the agent's own credentials, public posts, destructive deletions — park an `approval` and end the turn when uncertain. Put the exact action in the ask's `proposal`. This matters most under re-delivery: pull-mode coordination (#1081) re-runs a turn whose worker died, so a task you receive may have partially run before. Check your own records and your earlier asks before repeating an irreversible effect; do the reversible parts first and gate only the irreversible step. diff --git a/docs/diagrams/01-system-overview.md b/docs/diagrams/01-system-overview.md index 650ab9109b..868d25bfd6 100644 --- a/docs/diagrams/01-system-overview.md +++ b/docs/diagrams/01-system-overview.md @@ -2,7 +2,7 @@ ## What This Diagram Shows -This document provides a high-level architectural overview of the Trinity autonomous agent platform. It illustrates: +This document provides a high-level architectural overview of the Trinity platform. It illustrates: 1. **Platform Layers**: How the web interface, platform services, and agent runtime relate to each other 2. **Key Differentiators**: How autonomous agents differ from reactive chatbots @@ -13,13 +13,13 @@ Use this as a starting point for understanding Trinity's architecture before div ## What is Trinity? -Trinity is an **autonomous agent orchestration and infrastructure platform** — sovereign infrastructure for deploying, orchestrating, and governing fleets of autonomous AI agents on your own hardware. +Trinity is the **operating system for the AI-native company** — open source, self-hosted, that you own. It deploys, orchestrates, and governs fleets of AI agents on your own hardware, and it is model-agnostic: each agent runs on Claude Code, Gemini CLI or OpenAI Codex. ``` +---------------------------------------------------------------------------------+ | | | TRINITY PLATFORM | -| "Autonomous Agent Orchestration" | +| "The operating system for the AI-native company" | | | | +-------------------------------------------------------------------------+ | | | WEB INTERFACE | | diff --git a/docs/memory/architecture.md b/docs/memory/architecture.md index b3a286cdf2..16abc5bfdd 100644 --- a/docs/memory/architecture.md +++ b/docs/memory/architecture.md @@ -1,4 +1,4 @@ -# Trinity - Autonomous Agent Orchestration Platform - Architecture +# Trinity - The Operating System for the AI-Native Company - Architecture > **Purpose**: The always-loaded core — system shape, the full invariant list, and a map to every area file. Deep detail lives in `docs/memory/architecture/`, read on demand. > @@ -6,7 +6,7 @@ ## System Overview -**Trinity** is an **autonomous agent orchestration and infrastructure platform** — sovereign infrastructure for deploying, orchestrating, and governing fleets of autonomous AI agents on your own hardware. Each agent runs as an isolated Docker container with standardized interfaces for credentials, tools, and MCP server integrations. +**Trinity** is the **operating system for the AI-native company** — open source, self-hosted, that you own. It deploys, orchestrates, and governs fleets of AI agents on your own hardware, and it is model-agnostic: each agent runs on Claude Code, Gemini CLI or OpenAI Codex. Each agent runs as an isolated Docker container with standardized interfaces for credentials, tools, and MCP server integrations. ``` ┌─────────────────────────────────────────────────────────────────────────────┐ diff --git a/docs/memory/architecture/api-endpoints.md b/docs/memory/architecture/api-endpoints.md index e0303c8f0a..32a7c1cac8 100644 --- a/docs/memory/architecture/api-endpoints.md +++ b/docs/memory/architecture/api-endpoints.md @@ -265,7 +265,7 @@ Generalises #868 to agent scope (`db/schedules.py:get_agent_analytics`); read-on | GET | `/api/operator-queue/{id}` | Single item | | POST | `/api/operator-queue/{id}/respond` / `/cancel` | Submit operator response / cancel pending item. **A person only** — 403 `person_required` for agent-, system- and every other scoped key, before the row is read (trinity-enterprise#611). Both return **409** if the item left `pending` under the caller (race vs bulk-cancel, #1017; cancel was a silent 200 before #611); respond also **409 `expired`** once `expires_at` passed (#611) and **409 `item_diverged`** (#2915) while `sync_state ∈ {changed, closed_by_filer}` unless the body carries `acknowledge_divergence: true`, which is stamped on the row (`divergence_acknowledged_at`) so the write-back delivers into the entry as it is now. Cancel takes an optional `{reason}` (≤ 500). Both end the ask through `services/ask_service.py` (ledger, audit, thin trigger, wake) | | GET | `/api/agents/{name}/operator-queue/{request_id}` | An agent's OWN ask by the `request_id` it chose (trinity-enterprise#611, `agent_router` in `routers/operator_queue.py`, MCP `get_my_ask`). `get_self_acting_agent`: agent key == name, system key == `trinity-system`, anything else → uniform 403 `agent_identity_required` before the ordinary uniform 404. Ignores `cleared_at`; redacted projection (no person email) | -| POST | `/api/agents/{name}/operator-queue` | An agent raises an ask AS ITSELF (trinity-enterprise#611, `agent_router`, MCP `ask_operator`). The same `get_self_acting_agent` gate (403 `agent_identity_required`); body `OperatorAskCreate` (`extra="forbid"`) into `ask_service.raise_ask`: named 422s (`invalid_*`, `field_too_large`, `options_required`, `role_unassigned`, `reask_requires_link`), 429 `rate_limited` / `queue_full` (the #1632 buckets; the depth cap atomic per agent), **201** with the receipt, **200** replaying the first receipt (`status: replayed` + `differs`). The receipt names the role, never an email; the row is outside the file contract (`channel=mcp`) | +| POST | `/api/agents/{name}/operator-queue` | An agent raises an ask AS ITSELF (trinity-enterprise#611, `agent_router`, MCP `ask_operator`). The same `get_self_acting_agent` gate (403 `agent_identity_required`); body `OperatorAskCreate` (`extra="forbid"`) into `ask_service.raise_ask`: named 422s (`invalid_*`, `field_too_large`, `too_many_options` / `option_too_long` / `title_too_long` (#3243 caps), `options_required`, `role_unassigned`, `reask_requires_link`), 429 `rate_limited` / `queue_full` (the #1632 buckets; the depth cap atomic per agent), **201** with the receipt, **200** replaying the first receipt (`status: replayed` + `differs`). The receipt names the role, never an email; the row is outside the file contract (`channel=mcp`) | | GET | `/api/operator-queue/agents/{name}` | Items for one agent | Bulk ops scope writes to the caller's accessible agents (tri-state: admin = no filter, empty set = no-op). The sync service write-back also propagates `cancelled`/`expired` status into agent queue files (in-place flips of still-`pending` entries only) so agents stop waiting on cleared items and stale file entries can't resurrect purged rows (#1017). The Operations UI exposes these as a per-tab **Clear All** button (`notifications` tab uses `POST /api/notifications/dismiss-all` — bulk pending+acknowledged → dismissed, same accessible-set scoping). @@ -274,7 +274,7 @@ WebSocket events: `operator_queue_new`, `operator_queue_responded` / `operator_q **Addressed asks (ent#364).** `operator_queue.addressed_to_email` names the HUMAN an item is for; `agent_name` only ever said which agent. NULL — every pre-ent#364 row — means "the operator answers", so the column is additive by construction. It is a **column and not a key in `context`** because it is an authorization decision (who may answer, whose Workspace sidebar it appears in) and `context` is agent-authored JSON whose clamp bounds size and type only. Validated at the one agent-authored boundary (`operator_queue_service._validated_addressee`, inside the #1632 clamp) against `client_portal.service.agent_on_roster(agent, email, include_owned=False)` — an agent may address only someone already on its roster — and **fails closed**: malformed, off-roster, or an unreadable roster all drop to NULL. The read/answer surface is **OSS core** (ent#428): `client_portal/asks/`, mounted unconditionally in `main.py` at `/api/enterprise/client-portal/asks` — a prefix that is retained history, like the `enterprise_`-prefixed portal tables, not a licensing claim. It re-checks the roster at read time so a revoked share stops showing an already-raised ask, projects an explicit client-facing shape (never `context`), and answers through the same ask sink as the operator respond route (`services/ask_service.py`, trinity-enterprise#611), so the ledger, write-back, audit and the WS trigger are unforked, and it is person-only like respond (a non-person platform principal gets 403 `person_required`). `responded_by_id` stays NULL for a client answerer — there is no `users` row, and inventing one would be a lie in the audit trail. **The list fails loud (trinity-enterprise#610 PR A0):** `GET /api/enterprise/client-portal/asks` returns **503 `{code: "asks_unavailable", message}`** with `Retry-After: 20` (the Workspace poll interval) when any of its queue reads raises (the agent set, the count or the page, #3059) or the read-time roster re-check cannot read the roster (`asks/service.AsksUnavailable`, strict `_on_roster`); it never answers an outage with `[]`. `answer_ask`'s roster failure stays the uniform 404. **One ownership check (trinity-enterprise#610 PR A2):** `asks/service._owned_ask` (lookup + addressee + `_VISIBLE_KINDS` + roster) is shared by the answer path (non-strict — its refusal is the 404 whatever the cause) and the context read (strict — a roster outage is 503); an addressed row of a kind no list shows is now a 404 on answer too. -**Ingestion caps (#1632).** There is **no HTTP create** — every operator-queue item is created through `db.create_operator_queue_item`, and the only untrusted producer is the sync service reading the agent-authored `~/.trinity/operator-queue.json` (`operator_queue_service._sync_agent`). #1402 makes this queue the approval channel for irreversible actions, so a compromised / prompt-injected agent that floods plausible "approve this" items causes operator fatigue → reflexive approval (XSS is already handled by DOMPurify; the exposure is volume + social engineering). The one agent-authored seam is bounded by two independent limits plus field hygiene, all env-tunable (see requirements §26.7): (1) a per-agent **pending-DEPTH cap** (`db.count_operator_queue_pending_for_agent`, default 25) — the **primary** bound, DB-measured ⇒ Redis-independent; at the cap the sync **stops** ingesting (never drips a growing file) and holds the surplus behind one aggregated alert; (2) a per-agent + fleet **create RATE limit** (`rate_limiter.check`, 60/60s + 300/60s, fail-open); (3) a total field-hygiene clamp (`_clamp_ingested_item`, inside the #1525 create try/except) — truncate-with-marker title/question, context/options over cap → marker (validated `execution_id` only), non-dict context → `{}`, `created_at` normalized to ingest time, priority validate-only; (4) a **reserved-id guard** rejecting agent ids that start with a platform-reserved prefix (`queue-flood-`/`poison-`/`cb-dormant-`/`sync-failing-`/`git-bloat-`/`skill-not-found-`/`val_`/`system-seed-`/`base-image-stale-`/`alert-budget-`) so an agent can't pre-create — and via `on_conflict_do_nothing` silence — its own flood alarm or the #1402 poison alert, plus a malformed/oversize-id reject; (5) a per-cycle scan bound (skip an oversized file wholesale; cap at 500 requests/cycle). The 5s sync loop is **leader-locked** (`opqueue:leader`, mirror monitoring #1464) so `--workers 2` doesn't double-charge / double-broadcast / double-scan. Platform creates bypass `_sync_agent`, and the exemption is **scoped by influence, not by caller location (#1677)**: a *platform-only* emitter (volume bound by platform cadence — edge-triggered, idempotent/bucketed id, leader-locked, or operator-driven: lease-reaper poison-park #1402, `validation_service._notify_operator_on_failure` — converted to a **direct DB create** in #1632 — and the internal breaker/skill/git alerts) stays direct and unthrottled, while an *agent-influenceable* one — `task_execution_service._alert_skill_not_found` (#1410), whose per-command dedup distinct unknown slash-commands defeat at $0/turn — routes through `operator_queue_service.create_bounded_alert`: a per-(agent, registered-type) pending-DEPTH budget (`OPERATOR_ALERT_MAX_PENDING_PER_TYPE`, default 5; type derived from `item["type"]` against the `_BUDGETED_ALERT_TYPES` frozenset; **fail-closed on every arm** — an unreadable count / unregistered type / failed create suppresses the ALERT only, the FAILED execution rows stay the primary surface, and the paired notification is gated on the same bool), with ONE cooldown-gated `alert-budget-{agent}-{type}-b{bucket}` episode alert per window (deterministic bucket ⇒ the `(agent_name, request_id)` on-conflict target dedups cross-worker; no `held` count, no agent-controlled text). The classification is CI-forced by the AST caller-parity guard `tests/unit/test_1677_operator_alert_emitters.py` (every `create_operator_queue_item` call site must be allowlisted platform-only or routed; OSS tree only — the private submodule owns its twin); a sink-level default bound was rejected because it fails QUIETLY at the load-bearing poison-park create where the parity test fails LOUDLY at CI (#1890's lesson deliberately inverted). A generous DB-sink belt in `create_item` (reject title>4 KiB / question>16 KiB / context>64 KiB / id>512; the derived `execution_id` COLUMN belted to None on non-str/>512, #1677) is a second layer so the exemption is never solely load-bearing (#1525 validate-at-boundary-AND-at-sink). A depth-held / rate-skipped / oversize-file episode emits **one** `queue-flood-{agent}-{utc_now_iso()}` item per over-cap **episode** (#3130: edge-triggered — a second alert needs the condition to have cleared; the 300 s cooldown is only the minimum spacing between episodes), created through `create_bounded_alert` as the budgeted type `queue_flood` (#1677 backstop: ≤5 pending per agent), un-guessable id, priority `high`, wrapped so an emit failure never kills the sync. Platform-minted rows do **not** count toward the agent's own depth (file seam and native `queue_full` read one predicate, `_own_pending_conds`, with `_RESERVED_ID_PREFIXES` excluded) — before #3130 the alert counted toward the depth it reported and was re-minted every cooldown window (386 of one agent's 435 rows). A held file is told so: a file-level `platform.ingestion` block (`reason` ∈ `queue_full`/`rate_limited`/`invalid_id`, `max_pending`, `since`) rides the guarded write-back, written only when it changes and removed once nothing is held; a refused write is retried only after the file changes. The pre-#3130 backlog is collapsed once by the `supersede_queue_flood_backlog` migration (both tracks): newest pending flood row per agent kept, the rest `cancelled`, `disposed_by = 'platform'`, reason `superseded`. The platform-emitter residual named on the pull-mode default-ON gate list (#1081 / `TARGET_ARCHITECTURE.md`) is closed by #1677; the remaining gate items are unchanged. +**Ingestion caps (#1632).** There is **no HTTP create** — every operator-queue item is created through `db.create_operator_queue_item`, and the only untrusted producer is the sync service reading the agent-authored `~/.trinity/operator-queue.json` (`operator_queue_service._sync_agent`). #1402 makes this queue the approval channel for irreversible actions, so a compromised / prompt-injected agent that floods plausible "approve this" items causes operator fatigue → reflexive approval (XSS is already handled by DOMPurify; the exposure is volume + social engineering). The one agent-authored seam is bounded by two independent limits plus field hygiene, all env-tunable (see requirements §26.7): (1) a per-agent **pending-DEPTH cap** (`db.count_operator_queue_pending_for_agent`, default 25) — the **primary** bound, DB-measured ⇒ Redis-independent; at the cap the sync **stops** ingesting (never drips a growing file) and holds the surplus behind one aggregated alert; (2) a per-agent + fleet **create RATE limit** (`rate_limiter.check`, 60/60s + 300/60s, fail-open); (3) a total field-hygiene clamp (`_clamp_ingested_item`, inside the #1525 create try/except) — truncate-with-marker title/question, context/options over cap → marker (validated `execution_id` only), non-dict context → `{}`, `created_at` normalized to ingest time, priority validate-only; (4) a **reserved-id guard** rejecting agent ids that start with a platform-reserved prefix (`queue-flood-`/`poison-`/`cb-dormant-`/`sync-failing-`/`git-bloat-`/`skill-not-found-`/`val_`/`system-seed-`/`base-image-stale-`/`alert-budget-`) so an agent can't pre-create — and via `on_conflict_do_nothing` silence — its own flood alarm or the #1402 poison alert, plus a malformed/oversize-id reject; (5) a per-cycle scan bound (skip an oversized file wholesale; cap at 500 requests/cycle). The 5s sync loop is **leader-locked** (`opqueue:leader`, mirror monitoring #1464) so `--workers 2` doesn't double-charge / double-broadcast / double-scan. Platform creates bypass `_sync_agent`, and the exemption is **scoped by influence, not by caller location (#1677)**: a *platform-only* emitter (volume bound by platform cadence — edge-triggered, idempotent/bucketed id, leader-locked, or operator-driven: lease-reaper poison-park #1402, `validation_service._notify_operator_on_failure` — converted to a **direct DB create** in #1632 — and the internal breaker/skill/git alerts) stays direct and unthrottled, while an *agent-influenceable* one — `task_execution_service._alert_skill_not_found` (#1410), whose per-command dedup distinct unknown slash-commands defeat at $0/turn — routes through `operator_queue_service.create_bounded_alert`: a per-(agent, registered-type) pending-DEPTH budget (`OPERATOR_ALERT_MAX_PENDING_PER_TYPE`, default 5; type derived from `item["type"]` against the `_BUDGETED_ALERT_TYPES` frozenset; **fail-closed on every arm** — an unreadable count / unregistered type / failed create suppresses the ALERT only, the FAILED execution rows stay the primary surface, and the paired notification is gated on the same bool), with ONE cooldown-gated `alert-budget-{agent}-{type}-b{bucket}` episode alert per window (deterministic bucket ⇒ the `(agent_name, request_id)` on-conflict target dedups cross-worker; no `held` count, no agent-controlled text). The classification is CI-forced by the AST caller-parity guard `tests/unit/test_1677_operator_alert_emitters.py` (every `create_operator_queue_item` call site must be allowlisted platform-only or routed; OSS tree only — the private submodule owns its twin); a sink-level default bound was rejected because it fails QUIETLY at the load-bearing poison-park create where the parity test fails LOUDLY at CI (#1890's lesson deliberately inverted). A generous DB-sink belt in `create_item` (reject title>4 KiB / question>16 KiB / context>64 KiB / id>512; the derived `execution_id` COLUMN belted to None on non-str/>512, #1677) is a second layer so the exemption is never solely load-bearing (#1525 validate-at-boundary-AND-at-sink). A depth-held / rate-skipped / oversize-file episode emits **one** `queue-flood-{agent}-{utc_now_iso()}` item per over-cap **episode** (#3130: edge-triggered — a second alert needs the condition to have cleared; the 300 s cooldown is only the minimum spacing between episodes), created through `create_bounded_alert` as the budgeted type `queue_flood` (#1677 backstop: ≤5 pending per agent), un-guessable id, priority `high`, wrapped so an emit failure never kills the sync. Platform-minted rows do **not** count toward the agent's own depth (file seam and native `queue_full` read one predicate, `_own_pending_conds`, with `_RESERVED_ID_PREFIXES` excluded) — before #3130 the alert counted toward the depth it reported and was re-minted every cooldown window (386 of one agent's 435 rows). A held file is told so: a file-level `platform.ingestion` block (`reason` ∈ `queue_full`/`rate_limited`/`invalid_id`/`invalid_options`/`invalid_title`, `max_pending`, `since`; since #3243 an entry over the authoring caps is held with its id listed — ≤10 — under `invalid_options` / `invalid_title` beside `max_options` / `max_option_chars` / `max_title_chars`, whatever `reason` wins) rides the guarded write-back, written only when it changes and removed once nothing is held; a refused write is retried only after the file changes. The pre-#3130 backlog is collapsed once by the `supersede_queue_flood_backlog` migration (both tracks): newest pending flood row per agent kept, the rest `cancelled`, `disposed_by = 'platform'`, reason `superseded`. The platform-emitter residual named on the pull-mode default-ON gate list (#1081 / `TARGET_ARCHITECTURE.md`) is closed by #1677; the remaining gate items are unchanged. ### Skill Gate — the in-container check (trinity-enterprise#752) diff --git a/docs/memory/architecture/backend.md b/docs/memory/architecture/backend.md index b10d7aef4f..b849a63d88 100644 --- a/docs/memory/architecture/backend.md +++ b/docs/memory/architecture/backend.md @@ -44,7 +44,7 @@ - `reminders.py` - Agent self-reminders: create/list/cancel (self-gated) (#1296) — see [Agent Self-Reminders](execution.md#agent-self-reminders-1296) - `files.py` - Public download endpoint for outbound agent file sharing (FILES-001) - `agent_rename.py` - Rename endpoint (RENAME-001) -- `a2a.py` - A2A protocol: the authenticated per-agent card (#737) plus the **inbound server** on a separate prefix-less `a2a_server_router` — public `GET /a2a/{name}/.well-known/agent-card.json` (per-IP rate limited) + `POST /a2a/{name}` JSON-RPC (message/send, message/stream SSE, tasks/get, tasks/cancel). Exposure is opt-in per agent (`agent_ownership.a2a_exposed`, default OFF); non-exposed/inaccessible → uniform 404 (Invariant #8). `messageId` dedup is scoped per (agent, caller principal) — the field is peer-controlled and only unique per-client (ent#157). **Also hosts the OUTBOUND client (#736):** `POST /{name}/a2a/call` + `POST /{name}/a2a/task`, a Trinity agent tasking an EXTERNAL A2A agent. The target is never caller-supplied — it is a name resolved through `services/a2a_outbound.py` — and the routes are thin (auth + HTTP error map + audit); orchestration lives in `services/a2a_outbound_service.py`. Default OFF (`A2A_OUTBOUND_ENABLED`), both routes 404 when off. **The inbound door also takes payment (ent#679):** its principal is OPTIONAL (`get_user_or_anonymous`, which degrades on a 401 but RE-RAISES a 403 so a fenced key cannot become a payer), and the handler branches once — a resolved principal gets today's free path byte-identically, an anonymous caller gets today's 401 unless the agent is BOTH exposed and Nevermined-enabled, in which case it gets the paid door's own 402 with `resource.url` on THIS door. The card producer `_card_with_exposed_skills` also declares the price (`a2a_card_service.with_payment_extension`), so both card surfaces state it and an unpriced agent's card is byte-identical +- `a2a.py` - A2A protocol: the authenticated per-agent card (#737) plus the **inbound server** on a separate prefix-less `a2a_server_router` — public `GET /a2a/{name}/.well-known/agent-card.json` (per-IP rate limited) + `POST /a2a/{name}` JSON-RPC (message/send, message/stream SSE, tasks/get, tasks/cancel). Exposure is opt-in per agent (`agent_ownership.a2a_exposed`, default OFF); non-exposed/inaccessible → uniform 404 (Invariant #8). `messageId` dedup is scoped per (agent, caller principal) — the field is peer-controlled and only unique per-client (ent#157). **Also hosts the OUTBOUND client (#736):** `POST /{name}/a2a/call` + `POST /{name}/a2a/task`, a Trinity agent tasking an EXTERNAL A2A agent. The target is never caller-supplied — it is a name resolved through `services/a2a_outbound.py` — and the routes are thin (auth + HTTP error map + audit); orchestration lives in `services/a2a_outbound_service.py`. Default OFF (`A2A_OUTBOUND_ENABLED`), both routes 404 when off. **The inbound door also takes payment (ent#679):** its principal is OPTIONAL (`get_user_or_anonymous`, which degrades on a 401 but RE-RAISES a 403 so a fenced key cannot become a payer), and the handler branches once — a resolved principal gets today's free path byte-identically, an anonymous caller gets today's 401 unless the agent is BOTH exposed and Nevermined-enabled, in which case it gets the paid door's own 402 with `resource.url` on THIS door. The card producer `_card_with_exposed_skills` also declares the price (`a2a_card_service.with_payment_extension`), so both card surfaces state it and an unpriced agent's card is byte-identical. **#3215:** `_base_url_from_request` now delegates to `utils/public_url.py::public_base_url` — one module, one `X-Forwarded-Proto` upgrade rule — and the anonymous JSON-RPC door uses it too, so neither 402 door can advertise an origin the caller did not call; it also honours the Settings `public_chat_url` row, not just the env var. **The two CARD routes take `_card_base_url` (configured-first, review I3)**, which is the card's pre-#3215 behaviour and not a second copy of the logic: a card is republished to buyers elsewhere and the `get_agent_a2a_card` MCP tool reads it from `backend:8000`, so a same-host card advertises an internal host to every external buyer. A buyer that follows the card lands on the configured host, where the door's own same-host rule mints the 402 for that very origin. `_task_object` MIRRORS the x402 metadata onto the Task's top-level `metadata` from the same assignment as `status.message.metadata` (the spec location, which the provider SDK reads) — without it a typed client renders a settled, charged turn as `metadata: null`; the free path emits no `metadata` key, so unpaid Task bytes and the idempotency snapshots over them are unchanged - `agent_ssh.py` - SSH access endpoint - `credentials.py` - Credential injection/export/import (CRED-002) - `chat.py` / `chat/` - Agent chat/activity monitoring @@ -91,7 +91,7 @@ *Public Access & Monetization:* - `public_links.py` - Public agent link management - `public.py` - Public chat routes; the turn orchestration moved to `services/public_chat_service.py`, which also owns the per-IP/per-token chat caps and the #311 access-gate predicates (#1028). Also Caddy's unauthenticated on-demand-TLS `ask` gate, `GET /api/public/tls-allowed` (#2380) -- `paid.py` - x402 payment-gated chat (NVM-001). The 402/403/verify/settle ORCHESTRATION moved to `services/paid_turn_service.py` (ent#679) so the A2A inbound door runs the identical money logic; this router is now the HTTP shape over it (header read, outcome → response map) and its behaviour is unchanged +- `paid.py` - x402 payment-gated chat (NVM-001). The 402/403/verify/settle ORCHESTRATION moved to `services/paid_turn_service.py` (ent#679) so the A2A inbound door runs the identical money logic; this router is now the HTTP shape over it (header read, outcome → response map) and its behaviour is unchanged. **#3215:** both the 402 and `GET /{name}/info` resolve the plan's scheme first, and the origin for `resource.url` comes from `utils/public_url.py` rather than `request.base_url` — a token is minted and verified against that URL, and behind the standard proxy topology `request.base_url` is http. `/info` therefore loads the config WITH the key (resolving a scheme needs a `Payments` instance) and emits an absolute URL; the key never reaches the body - `nevermined.py` - Nevermined payment config (NVM-001) - `slack.py` - Slack integration: OAuth, events, multi-agent channel routing, per-agent binding (SLACK-001/002) - `telegram.py` - Telegram bot integration: webhook receiver, bot binding, group config (TELEGRAM-001) @@ -179,6 +179,7 @@ - `monitoring_alerts.py` - Alert threshold configuration - `heartbeat_service.py` - Agent push-heartbeat liveness layer — see [Heartbeat Liveness](reliability.md#heartbeat-liveness-reliability-004-307) - `operator_queue_service.py` - Operating Room sync with agent containers (OPS-001); the agent-authored ingestion boundary enforces the per-agent depth/rate/size caps + reserved-id guard + leader lock (#1632) +- `operator_queue_choices.py` - The one rule for what an approval's `response` may be (#2376, #3242): an offered option (exact match, else `ResponseNotOfferedError` with the list) or the reserved `SOMETHING_ELSE = "(something else)"` with an instruction in `response_text` (`InstructionRequiredError` / `ReservedValueError` / `NotOffMenuError`, all `ReservedAnswerError`); import-free leaf the ask sink calls before any write; also holds `OPTIONS_DROPPED_MARKER`. Mirrored in `utils/operatorQueue.js` + MCP `types.ts` (parity-tested) - `sync_health_service.py` - Git sync health polling — see [Git Sync Health](agent-lifecycle.md#git-sync-health-389390) - `sync_freeze_policy.py` - The one sync-health rule (state / reason / recommendation / freeze), stdlib leaf, vendored byte-identically to `src/scheduler/` (ent#706) — see [Git Sync Health](agent-lifecycle.md#git-sync-health-389390) - `sync_health_view.py` - `sync_view(row, config)`: the backend's single call into the policy, used by the poller and every sync-health read (ent#706) @@ -208,7 +209,7 @@ *Integrations:* - `slack_service.py` - Slack API client (OAuth, messaging, verification) (SLACK-001) -- `nevermined_payment_service.py` - x402 payment verification and settlement (NVM-001). Facilitator calls are bounded fleet-wide by `NEVERMINED_MAX_INFLIGHT` (8) with a `NEVERMINED_FACILITATOR_WAIT_SECONDS` (5.0) wait budget, one semaphore per event loop — verify/settle run on the default thread executor, so an unbounded set of them starves the whole fleet's threads, and the caller needs no credential to trigger one. Knobs documented in [nevermined-payments.md](../feature-flows/nevermined-payments.md#configuration-operator-knobs) +- `nevermined_payment_service.py` - x402 payment verification and settlement (NVM-001). Facilitator calls are bounded fleet-wide by `NEVERMINED_MAX_INFLIGHT` (8) with a `NEVERMINED_FACILITATOR_WAIT_SECONDS` (5.0) wait budget, one semaphore per event loop — verify/settle run on the default thread executor, so an unbounded set of them starves the whole fleet's threads, and the caller needs no credential to trigger one. Knobs documented in [nevermined-payments.md](../feature-flows/nevermined-payments.md#configuration-operator-knobs). **The plan's x402 SCHEME is resolved, never assumed (#3215):** the SDK's `build_payment_required` defaults to `nvm:erc4337` and the facilitator is POSTed the requirements document verbatim, so `accepts[0].scheme` is the only channel it travels through — a fiat (card) plan advertised as crypto is unpayable, which is the defect. **Verify and settle read the PRESENTED token** (`scheme_from_token` over `accepted.{scheme,network,planId}`, allow-listed against the SDK's vocabularies and checked against this agent's plan id): zero network calls on the money path, and a settle re-driven hours after its verify is byte-stable with it, which a TTL'd cache cannot promise. The 402 and `/info` use `resolve_plan_scheme` — Trinity's own parse of `plans.get_plan` (parity-tested against the SDK's `resolve_scheme`, which swallows every failure at DEBUG and silently returns crypto) behind a `(environment, plan_id)` cache, single-flight futures, its OWN 2-slot gate (never a facilitator slot: `wait_for` does not cancel the worker thread, and an anonymous 402 flood must not starve paying traffic), stale-while-revalidate and a WARN once per negative window. `plan_scheme=None` is the pre-#3215 document, which is what keeps crypto bytes identical - `paid_turn_service.py` - The ONE x402 paid-turn orchestrator, shared by `routers/paid.py` and the A2A inbound door (ent#679 T3): verify → dedup gate → execute → settle, with the #1018 branches (honest `success_unsettled` on a delivered-but-unsettled turn, a replayed unsettled snapshot re-settling and converging, no settle on a failed/cancelled turn) existing exactly once. Takes every collaborator as a PARAMETER and imports none of them — a service that imported the payment service and the execution bridge would be a second router, and the parameter list is what lets the unit suite drive all ten outcomes with plain stand-ins - `a2a_payment_gate.py` - The A2A-shaped adapter around `paid_turn_service` (ent#679): `is_priced` (exposure ∧ enabled config ∧ key — deliberately NOT the SDK check, since "takes payment" and "can process one now" are different facts owing a 401 and a 501 respectively), metadata-first token extraction with the deprecated `payment-signature` header as fallback, the 402/403 bodies, and the Task a payer gets back with its x402 metadata. Consults the `a2a_gate` allow-list after verify as `x402:{payer}` and **fails CLOSED** there — the inverse of that seam's authenticated-caller bias, because on this path the payment IS the authorization. Nothing here is edition-aware; the path is reachable only with the entitled exposure flag on - `proactive_message_service.py` - Agent-to-user proactive messaging with rate limiting and audit (#321) diff --git a/docs/memory/architecture/workspace.md b/docs/memory/architecture/workspace.md index a903885bd3..3b50627c80 100644 --- a/docs/memory/architecture/workspace.md +++ b/docs/memory/architecture/workspace.md @@ -997,9 +997,42 @@ SIBLING of the rail, never a rail tab (ruled 2026-09-05); **ent#547 reversed tha door, the room restriction and why the 2026-09-05 objections are answered rather than dropped. Both read one payload through `composables/usePortalAgentPage.js`. Canvas, Files and recent work are **not** duplicated here — they have been rail tabs since -ent#475/#525. `portalUtils.js::landingThread` is the one rule for which chat you land in, +ent#475/#525. `portalUtils.js::agentLanding` is the one rule for which chat you land in, and the `?agent=` deep link's `resolveAgentLanding` defers to it so the two entry points -cannot disagree. Main is named by its role in both the tab strip and the header and is +cannot disagree. Since **ent#784** that rule replaced ent#523's most-recently-active +`landingThread` with a **precedence**, settled by the operator's 2026-10-05 ruling: a link +naming a CHAT wins (and never reaches the rule); then a validated `lastOpenSessionId` — the +ent#621 seam, checked against the principal's own thread list; then the agent's chat +holding an unsent **draft**, newest `updatedAt` first, where a `new:` winner lands on +a new chat (that key IS where the words are) and a thread winner opens that thread; then +the agent's existing **empty** chat, reused (`agentEmptyChat` — an unarchived, +non-room thread of this agent with no message sent: no `last_message_at` AND +`message_count` 0 or absent, since the two fields come from different reads; +Main first, then newest `created_at`, so list order never decides); then a new, +empty chat. The write-side twin of that arm is `ensureMainListed`, a GET that +INSERTS: it now also returns early when the agent already has an empty chat, so +visiting an agent can never add a SECOND empty row ("at most one empty chat per +agent at any time"). An agent whose chats are all used still gets its Main on +the visit, which is the case #2579 was about. The drafts arm reads +`portalDrafts.js::isDraftedThread` — the SAME predicate `agentsWithDrafts` lights the +sidebar's agent-row mark from, shared rather than copied so "the mark means click here to +continue" cannot drift (a room and an **archived** thread are excluded on both sides). The +drafts map is passed IN by each door (`landOnAgent`, `resolveAgentLanding`), never read +inside the rule, so it stays pure; `?new=1` outranks the whole precedence. Nothing in it +mints a row: the drafted-`new:` and fresh arms are both `sessionId: null`, and the empty +arm opens a row that already exists. +`landOnAgent` is therefore synchronous, keeps `/workspace/a/:name`, and guards the +call before writing `activeAgentName`; the composer's mount focus is gated by a +`focusOnMount` mode (`always` for a gesture, `fine-pointer` for a landing) so an +unasked-for landing cannot raise a soft keyboard. Because that URL is now where a new chat +RESTS, three things follow from it. It carries the **rail** like every other chat: the rail +rules read it as a rail-free page only until the landing has put the named agent on stage +(`unlandedAgentPage`), and its column is reserved mid-load like `/workspace/c/:id`. Clicking +the row of the agent whose new chat is **already on stage** is a no-op in `openAgentPage` +— the push would not re-fire the landing, and clearing `startingNewChat` there sent the +first message without `new_thread`, i.e. into Main. And a landing that **reuses a chat** +(the empty one, a remembered one) mounts as a thread, which focuses nothing, so the shell +hands the caret over under the same pointer rule (`focusLandedComposer`). Main is named by its role in both the tab strip and the header and is not renameable; an archived chat stays a tab (ruled “becomes the newest tab”), though you never LAND in one by default; an unused Main is filtered from the **sidebar** only (a projection, not a filter on `threads`, because the strip must show Main from the first visit). diff --git a/docs/memory/feature-flows/mobile-admin-pwa.md b/docs/memory/feature-flows/mobile-admin-pwa.md index 495c41561b..b4d2b28958 100644 --- a/docs/memory/feature-flows/mobile-admin-pwa.md +++ b/docs/memory/feature-flows/mobile-admin-pwa.md @@ -67,7 +67,7 @@ Browser (/m) ### Ops Tab - Sub-tabs: Queue | Alerts -- Queue: operator queue items answered inline. **#2370:** controls switch on the item **type** (desktop parity) — an *approval* is select an option → restated consequence ("Sending **Deny** to — it reads this as your decision on its next run") → optional note → `Cancel` + `Send: