diff --git a/.github/AGENTS.md b/.github/AGENTS.md
index 5a9c3686b..105dd8ff3 100644
--- a/.github/AGENTS.md
+++ b/.github/AGENTS.md
@@ -9,16 +9,13 @@
- `workflows/` contains GitHub Actions workflow definitions.
- `scripts/` contains Python helpers called by workflows.
-- This file owns release automation rules; user-facing release documentation belongs under `docs/`.
+- Root-level release rules remain in the root `AGENTS.md`; this file owns automation-specific details.
## Local Contracts
- Docker publishing lives in `workflows/docker-publish.yml` and delegates planning to `scripts/docker_release_plan.py`.
- Releasable tags are `vX.Y` tags at or above `v1.0`, matching the workflow environment.
-- On `main`, the newest eligible tag publishes both the version tag and `latest`, then creates or updates its GitHub release after the image push succeeds; other allowed branches publish only their branch tag.
-- Manual dispatch without a tag backfills missing Docker Hub tags. Manual dispatch with a tag rebuilds that target and refreshes `latest` and the GitHub release only when it remains the newest eligible tag on `main`.
- Release-note generation reads `scripts/openrouter_release_notes_system_prompt.md` from the repository root and requires OpenRouter credentials from workflow environment variables.
-- Release notes compare against the previous published GitHub release tag and fall back to `No release notes.` when no meaningful summary is generated.
- Keep workflow secrets in GitHub Actions secrets or environment variables. Do not commit credentials, tokens, or generated release bodies containing private data.
- Workflow scripts must fail loudly with actionable messages when required environment variables or git refs are missing.
@@ -26,7 +23,7 @@
- Prefer deterministic, testable Python for workflow planning logic instead of complex inline shell in YAML.
- Preserve manual dispatch behavior when changing Docker publishing.
-- Keep branch, tag, and release behavior synchronized between workflow YAML, release scripts, tests, and user-facing release documentation.
+- Keep branch, tag, and release behavior synchronized between workflow YAML, release scripts, tests, and root documentation.
## Verification
diff --git a/AGENTS.md b/AGENTS.md
index 704ab7554..00a1570f9 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,83 +1,363 @@
-# Agent Zero DOX
+# Agent Zero - AGENTS.md
-## Purpose
+[Generated using reconnaissance on 2026-02-22]
-- Own project-wide engineering rules and the top-level DOX index.
-- Keep detailed contracts in the closest applicable child `AGENTS.md`.
+## Quick Reference
+Tech Stack: Python 3.12+ | Flask | Alpine.js | LiteLLM | WebSocket (Socket.io)
+Dev Server: python run_ui.py (runs on http://localhost:50001 by default)
+Run Tests: pytest (standard) or pytest tests/test_name.py (file-scoped)
+Documentation: README.md | docs/
+Frontend & Plugin DOX: [WebUI](webui/AGENTS.md) | [Components](webui/components/AGENTS.md) | [Frontend JS](webui/js/AGENTS.md) | [Plugins](plugins/AGENTS.md)
-## Project
+---
-- Stack: Python 3.12+ framework, Python 3.13 agent execution runtime, Flask, Alpine.js, LiteLLM, and Socket.IO.
-- Start the WebUI with `python run_ui.py`; discover its URL from startup output, Docker mappings, or explicit configuration rather than assuming a port.
-- Run the full test suite with `pytest` or a focused file with `pytest tests/test_name.py`.
-- Human-facing documentation lives in `README.md` and `docs/`.
+## Table of Contents
+1. [Project Overview](#project-overview)
+2. [Core Commands](#core-commands)
+3. [Docker Environment](#docker-environment)
+4. [Project Structure](#project-structure)
+5. [Development Patterns & Conventions](#development-patterns--conventions)
+6. [Safety and Permissions](#safety-and-permissions)
+7. [Code Examples](#code-examples)
+8. [Git Workflow](#git-workflow)
+9. [Release Notes](#release-notes)
+10. [Troubleshooting](#troubleshooting)
-## Root Ownership
+---
-- `agent.py` owns `Agent`, `AgentContext`, and loop data.
-- `initialize.py` owns framework initialization.
-- `models.py` owns model-provider configuration and LiteLLM integration.
-- `run_ui.py` is the WebUI entry point.
-- `DockerfileLocal` must remain compatible with the contracts under `docker/`.
-- Runtime or user state under `usr/` and `tmp/` is intentionally outside tracked DOX unless the user explicitly asks otherwise.
+## Project Overview
-## Project-Wide Contracts
+Agent Zero is a dynamic, organic agentic framework designed to grow and learn. It uses the operating system as a tool, featuring a multi-agent cooperation model where every agent can create subordinates to break down tasks.
-- Import `AgentContext` and `AgentContextType` from `agent`, not `helpers.context`.
-- Never commit secrets, `.env` files, API keys, tokens, or private user data.
-- Preserve authentication and CSRF protections.
-- Use Linux paths and commands in examples.
-- Treat the Docker container exposed at `localhost:32080` as the live plugin/backend runtime when that target is named.
-- Copy live core-plugin changes back into tracked source under `plugins/`.
-- Develop new custom plugins under ignored `usr/plugins/`; tracked bundled plugins live under `plugins/`.
-- Use the framework runtime for backend and plugin-hook verification, not the separate agent execution runtime.
+Type: Full-Stack Agentic Framework (Python Backend + Alpine.js Frontend)
+Status: Active Development
+Primary Language(s): Python, JavaScript (ES Modules)
-## Permissions
+---
-Allowed without asking:
+## Core Commands
-- Read repository files.
-- Update files under `usr/`.
+### Setup
+Do not combine these commands; run them individually:
+```bash
+pip install -r requirements.txt
+pip install -r requirements2.txt
+```
+- Start WebUI: python run_ui.py
-Ask before:
+---
-- Installing dependencies.
-- Deleting core files outside `usr/` or `tmp/`.
-- Modifying `agent.py` or `initialize.py`.
-- Creating commits or pushing branches.
+## Docker Environment
-## DOX Workflow
+When running in Docker, Agent Zero uses two distinct Python runtimes to isolate the framework from the code being executed:
-- `AGENTS.md` files are binding contracts for their subtrees.
-- Before editing, read this file and every `AGENTS.md` on the path to each target; the closest contract controls local details without weakening parent rules.
-- Keep work understandable from the applicable DOX chain. Put project-wide rules here and concrete ownership, workflows, inputs, outputs, side effects, and verification in child docs.
-- Create a child `AGENTS.md` only for a durable boundary with distinct ownership or workflow.
-- Child docs should use: Purpose, Ownership, Local Contracts, Work Guidance, Verification, and Child DOX Index.
-- After every meaningful change, re-check the affected paths, update the closest owning docs and indexes, remove stale guidance, and run relevant verification.
-- Do not document ignored `usr/` or `tmp/` changes unless explicitly requested.
-- Keep DOX concise, current, operational, and free of diary entries or duplicated parent guidance.
+### 1. Framework Runtime (/opt/venv-a0)
+- Version: Python 3.12.4
+- Purpose: Runs the Agent Zero backend, API, and core logic.
+- Packages: Contains all dependencies from requirements.txt.
+
+### 2. Execution Runtime (/opt/venv)
+- Version: Python 3.13
+- Purpose: Default environment for the interactive terminal and the agent's code execution tool.
+- Behavior: This is the environment active when you docker exec into the container. Packages installed by the agent via pip install during a task are stored here.
+
+---
+
+## Project Structure
+
+```
+/
+├── agent.py # Core Agent and AgentContext definitions
+├── initialize.py # Framework initialization logic
+├── models.py # LLM provider configurations
+├── run_ui.py # WebUI server entry point
+├── api/ # API Handlers (ApiHandler subclasses) + WsHandler subclasses (ws_*.py)
+├── extensions/ # Backend lifecycle extensions
+├── helpers/ # Shared Python utilities (plugins, files, etc.)
+├── tools/ # Agent tools (Tool subclasses)
+├── webui/
+│ ├── components/ # Alpine.js components
+│ ├── js/ # Core frontend logic (modals, stores, etc.)
+│ └── index.html # Main UI shell
+├── usr/ # User data directory (isolated from core)
+│ ├── plugins/ # Custom user plugins
+│ ├── settings.json # User-specific configuration
+│ └── workdir/ # Default agent workspace
+├── plugins/ # Core system plugins
+├── agents/ # Agent profiles (prompts and config)
+├── prompts/ # System and message prompt templates
+├── knowledge/
+│ └── main/about/ # Agent self-knowledge (indexed into vector DB for runtime recall)
+│ ├── identity.md # Philosophy, principles, project context
+│ ├── architecture.md # Agent loop, memory pipeline, multi-agent, extensions
+│ ├── capabilities.md # Detailed capabilities and limitations
+│ ├── configuration.md # LLM roles, providers, profiles, plugins, settings
+│ └── setup-and-deployment.md # Docker deployment, updates, troubleshooting
+└── tests/ # Pytest suite
+```
+
+Key Files:
+- agent.py: Defines AgentContext and the main Agent class.
+- helpers/plugins.py: Plugin discovery and configuration logic.
+- webui/js/AlpineStore.js: Store factory for reactive frontend state.
+- helpers/api.py: Base class for all API endpoints.
+- scripts/openrouter_release_notes_system_prompt.md: Editable system prompt used to generate GitHub release notes during Docker publishing.
+- knowledge/main/about/: Agent self-knowledge files, indexed into the vector DB for runtime recall. Not user-facing docs - written for the agent's internal reference.
+- webui/components/AGENTS.md: DOX contract for Alpine component architecture.
+- webui/js/AGENTS.md: DOX contract for frontend infrastructure, modal stack, API helpers, and extension loading.
+- plugins/AGENTS.md: DOX contract for bundled and custom plugin architecture; `usr/plugins/` remains ignored user state.
+
+---
+
+## Development Patterns & Conventions
+
+### Backend (Python)
+- Context Access: Use from agent import AgentContext, AgentContextType (not helpers.context).
+- Communication: Use mq from helpers.messages to log proactive UI messages:
+ mq.log_user_message(context.id, "Message", source="Plugin")
+- API Handlers: Derive from ApiHandler in helpers/api.py.
+- Extensions: Use the extension framework in helpers/extension.py for lifecycle hooks.
+- Error Handling: Use RepairableException for errors the LLM might be able to fix.
+
+### Frontend (Alpine.js)
+- Store Gating: Always wrap store-dependent content in a template:
+```html
+
+
+
...
+
+
+```
+- Store Registration: Use createStore from /js/AlpineStore.js.
+- Modals: Use openModal(path) and closeModal() from /js/modals.js.
+
+### Plugin Architecture
+- Location: Always develop new plugins in usr/plugins/.
+- Manifest: Every plugin requires a plugin.yaml with name, description, version, and optionally settings_sections, per_project_config, per_agent_config, and always_enabled.
+- Discovery: Conventions based on folder names (api/, tools/, webui/, extensions/).
+- Plugin-local Python imports: Prefer `usr.plugins....` for code that lives under `usr/plugins/`. Avoid `sys.path` hacks and avoid symlink-dependent `plugins....` imports for community plugins.
+- Runtime hooks: Plugins may also expose hooks in hooks.py, callable by the framework through helpers.plugins.call_plugin_hook(...).
+- Hook runtime: hooks.py executes inside the Agent Zero framework Python environment, so sys.executable -m pip installs dependencies into that same framework runtime.
+- Environment targeting: If a plugin needs packages or binaries for the separate agent execution runtime or system environment, it must explicitly switch environments in a subprocess by targeting the correct interpreter, virtualenv, or package manager.
+- Settings: Use get_plugin_config(plugin_name, agent=agent) to retrieve settings. Plugins can expose a UI for settings via webui/config.html. Plugin settings modals instantiate a local context from $store.pluginSettingsPrototype; bind plugin fields to config.* and use context.* for modal-level state and actions.
+- Activation: Global and scoped activation rules are stored as .toggle-1 (ON) and .toggle-0 (OFF). Scoped rules are handled via the plugin "Switch" modal.
+- Cleanup rule: Plugins should not permanently modify the system in ways that outlive the plugin. Deleting a plugin should not leave behind symlinks, unmanaged services, or stray files outside plugin-owned paths unless the user explicitly requested that behavior.
+
+### Releases
+- Docker publishing automation lives in `.github/workflows/docker-publish.yml`.
+- Releasable tags follow `v{X}.{Y}` and only tags `>= v1.0` are considered by the workflow.
+- The latest eligible tag on `main` also creates or updates a GitHub release after the Docker image push succeeds.
+- GitHub release notes are generated on the fly in `.github/scripts/docker_release_plan.py` by comparing the new tag against the previous published GitHub release tag, collecting commit subjects and descriptions in that range, and sending them to OpenRouter.
+- The OpenRouter call uses `OPENROUTER_API_KEY` and `OPENROUTER_MODEL_NAME` from the workflow environment, with the system prompt stored in `scripts/openrouter_release_notes_system_prompt.md`.
+- Prioritize user-visible features, important fixes, infra or packaging changes, and breaking notes. Skip low-signal churn.
+- If the generated summary has no meaningful content, the release body falls back to `No release notes.`
+
+### Lifecycle Synchronization
+| Action | Backend Extension | Frontend Lifecycle |
+|---|---|---|
+| Initialization | agent_init | init() in Store |
+| Mounting | N/A | x-create directive |
+| Processing | monologue_start/end | UI loading state |
+| Cleanup | context_deleted | x-destroy directive |
+
+---
+
+## Safety and Permissions
+
+### Allowed Without Asking
+- Read any file in the repository.
+- Update code files in usr/.
+
+### Ask Before Executing
+- pip install (new dependencies).
+- Deleting core files outside of usr/ or tmp/.
+- Modifying agent.py or initialize.py.
+- Making git commits or pushes.
+
+### Never Do
+- Commit, hardcode or leak secrets or .env files.
+- Bypass CSRF or authentication checks.
+- Hardcode API keys.
+
+---
+
+## Code Examples
+
+### API Handler (Good)
+```python
+from helpers.api import ApiHandler, Request, Response
+
+class MyHandler(ApiHandler):
+ async def process(self, input: dict, request: Request) -> dict | Response:
+ # Business logic here
+ return {"ok": True, "data": "result"}
+```
+
+### Alpine Store (Good)
+```javascript
+import { createStore } from "/js/AlpineStore.js";
+
+export const store = createStore("myStore", {
+ items: [],
+ init() { /* global setup */ },
+ onOpen() { /* mount setup */ },
+ cleanup() { /* unmount cleanup */ }
+});
+```
+
+### Tool Definition (Good)
+```python
+from helpers.tool import Tool, Response
+
+class MyTool(Tool):
+ async def execute(self, **kwargs):
+ # Tool logic
+ return Response(message="Success", break_loop=False)
+```
+
+---
+
+## Git Workflow
+
+- Docker publish automation lives in `.github/workflows/docker-publish.yml`.
+- Release tags handled by automation must match `vX.Y` and be `>= v1.0`.
+- Allowed release branches are configured at the top of the workflow. `main` publishes `` and `latest`; other allowed branches publish only the branch tag.
+- Manual dispatch accepts an optional tag. Without a tag it backfills missing Docker Hub tags. With a tag it rebuilds that exact target and only refreshes `latest` and the GitHub release when that tag is still the newest eligible tag on `main`.
+
+---
+
+## Release Notes
+
+- The latest eligible `main` tag generates its GitHub release notes during Docker publish instead of reading committed Markdown files.
+- The release-note prompt is editable in `scripts/openrouter_release_notes_system_prompt.md`.
+- The commit range starts at the previous published GitHub release tag, not merely the previous semantic tag in the repository.
+
+## Troubleshooting
+
+### Dependency Conflicts
+If pip install fails, try running in a clean virtual environment:
+```bash
+python -m venv .venv
+source .venv/bin/activate
+pip install -r requirements.txt
+pip install -r requirements2.txt
+```
+
+### WebSocket Connection Failures
+- Check if X-CSRF-Token is being sent.
+- Ensure the runtime ID in the session matches the current server instance.
+
+---
+
+*Last updated: 2026-06-01*
+*Maintained by: Agent Zero Core Team*
+
+
+# DOX framework
+
+- DOX is highly performant AGENTS.md hierarchy installed here
+- Agent must follow DOX instructions across any edits
+
+## Core Contract
+
+- AGENTS.md files are binding work contracts for their subtrees
+- Work products, source materials, instructions, records, assets, and durable docs must stay understandable from the nearest applicable AGENTS.md plus every parent AGENTS.md above it
+
+## Read Before Editing
+
+1. Read the root AGENTS.md
+2. Identify every file or folder you expect to touch
+3. Walk from the repository root to each target path
+4. Read every AGENTS.md found along each route
+5. If a parent AGENTS.md lists a child AGENTS.md whose scope contains the path, read that child and continue from there
+6. Use the nearest AGENTS.md as the local contract and parent docs for repo-wide rules
+7. If docs conflict, the closer doc controls local work details, but no child doc may weaken DOX
+
+Do not rely on memory. Re-read the applicable DOX chain in the current session before editing.
+
+## Update After Editing
+
+Every meaningful change requires a DOX pass before the task is done.
+
+Update the closest owning AGENTS.md when a change affects:
+
+- purpose, scope, ownership, or responsibilities
+- durable structure, contracts, workflows, or operating rules
+- required inputs, outputs, permissions, constraints, side effects, or artifacts
+- user preferences about behavior, communication, process, organization, or quality
+- AGENTS.md creation, deletion, move, rename, or index contents
+
+Update parent docs when parent-level structure, ownership, workflow, or child index changes. Update child docs when parent changes alter local rules. Remove stale or contradictory text immediately. Small edits that do not change behavior or contracts may leave docs unchanged, but the DOX pass still must happen.
+
+Do not create or update DOX docs for changes confined to ignored runtime or user-state folders under `usr/` or `tmp/` unless the user explicitly asks for those folders to be documented.
+
+## Hierarchy
+
+- Root AGENTS.md is the DOX rail: project-wide instructions, global preferences, durable workflow rules, and the top-level Child DOX Index
+- Child AGENTS.md files own domain-specific instructions and their own Child DOX Index
+- Each parent explains what its direct children cover and what stays owned by the parent
+- The closer a doc is to the work, the more specific and practical it must be
+
+## Child Doc Shape
+
+- Create a child AGENTS.md when a folder becomes a durable boundary with its own purpose, rules, responsibilities, workflow, materials, or quality standards
+- Work Guidance must reflect the current standards of the project or user instructions; if there are no specific standards or instructions yet, leave it empty
+- Verification must reflect an existing check; if no verification framework exists yet, leave it empty and update it when one exists
+
+Default section order:
+- Purpose
+- Ownership
+- Local Contracts
+- Work Guidance
+- Verification
+- Child DOX Index
+
+## Style
+
+- Keep docs concise, current, and operational
+- Document stable contracts, not diary entries
+- Put broad rules in parent docs and concrete details in child docs
+- Prefer direct bullets with explicit names
+- Do not duplicate rules across many files unless each scope needs a local version
+- Delete stale notes instead of explaining history
+- Trim obvious statements, repeated rules, misplaced detail, and warnings for risks that no longer exist
+
+## Closeout
+
+1. Re-check changed paths against the DOX chain
+2. Update nearest owning docs and any affected parents or children
+3. Refresh every affected Child DOX Index
+4. Remove stale or contradictory text
+5. Run existing verification when relevant
+6. Report any docs intentionally left unchanged and why
+
+## User Preferences
+
+- Do not document changes in `usr/` or `tmp/`; treat both as ignored runtime/user-state folders unless explicitly requested otherwise.
## Child DOX Index
+Direct child DOX files:
+
| Child | Scope |
| --- | --- |
| [.github/AGENTS.md](.github/AGENTS.md) | GitHub Actions workflows and release automation scripts. |
-| [agents/AGENTS.md](agents/AGENTS.md) | Bundled agent profiles, profile-local prompts, and tools. |
-| [api/AGENTS.md](api/AGENTS.md) | HTTP API and WebSocket handler entry points. |
+| [agents/AGENTS.md](agents/AGENTS.md) | Bundled agent profiles, profile-local prompts, and profile-local tools. |
+| [api/AGENTS.md](api/AGENTS.md) | HTTP API handlers and WebSocket handler entry points. |
| [conf/AGENTS.md](conf/AGENTS.md) | Repository-shipped configuration defaults and templates. |
-| [docker/AGENTS.md](docker/AGENTS.md) | Docker build contexts, images, compose files, and runtime layout. |
-| [docs/AGENTS.md](docs/AGENTS.md) | Human-facing documentation and screenshots. |
-| [extensions/AGENTS.md](extensions/AGENTS.md) | Backend and WebUI lifecycle extensions. |
-| [helpers/AGENTS.md](helpers/AGENTS.md) | Shared backend utilities and runtime services. |
-| [knowledge/AGENTS.md](knowledge/AGENTS.md) | Built-in agent self-knowledge. |
-| [lib/AGENTS.md](lib/AGENTS.md) | Lightweight browser-side helpers outside the WebUI bundle. |
-| [plugins/AGENTS.md](plugins/AGENTS.md) | Bundled system plugins and custom-plugin architecture. |
-| [prompts/AGENTS.md](prompts/AGENTS.md) | Core prompt templates. |
-| [scripts/AGENTS.md](scripts/AGENTS.md) | Repository maintenance scripts and automation inputs. |
-| [skills/AGENTS.md](skills/AGENTS.md) | Bundled Agent Zero skills. |
+| [docker/AGENTS.md](docker/AGENTS.md) | Docker build contexts, image definitions, and runtime compose files. |
+| [docs/AGENTS.md](docs/AGENTS.md) | Human-facing documentation, developer guides, screenshots, and agent deep dives. |
+| [extensions/AGENTS.md](extensions/AGENTS.md) | Core lifecycle extension hook implementations for backend and WebUI surfaces. |
+| [helpers/AGENTS.md](helpers/AGENTS.md) | Shared backend framework utilities and cross-cutting runtime services. |
+| [knowledge/AGENTS.md](knowledge/AGENTS.md) | Built-in agent self-knowledge and indexed reference material. |
+| [lib/AGENTS.md](lib/AGENTS.md) | Lightweight browser-side helper scripts outside the main WebUI bundle. |
+| [plugins/AGENTS.md](plugins/AGENTS.md) | Bundled system plugins shipped with the framework. |
+| [prompts/AGENTS.md](prompts/AGENTS.md) | Core prompt templates loaded by agents and framework workflows. |
+| [scripts/AGENTS.md](scripts/AGENTS.md) | Repository maintenance scripts invoked by automation or maintainers. |
+| [skills/AGENTS.md](skills/AGENTS.md) | Bundled Agent Zero skills and their agent-facing instructions. |
| [tests/AGENTS.md](tests/AGENTS.md) | Pytest regression and contract tests. |
| [tools/AGENTS.md](tools/AGENTS.md) | Core agent tool implementations. |
-| [webui/AGENTS.md](webui/AGENTS.md) | Alpine.js WebUI shell, components, JavaScript, CSS, and assets. |
+| [webui/AGENTS.md](webui/AGENTS.md) | Flask-served Alpine.js WebUI shell, frontend modules, components, CSS, assets, and vendor libraries. |
Intentionally unindexed local or generated roots:
@@ -87,6 +367,6 @@ Intentionally unindexed local or generated roots:
| `.pytest_cache/`, `__pycache__/` | Generated test and bytecode caches. |
| `.vscode/`, `.windsurf/` | Editor-local configuration and assistant metadata. |
| `logs/` | Runtime output. |
-| `tmp/` | Ignored runtime caches, uploads, and generated work. |
-| `usr/` | Ignored local user data, settings, plugins, chats, and workdirs. |
-| `python/` | Generated or legacy runtime mirror; current source is in root modules and tracked source directories. |
+| `tmp/` | Ignored runtime caches, uploads, and generated working files; do not document changes here unless explicitly requested. |
+| `usr/` | Ignored local user data, settings, plugins, uploads, chats, and workdirs; do not document changes here unless explicitly requested. |
+| `python/` | Generated or legacy runtime cache mirror; current source lives in root-level `api/`, `helpers/`, `tools/`, and `extensions/`. |
diff --git a/README.md b/README.md
index 45efd23d1..9b1dc4acf 100644
--- a/README.md
+++ b/README.md
@@ -3,62 +3,36 @@
# Agent Zero
-### Give your agent a full Linux computer.
+### A full Linux system for your AI agent.
-Agent Zero is an open agent framework for work that needs more than chat: a Dockerized Linux desktop, a browser with DOM annotation, live document cowork, projects, skills, plugins, and a bridge back to your host machine.
+Agent Zero is an open, dynamic, organic agentic framework. One Docker container ships a full Linux system with a desktop and a plugin hub that the agent can extend using Skills.
[](https://agent-zero.ai)
[](./docs/)
[](https://discord.gg/B8KZKNsPpj)
[](https://github.com/sponsors/agent0ai)
-[](https://deepwiki.com/agent0ai/agent-zero)
-[Quick Start](#quick-start) |
-[Why Agent Zero](#why-agent-zero) |
-[Try These First](#try-these-first) |
-[Deep Dives](#deep-dives) |
+[Install](#how-to-install) |
+[What's Different](#what-makes-agent-zero-different) |
+[A0 CLI](#a0-cli-connector-extend-onto-your-host-machine) |
[Docs](#documentation)
+[](https://deepwiki.com/agent0ai/agent-zero)
+[Ask ChatGPT](https://chatgpt.com/?q=Analyze%20this%3A%20https%3A%2F%2Fgithub.com%2Fagent0ai%2Fagent-zero) |
+[Ask Claude](https://claude.ai/new?q=Analyze%20this%3A%20https%3A%2F%2Fgithub.com%2Fagent0ai%2Fagent-zero)
+
+
-# Why Agent Zero
+# What Makes Agent Zero Different
-| Feature | Why it matters |
-| --- | --- |
-| **Full Linux desktop** | The agent can use real GUI software, terminals, files, and desktop apps inside the Canvas. |
-| **Browser DOM annotation** | Click page elements and turn them into inspect, change, lift, or review instructions. |
-| **Live document cowork** | Edit Markdown, Writer, Spreadsheet, and Presentation files together instead of losing work in chat. |
-| **Plugin Hub** | Install 100+ community plugins or publish your own extension points. |
-| **Projects and memory** | Keep files, instructions, secrets, memories, repositories, and model presets isolated per project. |
-| **Host-machine bridge** | Connect with the A0 CLI so the same agent can work in your real local repositories. |
-| **Multi-agent cooperation** | Let agents delegate research, coding, analysis, or review tasks to focused subagents. |
-| **Transparent internals** | Prompts, tools, plugins, skills, and settings are inspectable and editable. |
-
-# Quick Start
-
-## Recommended: A0 Launcher
-
-The desktop **A0 Launcher** is the fastest guided path on a personal machine. Download it, open it, and let it check Docker, create Instances, manage ports, and connect to local or remote Agent Zero installs.
-
-Agent Zero runs wherever Docker runs, from a $6 VPS or Raspberry Pi to a local workstation or GPU server.
-
-| Architecture | macOS | Linux | Windows |
-| --- | --- | --- | --- |
-| x86 | [Mac Intel](https://github.com/agent0ai/a0-launcher/releases/download/v1.2/a0-launcher-1.2-macos-x64.dmg) | [Linux x86](https://github.com/agent0ai/a0-launcher/releases/download/v1.2/a0-launcher-1.2-linux-x64.AppImage) | [Windows x86](https://github.com/agent0ai/a0-launcher/releases/download/v1.2/a0-launcher-1.2-windows-x64.exe) |
-| ARM64 | [Mac Apple Silicon](https://github.com/agent0ai/a0-launcher/releases/download/v1.2/a0-launcher-1.2-macos-arm64.dmg) | [Linux ARM64](https://github.com/agent0ai/a0-launcher/releases/download/v1.2/a0-launcher-1.2-linux-arm64.AppImage) | [Windows ARM64](https://github.com/agent0ai/a0-launcher/releases/download/v1.2/a0-launcher-1.2-windows-arm64.exe) |
-
-See the [A0 Launcher v1.2 release](https://github.com/agent0ai/a0-launcher/releases/tag/v1.2) for release notes and updater metadata. See the [Launcher guide](./docs/guides/launcher.md) for the first-run walkthrough.
-
-
-Other install paths
-
-## A0 Install
-
-Use **A0 Install** when you want the terminal path: SSH sessions, servers, recovery shells, or a scriptable setup. It creates Dockerized Agent Zero instances, mounts each instance's data into `/a0/usr` inside the container, and uses a reuse-before-setup policy: it tries your current Docker CLI configuration, `DOCKER_HOST`, Docker contexts, and known local Docker-compatible endpoints before setting up a runtime.
+## How To Install
### macOS / Linux
@@ -72,21 +46,7 @@ curl -fsSL https://bash.agent-zero.ai | bash
irm https://ps.agent-zero.ai | iex
```
-### Headless / scripted
-
-For servers and automation, run the installer in Quick Start mode so it creates one instance and exits without opening menus:
-
-```bash
-curl -fsSL https://bash.agent-zero.ai | bash -s -- --quick-start --name agent-zero --port 5080
-```
-
-```powershell
-& ([scriptblock]::Create((irm https://ps.agent-zero.ai))) -QuickStart -Name agent-zero -Port 5080
-```
-
-Use `--skip-runtime-setup` / `-SkipRuntimeSetup` when Docker must already be working and the installer should not try to set up a runtime. See the [A0 Install repository](https://github.com/agent0ai/a0-install) for all installer flags.
-
-## Docker already installed? Run this directly
+### Docker already installed? Run this directly
```bash
docker run -p 80:80 -v a0_usr:/a0/usr agent0ai/agent-zero
@@ -94,28 +54,11 @@ docker run -p 80:80 -v a0_usr:/a0/usr agent0ai/agent-zero
Open the Web UI, configure your LLM provider, and start with a concrete task. For the full setup and onboarding experience, see the [Installation guide](./docs/setup/installation.md).
-
-
-## Troubleshooting
-
-- **Docker is not running:** start Docker Desktop or your Docker service, then reopen the Launcher or rerun the install command.
-- **Port 80 is already in use:** use the Launcher to pick another port, or run Docker directly with `-p 5080:80` and open `http://localhost:5080`.
-- **Installing on a server:** use the A0 Install Quick Start command with `--quick-start --name agent-zero --port 5080`.
-- **Still blocked:** see the [Troubleshooting guide](./docs/guides/troubleshooting.md).
-
-# Try These First
-
-- **Annotate a design you like:** "Open this template site in the Browser. I'll annotate the hero section - re-implement it in my project's React + Tailwind stack."
-- **Cowork on a spreadsheet:** "Create an editable ODS budget model with assumptions and monthly projections."
-- **Drive a desktop app:** "Use the Linux Desktop to open Blender and create a simple 3D logo for me."
-- **Review a web UI:** "Open my local app in the Browser. I will annotate the page with comments; then implement the requested UI fixes."
-- **Create a specialist:** "Create an Agent Profile for financial analysis with cautious reasoning, clear assumptions, and spreadsheet-first deliverables."
-- **Recover a workspace:** "Show me recent Time Travel snapshots and explain what changed before I revert anything."
-
-# Deep Dives
-
## A Real Linux Desktop in the Canvas
+
+
+
Agent Zero opens its own Linux desktop inside the right-side Canvas. Not a remote VM, not a shared clipboard, but a real XFCE desktop session running in the container.
That means the agent can drive *real desktop software*: open Blender to model a 3D object, jump into a terminal window, manage files visually, run a GUI tool that has no API.
@@ -138,7 +81,7 @@ Annotate mode turns any webpage into an interactive directive surface. Click an
- **Lift it** - see a card, hero, or component on someone else's site that you like? Capture it and have the agent re-implement it in your own project's stack.
- **Comment it** - leave actionable notes pinned to elements during a UI review; the agent reads the comments and ships the fixes.
-The Docker browser is the default live Browser surface. Browser history keeps screenshots of important steps, so older chats can still show what the agent saw. The Browser also supports Chrome extensions inside the Docker browser, and **Bring Your Own Browser** through the A0 CLI Connector lets the agent drive Chrome, Edge, Brave, Opera, Vivaldi, or Chromium on your own machine.
+The Docker browser is the default live Browser surface. Browser history keeps screenshots of important steps, so older chats can still show what the agent saw. The Browser also supports Chrome extensions inside the Docker browser, and **Bring Your Own Browser** through the A0 CLI Connector lets the agent drive Chrome/Edge/Chromium on your own machine.
See the [Browser guide](./docs/guides/browser.md) for screenshots, settings, host-browser setup, and troubleshooting.
@@ -243,9 +186,31 @@ Almost nothing is hidden. Prompts live in `prompts/`, tools live in `tools/` or
Agent Zero supports plugins, MCP, A2A, custom tools, custom prompts, project-scoped configuration, environment-based deployment settings, and a Web UI designed to keep the agent's work readable in real time.
-## Time Travel
+## Try These First
-Time Travel gives Agent Zero-owned `/a0/usr` workspaces snapshot history, diff inspection, travel, and revert. It is designed for recoverable agent work: see what changed, compare files, inspect a past state, and roll back when needed.
+- **Annotate a design you like:** "Open this template site in the Browser. I'll annotate the hero section - re-implement it in my project's React + Tailwind stack."
+- **Cowork on a spreadsheet:** "Create an editable ODS budget model with assumptions and monthly projections."
+- **Drive a desktop app:** "Use the Linux Desktop to open Blender and create a simple 3D logo for me."
+- **Review a web UI:** "Open my local app in the Browser. I will annotate the page with comments; then implement the requested UI fixes."
+- **Create a specialist:** "Create an Agent Profile for financial analysis with cautious reasoning, clear assumptions, and spreadsheet-first deliverables."
+- **Recover a workspace:** "Show me recent Time Travel snapshots and explain what changed before I revert anything."
+
+## Agent Zero and Space Agent
+
+Agent Zero is the open framework and Linux-powered agent workbench.
+
+[Space Agent](https://github.com/agent0ai/space-agent) is our newer product direction for the agent-shaped workspace: a Space the agent can reshape from inside your browser, with live demos, a desktop app, and a path to running a real server for yourself or your team.
+
+
+
+
+
+If you want the raw power and deep customizability of an agent with a full Linux system, start here with Agent Zero. If you want the polished Space experience for easier personal, team, desktop, or self-hosted use, explore [Space Agent](https://github.com/agent0ai/space-agent).
+
+
+## Time Travel (powered by Space Agent)
+
+Time Travel gives Agent Zero-owned `/a0/usr` workspaces snapshot history, diff inspection, travel, and revert. It is designed for recoverable agent work: see what changed, compare files, inspect a past state, and roll back when needed. Try it in Space Agent as well (link above).
@@ -263,6 +228,17 @@ It is not a replacement for Git or backups. It is a practical safety layer for t
- **Client/project isolation:** keep memory, secrets, instructions, files, and model choices separated by project.
- **Scheduled operations:** run recurring checks and monitoring tasks with project-scoped context and credentials.
+## Safety Model
+
+Agent Zero is powerful because it can use a real environment.
+
+- Keep it running inside Docker or another isolated environment.
+- Do not mount your entire home directory unless you understand the risk.
+- Grant A0 CLI Read+Write access and remote code execution only for machines and workspaces you trust.
+- Store credentials in project secrets or settings, not in prompts or public files.
+- Review actions that touch accounts, money, production systems, or private data.
+- Keep backups for important workspaces.
+
## Documentation
| I want to... | Start here |
@@ -298,16 +274,3 @@ You can help by improving docs, creating skills, publishing plugins, testing mod
- [YouTube](https://www.youtube.com/@AgentZeroFW) for demos and tutorials.
- [X](https://x.com/Agent0ai), [LinkedIn](https://www.linkedin.com/company/109758317), and [Warpcast](https://warpcast.com/agent-zero) for updates.
- [GitHub Issues](https://github.com/agent0ai/agent-zero/issues) for bugs and feature requests.
-
-[Space Agent](https://github.com/agent0ai/space-agent) is the related, more polished product direction for the agent-shaped workspace. Agent Zero remains the open framework and Linux-powered workbench.
-
-## Safety Model
-
-Agent Zero is powerful because it can use a real environment.
-
-- Keep it running inside Docker or another isolated environment.
-- Do not mount your entire home directory unless you understand the risk.
-- Grant A0 CLI Read+Write access and remote code execution only for machines and workspaces you trust.
-- Store credentials in project secrets or settings, not in prompts or public files.
-- Review actions that touch accounts, money, production systems, or private data.
-- Keep backups for important workspaces.
diff --git a/agent.py b/agent.py
index f8c902da7..149c900e3 100644
--- a/agent.py
+++ b/agent.py
@@ -1,4 +1,4 @@
-import asyncio, json, random, re, string, threading
+import asyncio, random, string, threading
from collections import OrderedDict
from dataclasses import dataclass, field
@@ -32,15 +32,6 @@ from typing import Callable
from helpers.localization import Localization
from helpers import extension
from helpers.errors import RepairableException, InterventionException, HandledException
-from helpers.llm_result import (
- LLMResult,
- RESPONSE_METADATA_KEY,
- function_call_output_item,
- metadata_from_llm_result,
- result_from_metadata,
-)
-from helpers.litellm_transport import ResponsesTransport
-from helpers.responses_tools import build_responses_function_tools, original_tool_name
class AgentContextType(Enum):
USER = "user"
@@ -338,8 +329,6 @@ class LoopData:
self.system = []
self.user_message: history.Message | None = None
self.history_output: list[history.OutputMessage] = []
- self.protocol_temporary: OrderedDict[str, history.MessageContent] = OrderedDict()
- self.protocol_persistent: OrderedDict[str, history.MessageContent] = OrderedDict()
self.extras_temporary: OrderedDict[str, history.MessageContent] = OrderedDict()
self.extras_persistent: OrderedDict[str, history.MessageContent] = OrderedDict()
self.last_response = ""
@@ -357,9 +346,6 @@ class Agent:
DATA_NAME_SUPERIOR = "_superior"
DATA_NAME_SUBORDINATE = "_subordinate"
DATA_NAME_CTX_WINDOW = "ctx_window"
- DATA_NAME_RESPONSES_STATE = "responses_state"
- DATA_NAME_RESPONSES_TOOL_NAME_MAP = "responses_tool_name_map"
- DATA_NAME_RESPONSES_COMPUTER_SESSION = "responses_computer_session_id"
@extension.extensible
def __init__(
@@ -482,12 +468,11 @@ class Agent:
return stop_response
# call main LLM
- llm_result = await self.call_chat_model_turn(
+ agent_response, _reasoning = await self.call_chat_model(
messages=prompt,
response_callback=stream_callback,
reasoning_callback=reasoning_callback,
)
- agent_response = llm_result.response
await self.handle_intervention(agent_response)
# Notify extensions to finalize their stream filters
@@ -507,12 +492,7 @@ class Agent:
): # if assistant_response is the same as last message in history, let him know
# Append the assistant's response to the history
log_item = self.loop_data.params_temporary.get("log_item_generating")
- assistant_message = self.hist_add_ai_response(
- agent_response,
- id=log_item.id if log_item else "",
- llm_result=llm_result,
- )
- self._remember_llm_result_state(llm_result, assistant_message)
+ self.hist_add_ai_response(agent_response, id=log_item.id if log_item else "")
# Append warning message to the history
warning_msg = self.read_prompt("fw.msg_repeat.md")
wmsg = self.hist_add_warning(message=warning_msg)
@@ -524,16 +504,9 @@ class Agent:
else: # otherwise proceed with tool
# Append the assistant's response to the history
log_item = self.loop_data.params_temporary.get("log_item_generating")
- assistant_message = self.hist_add_ai_response(
- agent_response,
- id=log_item.id if log_item else "",
- llm_result=llm_result,
- )
- self._remember_llm_result_state(llm_result, assistant_message)
+ self.hist_add_ai_response(agent_response, id=log_item.id if log_item else "")
# process tools requested in agent message
- tools_result = await self.process_llm_result_tools(
- llm_result
- )
+ tools_result = await self.process_tools(agent_response)
if tools_result: # final response of message loop available
return tools_result # break the execution if the task is done
@@ -582,28 +555,24 @@ class Agent:
# concatenate system prompt
system_text = "\n\n".join(loop_data.system)
- # join protocol and extras
- protocol = self._build_context_message(
- "agent.context.protocol.md",
- "protocol",
- {**loop_data.protocol_persistent, **loop_data.protocol_temporary},
- include_empty=False,
- )
- extras = self._build_context_message(
- "agent.context.extras.md",
- "extras",
- {**loop_data.extras_persistent, **loop_data.extras_temporary},
- include_empty=True,
- )
- loop_data.protocol_temporary.clear()
+ # join extras
+ extras = history.Message( # type: ignore[abstract]
+ False,
+ content=self.read_prompt(
+ "agent.context.extras.md",
+ extras=dirty_json.stringify(
+ {**loop_data.extras_persistent, **loop_data.extras_temporary}
+ ),
+ ),
+ ).output()
loop_data.extras_temporary.clear()
- # convert protocol + history + extras to LLM format
+ # convert history + extras to LLM format
history_langchain: list[BaseMessage] = history.output_langchain(
- protocol + loop_data.history_output + extras
+ loop_data.history_output + extras
)
- # build full prompt from system prompt, protocol, message history and extras
+ # build full prompt from system prompt, message history and extrS
full_prompt: list[BaseMessage] = [
SystemMessage(content=system_text),
*history_langchain,
@@ -621,24 +590,6 @@ class Agent:
return full_prompt
- def _build_context_message(
- self,
- prompt_file: str,
- variable_name: str,
- values: dict[str, history.MessageContent],
- include_empty: bool,
- ) -> list[history.OutputMessage]:
- if not include_empty and not values:
- return []
-
- return history.Message( # type: ignore[abstract]
- False,
- content=self.read_prompt(
- prompt_file,
- **{variable_name: dirty_json.stringify(values)},
- ),
- ).output()
-
@extension.extensible
async def handle_exception(self, location: str, exception: Exception):
if exception:
@@ -713,12 +664,7 @@ class Agent:
@extension.extensible
def hist_add_message(
- self,
- ai: bool,
- content: history.MessageContent,
- tokens: int = 0,
- id: str = "",
- metadata: dict[str, Any] | None = None,
+ self, ai: bool, content: history.MessageContent, tokens: int = 0, id: str = ""
):
self.last_message = Localization.get().now()
# Allow extensions to process content before adding to history
@@ -727,11 +673,7 @@ class Agent:
"hist_add_before", self, content_data=content_data, ai=ai
)
return self.history.add_message(
- ai=ai,
- content=content_data["content"],
- tokens=tokens,
- id=id,
- metadata=metadata,
+ ai=ai, content=content_data["content"], tokens=tokens, id=id
)
@extension.extensible
@@ -764,17 +706,10 @@ class Agent:
return msg
@extension.extensible
- def hist_add_ai_response(
- self, message: str, id: str = "", llm_result: LLMResult | None = None
- ):
+ def hist_add_ai_response(self, message: str, id: str = ""):
self.loop_data.last_response = message
content = self.parse_prompt("fw.ai_response.md", message=message)
- return self.hist_add_message(
- True,
- content=content,
- id=id,
- metadata=metadata_from_llm_result(llm_result),
- )
+ return self.hist_add_message(True, content=content, id=id)
@extension.extensible
def hist_add_warning(self, message: history.MessageContent, id: str = ""):
@@ -784,28 +719,13 @@ class Agent:
@extension.extensible
def hist_add_tool_result(self, tool_name: str, tool_result: str, **kwargs):
msg_id = kwargs.pop("id", "")
- responses_item = kwargs.pop("_responses_output_item", None) or kwargs.pop(
- "responses_item", None
- )
- metadata = (
- {
- RESPONSE_METADATA_KEY: {
- "input_items": [responses_item],
- "output_items": [],
- "mode": "responses",
- "state": "provider",
- }
- }
- if isinstance(responses_item, dict)
- else None
- )
data = {
"tool_name": tool_name,
"tool_result": tool_result,
**kwargs,
}
extension.call_extensions_sync("hist_add_tool_result", self, data=data)
- return self.hist_add_message(False, content=data, id=msg_id, metadata=metadata)
+ return self.hist_add_message(False, content=data, id=msg_id)
def concat_messages(
self, messages
@@ -910,164 +830,6 @@ class Agent:
return response, reasoning
- @extension.extensible
- async def call_chat_model_turn(
- self,
- messages: list[BaseMessage],
- response_callback: Callable[[str, str], Awaitable[str | None]] | None = None,
- reasoning_callback: Callable[[str, str], Awaitable[None]] | None = None,
- background: bool = False,
- explicit_caching: bool = True,
- ) -> LLMResult:
- model = self.get_chat_model()
- model_kwargs = getattr(model, "kwargs", {}) if model else {}
- if isinstance(model_kwargs, dict) and model_kwargs.get("responses_delete_on_chat_delete") is False:
- self.set_data("responses_delete_on_chat_delete", False)
- response_tools, name_map = build_responses_function_tools(self)
- self.set_data(Agent.DATA_NAME_RESPONSES_TOOL_NAME_MAP, name_map)
-
- call_data = {
- "model": model,
- "messages": messages,
- "response_callback": response_callback,
- "reasoning_callback": reasoning_callback,
- "background": background,
- "explicit_caching": explicit_caching,
- "a0_responses_function_tools": response_tools,
- }
-
- previous_state = self._responses_state_for_model(model)
- if previous_state:
- history_counter = int(previous_state.get("history_counter", 0) or 0)
- call_data["previous_response_id"] = previous_state.get("response_id", "")
- call_data["responses_input_items"] = self._responses_input_items_since(
- model,
- history_counter,
- )
- call_data["responses_local_input_items"] = self._responses_prompt_input_items(
- model,
- messages,
- )
-
- await extension.call_extensions_async(
- "chat_model_call_before", self, call_data=call_data
- )
-
- turn_kwargs = {
- "a0_responses_function_tools": call_data.get(
- "a0_responses_function_tools"
- ),
- "responses_local_input_items": call_data.get(
- "responses_local_input_items"
- ),
- }
- for key in (
- "responses_builtin_tools",
- "responses_state",
- "previous_response_id",
- "responses_input_items",
- ):
- if call_data.get(key) is not None:
- turn_kwargs[key] = call_data.get(key)
-
- llm_result = await call_data["model"].unified_turn(
- messages=call_data["messages"],
- reasoning_callback=call_data["reasoning_callback"],
- response_callback=call_data["response_callback"],
- rate_limiter_callback=(
- self.rate_limiter_callback if not call_data["background"] else None
- ),
- explicit_caching=call_data["explicit_caching"],
- **turn_kwargs,
- )
-
- downgraded = llm_result.capability.get("builtin_tool_downgrades")
- if downgraded:
- self.context.log.log(
- type="info",
- heading="Responses capability downgrade",
- content=(
- "Provider rejected Responses built-in tool(s); omitted: "
- + ", ".join(str(item) for item in downgraded)
- ),
- )
-
- await extension.call_extensions_async(
- "chat_model_call_after",
- self,
- call_data=call_data,
- response=llm_result.response,
- reasoning=llm_result.reasoning,
- )
-
- return llm_result
-
- def _responses_state_for_model(self, model: Any) -> dict[str, Any]:
- state = self.get_data(Agent.DATA_NAME_RESPONSES_STATE)
- if not isinstance(state, dict):
- return {}
- provider_model_key = str(getattr(model, "model_name", "") or "")
- if state.get("provider_model_key") != provider_model_key:
- return {}
- if not state.get("response_id"):
- return {}
- return state
-
- def _responses_input_items_since(
- self, model: Any, sequence: int
- ) -> list[dict[str, Any]]:
- items: list[dict[str, Any]] = []
- for message in self.history.messages_since(sequence):
- items.extend(self._responses_input_items_for_message(model, message))
- return items
-
- def _responses_input_items_for_message(
- self, model: Any, message: history.Message
- ) -> list[dict[str, Any]]:
- result = result_from_metadata(message.metadata)
- if result:
- if message.ai and result.output_items:
- return [item.to_dict() for item in result.output_items]
- if not message.ai and result.input_items:
- return [dict(item) for item in result.input_items]
-
- output = message.output()
- langchain_messages = history.output_langchain(output)
- if hasattr(model, "_convert_messages"):
- converted = model._convert_messages(langchain_messages)
- return ResponsesTransport.input_from_messages(converted)
- return []
-
- def _responses_prompt_input_items(
- self, model: Any, messages: list[BaseMessage]
- ) -> list[dict[str, Any]]:
- if not hasattr(model, "_convert_messages"):
- return []
- converted = model._convert_messages(messages)
- return ResponsesTransport.input_from_messages(converted)
-
- def _remember_llm_result_state(
- self, llm_result: LLMResult, history_message: history.Message
- ) -> None:
- if not llm_result.response_id:
- return
- current = self.get_data(Agent.DATA_NAME_RESPONSES_STATE)
- response_ids = []
- if isinstance(current, dict) and isinstance(current.get("response_ids"), list):
- response_ids = [str(item) for item in current["response_ids"] if item]
- if llm_result.response_id not in response_ids:
- response_ids.append(llm_result.response_id)
- self.set_data(
- Agent.DATA_NAME_RESPONSES_STATE,
- {
- "response_id": llm_result.response_id,
- "previous_response_id": llm_result.previous_response_id,
- "provider_model_key": llm_result.provider_model_key,
- "history_counter": history_message.sequence,
- "response_ids": response_ids,
- },
- )
-
@extension.extensible
async def rate_limiter_callback(
self, message: str, key: str, total: int, limit: int
@@ -1101,310 +863,6 @@ class Agent:
while self.context.paused:
await asyncio.sleep(0.1)
- async def process_llm_result_tools(self, llm_result: LLMResult):
- await self._log_response_builtin_items(llm_result)
- if llm_result.function_calls:
- for function_call in llm_result.function_calls:
- name_map = self.get_data(Agent.DATA_NAME_RESPONSES_TOOL_NAME_MAP)
- tool_name = original_tool_name(function_call.name, name_map)
- response_item_factory = lambda response, call=function_call: function_call_output_item(
- call.call_id,
- response.message,
- )
- result = await self._execute_tool_request(
- tool_name=tool_name,
- tool_args=function_call.arguments,
- message=llm_result.response,
- raw_tool_name=tool_name,
- responses_item_factory=response_item_factory,
- )
- if result:
- return result
- return None
- if llm_result.builtin_items and not llm_result.response:
- return None
- if (
- llm_result.mode == "responses"
- and llm_result.response
- and extract_tools.json_parse_dirty(llm_result.response) is None
- ):
- return llm_result.response
- return await self.process_tools(llm_result.response)
-
- async def _execute_tool_request(
- self,
- tool_name: str,
- tool_args: dict,
- message: str,
- raw_tool_name: str = "",
- responses_item_factory: Callable[[Any], dict[str, Any]] | None = None,
- ):
- raw_tool_name = raw_tool_name or tool_name
- tool_method = None
- tool = None
-
- try:
- import helpers.mcp_handler as mcp_helper
-
- mcp_tool_candidate = mcp_helper.MCPConfig.get_instance().get_tool(
- self, tool_name
- )
- if mcp_tool_candidate:
- tool = mcp_tool_candidate
- except ImportError:
- PrintStyle(
- background_color="black", font_color="yellow", padding=True
- ).print("MCP helper module not found. Skipping MCP tool lookup.")
- except Exception as e:
- PrintStyle(background_color="black", font_color="red", padding=True).print(
- f"Failed to get MCP tool '{tool_name}': {e}"
- )
-
- if not tool:
- tool = self.get_tool(
- name=tool_name,
- method=tool_method,
- args=tool_args,
- message=message,
- loop_data=self.loop_data,
- )
-
- if not tool:
- error_detail = (
- f"Tool '{raw_tool_name}' not found or could not be initialized."
- )
- wmsg = self.hist_add_warning(error_detail)
- PrintStyle(font_color="red", padding=True).print(error_detail)
- self.context.log.log(
- type="warning",
- content=f"{self.agent_name}: {error_detail}",
- id=wmsg.id,
- )
- return None
-
- self.loop_data.current_tool = tool # type: ignore
- try:
- await self.handle_intervention()
-
- await tool.before_execution(**tool_args)
- await self.handle_intervention()
-
- await extension.call_extensions_async(
- "tool_execute_before",
- self,
- tool_args=tool_args or {},
- tool_name=tool_name,
- )
-
- response = await tool.execute(**tool_args)
- await self.handle_intervention()
-
- await extension.call_extensions_async(
- "tool_execute_after",
- self,
- response=response,
- tool_name=tool_name,
- )
-
- if responses_item_factory:
- response.additional = {
- **(response.additional or {}),
- "_responses_output_item": responses_item_factory(response),
- }
-
- await tool.after_execution(response)
- await self.handle_intervention()
-
- if response.break_loop:
- self._clear_responses_pending_state()
- return response.message
- finally:
- self.loop_data.current_tool = None
- return None
-
- async def _log_response_builtin_items(self, llm_result: LLMResult) -> None:
- for item in llm_result.builtin_items:
- if item.type == "computer_call":
- await self._handle_responses_computer_call(item.data)
- continue
- if item.type == "mcp_approval_request":
- self._handle_responses_mcp_approval_request(item.data)
- continue
- self.context.log.log(
- type="info",
- heading=f"Responses tool item: {item.type}",
- content=json.dumps(item.data, ensure_ascii=False, default=str),
- )
-
- async def _handle_responses_computer_call(self, item: dict[str, Any]) -> None:
- safety_checks = item.get("pending_safety_checks") or item.get("safety_checks")
- if safety_checks:
- message = (
- "Responses computer_call requested safety-check acknowledgement. "
- "Agent Zero requires explicit user acknowledgement before executing it."
- )
- output_item = {
- "type": "computer_call_output",
- "call_id": str(item.get("call_id") or item.get("id") or ""),
- "output": {"type": "input_text", "text": message},
- }
- self.hist_add_tool_result(
- "computer_call",
- message,
- responses_item=output_item,
- )
- self.context.log.log(type="warning", content=message)
- return
-
- args = self._computer_call_args(item)
- if not args:
- message = "Responses computer_call action is unsupported by Agent Zero."
- output_item = {
- "type": "computer_call_output",
- "call_id": str(item.get("call_id") or item.get("id") or ""),
- "output": {"type": "input_text", "text": message},
- }
- self.hist_add_tool_result(
- "computer_call",
- message,
- responses_item=output_item,
- )
- self.context.log.log(type="warning", content=message)
- return
-
- if args.get("action") != "start_session" and not args.get("session_id"):
- session_id = str(
- self.get_data(Agent.DATA_NAME_RESPONSES_COMPUTER_SESSION) or ""
- )
- if session_id:
- args["session_id"] = session_id
-
- response_item_factory = lambda response: self._computer_call_output_item(
- item,
- response,
- )
- result = await self._execute_tool_request(
- tool_name="computer_use_remote",
- tool_args=args,
- message=json.dumps(item, ensure_ascii=False, default=str),
- raw_tool_name="computer_call",
- responses_item_factory=response_item_factory,
- )
- _ = result
-
- def _handle_responses_mcp_approval_request(self, item: dict[str, Any]) -> None:
- request_id = str(
- item.get("approval_request_id") or item.get("id") or item.get("call_id") or ""
- )
- message = (
- "Responses MCP approval request received. Agent Zero denied it because "
- "provider-hosted MCP approval requires explicit user approval."
- )
- output_item = {
- "type": "mcp_approval_response",
- "approval_request_id": request_id,
- "approve": False,
- }
- self.hist_add_tool_result(
- "mcp_approval_request",
- message,
- responses_item=output_item,
- )
- self.context.log.log(
- type="warning",
- heading="Responses MCP approval required",
- content=message,
- )
-
- def _computer_call_args(self, item: dict[str, Any]) -> dict[str, Any]:
- action = item.get("action")
- action_data = dict(action) if isinstance(action, dict) else {}
- action_type = str(
- action_data.get("type")
- or action_data.get("action")
- or item.get("action_type")
- or ""
- ).strip().lower()
- args: dict[str, Any] = {}
-
- if action_type in {"screenshot", "capture"}:
- args["action"] = "capture"
- elif action_type in {"move", "mousemove"}:
- args.update({"action": "move", "x": action_data.get("x"), "y": action_data.get("y")})
- elif action_type in {"click", "double_click"}:
- args.update(
- {
- "action": "click",
- "x": action_data.get("x"),
- "y": action_data.get("y"),
- "button": action_data.get("button", "left"),
- "count": 2 if action_type == "double_click" else action_data.get("count", 1),
- }
- )
- elif action_type == "scroll":
- args.update(
- {
- "action": "scroll",
- "dx": action_data.get("dx", action_data.get("scroll_x", 0)),
- "dy": action_data.get("dy", action_data.get("scroll_y", 0)),
- }
- )
- elif action_type in {"keypress", "key"}:
- args.update(
- {
- "action": "key",
- "keys": action_data.get("keys") or action_data.get("key"),
- }
- )
- elif action_type in {"type", "input_text"}:
- args.update({"action": "type", "text": action_data.get("text", "")})
- else:
- return {}
-
- session_id = item.get("session_id") or action_data.get("session_id")
- if session_id:
- args["session_id"] = session_id
- return args
-
- def _computer_call_output_item(
- self, source_item: dict[str, Any], response: Any
- ) -> dict[str, Any]:
- output: dict[str, Any] = {
- "type": "input_text",
- "text": str(getattr(response, "message", "") or ""),
- }
- additional = getattr(response, "additional", None)
- raw_content = additional.get("raw_content") if isinstance(additional, dict) else None
- if isinstance(raw_content, list):
- for content in raw_content:
- if not isinstance(content, dict):
- continue
- if content.get("type") != "image_url":
- continue
- image_url = content.get("image_url")
- url = image_url.get("url") if isinstance(image_url, dict) else image_url
- if url:
- output = {"type": "input_image", "image_url": url}
- break
-
- session_id_match = re_search_session_id(str(getattr(response, "message", "") or ""))
- if session_id_match:
- self.set_data(Agent.DATA_NAME_RESPONSES_COMPUTER_SESSION, session_id_match)
-
- return {
- "type": "computer_call_output",
- "call_id": str(source_item.get("call_id") or source_item.get("id") or ""),
- "output": output,
- }
-
- def _clear_responses_pending_state(self) -> None:
- state = self.get_data(Agent.DATA_NAME_RESPONSES_STATE)
- if isinstance(state, dict):
- state = dict(state)
- state.pop("response_id", None)
- state.pop("previous_response_id", None)
- self.set_data(Agent.DATA_NAME_RESPONSES_STATE, state)
-
@extension.extensible
async def process_tools(self, msg: str):
# search for tool usage requests in agent message
@@ -1579,8 +1037,3 @@ class Agent:
loop_data=loop_data,
**kwargs,
)
-
-
-def re_search_session_id(text: str) -> str:
- match = re.search(r"session_id=([A-Za-z0-9_.:-]+)", text or "")
- return match.group(1) if match else ""
diff --git a/agents/AGENTS.md b/agents/AGENTS.md
index ae6bda3c5..cce3cbc96 100644
--- a/agents/AGENTS.md
+++ b/agents/AGENTS.md
@@ -31,14 +31,4 @@
## Child DOX Index
-Direct child DOX files:
-
-| Child | Scope |
-| --- | --- |
-| [_example/AGENTS.md](_example/AGENTS.md) | Reference profile demonstrating profile-local prompts, tools, and extensions. |
-| [agent0/AGENTS.md](agent0/AGENTS.md) | Main user-facing Agent Zero profile metadata. |
-| [default/AGENTS.md](default/AGENTS.md) | Base profile metadata and inherited prompt specifics. |
-| [developer/AGENTS.md](developer/AGENTS.md) | Software development specialist profile. |
-| [hacker/AGENTS.md](hacker/AGENTS.md) | Cyber security and penetration testing specialist profile. |
-| [researcher/AGENTS.md](researcher/AGENTS.md) | Research, data analysis, and reporting specialist profile. |
-| [tiny-local/AGENTS.md](tiny-local/AGENTS.md) | Small/local model profile with an action-first communication prompt. |
+No child DOX files.
diff --git a/agents/_example/AGENTS.md b/agents/_example/AGENTS.md
deleted file mode 100644
index 555428ee6..000000000
--- a/agents/_example/AGENTS.md
+++ /dev/null
@@ -1,33 +0,0 @@
-# Example Agent Profile DOX
-
-## Purpose
-
-- Own the reference profile used to demonstrate bundled profile layout.
-- Show how profile-local prompts, tools, and extensions fit beside `agent.yaml`.
-
-## Ownership
-
-- `agent.yaml` owns the example profile metadata.
-- `prompts/` owns prompt override examples.
-- `tools/` owns profile-local tool examples.
-- `extensions/` owns profile-local lifecycle extension examples.
-
-## Local Contracts
-
-- Keep this profile generic, minimal, and safe to copy into user or plugin profile work.
-- Do not add product behavior here that should live in a real bundled profile.
-- Profile-local tools and extensions must follow the same contracts as root tools and extensions.
-
-## Work Guidance
-
-- Prefer simple examples that illustrate structure over complex behavior.
-- Update related skill guidance when the example profile layout changes.
-
-## Verification
-
-- Manually inspect YAML and prompt filenames after edits.
-- Run profile-loading tests when changing discovery or profile schema assumptions.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/agents/agent0/AGENTS.md b/agents/agent0/AGENTS.md
deleted file mode 100644
index 65528190a..000000000
--- a/agents/agent0/AGENTS.md
+++ /dev/null
@@ -1,31 +0,0 @@
-# Agent 0 Profile DOX
-
-## Purpose
-
-- Own the main user-facing Agent Zero profile metadata.
-- Keep the primary assistant profile discoverable and distinct from subordinate specialist profiles.
-
-## Ownership
-
-- `agent.yaml` owns the profile title, description, and delegation context.
-- Prompt behavior is inherited from the default profile unless a local prompt override is added.
-
-## Local Contracts
-
-- Keep `Agent 0` suitable as the direct conversation agent for the system.
-- Do not add narrow specialist behavior that belongs in `developer/`, `researcher/`, `hacker/`, or a custom user profile.
-- Do not store user-specific preferences, provider settings, or secrets in this profile.
-
-## Work Guidance
-
-- Keep metadata concise because it appears in profile selection and delegation contexts.
-- Coordinate substantial behavior changes with default prompts and WebUI profile selection.
-
-## Verification
-
-- Manually inspect `agent.yaml` for valid YAML after edits.
-- Run profile-loading tests when changing schema or discovery behavior.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/agents/default/AGENTS.md b/agents/default/AGENTS.md
deleted file mode 100644
index 130d84cc1..000000000
--- a/agents/default/AGENTS.md
+++ /dev/null
@@ -1,32 +0,0 @@
-# Default Agent Profile DOX
-
-## Purpose
-
-- Own base profile metadata and default prompt specifics inherited by specialized profiles.
-- Provide the shared behavior layer for bundled and custom profiles.
-
-## Ownership
-
-- `agent.yaml` owns default profile metadata.
-- `agent.system.main.specifics.md` owns default profile-specific system prompt content.
-- Additional prompt overrides under this directory become shared defaults unless a child profile overrides them.
-
-## Local Contracts
-
-- Keep default behavior broad, framework-compatible, and safe for inheritance.
-- Avoid role-specific instructions that belong in specialist profiles.
-- Prompt filenames must match the framework prompt override names they target.
-
-## Work Guidance
-
-- Prefer small, explicit prompt changes with clear inheritance impact.
-- Check bundled specialist profiles after changing default behavior.
-
-## Verification
-
-- Manually inspect YAML and prompt rendering assumptions after edits.
-- Run prompt/profile tests when changing inherited prompt behavior.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/agents/developer/AGENTS.md b/agents/developer/AGENTS.md
deleted file mode 100644
index b2d189fff..000000000
--- a/agents/developer/AGENTS.md
+++ /dev/null
@@ -1,32 +0,0 @@
-# Developer Agent Profile DOX
-
-## Purpose
-
-- Own the bundled software development specialist profile.
-- Keep development, debugging, refactoring, and architecture behavior separate from general agent defaults.
-
-## Ownership
-
-- `agent.yaml` owns title, description, and delegation context for software development work.
-- `prompts/` owns developer-specific prompt overrides when present.
-- `extensions/` owns developer-specific lifecycle hooks when present.
-
-## Local Contracts
-
-- Keep this profile focused on software engineering tasks.
-- Do not hardcode repository-local credentials, paths, or project-specific conventions.
-- Prompt overrides must preserve the framework tool-call and response contracts.
-
-## Work Guidance
-
-- Align developer behavior with the root engineering and tool contracts.
-- Prefer profile prompt edits over core prompt edits when the behavior is specific to development tasks.
-
-## Verification
-
-- Manually inspect `agent.yaml` for valid YAML after edits.
-- Run prompt/profile tests when changing profile loading or developer prompt behavior.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/agents/hacker/AGENTS.md b/agents/hacker/AGENTS.md
deleted file mode 100644
index 9daeaed00..000000000
--- a/agents/hacker/AGENTS.md
+++ /dev/null
@@ -1,31 +0,0 @@
-# Hacker Agent Profile DOX
-
-## Purpose
-
-- Own the bundled cyber security and penetration testing specialist profile.
-- Keep security-audit behavior scoped to this profile instead of default agent behavior.
-
-## Ownership
-
-- `agent.yaml` owns title, description, and delegation context for security work.
-- `prompts/` owns security-specific prompt overrides when present.
-
-## Local Contracts
-
-- Keep the profile focused on authorized security analysis, vulnerability research, and defensive audit tasks.
-- Do not add secrets, target-specific credentials, or local environment assumptions.
-- Preserve the framework tool-call contract and safety expectations.
-
-## Work Guidance
-
-- Keep security instructions operational and bounded to legitimate testing contexts.
-- Coordinate broad safety changes with core prompts and relevant tests.
-
-## Verification
-
-- Manually inspect `agent.yaml` for valid YAML after edits.
-- Run prompt/profile tests when changing profile discovery or security prompt behavior.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/agents/researcher/AGENTS.md b/agents/researcher/AGENTS.md
deleted file mode 100644
index dd7e41def..000000000
--- a/agents/researcher/AGENTS.md
+++ /dev/null
@@ -1,31 +0,0 @@
-# Researcher Agent Profile DOX
-
-## Purpose
-
-- Own the bundled research, data analysis, and reporting specialist profile.
-- Keep evidence-gathering and report-oriented behavior separate from general defaults.
-
-## Ownership
-
-- `agent.yaml` owns title, description, and delegation context for research work.
-- `prompts/` owns researcher-specific prompt overrides when present.
-
-## Local Contracts
-
-- Keep this profile focused on information gathering, analysis, synthesis, and reporting.
-- Do not bake in project-specific sources, credentials, or local paths.
-- Preserve the framework tool-call and response contracts.
-
-## Work Guidance
-
-- Prefer prompt changes that improve citation, evidence handling, and analysis quality for research tasks.
-- Coordinate broad research behavior changes with document or browser plugin contracts when relevant.
-
-## Verification
-
-- Manually inspect `agent.yaml` for valid YAML after edits.
-- Run prompt/profile tests when changing discovery or researcher prompt behavior.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/agents/tiny-local/AGENTS.md b/agents/tiny-local/AGENTS.md
deleted file mode 100644
index 86715f4dc..000000000
--- a/agents/tiny-local/AGENTS.md
+++ /dev/null
@@ -1,38 +0,0 @@
-# Tiny Local Agent Profile DOX
-
-## Purpose
-
-- Own the bundled Tiny Local profile for small/local chat models.
-- Keep local-model behavior prompt-only and isolated from core framework execution.
-
-## Ownership
-
-- `agent.yaml` owns profile metadata for discovery and profile switching.
-- `prompts/agent.system.main.communication.md` owns the local-model communication contract.
-- `prompts/agent.system.main.solving.md` owns the local-model problem-solving contract and suppresses inherited visible reasoning requirements.
-- `prompts/fw.msg_repeat.md` owns Tiny Local's profile-specific recovery instructions when the framework rejects a duplicate assistant message.
-- `prompts/agent.system.tools.md` owns the Tiny Local tools wrapper and final output-shape reminder after tool listing.
-- `prompts/agent.system.tool.*.md` files own Tiny Local-specific tool examples that avoid inherited reasoning fields and repeated writes.
-
-## Local Contracts
-
-- Preserve the normal Agent Zero tool-call shape: `tool_name` plus `tool_args`.
-- Do not add parser repair, duplicate suppression runtime, model transport, or text-editor runtime behavior here.
-- Duplicate-message handling may be tightened through profile prompts only.
-- Keep prompt text short enough for small local models to follow.
-- Treat continuation requests such as `proceed` or `continue` as commands to execute the next unfinished step, not as prompts for another status response.
-- Do not include user-specific provider names, API keys, local paths, or secrets.
-
-## Work Guidance
-
-- Prefer prompt wording changes over new files when tightening this profile, except when replacing inherited tool examples for local-model compliance.
-- Keep this profile suitable for Ollama, LM Studio, Qwen, and comparable local models.
-
-## Verification
-
-- Render the `tiny-local` system prompt after communication prompt changes.
-- Run `pytest tests/test_default_prompt_budget.py` for prompt and profile regressions.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/agents/tiny-local/agent.yaml b/agents/tiny-local/agent.yaml
deleted file mode 100644
index 413dcbfbf..000000000
--- a/agents/tiny-local/agent.yaml
+++ /dev/null
@@ -1,3 +0,0 @@
-title: Tiny Local
-description: Action-first profile for small local models that need a minimal tool-call contract.
-context: Use this agent when running small local chat models through Ollama, LM Studio, or similar providers and the model tends to explain actions instead of calling tools.
diff --git a/agents/tiny-local/prompts/agent.system.main.communication.md b/agents/tiny-local/prompts/agent.system.main.communication.md
deleted file mode 100644
index 65f79edef..000000000
--- a/agents/tiny-local/prompts/agent.system.main.communication.md
+++ /dev/null
@@ -1,31 +0,0 @@
-## Communication
-
-You are Agent Zero. Act on the user's behalf.
-
-When the user asks you to do something, do it directly. Do not explain how the user could do it themselves.
-
-Your visible assistant message must be exactly one valid JSON object.
-
-Use exactly these top-level fields: `"tool_name"` and `"tool_args"`.
-
-Do not include markdown fences, prose before the JSON, prose after the JSON, hidden reasoning, analysis, thoughts, or headlines.
-
-Choose a tool from the tools listed in this system prompt. Do not invent tool names, action names, or generic names such as `read`, `write`, `terminal`, or `multi`.
-
-For a final user-facing answer, use the `response` tool.
-
-Use `response` only when the work is complete, blocked, or the user is only acknowledging completed work.
-
-If the user says "proceed", "continue", "go ahead", "do it", "excellent proceed", or similar after you named a next step or there is unfinished work, do not answer with a promise or status update. Call the next appropriate tool.
-
-Final-answer shape:
-
-`{"tool_name":"response","tool_args":{"text":"Answer briefly."}}`
-
-For work that requires a command, file action, browser action, or any other available tool, call the appropriate tool immediately. Do not explain what command the user could run manually.
-
-If the framework warns that your prior message was malformed, repeated, or reasoning-only, output a corrected JSON tool request immediately without explaining the warning.
-
-When the warning says you sent the same message again, do not resend the same JSON. Change the tool, action, arguments, or final answer so the next message is meaningfully different.
-
-{{ include "agent.system.main.communication_additions.md" }}
diff --git a/agents/tiny-local/prompts/agent.system.main.solving.md b/agents/tiny-local/prompts/agent.system.main.solving.md
deleted file mode 100644
index 2e410be4a..000000000
--- a/agents/tiny-local/prompts/agent.system.main.solving.md
+++ /dev/null
@@ -1,18 +0,0 @@
-## Problem Solving
-
-Act directly and keep hidden reasoning out of the visible JSON.
-
-For simple questions, answer with the `response` tool.
-
-Continuation words such as "proceed", "continue", "go ahead", "do it", and "excellent proceed" mean execute the next unfinished step. Do not respond by saying you will begin, continue, start, proceed, or investigate. Use a real tool call unless the task is already complete or blocked.
-
-For tasks that need shell commands, files, browser actions, or other capabilities:
-- choose the appropriate listed tool immediately
-- keep one tool call per turn unless the `parallel` tool is listed and truly useful
-- inspect outputs before deciding the next tool call
-- never claim success from timeout output or a still-running command
-- after a successful tool result, do not repeat the same exact tool call
-- after a repeated-message warning, do not repeat the same status response or exact tool request; choose the next different executable action or report a blocker
-- when finished, use the `response` tool with a brief result
-
-Do not include `thoughts`, `headline`, analysis, plans, or prose outside the JSON object.
diff --git a/agents/tiny-local/prompts/agent.system.tool.code_exe.md b/agents/tiny-local/prompts/agent.system.tool.code_exe.md
deleted file mode 100644
index a42a3314f..000000000
--- a/agents/tiny-local/prompts/agent.system.tool.code_exe.md
+++ /dev/null
@@ -1,24 +0,0 @@
-### code_execution_tool
-Run terminal, Python, or Node.js commands.
-
-Arguments in `tool_args`:
-- `runtime`: `terminal`, `python`, `nodejs`, or `output`
-- `code`: command or script code
-- `session`: terminal session id; default `0`
-- `reset`: kill a session before running; `true` or `false`
-
-Rules:
-- Put the command or script in `code`.
-- Use `runtime=output` to poll running work.
-- Use `input` for interactive terminal prompts.
-- If a session is stuck, call this tool again with the same `session` and `reset=true`.
-- Do not claim success from timeout output or a still-running command.
-- When counting files, prefer `find` over `ls` so hidden files and type filters are handled.
-
-Examples:
-
-`{"tool_name":"code_execution_tool","tool_args":{"runtime":"terminal","session":0,"reset":false,"code":"ls -1 /tmp | wc -l"}}`
-
-`{"tool_name":"code_execution_tool","tool_args":{"runtime":"python","session":0,"reset":false,"code":"import os\nprint(os.getcwd())"}}`
-
-`{"tool_name":"code_execution_tool","tool_args":{"runtime":"output","session":0}}`
diff --git a/agents/tiny-local/prompts/agent.system.tool.response.md b/agents/tiny-local/prompts/agent.system.tool.response.md
deleted file mode 100644
index de0709179..000000000
--- a/agents/tiny-local/prompts/agent.system.tool.response.md
+++ /dev/null
@@ -1,13 +0,0 @@
-### response
-Final answer to the user.
-
-Use this tool only when the task is done, blocked, or no tool is needed.
-
-Do not use this tool for "proceed", "continue", "go ahead", or similar continuation requests when there is an unfinished next step. Call a real tool instead.
-
-Arguments in `tool_args`:
-- `text`: concise final answer text
-
-Example:
-
-`{"tool_name":"response","tool_args":{"text":"There are 24 files in /tmp."}}`
diff --git a/agents/tiny-local/prompts/agent.system.tool.text_editor.md b/agents/tiny-local/prompts/agent.system.tool.text_editor.md
deleted file mode 100644
index fdf7ad141..000000000
--- a/agents/tiny-local/prompts/agent.system.tool.text_editor.md
+++ /dev/null
@@ -1,26 +0,0 @@
-### text_editor
-Read, write, or patch Markdown and plain text files.
-
-Actions in `tool_args.action`:
-- `read`: read a file
-- `write`: create or overwrite a file
-- `patch`: edit an existing file
-
-Common arguments:
-- `path`: absolute file path
-- `content`: full file content for `write`
-- `open_in_canvas`: set `true` when the user explicitly asks to open a Markdown file in the Canvas or Editor
-
-Rules:
-- Use this tool for `.md` and plain text files.
-- Use `write` to create a new Markdown file.
-- If the user asks to open the file in the Canvas or Editor, include `"open_in_canvas": true` in the same `write` or `patch` call.
-- After a successful write or patch result, do not repeat the same tool call. Use the `response` tool unless a different action is needed.
-
-Examples:
-
-`{"tool_name":"text_editor","tool_args":{"action":"write","path":"/a0/usr/workdir/TODO.md","content":"# TODO\n- [ ] First item\n","open_in_canvas":true}}`
-
-`{"tool_name":"text_editor","tool_args":{"action":"read","path":"/a0/usr/workdir/TODO.md"}}`
-
-`{"tool_name":"text_editor","tool_args":{"action":"patch","path":"/a0/usr/workdir/TODO.md","old_text":"- [ ] First item","new_text":"- [x] First item"}}`
diff --git a/agents/tiny-local/prompts/agent.system.tools.md b/agents/tiny-local/prompts/agent.system.tools.md
deleted file mode 100644
index 7c50f33b0..000000000
--- a/agents/tiny-local/prompts/agent.system.tools.md
+++ /dev/null
@@ -1,17 +0,0 @@
-## Available Tools
-
-Use only the tools listed below. Match tool names exactly.
-
-Every tool request must be exactly one JSON object with only these top-level fields:
-- `tool_name`
-- `tool_args`
-
-Action names are not tool names. Do not invent top-level `multi`, `read`, `write`, `terminal`, or generic batch tools.
-
-{{tools}}
-
-## Tiny Local Output Rule
-
-Some inherited tool examples may show `thoughts` or `headline`. Ignore that shape for this profile.
-
-Do not include `thoughts`, `headline`, analysis, markdown fences, or prose outside the JSON object.
diff --git a/agents/tiny-local/prompts/fw.msg_repeat.md b/agents/tiny-local/prompts/fw.msg_repeat.md
deleted file mode 100644
index 10602a558..000000000
--- a/agents/tiny-local/prompts/fw.msg_repeat.md
+++ /dev/null
@@ -1,13 +0,0 @@
-You have sent the same message again. You have to do something else.
-
-Your repeated JSON was recorded, but it did not execute another tool. Do not send the same JSON object again.
-
-Choose one different action now:
-- If work is unfinished, call a real tool for the next unfinished step.
-- If your previous JSON used `response` while work remains, replace it with the next real tool call.
-- If a file write or patch already succeeded, read that file or answer with the observed result.
-- If a command already ran, inspect its output or run a different next command.
-- If the user only said "proceed" or "continue", continue with the next real tool call.
-- If no different action is possible, use `response` with a brief blocker.
-
-Output exactly one JSON object with `tool_name` and `tool_args`. No prose or markdown.
diff --git a/api/AGENTS.md b/api/AGENTS.md
index 02344d435..7e471b399 100644
--- a/api/AGENTS.md
+++ b/api/AGENTS.md
@@ -19,23 +19,17 @@
- Keep CSRF and authentication protections intact for browser-facing state-changing endpoints.
- WebSocket handlers must derive from `helpers.ws.WsHandler` and validate event data before using it.
- Do not return secrets, raw environment values, private files, or unfiltered exception details to clients.
-- This directory is a file-documented DOX profile: every direct `*.py` endpoint or WebSocket module must have a same-directory `*.py.dox.md` file named by appending `.dox.md` to the full Python filename.
-- The `*.py.dox.md` file owns endpoint purpose, request/response concepts, auth/CSRF/API-key/loopback assumptions, side effects, important helper dependencies, and verification guidance.
-- When a Python endpoint is added, removed, renamed, or behaviorally changed, update its matching `*.py.dox.md` in the same change.
-- Do not leave stale file-level DOX after endpoint deletion or rename.
## Work Guidance
- Use helpers for shared behavior instead of duplicating persistence, auth, file, project, plugin, or notification logic in endpoints.
- Keep request and response payloads stable; update frontend callers and tests together when payloads change.
- Prefer `Response` for files, redirects, status codes, and plain-text errors; return dictionaries for JSON success payloads.
-- During the DOX pass, verify that every direct `*.py` file has a matching `*.py.dox.md` and that changed endpoint behavior is described there.
## Verification
- Run targeted `pytest tests/test_*api*.py`, endpoint-specific tests, or WebSocket tests after changing handler behavior.
- For auth, CSRF, upload/download, tunnel, or file endpoints, run the nearest security regression tests.
-- Check file-level documentation coverage with a script or shell loop that verifies each `api/*.py` has a matching `api/*.py.dox.md`.
## Child DOX Index
diff --git a/api/agent_profile_set.py b/api/agent_profile_set.py
index bec0b2ef1..331e21fd0 100644
--- a/api/agent_profile_set.py
+++ b/api/agent_profile_set.py
@@ -1,4 +1,4 @@
-from agent import AgentContext
+from agent import Agent, AgentContext
from helpers import subagents
from helpers.api import ApiHandler, Request, Response
from helpers.persist_chat import save_tmp_chat
@@ -39,7 +39,11 @@ class SetAgentProfile(ApiHandler):
config = initialize_agent(override_settings={"agent_profile": profile})
context.config = config
- context.agent0.config = config
+
+ agent = context.agent0
+ while agent:
+ agent.config = config
+ agent = agent.get_data(Agent.DATA_NAME_SUBORDINATE)
save_tmp_chat(context)
mark_dirty_for_context(context.id, reason="agent_profile_change")
diff --git a/api/agent_profile_set.py.dox.md b/api/agent_profile_set.py.dox.md
deleted file mode 100644
index 8e891085d..000000000
--- a/api/agent_profile_set.py.dox.md
+++ /dev/null
@@ -1,48 +0,0 @@
-# agent_profile_set.py DOX
-
-## Purpose
-
-- Own the `agent_profile_set.py` API endpoint.
-- This module sets the active agent profile for a chat context and returns profile label metadata.
-- Keep this file-level DOX profile synchronized with `agent_profile_set.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `agent_profile_set.py` owns the runtime implementation.
-- `agent_profile_set.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `SetAgentProfile` (`ApiHandler`)
- - `async process(self, input: dict, request: Request) -> dict | Response`
-- Top-level functions:
-- `_agent_profile_labels() -> dict[str, str]`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `SetAgentProfile` is an `ApiHandler`.
-- `SetAgentProfile` defines `process(...)`.
-- Observed side-effect areas: filesystem writes, settings/state persistence.
-- Switching a chat profile updates the context and top-level agent profile only; existing subordinate agents keep their own profile configs.
-- Imported dependency areas include: `agent`, `helpers`, `helpers.api`, `helpers.persist_chat`, `helpers.state_monitor_integration`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `str.strip`, `context.is_running`, `_agent_profile_labels`, `initialize_agent`, `context.agent0.config`, `save_tmp_chat`, `mark_dirty_for_context`, `subagents.get_all_agents_list`, `Response`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- Related tests observed by source search:
- - `tests/test_subagent_profiles.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/agents.py.dox.md b/api/agents.py.dox.md
deleted file mode 100644
index e7f6895ac..000000000
--- a/api/agents.py.dox.md
+++ /dev/null
@@ -1,48 +0,0 @@
-# agents.py DOX
-
-## Purpose
-
-- Own the `agents.py` API endpoint.
-- This module lists available agent profiles for selection and delegation UI flows.
-- Keep this file-level DOX profile synchronized with `agents.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `agents.py` owns the runtime implementation.
-- `agents.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `Agents` (`ApiHandler`)
- - `async process(self, input: Input, request: Request) -> Output`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `Agents` is an `ApiHandler`.
-- `Agents` defines `process(...)`.
-- Imported dependency areas include: `helpers`, `helpers.api`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `subagents.get_all_agents_list`, `Exception`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- Related tests observed by source search:
- - `tests/test_default_prompt_budget.py`
- - `tests/test_office_document_store.py`
- - `tests/test_projects.py`
- - `tests/test_skills_runtime.py`
- - `tests/test_time_travel.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/api_files_get.py.dox.md b/api/api_files_get.py.dox.md
deleted file mode 100644
index c14bf9f13..000000000
--- a/api/api_files_get.py.dox.md
+++ /dev/null
@@ -1,52 +0,0 @@
-# api_files_get.py DOX
-
-## Purpose
-
-- Own the `api_files_get.py` API endpoint.
-- This module returns downloadable or inspectable files exposed through the external API surface.
-- Keep this file-level DOX profile synchronized with `api_files_get.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `api_files_get.py` owns the runtime implementation.
-- `api_files_get.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `ApiFilesGet` (`ApiHandler`)
- - `requires_auth(cls) -> bool`
- - `requires_csrf(cls) -> bool`
- - `requires_api_key(cls) -> bool`
- - `get_methods(cls) -> list[str]`
- - `async process(self, input: dict, request: Request) -> dict | Response`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `ApiFilesGet` is an `ApiHandler`.
-- `ApiFilesGet` defines `process(...)`.
-- `ApiFilesGet` defines `get_methods(...)`.
-- `ApiFilesGet` defines `requires_auth(...)`.
-- `ApiFilesGet` defines `requires_csrf(...)`.
-- `ApiFilesGet` defines `requires_api_key(...)`.
-- Observed side-effect areas: filesystem reads, filesystem writes, settings/state persistence.
-- Imported dependency areas include: `base64`, `helpers`, `helpers.api`, `helpers.print_style`, `json`, `os`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `Response`, `PrintStyle.error`, `path.startswith`, `PrintStyle`, `json.dumps`, `path.replace`, `files.get_abs_path`, `os.path.basename`, `os.path.exists`, `PrintStyle.warning`, `f.read`, `base64.b64encode.decode`, `base64.b64encode`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/api_log_get.py.dox.md b/api/api_log_get.py.dox.md
deleted file mode 100644
index 043a9c9cf..000000000
--- a/api/api_log_get.py.dox.md
+++ /dev/null
@@ -1,52 +0,0 @@
-# api_log_get.py DOX
-
-## Purpose
-
-- Own the `api_log_get.py` API endpoint.
-- This module returns API/chat log data for external API clients.
-- Keep this file-level DOX profile synchronized with `api_log_get.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `api_log_get.py` owns the runtime implementation.
-- `api_log_get.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `ApiLogGet` (`ApiHandler`)
- - `get_methods(cls) -> list[str]`
- - `requires_auth(cls) -> bool`
- - `requires_csrf(cls) -> bool`
- - `requires_api_key(cls) -> bool`
- - `async process(self, input: dict, request: Request) -> dict | Response`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `ApiLogGet` is an `ApiHandler`.
-- `ApiLogGet` defines `process(...)`.
-- `ApiLogGet` defines `get_methods(...)`.
-- `ApiLogGet` defines `requires_auth(...)`.
-- `ApiLogGet` defines `requires_csrf(...)`.
-- `ApiLogGet` defines `requires_api_key(...)`.
-- Observed side-effect areas: settings/state persistence, secret handling.
-- Imported dependency areas include: `agent`, `helpers.api`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `AgentContext.use`, `Response`, `context.log.output`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/api_message.py.dox.md b/api/api_message.py.dox.md
deleted file mode 100644
index 646939cad..000000000
--- a/api/api_message.py.dox.md
+++ /dev/null
@@ -1,51 +0,0 @@
-# api_message.py DOX
-
-## Purpose
-
-- Own the `api_message.py` API endpoint.
-- This module accepts external API messages and dispatches them into Agent Zero chat processing.
-- Keep this file-level DOX profile synchronized with `api_message.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `api_message.py` owns the runtime implementation.
-- `api_message.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `ApiMessage` (`ApiHandler`)
- - `requires_auth(cls) -> bool`
- - `requires_csrf(cls) -> bool`
- - `requires_api_key(cls) -> bool`
- - `async process(self, input: dict, request: Request) -> dict | Response`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `ApiMessage` is an `ApiHandler`.
-- `ApiMessage` defines `process(...)`.
-- `ApiMessage` defines `requires_auth(...)`.
-- `ApiMessage` defines `requires_csrf(...)`.
-- `ApiMessage` defines `requires_api_key(...)`.
-- Observed side-effect areas: filesystem reads, filesystem writes, settings/state persistence, secret handling, scheduler state.
-- Imported dependency areas include: `agent`, `base64`, `datetime`, `helpers`, `helpers.api`, `helpers.print_style`, `helpers.projects`, `helpers.security`, `initialize`, `os`, `uuid`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `context.set_data`, `datetime.now`, `Response`, `files.get_abs_path`, `os.makedirs`, `AgentContext.use`, `context.get_data`, `initialize_agent`, `AgentContext`, `context.log.log`, `context.communicate`, `ValueError`, `uuid.uuid4`, `UserMessage`, `task.result`, `PrintStyle.error`, `safe_filename`, `base64.b64decode`, `os.path.join`, `activate_project`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- Related tests observed by source search:
- - `tests/test_api_chat_lifetime.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/api_reset_chat.py.dox.md b/api/api_reset_chat.py.dox.md
deleted file mode 100644
index 5120f46cd..000000000
--- a/api/api_reset_chat.py.dox.md
+++ /dev/null
@@ -1,52 +0,0 @@
-# api_reset_chat.py DOX
-
-## Purpose
-
-- Own the `api_reset_chat.py` API endpoint.
-- This module resets an API-created chat context.
-- Keep this file-level DOX profile synchronized with `api_reset_chat.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `api_reset_chat.py` owns the runtime implementation.
-- `api_reset_chat.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `ApiResetChat` (`ApiHandler`)
- - `requires_auth(cls) -> bool`
- - `requires_csrf(cls) -> bool`
- - `requires_api_key(cls) -> bool`
- - `get_methods(cls) -> list[str]`
- - `async process(self, input: dict, request: Request) -> dict | Response`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `ApiResetChat` is an `ApiHandler`.
-- `ApiResetChat` defines `process(...)`.
-- `ApiResetChat` defines `get_methods(...)`.
-- `ApiResetChat` defines `requires_auth(...)`.
-- `ApiResetChat` defines `requires_csrf(...)`.
-- `ApiResetChat` defines `requires_api_key(...)`.
-- Observed side-effect areas: filesystem writes, filesystem deletion, settings/state persistence.
-- Imported dependency areas include: `agent`, `helpers`, `helpers.api`, `helpers.print_style`, `json`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `AgentContext.use`, `context.reset`, `persist_chat.save_tmp_chat`, `persist_chat.remove_msg_files`, `Response`, `PrintStyle.error`, `PrintStyle`, `json.dumps`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/api_terminate_chat.py.dox.md b/api/api_terminate_chat.py.dox.md
deleted file mode 100644
index 541a6d007..000000000
--- a/api/api_terminate_chat.py.dox.md
+++ /dev/null
@@ -1,52 +0,0 @@
-# api_terminate_chat.py DOX
-
-## Purpose
-
-- Own the `api_terminate_chat.py` API endpoint.
-- This module terminates an API-created chat context.
-- Keep this file-level DOX profile synchronized with `api_terminate_chat.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `api_terminate_chat.py` owns the runtime implementation.
-- `api_terminate_chat.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `ApiTerminateChat` (`ApiHandler`)
- - `requires_auth(cls) -> bool`
- - `requires_csrf(cls) -> bool`
- - `requires_api_key(cls) -> bool`
- - `get_methods(cls) -> list[str]`
- - `async process(self, input: dict, request: Request) -> dict | Response`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `ApiTerminateChat` is an `ApiHandler`.
-- `ApiTerminateChat` defines `process(...)`.
-- `ApiTerminateChat` defines `get_methods(...)`.
-- `ApiTerminateChat` defines `requires_auth(...)`.
-- `ApiTerminateChat` defines `requires_csrf(...)`.
-- `ApiTerminateChat` defines `requires_api_key(...)`.
-- Observed side-effect areas: filesystem writes, filesystem deletion, settings/state persistence.
-- Imported dependency areas include: `agent`, `helpers.api`, `helpers.persist_chat`, `helpers.print_style`, `json`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `AgentContext.use`, `AgentContext.remove`, `remove_chat`, `Response`, `PrintStyle.error`, `PrintStyle`, `json.dumps`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/backup_create.py.dox.md b/api/backup_create.py.dox.md
deleted file mode 100644
index be6076326..000000000
--- a/api/backup_create.py.dox.md
+++ /dev/null
@@ -1,49 +0,0 @@
-# backup_create.py DOX
-
-## Purpose
-
-- Own the `backup_create.py` API endpoint.
-- This module handles backup create requests.
-- Keep this file-level DOX profile synchronized with `backup_create.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `backup_create.py` owns the runtime implementation.
-- `backup_create.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `BackupCreate` (`ApiHandler`)
- - `requires_auth(cls) -> bool`
- - `requires_loopback(cls) -> bool`
- - `async process(self, input: dict, request: Request) -> dict | Response`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `BackupCreate` is an `ApiHandler`.
-- `BackupCreate` defines `process(...)`.
-- `BackupCreate` defines `requires_auth(...)`.
-- `BackupCreate` defines `requires_loopback(...)`.
-- Observed side-effect areas: filesystem writes.
-- Imported dependency areas include: `helpers.api`, `helpers.backup`, `helpers.persist_chat`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `save_tmp_chats`, `BackupService`, `send_file`, `backup_service.create_backup`, `line.strip`, `line.startswith`, `patterns_string.split`, `line.strip.startswith`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- Related tests observed by source search:
- - `tests/test_download_toast_regressions.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/backup_get_defaults.py.dox.md b/api/backup_get_defaults.py.dox.md
deleted file mode 100644
index 8f2c1966d..000000000
--- a/api/backup_get_defaults.py.dox.md
+++ /dev/null
@@ -1,47 +0,0 @@
-# backup_get_defaults.py DOX
-
-## Purpose
-
-- Own the `backup_get_defaults.py` API endpoint.
-- This module handles backup get defaults requests.
-- Keep this file-level DOX profile synchronized with `backup_get_defaults.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `backup_get_defaults.py` owns the runtime implementation.
-- `backup_get_defaults.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `BackupGetDefaults` (`ApiHandler`)
- - `requires_auth(cls) -> bool`
- - `requires_loopback(cls) -> bool`
- - `async process(self, input: dict, request: Request) -> dict | Response`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `BackupGetDefaults` is an `ApiHandler`.
-- `BackupGetDefaults` defines `process(...)`.
-- `BackupGetDefaults` defines `requires_auth(...)`.
-- `BackupGetDefaults` defines `requires_loopback(...)`.
-- Imported dependency areas include: `helpers.api`, `helpers.backup`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `BackupService`, `backup_service.get_default_backup_metadata`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/backup_inspect.py.dox.md b/api/backup_inspect.py.dox.md
deleted file mode 100644
index 363dd63dc..000000000
--- a/api/backup_inspect.py.dox.md
+++ /dev/null
@@ -1,47 +0,0 @@
-# backup_inspect.py DOX
-
-## Purpose
-
-- Own the `backup_inspect.py` API endpoint.
-- This module handles backup inspect requests.
-- Keep this file-level DOX profile synchronized with `backup_inspect.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `backup_inspect.py` owns the runtime implementation.
-- `backup_inspect.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `BackupInspect` (`ApiHandler`)
- - `requires_auth(cls) -> bool`
- - `requires_loopback(cls) -> bool`
- - `async process(self, input: dict, request: Request) -> dict | Response`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `BackupInspect` is an `ApiHandler`.
-- `BackupInspect` defines `process(...)`.
-- `BackupInspect` defines `requires_auth(...)`.
-- `BackupInspect` defines `requires_loopback(...)`.
-- Imported dependency areas include: `helpers.api`, `helpers.backup`, `werkzeug.datastructures`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `BackupService`, `backup_service.inspect_backup`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/backup_preview_grouped.py.dox.md b/api/backup_preview_grouped.py.dox.md
deleted file mode 100644
index 24e075981..000000000
--- a/api/backup_preview_grouped.py.dox.md
+++ /dev/null
@@ -1,47 +0,0 @@
-# backup_preview_grouped.py DOX
-
-## Purpose
-
-- Own the `backup_preview_grouped.py` API endpoint.
-- This module handles backup preview grouped requests.
-- Keep this file-level DOX profile synchronized with `backup_preview_grouped.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `backup_preview_grouped.py` owns the runtime implementation.
-- `backup_preview_grouped.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `BackupPreviewGrouped` (`ApiHandler`)
- - `requires_auth(cls) -> bool`
- - `requires_loopback(cls) -> bool`
- - `async process(self, input: dict, request: Request) -> dict | Response`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `BackupPreviewGrouped` is an `ApiHandler`.
-- `BackupPreviewGrouped` defines `process(...)`.
-- `BackupPreviewGrouped` defines `requires_auth(...)`.
-- `BackupPreviewGrouped` defines `requires_loopback(...)`.
-- Imported dependency areas include: `helpers.api`, `helpers.backup`, `typing`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `BackupService`, `search_filter.strip`, `backup_service.test_patterns`, `search_filter.lower`, `path.strip.split`, `line.strip`, `line.startswith`, `groups.add`, `patterns_string.split`, `path.strip`, `join`, `f.lower`, `line.strip.startswith`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/backup_restore.py.dox.md b/api/backup_restore.py.dox.md
deleted file mode 100644
index 78e74737d..000000000
--- a/api/backup_restore.py.dox.md
+++ /dev/null
@@ -1,49 +0,0 @@
-# backup_restore.py DOX
-
-## Purpose
-
-- Own the `backup_restore.py` API endpoint.
-- This module handles backup restore requests.
-- Keep this file-level DOX profile synchronized with `backup_restore.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `backup_restore.py` owns the runtime implementation.
-- `backup_restore.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `BackupRestore` (`ApiHandler`)
- - `requires_auth(cls) -> bool`
- - `requires_loopback(cls) -> bool`
- - `async process(self, input: dict, request: Request) -> dict | Response`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `BackupRestore` is an `ApiHandler`.
-- `BackupRestore` defines `process(...)`.
-- `BackupRestore` defines `requires_auth(...)`.
-- `BackupRestore` defines `requires_loopback(...)`.
-- Observed side-effect areas: filesystem writes, filesystem deletion, settings/state persistence.
-- Imported dependency areas include: `helpers.api`, `helpers.backup`, `helpers.persist_chat`, `json`, `werkzeug.datastructures`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `request.form.get.lower`, `json.loads`, `BackupService`, `load_tmp_chats`, `backup_service.restore_backup`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- Related tests observed by source search:
- - `tests/test_self_update_tag_filter.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/backup_restore_preview.py.dox.md b/api/backup_restore_preview.py.dox.md
deleted file mode 100644
index 572473a0a..000000000
--- a/api/backup_restore_preview.py.dox.md
+++ /dev/null
@@ -1,48 +0,0 @@
-# backup_restore_preview.py DOX
-
-## Purpose
-
-- Own the `backup_restore_preview.py` API endpoint.
-- This module handles backup restore preview requests.
-- Keep this file-level DOX profile synchronized with `backup_restore_preview.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `backup_restore_preview.py` owns the runtime implementation.
-- `backup_restore_preview.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `BackupRestorePreview` (`ApiHandler`)
- - `requires_auth(cls) -> bool`
- - `requires_loopback(cls) -> bool`
- - `async process(self, input: dict, request: Request) -> dict | Response`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `BackupRestorePreview` is an `ApiHandler`.
-- `BackupRestorePreview` defines `process(...)`.
-- `BackupRestorePreview` defines `requires_auth(...)`.
-- `BackupRestorePreview` defines `requires_loopback(...)`.
-- Observed side-effect areas: filesystem deletion, settings/state persistence.
-- Imported dependency areas include: `helpers.api`, `helpers.backup`, `json`, `werkzeug.datastructures`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `request.form.get.lower`, `json.loads`, `BackupService`, `backup_service.preview_restore`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/backup_test.py b/api/backup_test.py
index c7a748ebd..b5b43eac8 100644
--- a/api/backup_test.py
+++ b/api/backup_test.py
@@ -47,13 +47,12 @@ class BackupTest(ApiHandler):
backup_service = BackupService()
matched_files = await backup_service.test_patterns(metadata, max_files=max_files)
- truncated = max_files is not None and len(matched_files) >= max_files
return {
"success": True,
"files": matched_files,
"total_count": len(matched_files),
- "truncated": truncated
+ "truncated": len(matched_files) >= max_files
}
except Exception as e:
diff --git a/api/backup_test.py.dox.md b/api/backup_test.py.dox.md
deleted file mode 100644
index a8e06cc6b..000000000
--- a/api/backup_test.py.dox.md
+++ /dev/null
@@ -1,48 +0,0 @@
-# backup_test.py DOX
-
-## Purpose
-
-- Own the `backup_test.py` API endpoint.
-- This module handles backup test requests.
-- Keep this file-level DOX profile synchronized with `backup_test.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `backup_test.py` owns the runtime implementation.
-- `backup_test.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `BackupTest` (`ApiHandler`)
- - `requires_auth(cls) -> bool`
- - `requires_loopback(cls) -> bool`
- - `async process(self, input: dict, request: Request) -> dict | Response`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `BackupTest` is an `ApiHandler`.
-- `BackupTest` defines `process(...)`.
-- `BackupTest` defines `requires_auth(...)`.
-- `BackupTest` defines `requires_loopback(...)`.
-- Imported dependency areas include: `helpers.api`, `helpers.backup`.
-- The `truncated` response flag is true only when a finite `max_files` limit is supplied and the result reaches that limit.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `BackupService`, `backup_service.test_patterns`, `line.strip`, `line.startswith`, `patterns_string.split`, `line.strip.startswith`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/banners.py.dox.md b/api/banners.py.dox.md
deleted file mode 100644
index 9d007e7d5..000000000
--- a/api/banners.py.dox.md
+++ /dev/null
@@ -1,46 +0,0 @@
-# banners.py DOX
-
-## Purpose
-
-- Own the `banners.py` API endpoint.
-- This module collects alert banners and discovery cards from backend extensions.
-- Keep this file-level DOX profile synchronized with `banners.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `banners.py` owns the runtime implementation.
-- `banners.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `GetBanners` (`ApiHandler`)
- - `async process(self, input: dict, request: Request) -> dict | Response`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `GetBanners` is an `ApiHandler`.
-- `GetBanners` defines `process(...)`.
-- Imported dependency areas include: `helpers.api`, `helpers.extension`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `call_extensions_async`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- Related tests observed by source search:
- - `tests/test_model_config_api_keys.py`
- - `tests/test_oauth_static.py`
- - `tests/test_webui_extension_surfaces.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/cache_reset.py.dox.md b/api/cache_reset.py.dox.md
deleted file mode 100644
index a7164d5d5..000000000
--- a/api/cache_reset.py.dox.md
+++ /dev/null
@@ -1,53 +0,0 @@
-# cache_reset.py DOX
-
-## Purpose
-
-- Own the `cache_reset.py` API endpoint.
-- This module handles cache reset API requests.
-- Keep this file-level DOX profile synchronized with `cache_reset.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `cache_reset.py` owns the runtime implementation.
-- `cache_reset.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `CacheReset` (`ApiHandler`)
- - `requires_auth(cls) -> bool`
- - `requires_csrf(cls) -> bool`
- - `requires_api_key(cls) -> bool`
- - `requires_loopback(cls) -> bool`
- - `get_methods(cls) -> list[str]`
- - `async process(self, input: dict, request: Request) -> dict | Response`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `CacheReset` is an `ApiHandler`.
-- `CacheReset` defines `process(...)`.
-- `CacheReset` defines `get_methods(...)`.
-- `CacheReset` defines `requires_auth(...)`.
-- `CacheReset` defines `requires_csrf(...)`.
-- `CacheReset` defines `requires_api_key(...)`.
-- `CacheReset` defines `requires_loopback(...)`.
-- Imported dependency areas include: `helpers`, `helpers.api`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `cache.clear_all`, `cache.clear`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/chat_create.py.dox.md b/api/chat_create.py.dox.md
deleted file mode 100644
index 3546f4389..000000000
--- a/api/chat_create.py.dox.md
+++ /dev/null
@@ -1,45 +0,0 @@
-# chat_create.py DOX
-
-## Purpose
-
-- Own the `chat_create.py` API endpoint.
-- This module handles chat create requests.
-- Keep this file-level DOX profile synchronized with `chat_create.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `chat_create.py` owns the runtime implementation.
-- `chat_create.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `CreateChat` (`ApiHandler`)
- - `async process(self, input: Input, request: Request) -> Output`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `CreateChat` is an `ApiHandler`.
-- `CreateChat` defines `process(...)`.
-- Observed side-effect areas: filesystem writes, model calls, plugin state, settings/state persistence.
-- Imported dependency areas include: `agent`, `helpers`, `helpers.api`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `self.use_context`, `mark_dirty_all`, `guids.generate_id`, `current_context.get_data`, `current_context.get_output_data`, `new_context.set_data`, `new_context.set_output_data`, `is_chat_override_allowed`, `settings.get_settings`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- Related tests observed by source search:
- - `tests/test_browser_agent_regressions.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/chat_export.py.dox.md b/api/chat_export.py.dox.md
deleted file mode 100644
index 03b5a0c91..000000000
--- a/api/chat_export.py.dox.md
+++ /dev/null
@@ -1,44 +0,0 @@
-# chat_export.py DOX
-
-## Purpose
-
-- Own the `chat_export.py` API endpoint.
-- This module handles chat export requests.
-- Keep this file-level DOX profile synchronized with `chat_export.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `chat_export.py` owns the runtime implementation.
-- `chat_export.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `ExportChat` (`ApiHandler`)
- - `async process(self, input: Input, request: Request) -> Output`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `ExportChat` is an `ApiHandler`.
-- `ExportChat` defines `process(...)`.
-- Observed side-effect areas: filesystem writes.
-- Imported dependency areas include: `helpers`, `helpers.api`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `self.use_context`, `persist_chat.export_json_chat`, `Exception`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/chat_files_path_get.py.dox.md b/api/chat_files_path_get.py.dox.md
deleted file mode 100644
index 63f0a2b6e..000000000
--- a/api/chat_files_path_get.py.dox.md
+++ /dev/null
@@ -1,44 +0,0 @@
-# chat_files_path_get.py DOX
-
-## Purpose
-
-- Own the `chat_files_path_get.py` API endpoint.
-- This module handles chat files path get requests.
-- Keep this file-level DOX profile synchronized with `chat_files_path_get.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `chat_files_path_get.py` owns the runtime implementation.
-- `chat_files_path_get.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `GetChatFilesPath` (`ApiHandler`)
- - `async process(self, input: dict, request: Request) -> dict | Response`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `GetChatFilesPath` is an `ApiHandler`.
-- `GetChatFilesPath` defines `process(...)`.
-- Observed side-effect areas: settings/state persistence.
-- Imported dependency areas include: `helpers`, `helpers.api`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `self.use_context`, `projects.get_context_project_name`, `Exception`, `files.normalize_a0_path`, `projects.get_project_folder`, `settings.get_settings`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/chat_load.py.dox.md b/api/chat_load.py.dox.md
deleted file mode 100644
index 4718a9fc6..000000000
--- a/api/chat_load.py.dox.md
+++ /dev/null
@@ -1,44 +0,0 @@
-# chat_load.py DOX
-
-## Purpose
-
-- Own the `chat_load.py` API endpoint.
-- This module handles chat load requests.
-- Keep this file-level DOX profile synchronized with `chat_load.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `chat_load.py` owns the runtime implementation.
-- `chat_load.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `LoadChats` (`ApiHandler`)
- - `async process(self, input: Input, request: Request) -> Output`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `LoadChats` is an `ApiHandler`.
-- `LoadChats` defines `process(...)`.
-- Observed side-effect areas: filesystem writes.
-- Imported dependency areas include: `helpers`, `helpers.api`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `persist_chat.load_json_chats`, `Exception`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/chat_remove.py.dox.md b/api/chat_remove.py.dox.md
deleted file mode 100644
index ac1ffc0f8..000000000
--- a/api/chat_remove.py.dox.md
+++ /dev/null
@@ -1,44 +0,0 @@
-# chat_remove.py DOX
-
-## Purpose
-
-- Own the `chat_remove.py` API endpoint.
-- This module handles chat remove requests.
-- Keep this file-level DOX profile synchronized with `chat_remove.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `chat_remove.py` owns the runtime implementation.
-- `chat_remove.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `RemoveChat` (`ApiHandler`)
- - `async process(self, input: Input, request: Request) -> Output`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `RemoveChat` is an `ApiHandler`.
-- `RemoveChat` defines `process(...)`.
-- Observed side-effect areas: filesystem writes, filesystem deletion, settings/state persistence, scheduler state.
-- Imported dependency areas include: `agent`, `helpers`, `helpers.api`, `helpers.task_scheduler`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `scheduler.cancel_tasks_by_context`, `AgentContext.use`, `AgentContext.remove`, `persist_chat.remove_chat`, `scheduler.get_tasks_by_context_id`, `mark_dirty_all`, `context.reset`, `scheduler.reload`, `scheduler.remove_task_by_uuid`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/chat_reset.py.dox.md b/api/chat_reset.py.dox.md
deleted file mode 100644
index 39a886a53..000000000
--- a/api/chat_reset.py.dox.md
+++ /dev/null
@@ -1,44 +0,0 @@
-# chat_reset.py DOX
-
-## Purpose
-
-- Own the `chat_reset.py` API endpoint.
-- This module handles chat reset requests.
-- Keep this file-level DOX profile synchronized with `chat_reset.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `chat_reset.py` owns the runtime implementation.
-- `chat_reset.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `Reset` (`ApiHandler`)
- - `async process(self, input: Input, request: Request) -> Output`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `Reset` is an `ApiHandler`.
-- `Reset` defines `process(...)`.
-- Observed side-effect areas: filesystem writes, filesystem deletion, settings/state persistence, scheduler state.
-- Imported dependency areas include: `helpers`, `helpers.api`, `helpers.task_scheduler`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `TaskScheduler.get.cancel_tasks_by_context`, `self.use_context`, `context.reset`, `persist_chat.save_tmp_chat`, `persist_chat.remove_msg_files`, `mark_dirty_all`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/csrf_token.py b/api/csrf_token.py
index 6cbd355c0..5f4ba13d5 100644
--- a/api/csrf_token.py
+++ b/api/csrf_token.py
@@ -1,4 +1,5 @@
import secrets
+from urllib.parse import urlparse
from helpers.api import (
ApiHandler,
Input,
@@ -8,7 +9,6 @@ from helpers.api import (
session,
)
from helpers import runtime, dotenv, login
-from helpers.tunnel_origins import origin_from_url
import fnmatch
ALLOWED_ORIGINS_KEY = "ALLOWED_ORIGINS"
@@ -82,7 +82,11 @@ class GetCsrfToken(ApiHandler):
)
if not r:
return None
- return origin_from_url(r)
+ # parse and normalize
+ p = urlparse(r)
+ if not p.scheme or not p.hostname:
+ return None
+ return f"{p.scheme}://{p.hostname}" + (f":{p.port}" if p.port else "")
async def get_allowed_origins(self) -> list[str]:
# get the allowed origins from the environment
@@ -103,10 +107,8 @@ class GetCsrfToken(ApiHandler):
from api.tunnel_proxy import process as tunnel_api_process
tunnel = await tunnel_api_process({"action": "get"})
- if tunnel and isinstance(tunnel, dict) and tunnel.get("success"):
- tunnel_origin = origin_from_url(tunnel.get("tunnel_url"))
- if tunnel_origin:
- allowed_origins.append(tunnel_origin)
+ if tunnel and isinstance(tunnel, dict) and tunnel["success"]:
+ allowed_origins.append(tunnel["tunnel_url"])
except Exception:
pass
diff --git a/api/csrf_token.py.dox.md b/api/csrf_token.py.dox.md
deleted file mode 100644
index 3570d2471..000000000
--- a/api/csrf_token.py.dox.md
+++ /dev/null
@@ -1,57 +0,0 @@
-# csrf_token.py DOX
-
-## Purpose
-
-- Own the `csrf_token.py` API endpoint.
-- This module issues or refreshes CSRF tokens for browser API clients.
-- Keep this file-level DOX profile synchronized with `csrf_token.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `csrf_token.py` owns the runtime implementation.
-- `csrf_token.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `GetCsrfToken` (`ApiHandler`)
- - `get_methods(cls) -> list[str]`
- - `requires_csrf(cls) -> bool`
- - `async process(self, input: Input, request: Request) -> Output`
- - `async check_allowed_origin(self, request: Request)`
- - `async is_allowed_origin(self, request: Request)`
- - `get_origin_from_request(self, request: Request)`
- - `async get_allowed_origins(self) -> list[str]`
- - `get_default_allowed_origins(self) -> list[str]`
-- Notable constants/configuration names: `ALLOWED_ORIGINS_KEY`.
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `GetCsrfToken` is an `ApiHandler`.
-- `GetCsrfToken` defines `process(...)`.
-- `GetCsrfToken` defines `get_methods(...)`.
-- `GetCsrfToken` defines `requires_csrf(...)`.
-- Observed side-effect areas: filesystem writes, network calls, secret handling, tunnel state.
-- Imported dependency areas include: `fnmatch`, `helpers`, `helpers.api`, `secrets`, `urllib.parse`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `login.is_login_required`, `self.initialize_allowed_origins`, `self.get_origin_from_request`, `urlparse`, `dotenv.get_dotenv_value`, `self.get_default_allowed_origins`, `dotenv.save_dotenv_value`, `self.check_allowed_origin`, `secrets.token_urlsafe`, `runtime.get_runtime_id`, `self.is_allowed_origin`, `self.get_allowed_origins`, `origin.strip`, `join`, `fnmatch.fnmatch`, `split`, `tunnel_api_process`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- Related tests observed by source search:
- - `tests/test_http_auth_csrf.py`
- - `tests/test_self_update_tag_filter.py`
- - `tests/test_ws_security.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/ctx_window_get.py.dox.md b/api/ctx_window_get.py.dox.md
deleted file mode 100644
index 2de01bf37..000000000
--- a/api/ctx_window_get.py.dox.md
+++ /dev/null
@@ -1,44 +0,0 @@
-# ctx_window_get.py DOX
-
-## Purpose
-
-- Own the `ctx_window_get.py` API endpoint.
-- This module handles ctx window get API requests.
-- Keep this file-level DOX profile synchronized with `ctx_window_get.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `ctx_window_get.py` owns the runtime implementation.
-- `ctx_window_get.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `GetCtxWindow` (`ApiHandler`)
- - `async process(self, input: Input, request: Request) -> Output`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `GetCtxWindow` is an `ApiHandler`.
-- `GetCtxWindow` defines `process(...)`.
-- Observed side-effect areas: secret handling.
-- Imported dependency areas include: `helpers`, `helpers.api`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `self.use_context`, `agent.get_data`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/delete_work_dir_file.py.dox.md b/api/delete_work_dir_file.py.dox.md
deleted file mode 100644
index 536b70fcb..000000000
--- a/api/delete_work_dir_file.py.dox.md
+++ /dev/null
@@ -1,46 +0,0 @@
-# delete_work_dir_file.py DOX
-
-## Purpose
-
-- Own the `delete_work_dir_file.py` API endpoint.
-- This module handles workdir file operations for delete work dir file.
-- Keep this file-level DOX profile synchronized with `delete_work_dir_file.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `delete_work_dir_file.py` owns the runtime implementation.
-- `delete_work_dir_file.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `DeleteWorkDirFile` (`ApiHandler`)
- - `async process(self, input: Input, request: Request) -> Output`
-- Top-level functions:
-- `async delete_file(file_path: str)`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `DeleteWorkDirFile` is an `ApiHandler`.
-- `DeleteWorkDirFile` defines `process(...)`.
-- Observed side-effect areas: filesystem deletion.
-- Imported dependency areas include: `api`, `helpers`, `helpers.api`, `helpers.file_browser`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `FileBrowser`, `browser.delete_file`, `file_path.startswith`, `runtime.call_development_function`, `extension.call_extensions_async`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/delete_work_dir_files.py.dox.md b/api/delete_work_dir_files.py.dox.md
deleted file mode 100644
index 2c68d3ca2..000000000
--- a/api/delete_work_dir_files.py.dox.md
+++ /dev/null
@@ -1,47 +0,0 @@
-# delete_work_dir_files.py DOX
-
-## Purpose
-
-- Own the `delete_work_dir_files.py` API endpoint.
-- This module handles workdir file operations for delete work dir files.
-- Keep this file-level DOX profile synchronized with `delete_work_dir_files.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `delete_work_dir_files.py` owns the runtime implementation.
-- `delete_work_dir_files.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `DeleteWorkDirFiles` (`ApiHandler`)
- - `async process(self, input: Input, request: Request) -> Output`
-- Top-level functions:
-- `async delete_files(paths: list[str]) -> dict`
-- `collapse_nested_paths(paths: list[str]) -> list[str]`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `DeleteWorkDirFiles` is an `ApiHandler`.
-- `DeleteWorkDirFiles` defines `process(...)`.
-- Observed side-effect areas: filesystem deletion.
-- Imported dependency areas include: `api`, `api.download_work_dir_files`, `helpers`, `helpers.api`, `helpers.file_browser`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `FileBrowser`, `collapse_nested_paths`, `browser.delete_file`, `normalize_paths`, `runtime.call_development_function`, `path.strip`, `extension.call_extensions_async`, `item.count`, `clean_path.startswith`, `parent.rstrip`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/download_work_dir_file.py.dox.md b/api/download_work_dir_file.py.dox.md
deleted file mode 100644
index 87085765a..000000000
--- a/api/download_work_dir_file.py.dox.md
+++ /dev/null
@@ -1,53 +0,0 @@
-# download_work_dir_file.py DOX
-
-## Purpose
-
-- Own the `download_work_dir_file.py` API endpoint.
-- This module handles workdir file operations for download work dir file.
-- Keep this file-level DOX profile synchronized with `download_work_dir_file.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `download_work_dir_file.py` owns the runtime implementation.
-- `download_work_dir_file.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `DownloadFile` (`ApiHandler`)
- - `get_methods(cls)`
- - `async process(self, input: Input, request: Request) -> Output`
-- Top-level functions:
-- `stream_file_download(file_source, download_name, chunk_size=...)`: Create a streaming response for file downloads that shows progress in browser.
-- `make_disposition(download_name: str) -> str`
-- `resolve_download_path(path: str) -> str`: Resolve a requested download path and keep it within the runtime base dir.
-- `async fetch_file(path)`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `DownloadFile` is an `ApiHandler`.
-- `DownloadFile` defines `process(...)`.
-- `DownloadFile` defines `get_methods(...)`.
-- Observed side-effect areas: filesystem reads, network calls.
-- Imported dependency areas include: `api`, `base64`, `flask`, `helpers`, `helpers.api`, `io`, `mimetypes`, `os`, `pathlib`, `urllib.parse`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `mimetypes.guess_type`, `Response`, `quote`, `Path.resolve`, `Path`, `candidate.is_absolute`, `os.path.getsize`, `generate`, `download_name.encode.decode`, `candidate.resolve`, `resolve`, `resolved.relative_to`, `Exception`, `file.read`, `base64.b64encode.decode`, `file_source.tell`, `file_source.seek`, `ValueError`, `file_path.startswith`, `runtime.call_development_function`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- Related tests observed by source search:
- - `tests/test_download_toast_regressions.py`
- - `tests/test_office_canvas_setup.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/download_work_dir_files.py.dox.md b/api/download_work_dir_files.py.dox.md
deleted file mode 100644
index 478669fa8..000000000
--- a/api/download_work_dir_files.py.dox.md
+++ /dev/null
@@ -1,54 +0,0 @@
-# download_work_dir_files.py DOX
-
-## Purpose
-
-- Own the `download_work_dir_files.py` API endpoint.
-- This module handles workdir file operations for download work dir files.
-- Keep this file-level DOX profile synchronized with `download_work_dir_files.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `download_work_dir_files.py` owns the runtime implementation.
-- `download_work_dir_files.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `DownloadFiles` (`ApiHandler`)
- - `async process(self, input: Input, request: Request) -> Output`
-- Top-level functions:
-- `normalize_paths(paths) -> list[str]`
-- `selected_archive_name(count: int) -> str`
-- `create_selected_zip(paths: list[str], current_path: str=...) -> str`
-- `resolve_download_path(path: str, base_dir: Path) -> Path`
-- `collapse_nested_paths(paths: list[Path]) -> list[Path]`
-- `archive_root_name(source_path: Path, current_dir: Path | None, base_dir: Path) -> str`
-- `unique_archive_name(name: str, used_names: set[str]) -> str`
-- `write_zip_entry(zip_file: zipfile.ZipFile, source_path: Path, arc_root: str) -> None`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `DownloadFiles` is an `ApiHandler`.
-- `DownloadFiles` defines `process(...)`.
-- Observed side-effect areas: filesystem reads, filesystem writes, filesystem deletion.
-- Imported dependency areas include: `api.download_work_dir_file`, `base64`, `flask`, `helpers`, `helpers.api`, `helpers.localization`, `io`, `os`, `pathlib`, `tempfile`, `zipfile`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `Localization.get.now.strftime`, `Path.resolve`, `normalize_paths`, `collapse_nested_paths`, `Path`, `os.path.splitext`, `source_path.is_dir`, `zip_file.write`, `selected_archive_name`, `runtime.is_development`, `stream_file_download`, `ValueError`, `raw_path.strip`, `resolve_download_path`, `current_dir.is_file`, `resolved.exists`, `FileNotFoundError`, `tempfile.NamedTemporaryFile`, `zipfile.ZipFile`, `candidate.is_absolute`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- Related tests observed by source search:
- - `tests/test_download_toast_regressions.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/edit_work_dir_file.py.dox.md b/api/edit_work_dir_file.py.dox.md
deleted file mode 100644
index e5615517b..000000000
--- a/api/edit_work_dir_file.py.dox.md
+++ /dev/null
@@ -1,50 +0,0 @@
-# edit_work_dir_file.py DOX
-
-## Purpose
-
-- Own the `edit_work_dir_file.py` API endpoint.
-- This module handles workdir file operations for edit work dir file.
-- Keep this file-level DOX profile synchronized with `edit_work_dir_file.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `edit_work_dir_file.py` owns the runtime implementation.
-- `edit_work_dir_file.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `EditWorkDirFile` (`ApiHandler`)
- - `get_methods(cls)`
- - `async process(self, input: Input, request: Request) -> Output`
-- Top-level functions:
-- `async load_file(file_path: str) -> dict`
-- `save_file(file_path: str, content: str) -> bool`
-- Notable constants/configuration names: `MAX_EDIT_FILE_SIZE`, `BINARY_SAMPLE_SIZE`.
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `EditWorkDirFile` is an `ApiHandler`.
-- `EditWorkDirFile` defines `process(...)`.
-- `EditWorkDirFile` defines `get_methods(...)`.
-- Observed side-effect areas: filesystem reads, filesystem writes.
-- Imported dependency areas include: `helpers`, `helpers.api`, `helpers.file_browser`, `mimetypes`, `os`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `FileBrowser`, `browser.get_full_path`, `os.path.isdir`, `os.path.getsize`, `files.is_probably_binary_file`, `mimetypes.guess_type`, `browser.save_text_file`, `error_str.strip`, `Exception`, `os.path.basename`, `error_str.split`, `file.read`, `line.split.strip`, `file_path.startswith`, `content.encode`, `runtime.call_development_function`, `extension.call_extensions_async`, `self._extract_error_message`, `line.split`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/file_info.py.dox.md b/api/file_info.py.dox.md
deleted file mode 100644
index e3612589d..000000000
--- a/api/file_info.py.dox.md
+++ /dev/null
@@ -1,47 +0,0 @@
-# file_info.py DOX
-
-## Purpose
-
-- Own the `file_info.py` API endpoint.
-- This module handles file info API requests.
-- Keep this file-level DOX profile synchronized with `file_info.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `file_info.py` owns the runtime implementation.
-- `file_info.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `FileInfoApi` (`ApiHandler`)
- - `async process(self, input: Input, request: Request) -> Output`
-- `FileInfo` (`TypedDict`)
-- Top-level functions:
-- `async get_file_info(path: str) -> FileInfo`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `FileInfoApi` is an `ApiHandler`.
-- `FileInfoApi` defines `process(...)`.
-- Observed side-effect areas: filesystem reads.
-- Imported dependency areas include: `helpers`, `helpers.api`, `os`, `typing`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `files.get_abs_path`, `os.path.exists`, `os.path.dirname`, `os.path.basename`, `runtime.call_development_function`, `os.path.isdir`, `os.path.isfile`, `os.path.islink`, `os.path.getsize`, `os.path.getmtime`, `os.path.getctime`, `os.path.splitext`, `os.stat`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/get_work_dir_files.py b/api/get_work_dir_files.py
index 68aee4990..7e8e99c7f 100644
--- a/api/get_work_dir_files.py
+++ b/api/get_work_dir_files.py
@@ -9,7 +9,7 @@ class GetWorkDirFiles(ApiHandler):
return ["GET"]
async def process(self, input: dict, request: Request) -> dict | Response:
- current_path = request.args.get("path", "") or "$WORK_DIR"
+ current_path = request.args.get("path", "")
if current_path == "$WORK_DIR":
# if runtime.is_development():
# current_path = "work_dir"
diff --git a/api/get_work_dir_files.py.dox.md b/api/get_work_dir_files.py.dox.md
deleted file mode 100644
index f5ff3e005..000000000
--- a/api/get_work_dir_files.py.dox.md
+++ /dev/null
@@ -1,48 +0,0 @@
-# get_work_dir_files.py DOX
-
-## Purpose
-
-- Own the `get_work_dir_files.py` API endpoint.
-- This module handles workdir file operations for get work dir files.
-- Keep this file-level DOX profile synchronized with `get_work_dir_files.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `get_work_dir_files.py` owns the runtime implementation.
-- `get_work_dir_files.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `GetWorkDirFiles` (`ApiHandler`)
- - `get_methods(cls)`
- - `async process(self, input: dict, request: Request) -> dict | Response`
-- Top-level functions:
-- `async get_files(path)`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `GetWorkDirFiles` is an `ApiHandler`.
-- `GetWorkDirFiles` defines `process(...)`.
-- `GetWorkDirFiles` defines `get_methods(...)`.
-- Imported dependency areas include: `helpers`, `helpers.api`, `helpers.file_browser`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `FileBrowser`, `browser.get_files`, `runtime.call_development_function`.
-- Empty `path` requests and explicit `$WORK_DIR` requests resolve to the default workdir path before `FileBrowser` is called, so the WebUI never receives an empty startup path for the default file browser view.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/health.py.dox.md b/api/health.py.dox.md
deleted file mode 100644
index 236812f56..000000000
--- a/api/health.py.dox.md
+++ /dev/null
@@ -1,52 +0,0 @@
-# health.py DOX
-
-## Purpose
-
-- Own the `health.py` API endpoint.
-- This module reports process health for probes and startup checks.
-- Keep this file-level DOX profile synchronized with `health.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `health.py` owns the runtime implementation.
-- `health.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `HealthCheck` (`ApiHandler`)
- - `requires_auth(cls) -> bool`
- - `requires_csrf(cls) -> bool`
- - `get_methods(cls) -> list[str]`
- - `async process(self, input: dict, request: Request) -> dict | Response`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `HealthCheck` is an `ApiHandler`.
-- `HealthCheck` defines `process(...)`.
-- `HealthCheck` defines `get_methods(...)`.
-- `HealthCheck` defines `requires_auth(...)`.
-- `HealthCheck` defines `requires_csrf(...)`.
-- Imported dependency areas include: `helpers`, `helpers.api`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `git.get_git_info`, `errors.error_text`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- Related tests observed by source search:
- - `tests/test_oauth_providers.py`
- - `tests/test_office_document_store.py`
- - `tests/test_self_update_tag_filter.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/history_get.py.dox.md b/api/history_get.py.dox.md
deleted file mode 100644
index 8f2f1ddca..000000000
--- a/api/history_get.py.dox.md
+++ /dev/null
@@ -1,44 +0,0 @@
-# history_get.py DOX
-
-## Purpose
-
-- Own the `history_get.py` API endpoint.
-- This module handles history get API requests.
-- Keep this file-level DOX profile synchronized with `history_get.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `history_get.py` owns the runtime implementation.
-- `history_get.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `GetHistory` (`ApiHandler`)
- - `async process(self, input: dict, request: Request) -> dict | Response`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `GetHistory` is an `ApiHandler`.
-- `GetHistory` defines `process(...)`.
-- Observed side-effect areas: secret handling.
-- Imported dependency areas include: `helpers.api`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `self.use_context`, `agent.history.output_text`, `agent.history.get_tokens`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/image_get.py.dox.md b/api/image_get.py.dox.md
deleted file mode 100644
index b9b0da35b..000000000
--- a/api/image_get.py.dox.md
+++ /dev/null
@@ -1,53 +0,0 @@
-# image_get.py DOX
-
-## Purpose
-
-- Own the `image_get.py` API endpoint.
-- This module serves allowed image references and fallback file-type icons.
-- Keep this file-level DOX profile synchronized with `image_get.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `image_get.py` owns the runtime implementation.
-- `image_get.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `ImageGet` (`ApiHandler`)
- - `get_methods(cls) -> list[str]`
- - `async process(self, input: dict, request: Request) -> dict | Response`
-- Top-level functions:
-- `_resolve_allowed_image_path(path: str) -> str`: Resolve a requested image path and keep it inside Agent Zero's base dir.
-- `_set_image_headers(response: Response, filename: str, file_ext: str) -> None`
-- `_send_file_type_icon(file_ext, filename=...)`: Return appropriate icon for file type
-- `_send_fallback_icon(icon_name)`: Return fallback icon from public directory
-- Notable constants/configuration names: `IMAGE_EXTENSIONS`, `SVG_EXTENSIONS`, `SVG_CONTENT_SECURITY_POLICY`.
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `ImageGet` is an `ApiHandler`.
-- `ImageGet` defines `process(...)`.
-- `ImageGet` defines `get_methods(...)`.
-- Observed side-effect areas: filesystem reads, network calls, subprocess/runtime control, settings/state persistence.
-- Imported dependency areas include: `base64`, `helpers`, `helpers.api`, `io`, `mimetypes`, `os`, `pathlib`, `urllib.parse`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `runtime.is_development`, `Path.resolve`, `candidate.resolve`, `quote`, `_send_fallback_icon`, `files.get_abs_path`, `send_file`, `os.path.splitext.lower`, `os.path.basename`, `Path`, `candidate.is_absolute`, `resolved.relative_to`, `os.path.exists`, `ValueError`, `_set_image_headers`, `_send_file_type_icon`, `files.fix_dev_path`, `_resolve_allowed_image_path`, `files.exists`, `files.get_base_dir`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- Related tests observed by source search:
- - `tests/test_image_get_security.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/load_webui_extensions.py.dox.md b/api/load_webui_extensions.py.dox.md
deleted file mode 100644
index c2d41e06a..000000000
--- a/api/load_webui_extensions.py.dox.md
+++ /dev/null
@@ -1,44 +0,0 @@
-# load_webui_extensions.py DOX
-
-## Purpose
-
-- Own the `load_webui_extensions.py` API endpoint.
-- This module returns frontend extension manifests/files for a WebUI extension point.
-- Keep this file-level DOX profile synchronized with `load_webui_extensions.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `load_webui_extensions.py` owns the runtime implementation.
-- `load_webui_extensions.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `LoadWebuiExtensions` (`ApiHandler`)
- - `async process(self, input: dict, request: Request) -> dict | Response`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `LoadWebuiExtensions` is an `ApiHandler`.
-- `LoadWebuiExtensions` defines `process(...)`.
-- Imported dependency areas include: `helpers`, `helpers.api`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `extension.get_webui_extensions`, `Response`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- Related tests observed by source search:
- - `tests/test_webui_extension_surfaces.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/logout.py.dox.md b/api/logout.py.dox.md
deleted file mode 100644
index e125a43e7..000000000
--- a/api/logout.py.dox.md
+++ /dev/null
@@ -1,47 +0,0 @@
-# logout.py DOX
-
-## Purpose
-
-- Own the `logout.py` API endpoint.
-- This module clears login/session state for the current client.
-- Keep this file-level DOX profile synchronized with `logout.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `logout.py` owns the runtime implementation.
-- `logout.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `ApiLogout` (`ApiHandler`)
- - `requires_auth(cls) -> bool`
- - `async process(self, input: dict, request: Request) -> dict`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `ApiLogout` is an `ApiHandler`.
-- `ApiLogout` defines `process(...)`.
-- `ApiLogout` defines `requires_auth(...)`.
-- Observed side-effect areas: secret handling.
-- Imported dependency areas include: `helpers.api`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `session.clear`, `session.pop`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- Related tests observed by source search:
- - `tests/test_office_document_store.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/mcp_server_get_detail.py b/api/mcp_server_get_detail.py
index 25f865a41..bc5552c4d 100644
--- a/api/mcp_server_get_detail.py
+++ b/api/mcp_server_get_detail.py
@@ -9,11 +9,9 @@ class McpServerGetDetail(ApiHandler):
# try:
server_name = input.get("server_name")
- project_name = str(input.get("project_name", "") or "").strip()
if not server_name:
return {"success": False, "error": "Missing server_name"}
- config = MCPConfig.get_project_instance(project_name) if project_name else MCPConfig.get_instance()
- detail = config.get_server_detail(server_name)
+ detail = MCPConfig.get_instance().get_server_detail(server_name)
return {"success": True, "detail": detail}
# except Exception as e:
# return {"success": False, "error": str(e)}
diff --git a/api/mcp_server_get_detail.py.dox.md b/api/mcp_server_get_detail.py.dox.md
deleted file mode 100644
index d71ed2551..000000000
--- a/api/mcp_server_get_detail.py.dox.md
+++ /dev/null
@@ -1,45 +0,0 @@
-# mcp_server_get_detail.py DOX
-
-## Purpose
-
-- Own the `mcp_server_get_detail.py` API endpoint.
-- This module handles MCP server detail requests for global or project scope.
-- Keep this file-level DOX profile synchronized with `mcp_server_get_detail.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `mcp_server_get_detail.py` owns the runtime implementation.
-- `mcp_server_get_detail.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `McpServerGetDetail` (`ApiHandler`)
- - `async process(self, input: dict[Any, Any], request: Request) -> dict[Any, Any] | Response`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- The request accepts `server_name` and optional `project_name`; when `project_name` is present, detail resolves through the project-scoped MCP configuration.
-- Detail responses include the server tools visible to the manager UI. Tools disabled through a server `disabled_tools` config list remain present in this detail list with a `disabled` flag so the UI can re-enable them.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `McpServerGetDetail` is an `ApiHandler`.
-- `McpServerGetDetail` defines `process(...)`.
-- Imported dependency areas include: `helpers.api`, `helpers.mcp_handler`, `typing`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `MCPConfig.get_instance.get_server_detail`, `MCPConfig.get_project_instance`, `MCPConfig.get_instance`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/mcp_server_get_log.py b/api/mcp_server_get_log.py
index 89b53b58f..3305430ea 100644
--- a/api/mcp_server_get_log.py
+++ b/api/mcp_server_get_log.py
@@ -9,11 +9,9 @@ class McpServerGetLog(ApiHandler):
# try:
server_name = input.get("server_name")
- project_name = str(input.get("project_name", "") or "").strip()
if not server_name:
return {"success": False, "error": "Missing server_name"}
- config = MCPConfig.get_project_instance(project_name) if project_name else MCPConfig.get_instance()
- log = config.get_server_log(server_name)
+ log = MCPConfig.get_instance().get_server_log(server_name)
return {"success": True, "log": log}
# except Exception as e:
# return {"success": False, "error": str(e)}
diff --git a/api/mcp_server_get_log.py.dox.md b/api/mcp_server_get_log.py.dox.md
deleted file mode 100644
index 0937274a4..000000000
--- a/api/mcp_server_get_log.py.dox.md
+++ /dev/null
@@ -1,44 +0,0 @@
-# mcp_server_get_log.py DOX
-
-## Purpose
-
-- Own the `mcp_server_get_log.py` API endpoint.
-- This module handles MCP server log requests for global or project scope.
-- Keep this file-level DOX profile synchronized with `mcp_server_get_log.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `mcp_server_get_log.py` owns the runtime implementation.
-- `mcp_server_get_log.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `McpServerGetLog` (`ApiHandler`)
- - `async process(self, input: dict[Any, Any], request: Request) -> dict[Any, Any] | Response`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- The request accepts `server_name` and optional `project_name`; when `project_name` is present, logs resolve through the project-scoped MCP configuration.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `McpServerGetLog` is an `ApiHandler`.
-- `McpServerGetLog` defines `process(...)`.
-- Imported dependency areas include: `helpers.api`, `helpers.mcp_handler`, `typing`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `MCPConfig.get_instance.get_server_log`, `MCPConfig.get_project_instance`, `MCPConfig.get_instance`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/mcp_server_scan.py b/api/mcp_server_scan.py
deleted file mode 100644
index 4639426d3..000000000
--- a/api/mcp_server_scan.py
+++ /dev/null
@@ -1,232 +0,0 @@
-import asyncio
-from shutil import which
-from typing import Any
-from urllib.parse import urlparse
-
-from helpers.api import ApiHandler, Request, Response
-from helpers.mcp_handler import MCPConfig, normalize_name
-
-
-_PROMPT_INJECTION_MARKERS = (
- "ignore previous",
- "ignore all previous",
- "system prompt",
- "developer message",
- "hidden instruction",
- "exfiltrate",
- "leak secret",
- "credential",
-)
-
-
-class McpServerScan(ApiHandler):
- async def process(self, input: dict[Any, Any], request: Request) -> dict[Any, Any] | Response:
- server = dict(input.get("server") or {})
- allow_local_execution = bool(input.get("allow_local_execution", False))
- allow_remote_network = bool(input.get("allow_remote_network", False))
- inspect_runtime = input.get("inspect_runtime", True) is not False
-
- server = self._normalize_server(server)
- warnings = self._static_warnings(server)
- is_local = not (server.get("url") or server.get("serverUrl"))
- has_static_errors = any(warning.get("level") == "error" for warning in warnings)
-
- runtime_status: list[dict[str, Any]] = []
- runtime_detail: dict[str, Any] = {}
- runtime_error = ""
-
- should_inspect_runtime = (
- inspect_runtime
- and not has_static_errors
- and ((is_local and allow_local_execution) or (not is_local and allow_remote_network))
- )
-
- if should_inspect_runtime:
- try:
- scan_config = await asyncio.to_thread(
- lambda: MCPConfig(servers_list=[server], config_scope="scan")
- )
- runtime_status = scan_config.get_servers_status()
- runtime_detail = scan_config.get_server_detail(server.get("name", ""))
- warnings.extend(self._tool_warnings(runtime_detail.get("tools", [])))
- except Exception as exc:
- runtime_error = str(exc)
- warnings.append(
- {
- "level": "error",
- "title": "Runtime inspection failed",
- "message": runtime_error,
- }
- )
- elif is_local and inspect_runtime:
- warnings.append(
- {
- "level": "warning",
- "title": "Local command not executed",
- "message": "Local stdio MCP inspection requires explicit trust because it runs the configured command.",
- }
- )
- elif not is_local and inspect_runtime and has_static_errors:
- warnings.append(
- {
- "level": "info",
- "title": "Runtime inspection skipped",
- "message": "Fix static scan errors before attempting runtime MCP inspection.",
- }
- )
- elif not is_local and inspect_runtime:
- warnings.append(
- {
- "level": "info",
- "title": "Remote runtime inspection skipped",
- "message": "Enable trusted remote inspection to contact the MCP URL and list exposed tools.",
- }
- )
-
- return {
- "success": True,
- "server": self._redact_server(server),
- "risk_level": self._risk_level(warnings),
- "warnings": warnings,
- "status": runtime_status,
- "detail": runtime_detail,
- "runtime_error": runtime_error,
- }
-
- def _normalize_server(self, server: dict[str, Any]) -> dict[str, Any]:
- name = str(server.get("name") or "").strip()
- url = str(server.get("url") or server.get("serverUrl") or "").strip()
- command = str(server.get("command") or "").strip()
-
- if not name:
- name = self._derive_name(url, command)
- server["name"] = normalize_name(name or "mcp_server")
-
- if url:
- server["url"] = url
- server.setdefault("type", "streamable-http")
- elif command:
- server["command"] = command
- server["type"] = "stdio"
-
- return server
-
- def _derive_name(self, url: str, command: str) -> str:
- if url:
- parsed = urlparse(url)
- parts = [part for part in parsed.path.split("/") if part]
- return parts[-1] if parts else parsed.hostname or "remote_mcp"
- if command:
- return command.rsplit("/", 1)[-1]
- return "mcp_server"
-
- def _static_warnings(self, server: dict[str, Any]) -> list[dict[str, str]]:
- warnings: list[dict[str, str]] = []
- url = str(server.get("url") or "").strip()
- command = str(server.get("command") or "").strip()
-
- if url:
- parsed = urlparse(url)
- if parsed.scheme not in {"http", "https"}:
- warnings.append(
- {
- "level": "error",
- "title": "Unsupported URL scheme",
- "message": "Remote MCP URLs should use http or https.",
- }
- )
- elif parsed.scheme == "http" and parsed.hostname not in {"localhost", "127.0.0.1", "::1"}:
- warnings.append(
- {
- "level": "warning",
- "title": "Unencrypted remote URL",
- "message": "Prefer HTTPS for remote MCP servers outside localhost.",
- }
- )
- if not parsed.netloc:
- warnings.append(
- {
- "level": "error",
- "title": "Invalid remote URL",
- "message": "The remote MCP URL is missing a host.",
- }
- )
- elif command:
- if which(command) is None:
- warnings.append(
- {
- "level": "warning",
- "title": "Command not found",
- "message": f"'{command}' is not currently available on PATH.",
- }
- )
- if command in {"bash", "sh", "zsh", "fish", "python", "python3", "node"}:
- warnings.append(
- {
- "level": "warning",
- "title": "General-purpose interpreter",
- "message": "Review the command and arguments carefully before running this local MCP server.",
- }
- )
- else:
- warnings.append(
- {
- "level": "error",
- "title": "Missing connection target",
- "message": "Provide either a remote URL or a local command.",
- }
- )
-
- if isinstance(server.get("headers"), dict) and server["headers"]:
- warnings.append(
- {
- "level": "info",
- "title": "Headers configured",
- "message": "Header values are redacted in scan output. Keep tokens in trusted settings only.",
- }
- )
-
- if isinstance(server.get("env"), dict) and server["env"]:
- warnings.append(
- {
- "level": "info",
- "title": "Environment configured",
- "message": "Environment values are redacted in scan output. Avoid hardcoding secrets in MCP configs.",
- }
- )
-
- return warnings
-
- def _tool_warnings(self, tools: Any) -> list[dict[str, str]]:
- warnings: list[dict[str, str]] = []
- if not isinstance(tools, list):
- return warnings
-
- for tool in tools:
- if not isinstance(tool, dict):
- continue
- haystack = f"{tool.get('name', '')}\n{tool.get('description', '')}".lower()
- if any(marker in haystack for marker in _PROMPT_INJECTION_MARKERS):
- warnings.append(
- {
- "level": "warning",
- "title": "Suspicious tool description",
- "message": f"Review tool '{tool.get('name', 'unknown')}' for prompt-injection style language.",
- }
- )
- return warnings
-
- def _redact_server(self, server: dict[str, Any]) -> dict[str, Any]:
- redacted = dict(server)
- for key in ("headers", "env"):
- if isinstance(redacted.get(key), dict):
- redacted[key] = {name: "***" for name in redacted[key]}
- return redacted
-
- def _risk_level(self, warnings: list[dict[str, str]]) -> str:
- levels = {warning.get("level", "info") for warning in warnings}
- if "error" in levels:
- return "error"
- if "warning" in levels:
- return "warning"
- return "ok"
diff --git a/api/mcp_server_scan.py.dox.md b/api/mcp_server_scan.py.dox.md
deleted file mode 100644
index 5487106aa..000000000
--- a/api/mcp_server_scan.py.dox.md
+++ /dev/null
@@ -1,45 +0,0 @@
-# mcp_server_scan.py DOX
-
-## Purpose
-
-- Own the `mcp_server_scan.py` API endpoint.
-- Provide static and optional runtime inspection for a single MCP server draft before it is added to global or project MCP config.
-- Keep this file-level DOX profile synchronized with `mcp_server_scan.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `mcp_server_scan.py` owns the runtime implementation.
-- `mcp_server_scan.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `McpServerScan` (`ApiHandler`)
- - `async process(self, input: dict[Any, Any], request: Request) -> dict[Any, Any] | Response`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- The request accepts a `server` draft object, `inspect_runtime`, `allow_remote_network`, and `allow_local_execution`.
-- Remote runtime inspection may contact the configured MCP URL to list tools only when `allow_remote_network` is true and static checks have no errors.
-- Local stdio runtime inspection must not execute unless `allow_local_execution` is true.
-- Response data redacts `headers` and `env` values.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- Imported dependency areas include: `asyncio`, `helpers.api`, `helpers.mcp_handler`, `shutil`, `typing`, `urllib.parse`.
-
-## Key Concepts
-
-- Static checks report invalid URLs, non-HTTPS remote URLs, missing local commands, interpreter-style local commands, headers/env presence, and obvious prompt-injection markers in inspected tool descriptions.
-- Static errors skip runtime inspection; remote network inspection and local command execution both require explicit trust flags.
-- Runtime inspection creates a temporary `MCPConfig` in a worker thread so stdio/remote tool listing does not call `asyncio.run()` inside the request event loop.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Do not return secret values, raw environment values, or private files.
-- Keep scanner warnings explicit about local command execution risk.
-
-## Verification
-
-- Run endpoint-specific or MCP helper tests for changed behavior; smoke-test remote URL and local-command scan paths when practical.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/mcp_servers_apply.py b/api/mcp_servers_apply.py
index 36877807a..7ea5275db 100644
--- a/api/mcp_servers_apply.py
+++ b/api/mcp_servers_apply.py
@@ -5,27 +5,20 @@ from typing import Any
from helpers.mcp_handler import MCPConfig
from helpers.settings import set_settings_delta
-from helpers import projects
class McpServersApply(ApiHandler):
async def process(self, input: dict[Any, Any], request: Request) -> dict[Any, Any] | Response:
mcp_servers = input["mcp_servers"]
- project_name = str(input.get("project_name", "") or "").strip()
try:
- if project_name:
- projects.save_project_mcp_servers(project_name, mcp_servers)
- config = MCPConfig.refresh_project(project_name)
- else:
- # MCPConfig.update(mcp_servers) # done in settings automatically
- set_settings_delta({"mcp_servers": "[]"}) # to force reinitialization
- set_settings_delta({"mcp_servers": mcp_servers})
+ # MCPConfig.update(mcp_servers) # done in settings automatically
+ set_settings_delta({"mcp_servers": "[]"}) # to force reinitialization
+ set_settings_delta({"mcp_servers": mcp_servers})
- time.sleep(1) # wait at least a second
- # MCPConfig.wait_for_lock() # wait until config lock is released
- config = MCPConfig.get_instance()
- status = config.get_servers_status()
- return {"success": True, "status": status, "mcp_servers": mcp_servers, "project_name": project_name}
+ time.sleep(1) # wait at least a second
+ # MCPConfig.wait_for_lock() # wait until config lock is released
+ status = MCPConfig.get_instance().get_servers_status()
+ return {"success": True, "status": status}
except Exception as e:
return {"success": False, "error": str(e)}
diff --git a/api/mcp_servers_apply.py.dox.md b/api/mcp_servers_apply.py.dox.md
deleted file mode 100644
index 361c331b2..000000000
--- a/api/mcp_servers_apply.py.dox.md
+++ /dev/null
@@ -1,47 +0,0 @@
-# mcp_servers_apply.py DOX
-
-## Purpose
-
-- Own the `mcp_servers_apply.py` API endpoint.
-- This module handles MCP servers apply requests for global or project scope.
-- Keep this file-level DOX profile synchronized with `mcp_servers_apply.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `mcp_servers_apply.py` owns the runtime implementation.
-- `mcp_servers_apply.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `McpServersApply` (`ApiHandler`)
- - `async process(self, input: dict[Any, Any], request: Request) -> dict[Any, Any] | Response`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- The request accepts `config` and optional `project_name`.
-- Without `project_name`, the endpoint persists global `mcp_servers_config` through settings and refreshes the global `MCPConfig`.
-- With `project_name`, the endpoint saves `.a0proj/mcp_servers.json` through `helpers.projects.save_project_mcp_servers(...)` and refreshes that project's merged MCP config.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `McpServersApply` is an `ApiHandler`.
-- `McpServersApply` defines `process(...)`.
-- Observed side-effect areas: filesystem writes, settings/state persistence.
-- Imported dependency areas include: `helpers.api`, `helpers.mcp_handler`, `helpers.projects`, `helpers.settings`, `time`, `typing`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `set_settings_delta`, `projects.save_project_mcp_servers`, `MCPConfig.refresh_project`, `time.sleep`, `MCPConfig.get_instance.get_servers_status`, `MCPConfig.get_instance`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/mcp_servers_status.py b/api/mcp_servers_status.py
index 01afff421..20f8a64c8 100644
--- a/api/mcp_servers_status.py
+++ b/api/mcp_servers_status.py
@@ -9,9 +9,7 @@ class McpServersStatuss(ApiHandler):
async def process(self, input: dict[Any, Any], request: Request) -> dict[Any, Any] | Response:
# try:
- project_name = (input or {}).get("project_name") if isinstance(input, dict) else None
- config = MCPConfig.get_project_instance(project_name) if project_name else MCPConfig.get_instance()
- status = config.get_servers_status()
+ status = MCPConfig.get_instance().get_servers_status()
return {"success": True, "status": status}
# except Exception as e:
# return {"success": False, "error": str(e)}
diff --git a/api/mcp_servers_status.py.dox.md b/api/mcp_servers_status.py.dox.md
deleted file mode 100644
index 90b4065be..000000000
--- a/api/mcp_servers_status.py.dox.md
+++ /dev/null
@@ -1,45 +0,0 @@
-# mcp_servers_status.py DOX
-
-## Purpose
-
-- Own the `mcp_servers_status.py` API endpoint.
-- This module handles MCP servers status requests for global or project scope.
-- Keep this file-level DOX profile synchronized with `mcp_servers_status.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `mcp_servers_status.py` owns the runtime implementation.
-- `mcp_servers_status.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `McpServersStatuss` (`ApiHandler`)
- - `async process(self, input: dict[Any, Any], request: Request) -> dict[Any, Any] | Response`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- The request accepts optional `project_name`; when present, status resolves through the merged project-scoped MCP configuration.
-- `tool_count` reports enabled MCP tools only; tools disabled by a server `disabled_tools` list stay hidden from agent-facing status counts.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `McpServersStatuss` is an `ApiHandler`.
-- `McpServersStatuss` defines `process(...)`.
-- Imported dependency areas include: `helpers.api`, `helpers.mcp_handler`, `typing`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `MCPConfig.get_instance.get_servers_status`, `MCPConfig.get_project_instance`, `MCPConfig.get_instance`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/message.py.dox.md b/api/message.py.dox.md
deleted file mode 100644
index 9ec63fc71..000000000
--- a/api/message.py.dox.md
+++ /dev/null
@@ -1,54 +0,0 @@
-# message.py DOX
-
-## Purpose
-
-- Own the `message.py` API endpoint.
-- This module submits a user message and runs agent processing synchronously through the UI API.
-- Keep this file-level DOX profile synchronized with `message.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `message.py` owns the runtime implementation.
-- `message.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `Message` (`ApiHandler`)
- - `async process(self, input: dict, request: Request) -> dict | Response`
- - `async respond(self, task: DeferredTask, context: AgentContext)`
- - `async communicate(self, input: dict, request: Request)`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `Message` is an `ApiHandler`.
-- `Message` defines `process(...)`.
-- Observed side-effect areas: filesystem reads, filesystem writes, settings/state persistence, scheduler state.
-- Imported dependency areas include: `agent`, `helpers`, `helpers.api`, `helpers.defer`, `helpers.security`, `os`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `request.content_type.startswith`, `self.use_context`, `mq.log_user_message`, `self.communicate`, `self.respond`, `task.result`, `request.files.getlist`, `files.get_abs_path`, `request.get_json`, `extension.call_extensions_async`, `context.communicate`, `os.makedirs`, `UserMessage`, `safe_filename`, `attachment.save`, `context.get_agent`, `os.path.join`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- Related tests observed by source search:
- - `tests/email_parser_test.py`
- - `tests/rate_limiter_test.py`
- - `tests/test_api_chat_lifetime.py`
- - `tests/test_browser_agent_regressions.py`
- - `tests/test_chat_compaction.py`
- - `tests/test_docker_release_plan.py`
- - `tests/test_document_query_fallback.py`
- - `tests/test_download_toast_regressions.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/message_async.py.dox.md b/api/message_async.py.dox.md
deleted file mode 100644
index 6a6ba0093..000000000
--- a/api/message_async.py.dox.md
+++ /dev/null
@@ -1,42 +0,0 @@
-# message_async.py DOX
-
-## Purpose
-
-- Own the `message_async.py` API endpoint.
-- This module submits a user message for asynchronous agent processing.
-- Keep this file-level DOX profile synchronized with `message_async.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `message_async.py` owns the runtime implementation.
-- `message_async.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `MessageAsync` (`Message`)
- - `async respond(self, task: DeferredTask, context: AgentContext)`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- Observed side-effect areas: scheduler state.
-- Imported dependency areas include: `agent`, `api.message`, `helpers.defer`.
-
-## Key Concepts
-
-- This module is primarily declarative or delegates behavior through classes/imported objects.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/message_queue_add.py.dox.md b/api/message_queue_add.py.dox.md
deleted file mode 100644
index f727a70f1..000000000
--- a/api/message_queue_add.py.dox.md
+++ /dev/null
@@ -1,44 +0,0 @@
-# message_queue_add.py DOX
-
-## Purpose
-
-- Own the `message_queue_add.py` API endpoint.
-- This module handles message queue add API requests.
-- Keep this file-level DOX profile synchronized with `message_queue_add.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `message_queue_add.py` owns the runtime implementation.
-- `message_queue_add.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `MessageQueueAdd` (`ApiHandler`)
- - `async process(self, input: dict, request: Request) -> dict | Response`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `MessageQueueAdd` is an `ApiHandler`.
-- `MessageQueueAdd` defines `process(...)`.
-- Observed side-effect areas: settings/state persistence.
-- Imported dependency areas include: `agent`, `helpers`, `helpers.api`, `helpers.state_monitor_integration`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `input.get.strip`, `mq.add`, `mark_dirty_for_context`, `Response`, `mq.get_queue`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/message_queue_remove.py.dox.md b/api/message_queue_remove.py.dox.md
deleted file mode 100644
index 8bf876b05..000000000
--- a/api/message_queue_remove.py.dox.md
+++ /dev/null
@@ -1,44 +0,0 @@
-# message_queue_remove.py DOX
-
-## Purpose
-
-- Own the `message_queue_remove.py` API endpoint.
-- This module handles message queue remove API requests.
-- Keep this file-level DOX profile synchronized with `message_queue_remove.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `message_queue_remove.py` owns the runtime implementation.
-- `message_queue_remove.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `MessageQueueRemove` (`ApiHandler`)
- - `async process(self, input: dict, request: Request) -> dict | Response`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `MessageQueueRemove` is an `ApiHandler`.
-- `MessageQueueRemove` defines `process(...)`.
-- Observed side-effect areas: filesystem deletion, settings/state persistence.
-- Imported dependency areas include: `agent`, `helpers`, `helpers.api`, `helpers.state_monitor_integration`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `mq.remove`, `mark_dirty_for_context`, `Response`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/message_queue_send.py.dox.md b/api/message_queue_send.py.dox.md
deleted file mode 100644
index 4f7294a94..000000000
--- a/api/message_queue_send.py.dox.md
+++ /dev/null
@@ -1,44 +0,0 @@
-# message_queue_send.py DOX
-
-## Purpose
-
-- Own the `message_queue_send.py` API endpoint.
-- This module handles message queue send API requests.
-- Keep this file-level DOX profile synchronized with `message_queue_send.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `message_queue_send.py` owns the runtime implementation.
-- `message_queue_send.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `MessageQueueSend` (`ApiHandler`)
- - `async process(self, input: dict, request: Request) -> dict | Response`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `MessageQueueSend` is an `ApiHandler`.
-- `MessageQueueSend` defines `process(...)`.
-- Observed side-effect areas: settings/state persistence.
-- Imported dependency areas include: `agent`, `helpers`, `helpers.api`, `helpers.state_monitor_integration`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `mq.send_message`, `mark_dirty_for_context`, `Response`, `mq.has_queue`, `mq.send_all_aggregated`, `mq.pop_item`, `mq.pop_first`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/notification_create.py.dox.md b/api/notification_create.py.dox.md
deleted file mode 100644
index a83b82cf4..000000000
--- a/api/notification_create.py.dox.md
+++ /dev/null
@@ -1,46 +0,0 @@
-# notification_create.py DOX
-
-## Purpose
-
-- Own the `notification_create.py` API endpoint.
-- This module handles notification notification create requests.
-- Keep this file-level DOX profile synchronized with `notification_create.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `notification_create.py` owns the runtime implementation.
-- `notification_create.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `NotificationCreate` (`ApiHandler`)
- - `requires_auth(cls) -> bool`
- - `async process(self, input: dict, request: Request) -> dict | Response`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `NotificationCreate` is an `ApiHandler`.
-- `NotificationCreate` defines `process(...)`.
-- `NotificationCreate` defines `requires_auth(...)`.
-- Imported dependency areas include: `flask`, `helpers.api`, `helpers.notification`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `NotificationManager.send_notification`, `NotificationType`, `notification.output`, `notification_type.lower`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- Related tests observed by source search:
- - `tests/test_download_toast_regressions.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/notifications_clear.py.dox.md b/api/notifications_clear.py.dox.md
deleted file mode 100644
index dc5503b73..000000000
--- a/api/notifications_clear.py.dox.md
+++ /dev/null
@@ -1,45 +0,0 @@
-# notifications_clear.py DOX
-
-## Purpose
-
-- Own the `notifications_clear.py` API endpoint.
-- This module handles notification notifications clear requests.
-- Keep this file-level DOX profile synchronized with `notifications_clear.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `notifications_clear.py` owns the runtime implementation.
-- `notifications_clear.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `NotificationsClear` (`ApiHandler`)
- - `requires_auth(cls) -> bool`
- - `async process(self, input: dict, request: Request) -> dict | Response`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `NotificationsClear` is an `ApiHandler`.
-- `NotificationsClear` defines `process(...)`.
-- `NotificationsClear` defines `requires_auth(...)`.
-- Imported dependency areas include: `agent`, `flask`, `helpers.api`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `AgentContext.get_notification_manager`, `notification_manager.clear_all`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/notifications_history.py.dox.md b/api/notifications_history.py.dox.md
deleted file mode 100644
index a14b5bc86..000000000
--- a/api/notifications_history.py.dox.md
+++ /dev/null
@@ -1,45 +0,0 @@
-# notifications_history.py DOX
-
-## Purpose
-
-- Own the `notifications_history.py` API endpoint.
-- This module handles notification notifications history requests.
-- Keep this file-level DOX profile synchronized with `notifications_history.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `notifications_history.py` owns the runtime implementation.
-- `notifications_history.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `NotificationsHistory` (`ApiHandler`)
- - `requires_auth(cls) -> bool`
- - `async process(self, input: dict, request: Request) -> dict | Response`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `NotificationsHistory` is an `ApiHandler`.
-- `NotificationsHistory` defines `process(...)`.
-- `NotificationsHistory` defines `requires_auth(...)`.
-- Imported dependency areas include: `agent`, `flask`, `helpers.api`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `AgentContext.get_notification_manager`, `notification_manager.output_all`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/notifications_mark_read.py.dox.md b/api/notifications_mark_read.py.dox.md
deleted file mode 100644
index a06cbbdcf..000000000
--- a/api/notifications_mark_read.py.dox.md
+++ /dev/null
@@ -1,45 +0,0 @@
-# notifications_mark_read.py DOX
-
-## Purpose
-
-- Own the `notifications_mark_read.py` API endpoint.
-- This module handles notification notifications mark read requests.
-- Keep this file-level DOX profile synchronized with `notifications_mark_read.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `notifications_mark_read.py` owns the runtime implementation.
-- `notifications_mark_read.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `NotificationsMarkRead` (`ApiHandler`)
- - `requires_auth(cls) -> bool`
- - `async process(self, input: dict, request: Request) -> dict | Response`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `NotificationsMarkRead` is an `ApiHandler`.
-- `NotificationsMarkRead` defines `process(...)`.
-- `NotificationsMarkRead` defines `requires_auth(...)`.
-- Imported dependency areas include: `agent`, `flask`, `helpers.api`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `AgentContext.get_notification_manager`, `notification_manager.mark_read_by_ids`, `notification_manager.mark_all_read`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/nudge.py.dox.md b/api/nudge.py.dox.md
deleted file mode 100644
index f1be2cf3d..000000000
--- a/api/nudge.py.dox.md
+++ /dev/null
@@ -1,44 +0,0 @@
-# nudge.py DOX
-
-## Purpose
-
-- Own the `nudge.py` API endpoint.
-- This module handles nudge API requests.
-- Keep this file-level DOX profile synchronized with `nudge.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `nudge.py` owns the runtime implementation.
-- `nudge.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `Nudge` (`ApiHandler`)
- - `async process(self, input: dict, request: Request) -> dict | Response`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `Nudge` is an `ApiHandler`.
-- `Nudge` defines `process(...)`.
-- Imported dependency areas include: `helpers.api`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `self.use_context`, `context.nudge`, `context.log.log`, `Exception`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- Related tests observed by source search:
- - `tests/test_browser_agent_regressions.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/pause.py.dox.md b/api/pause.py.dox.md
deleted file mode 100644
index 37d0528e9..000000000
--- a/api/pause.py.dox.md
+++ /dev/null
@@ -1,45 +0,0 @@
-# pause.py DOX
-
-## Purpose
-
-- Own the `pause.py` API endpoint.
-- This module handles pause API requests.
-- Keep this file-level DOX profile synchronized with `pause.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `pause.py` owns the runtime implementation.
-- `pause.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `Pause` (`ApiHandler`)
- - `async process(self, input: dict, request: Request) -> dict | Response`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `Pause` is an `ApiHandler`.
-- `Pause` defines `process(...)`.
-- Imported dependency areas include: `helpers.api`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `self.use_context`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- Related tests observed by source search:
- - `tests/test_multi_tab_isolation.py`
- - `tests/test_snapshot_schema_v1.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/plugins.py.dox.md b/api/plugins.py.dox.md
deleted file mode 100644
index e64b22dc9..000000000
--- a/api/plugins.py.dox.md
+++ /dev/null
@@ -1,52 +0,0 @@
-# plugins.py DOX
-
-## Purpose
-
-- Own the `plugins.py` API endpoint.
-- This module manages plugin actions and plugin settings through the core API.
-- Keep this file-level DOX profile synchronized with `plugins.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `plugins.py` owns the runtime implementation.
-- `plugins.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `Plugins` (`ApiHandler`)
- - `async process(self, input: dict, request: Request) -> dict | Response`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `Plugins` is an `ApiHandler`.
-- `Plugins` defines `process(...)`.
-- Observed side-effect areas: filesystem reads, filesystem writes, filesystem deletion, subprocess/runtime control, plugin state, settings/state persistence.
-- Imported dependency areas include: `helpers`, `helpers.api`, `helpers.localization`, `json`, `os`, `subprocess`, `sys`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `Response`, `plugins.find_plugin_assets`, `plugins.get_plugin_meta`, `plugins.get_default_plugin_config`, `plugins.save_plugin_config`, `plugins.toggle_plugin`, `plugins.find_plugin_dir`, `files.get_abs_path`, `Localization.get.now_iso`, `plugins.determine_plugin_asset_path`, `self._get_config`, `self._get_toggle_status`, `self._list_configs`, `self._delete_config`, `self._delete_plugin`, `self._get_default_config`, `self._save_config`, `self._toggle_plugin`, `self._get_doc`, `self._run_execute_script`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- Related tests observed by source search:
- - `tests/test_a0_connector_computer_use_metadata.py`
- - `tests/test_a0_connector_prompt_gating.py`
- - `tests/test_browser_agent_regressions.py`
- - `tests/test_chat_compaction.py`
- - `tests/test_default_prompt_budget.py`
- - `tests/test_document_query_plugin.py`
- - `tests/test_error_retry_plugin.py`
- - `tests/test_host_browser_connector.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/plugins_list.py.dox.md b/api/plugins_list.py.dox.md
deleted file mode 100644
index 599b5a307..000000000
--- a/api/plugins_list.py.dox.md
+++ /dev/null
@@ -1,46 +0,0 @@
-# plugins_list.py DOX
-
-## Purpose
-
-- Own the `plugins_list.py` API endpoint.
-- This module returns plugin inventory and activation metadata for plugin UI surfaces.
-- Keep this file-level DOX profile synchronized with `plugins_list.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `plugins_list.py` owns the runtime implementation.
-- `plugins_list.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `PluginsList` (`ApiHandler`)
- - `async process(self, input: Input, request: Request) -> Output`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `PluginsList` is an `ApiHandler`.
-- `PluginsList` defines `process(...)`.
-- Observed side-effect areas: plugin state, settings/state persistence.
-- Imported dependency areas include: `helpers`, `helpers.api`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `plugins.get_enhanced_plugins_list`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- Related tests observed by source search:
- - `tests/test_plugin_activation_ui.py`
- - `tests/test_speech_plugin_split.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/poll.py.dox.md b/api/poll.py.dox.md
deleted file mode 100644
index 851a03318..000000000
--- a/api/poll.py.dox.md
+++ /dev/null
@@ -1,52 +0,0 @@
-# poll.py DOX
-
-## Purpose
-
-- Own the `poll.py` API endpoint.
-- This module returns chat/log/status changes for polling clients.
-- Keep this file-level DOX profile synchronized with `poll.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `poll.py` owns the runtime implementation.
-- `poll.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `Poll` (`ApiHandler`)
- - `async process(self, input: dict, request: Request) -> dict | Response`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `Poll` is an `ApiHandler`.
-- `Poll` defines `process(...)`.
-- Observed side-effect areas: settings/state persistence.
-- Imported dependency areas include: `helpers.api`, `helpers.state_snapshot`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `build_snapshot`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- Related tests observed by source search:
- - `tests/test_multi_tab_isolation.py`
- - `tests/test_oauth_github_copilot.py`
- - `tests/test_oauth_providers.py`
- - `tests/test_office_document_store.py`
- - `tests/test_snapshot_parity.py`
- - `tests/test_snapshot_schema_v1.py`
- - `tests/test_timezone_regressions.py`
- - `tests/test_tunnel_remote_link.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/projects.py.dox.md b/api/projects.py.dox.md
deleted file mode 100644
index d55500c21..000000000
--- a/api/projects.py.dox.md
+++ /dev/null
@@ -1,59 +0,0 @@
-# projects.py DOX
-
-## Purpose
-
-- Own the `projects.py` API endpoint.
-- This module manages project create, update, delete, clone, and metadata flows.
-- Keep this file-level DOX profile synchronized with `projects.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `projects.py` owns the runtime implementation.
-- `projects.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `Projects` (`ApiHandler`)
- - `async process(self, input: Input, request: Request) -> Output`
- - `get_active_projects_list(self)`
- - `get_active_projects_options(self)`
- - `create_project(self, project: dict | None)`
- - `clone_project(self, project: dict | None)`
- - `load_project(self, name: str | None)`
- - `update_project(self, project: dict | None)`
- - `delete_project(self, name: str | None)`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `Projects` is an `ApiHandler`.
-- `Projects` defines `process(...)`.
-- Observed side-effect areas: filesystem deletion, settings/state persistence.
-- Imported dependency areas include: `helpers`, `helpers.api`, `helpers.notification`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `projects.get_active_projects_list`, `projects.BasicProjectData`, `projects.create_project`, `projects.load_edit_project_data`, `NotificationManager.send_notification`, `projects.EditProjectData`, `projects.update_project`, `projects.delete_project`, `projects.activate_project`, `projects.deactivate_project`, `projects.load_basic_project_data`, `projects.get_file_structure`, `self.use_context`, `Exception`, `projects.clone_git_project`, `self.get_active_projects_list`, `self.get_active_projects_options`, `self.load_project`, `self.create_project`, `self.clone_project`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- Related tests observed by source search:
- - `tests/test_model_config_project_presets.py`
- - `tests/test_office_document_store.py`
- - `tests/test_plugin_activation_ui.py`
- - `tests/test_projects.py`
- - `tests/test_skills_runtime.py`
- - `tests/test_task_scheduler_timezone.py`
- - `tests/test_time_travel.py`
- - `tests/test_tool_action_contracts.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/rename_work_dir_file.py.dox.md b/api/rename_work_dir_file.py.dox.md
deleted file mode 100644
index 624bedc54..000000000
--- a/api/rename_work_dir_file.py.dox.md
+++ /dev/null
@@ -1,47 +0,0 @@
-# rename_work_dir_file.py DOX
-
-## Purpose
-
-- Own the `rename_work_dir_file.py` API endpoint.
-- This module handles workdir file operations for rename work dir file.
-- Keep this file-level DOX profile synchronized with `rename_work_dir_file.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `rename_work_dir_file.py` owns the runtime implementation.
-- `rename_work_dir_file.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `RenameWorkDirFile` (`ApiHandler`)
- - `async process(self, input: Input, request: Request) -> Output`
-- Top-level functions:
-- `async rename_item(file_path: str, new_name: str) -> bool`
-- `async create_folder(parent_path: str, folder_name: str) -> bool`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `RenameWorkDirFile` is an `ApiHandler`.
-- `RenameWorkDirFile` defines `process(...)`.
-- Observed side-effect areas: filesystem writes.
-- Imported dependency areas include: `api`, `helpers`, `helpers.api`, `helpers.file_browser`, `posixpath`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `FileBrowser`, `browser.rename_item`, `browser.create_folder`, `strip`, `runtime.call_development_function`, `posixpath.join`, `file_path.startswith`, `extension.call_extensions_async`, `str.rstrip`, `posixpath.dirname`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/restart.py.dox.md b/api/restart.py.dox.md
deleted file mode 100644
index 4e3156f6e..000000000
--- a/api/restart.py.dox.md
+++ /dev/null
@@ -1,48 +0,0 @@
-# restart.py DOX
-
-## Purpose
-
-- Own the `restart.py` API endpoint.
-- This module requests server restart or reload behavior.
-- Keep this file-level DOX profile synchronized with `restart.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `restart.py` owns the runtime implementation.
-- `restart.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `Restart` (`ApiHandler`)
- - `async process(self, input: dict, request: Request) -> dict | Response`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `Restart` is an `ApiHandler`.
-- `Restart` defines `process(...)`.
-- Imported dependency areas include: `helpers`, `helpers.api`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `process.reload`, `Response`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- Related tests observed by source search:
- - `tests/test_browser_agent_regressions.py`
- - `tests/test_download_toast_regressions.py`
- - `tests/test_self_update_tag_filter.py`
- - `tests/test_timezone_regressions.py`
- - `tests/test_ws_manager.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/rfc.py.dox.md b/api/rfc.py.dox.md
deleted file mode 100644
index 6598649ea..000000000
--- a/api/rfc.py.dox.md
+++ /dev/null
@@ -1,47 +0,0 @@
-# rfc.py DOX
-
-## Purpose
-
-- Own the `rfc.py` API endpoint.
-- This module dispatches remote function calls through the RFC helper layer.
-- Keep this file-level DOX profile synchronized with `rfc.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `rfc.py` owns the runtime implementation.
-- `rfc.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `RFC` (`ApiHandler`)
- - `requires_csrf(cls) -> bool`
- - `requires_auth(cls) -> bool`
- - `async process(self, input: dict, request: Request) -> dict | Response`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `RFC` is an `ApiHandler`.
-- `RFC` defines `process(...)`.
-- `RFC` defines `requires_auth(...)`.
-- `RFC` defines `requires_csrf(...)`.
-- Imported dependency areas include: `helpers`, `helpers.api`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `runtime.handle_rfc`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/scheduler_task_create.py.dox.md b/api/scheduler_task_create.py.dox.md
deleted file mode 100644
index 332118085..000000000
--- a/api/scheduler_task_create.py.dox.md
+++ /dev/null
@@ -1,44 +0,0 @@
-# scheduler_task_create.py DOX
-
-## Purpose
-
-- Own the `scheduler_task_create.py` API endpoint.
-- This module handles scheduler task create requests.
-- Keep this file-level DOX profile synchronized with `scheduler_task_create.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `scheduler_task_create.py` owns the runtime implementation.
-- `scheduler_task_create.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `SchedulerTaskCreate` (`ApiHandler`)
- - `async process(self, input: Input, request: Request) -> Output`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `SchedulerTaskCreate` is an `ApiHandler`.
-- `SchedulerTaskCreate` defines `process(...)`.
-- Observed side-effect areas: filesystem writes, secret handling, scheduler state.
-- Imported dependency areas include: `helpers.api`, `helpers.localization`, `helpers.print_style`, `helpers.projects`, `helpers.task_scheduler`, `random`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `PrintStyle`, `scheduler.get_task_by_uuid`, `serialize_task`, `Localization.get.set_timezone`, `scheduler.reload`, `ValueError`, `ScheduledTask.create`, `scheduler.add_task`, `requested_project_slug.strip`, `load_basic_project_data`, `random.randint`, `schedule.split`, `TaskSchedule`, `PlannedTask.create`, `AdHocTask.create`, `printer.error`, `type`, `parse_task_plan`, `parse_task_schedule`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/scheduler_task_delete.py.dox.md b/api/scheduler_task_delete.py.dox.md
deleted file mode 100644
index 2112e95ee..000000000
--- a/api/scheduler_task_delete.py.dox.md
+++ /dev/null
@@ -1,44 +0,0 @@
-# scheduler_task_delete.py DOX
-
-## Purpose
-
-- Own the `scheduler_task_delete.py` API endpoint.
-- This module handles scheduler task delete requests.
-- Keep this file-level DOX profile synchronized with `scheduler_task_delete.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `scheduler_task_delete.py` owns the runtime implementation.
-- `scheduler_task_delete.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `SchedulerTaskDelete` (`ApiHandler`)
- - `async process(self, input: Input, request: Request) -> Output`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `SchedulerTaskDelete` is an `ApiHandler`.
-- `SchedulerTaskDelete` defines `process(...)`.
-- Observed side-effect areas: filesystem writes, filesystem deletion, settings/state persistence, scheduler state.
-- Imported dependency areas include: `agent`, `helpers`, `helpers.api`, `helpers.localization`, `helpers.task_scheduler`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `scheduler.get_task_by_uuid`, `Localization.get.set_timezone`, `scheduler.reload`, `self.use_context`, `scheduler.cancel_running_task`, `AgentContext.remove`, `persist_chat.remove_chat`, `scheduler.remove_task_by_uuid`, `context.reset`, `scheduler.update_task`, `scheduler.save`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/scheduler_task_run.py.dox.md b/api/scheduler_task_run.py.dox.md
deleted file mode 100644
index f4aa4b158..000000000
--- a/api/scheduler_task_run.py.dox.md
+++ /dev/null
@@ -1,44 +0,0 @@
-# scheduler_task_run.py DOX
-
-## Purpose
-
-- Own the `scheduler_task_run.py` API endpoint.
-- This module handles scheduler task run requests.
-- Keep this file-level DOX profile synchronized with `scheduler_task_run.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `scheduler_task_run.py` owns the runtime implementation.
-- `scheduler_task_run.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `SchedulerTaskRun` (`ApiHandler`)
- - `async process(self, input: Input, request: Request) -> Output`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `SchedulerTaskRun` is an `ApiHandler`.
-- `SchedulerTaskRun` defines `process(...)`.
-- Observed side-effect areas: settings/state persistence, scheduler state.
-- Imported dependency areas include: `helpers.api`, `helpers.localization`, `helpers.print_style`, `helpers.task_scheduler`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `PrintStyle`, `scheduler.get_task_by_uuid`, `Localization.get.set_timezone`, `scheduler.reload`, `self._printer.error`, `scheduler.serialize_task`, `scheduler.run_task_by_uuid`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/scheduler_task_update.py.dox.md b/api/scheduler_task_update.py.dox.md
deleted file mode 100644
index a7c9bfced..000000000
--- a/api/scheduler_task_update.py.dox.md
+++ /dev/null
@@ -1,44 +0,0 @@
-# scheduler_task_update.py DOX
-
-## Purpose
-
-- Own the `scheduler_task_update.py` API endpoint.
-- This module handles scheduler task update requests.
-- Keep this file-level DOX profile synchronized with `scheduler_task_update.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `scheduler_task_update.py` owns the runtime implementation.
-- `scheduler_task_update.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `SchedulerTaskUpdate` (`ApiHandler`)
- - `async process(self, input: Input, request: Request) -> Output`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `SchedulerTaskUpdate` is an `ApiHandler`.
-- `SchedulerTaskUpdate` defines `process(...)`.
-- Observed side-effect areas: settings/state persistence, secret handling, scheduler state.
-- Imported dependency areas include: `helpers.api`, `helpers.localization`, `helpers.task_scheduler`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `scheduler.get_task_by_uuid`, `serialize_task`, `Localization.get.set_timezone`, `scheduler.reload`, `TaskState`, `scheduler.update_task`, `parse_task_schedule`, `parse_task_plan`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/scheduler_tasks_list.py.dox.md b/api/scheduler_tasks_list.py.dox.md
deleted file mode 100644
index 5e5533206..000000000
--- a/api/scheduler_tasks_list.py.dox.md
+++ /dev/null
@@ -1,44 +0,0 @@
-# scheduler_tasks_list.py DOX
-
-## Purpose
-
-- Own the `scheduler_tasks_list.py` API endpoint.
-- This module handles scheduler tasks list requests.
-- Keep this file-level DOX profile synchronized with `scheduler_tasks_list.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `scheduler_tasks_list.py` owns the runtime implementation.
-- `scheduler_tasks_list.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `SchedulerTasksList` (`ApiHandler`)
- - `async process(self, input: Input, request: Request) -> Output`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `SchedulerTasksList` is an `ApiHandler`.
-- `SchedulerTasksList` defines `process(...)`.
-- Observed side-effect areas: scheduler state.
-- Imported dependency areas include: `helpers.api`, `helpers.localization`, `helpers.print_style`, `helpers.task_scheduler`, `traceback`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `scheduler.serialize_all_tasks`, `Localization.get.set_timezone`, `scheduler.reload`, `PrintStyle.error`, `traceback.format_exc`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/scheduler_tick.py.dox.md b/api/scheduler_tick.py.dox.md
deleted file mode 100644
index d19ae305c..000000000
--- a/api/scheduler_tick.py.dox.md
+++ /dev/null
@@ -1,50 +0,0 @@
-# scheduler_tick.py DOX
-
-## Purpose
-
-- Own the `scheduler_tick.py` API endpoint.
-- This module handles scheduler tick requests.
-- Keep this file-level DOX profile synchronized with `scheduler_tick.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `scheduler_tick.py` owns the runtime implementation.
-- `scheduler_tick.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `SchedulerTick` (`ApiHandler`)
- - `requires_loopback(cls) -> bool`
- - `requires_auth(cls) -> bool`
- - `requires_csrf(cls) -> bool`
- - `async process(self, input: Input, request: Request) -> Output`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `SchedulerTick` is an `ApiHandler`.
-- `SchedulerTick` defines `process(...)`.
-- `SchedulerTick` defines `requires_auth(...)`.
-- `SchedulerTick` defines `requires_csrf(...)`.
-- `SchedulerTick` defines `requires_loopback(...)`.
-- Observed side-effect areas: settings/state persistence, scheduler state.
-- Imported dependency areas include: `datetime`, `helpers.api`, `helpers.localization`, `helpers.print_style`, `helpers.task_scheduler`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `datetime.now.strftime`, `PrintStyle`, `scheduler.get_tasks`, `scheduler.serialize_all_tasks`, `Localization.get.set_timezone`, `scheduler.reload`, `scheduler.tick`, `datetime.now`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/self_update_get.py.dox.md b/api/self_update_get.py.dox.md
deleted file mode 100644
index 261cac93f..000000000
--- a/api/self_update_get.py.dox.md
+++ /dev/null
@@ -1,45 +0,0 @@
-# self_update_get.py DOX
-
-## Purpose
-
-- Own the `self_update_get.py` API endpoint.
-- This module handles self update get API requests.
-- Keep this file-level DOX profile synchronized with `self_update_get.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `self_update_get.py` owns the runtime implementation.
-- `self_update_get.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `SelfUpdateGet` (`ApiHandler`)
- - `get_methods(cls) -> list[str]`
- - `async process(self, input: dict, request: Request) -> dict | Response`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `SelfUpdateGet` is an `ApiHandler`.
-- `SelfUpdateGet` defines `process(...)`.
-- `SelfUpdateGet` defines `get_methods(...)`.
-- Imported dependency areas include: `helpers`, `helpers.api`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `self_update.get_update_info`, `runtime.is_dockerized`, `self_update.load_pending_update`, `self_update.load_last_status`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/self_update_schedule.py.dox.md b/api/self_update_schedule.py.dox.md
deleted file mode 100644
index b865070c9..000000000
--- a/api/self_update_schedule.py.dox.md
+++ /dev/null
@@ -1,45 +0,0 @@
-# self_update_schedule.py DOX
-
-## Purpose
-
-- Own the `self_update_schedule.py` API endpoint.
-- This module handles self update schedule API requests.
-- Keep this file-level DOX profile synchronized with `self_update_schedule.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `self_update_schedule.py` owns the runtime implementation.
-- `self_update_schedule.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `SelfUpdateSchedule` (`ApiHandler`)
- - `async process(self, input: dict, request: Request) -> dict | Response`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `SelfUpdateSchedule` is an `ApiHandler`.
-- `SelfUpdateSchedule` defines `process(...)`.
-- Observed side-effect areas: filesystem writes, subprocess/runtime control.
-- Imported dependency areas include: `helpers`, `helpers.api`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `runtime.is_dockerized`, `self_update.schedule_update`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- Related tests observed by source search:
- - `tests/test_self_update_tag_filter.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/self_update_tags.py.dox.md b/api/self_update_tags.py.dox.md
deleted file mode 100644
index fdf78cd23..000000000
--- a/api/self_update_tags.py.dox.md
+++ /dev/null
@@ -1,43 +0,0 @@
-# self_update_tags.py DOX
-
-## Purpose
-
-- Own the `self_update_tags.py` API endpoint.
-- This module handles self update tags API requests.
-- Keep this file-level DOX profile synchronized with `self_update_tags.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `self_update_tags.py` owns the runtime implementation.
-- `self_update_tags.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `SelfUpdateTags` (`ApiHandler`)
- - `async process(self, input: dict, request: Request) -> dict | Response`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `SelfUpdateTags` is an `ApiHandler`.
-- `SelfUpdateTags` defines `process(...)`.
-- Imported dependency areas include: `helpers`, `helpers.api`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `str.strip.lower`, `self_update.get_repo_version_info.get.strip.lower`, `self_update.get_available_branch_values`, `self_update.get_selector_tag_options`, `str.strip`, `self_update.get_repo_version_info.get.strip`, `runtime.is_dockerized`, `self_update.get_repo_version_info`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/settings_get.py.dox.md b/api/settings_get.py.dox.md
deleted file mode 100644
index 46b2ccdd7..000000000
--- a/api/settings_get.py.dox.md
+++ /dev/null
@@ -1,47 +0,0 @@
-# settings_get.py DOX
-
-## Purpose
-
-- Own the `settings_get.py` API endpoint.
-- This module returns current application settings.
-- Keep this file-level DOX profile synchronized with `settings_get.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `settings_get.py` owns the runtime implementation.
-- `settings_get.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `GetSettings` (`ApiHandler`)
- - `async process(self, input: dict, request: Request) -> dict | Response`
- - `get_methods(cls) -> list[str]`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `GetSettings` is an `ApiHandler`.
-- `GetSettings` defines `process(...)`.
-- `GetSettings` defines `get_methods(...)`.
-- Observed side-effect areas: settings/state persistence.
-- Imported dependency areas include: `helpers`, `helpers.api`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `settings.get_settings`, `settings.convert_out`.
-- The returned settings map includes normalized `ui_control_visibility` mobile and desktop flags for the configurable WebUI controls.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/settings_set.py.dox.md b/api/settings_set.py.dox.md
deleted file mode 100644
index 0d3e62e6c..000000000
--- a/api/settings_set.py.dox.md
+++ /dev/null
@@ -1,45 +0,0 @@
-# settings_set.py DOX
-
-## Purpose
-
-- Own the `settings_set.py` API endpoint.
-- This module persists application settings updates.
-- Keep this file-level DOX profile synchronized with `settings_set.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `settings_set.py` owns the runtime implementation.
-- `settings_set.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `SetSettings` (`ApiHandler`)
- - `async process(self, input: dict[Any, Any], request: Request) -> dict[Any, Any] | Response`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `SetSettings` is an `ApiHandler`.
-- `SetSettings` defines `process(...)`.
-- Observed side-effect areas: settings/state persistence.
-- Imported dependency areas include: `helpers`, `helpers.api`, `typing`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `settings.convert_in`, `settings.set_settings`, `settings.convert_out`, `settings.Settings`.
-- The settings payload accepts normalized `ui_control_visibility` mobile and desktop flags and returns the persisted map with the rest of the settings.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/settings_workdir_file_structure.py.dox.md b/api/settings_workdir_file_structure.py.dox.md
deleted file mode 100644
index e52a742ef..000000000
--- a/api/settings_workdir_file_structure.py.dox.md
+++ /dev/null
@@ -1,46 +0,0 @@
-# settings_workdir_file_structure.py DOX
-
-## Purpose
-
-- Own the `settings_workdir_file_structure.py` API endpoint.
-- This module handles settings workdir file structure API requests.
-- Keep this file-level DOX profile synchronized with `settings_workdir_file_structure.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `settings_workdir_file_structure.py` owns the runtime implementation.
-- `settings_workdir_file_structure.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `SettingsWorkdirFileStructure` (`ApiHandler`)
- - `async process(self, input: dict, request: Request) -> dict | Response`
- - `get_methods(cls) -> list[str]`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `SettingsWorkdirFileStructure` is an `ApiHandler`.
-- `SettingsWorkdirFileStructure` defines `process(...)`.
-- `SettingsWorkdirFileStructure` defines `get_methods(...)`.
-- Observed side-effect areas: filesystem reads, settings/state persistence.
-- Imported dependency areas include: `helpers`, `helpers.api`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `files.get_abs_path_development`, `Exception`, `file_tree.file_tree`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/skills.py.dox.md b/api/skills.py.dox.md
deleted file mode 100644
index 8da5af317..000000000
--- a/api/skills.py.dox.md
+++ /dev/null
@@ -1,54 +0,0 @@
-# skills.py DOX
-
-## Purpose
-
-- Own the `skills.py` API endpoint.
-- This module lists and manages available skills for settings and agent-facing skill flows.
-- Keep this file-level DOX profile synchronized with `skills.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `skills.py` owns the runtime implementation.
-- `skills.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `Skills` (`ApiHandler`)
- - `async process(self, input: Input, request: Request) -> Output`
- - `list_skills(self, input: Input)`
- - `delete_skill(self, input: Input)`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `Skills` is an `ApiHandler`.
-- `Skills` defines `process(...)`.
-- Observed side-effect areas: filesystem reads, filesystem deletion.
-- Imported dependency areas include: `helpers`, `helpers.api`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `skills.list_skills`, `result.sort`, `str.strip`, `skills.delete_skill`, `projects.get_project_folder`, `runtime.is_development`, `Exception`, `self.list_skills`, `strip`, `files.normalize_a0_path`, `files.get_abs_path`, `self.delete_skill`, `files.is_in_dir`, `projects.get_project_meta`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- Related tests observed by source search:
- - `tests/test_a0_connector_prompt_gating.py`
- - `tests/test_browser_agent_regressions.py`
- - `tests/test_document_query_plugin.py`
- - `tests/test_fasta2a_client.py`
- - `tests/test_office_canvas_setup.py`
- - `tests/test_office_document_store.py`
- - `tests/test_skills_runtime.py`
- - `tests/test_time_travel.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/skills_import.py.dox.md b/api/skills_import.py.dox.md
deleted file mode 100644
index 4940e833a..000000000
--- a/api/skills_import.py.dox.md
+++ /dev/null
@@ -1,44 +0,0 @@
-# skills_import.py DOX
-
-## Purpose
-
-- Own the `skills_import.py` API endpoint.
-- This module handles skills import API requests.
-- Keep this file-level DOX profile synchronized with `skills_import.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `skills_import.py` owns the runtime implementation.
-- `skills_import.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `SkillsImport` (`ApiHandler`)
- - `async process(self, input: dict, request: Request) -> dict | Response`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `SkillsImport` is an `ApiHandler`.
-- `SkillsImport` defines `process(...)`.
-- Observed side-effect areas: filesystem reads, filesystem writes, filesystem deletion.
-- Imported dependency areas include: `__future__`, `helpers`, `helpers.api`, `helpers.skills_import`, `os`, `pathlib`, `time`, `uuid`, `werkzeug.datastructures`, `werkzeug.utils`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `self.use_context`, `strip.lower`, `Path`, `tmp_dir.mkdir`, `secure_filename`, `time.strftime`, `skills_file.save`, `strip`, `files.get_abs_path`, `base.lower.endswith`, `import_skills`, `files.deabsolute_path`, `uuid.uuid4`, `tmp_path.unlink`, `base.lower`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/skills_import_preview.py.dox.md b/api/skills_import_preview.py.dox.md
deleted file mode 100644
index 107f55b2a..000000000
--- a/api/skills_import_preview.py.dox.md
+++ /dev/null
@@ -1,44 +0,0 @@
-# skills_import_preview.py DOX
-
-## Purpose
-
-- Own the `skills_import_preview.py` API endpoint.
-- This module handles skills import preview API requests.
-- Keep this file-level DOX profile synchronized with `skills_import_preview.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `skills_import_preview.py` owns the runtime implementation.
-- `skills_import_preview.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `SkillsImportPreview` (`ApiHandler`)
- - `async process(self, input: dict, request: Request) -> dict | Response`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `SkillsImportPreview` is an `ApiHandler`.
-- `SkillsImportPreview` defines `process(...)`.
-- Observed side-effect areas: filesystem reads, filesystem writes, filesystem deletion.
-- Imported dependency areas include: `__future__`, `helpers`, `helpers.api`, `helpers.skills_import`, `os`, `pathlib`, `time`, `uuid`, `werkzeug.datastructures`, `werkzeug.utils`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `self.use_context`, `strip.lower`, `Path`, `tmp_dir.mkdir`, `secure_filename`, `time.strftime`, `skills_file.save`, `strip`, `files.get_abs_path`, `base.lower.endswith`, `import_skills`, `files.deabsolute_path`, `uuid.uuid4`, `tmp_path.unlink`, `base.lower`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/skills_scan.py b/api/skills_scan.py
deleted file mode 100644
index c1ba6871a..000000000
--- a/api/skills_scan.py
+++ /dev/null
@@ -1,124 +0,0 @@
-from __future__ import annotations
-
-import shutil
-import time
-import uuid
-from pathlib import Path
-from typing import Any
-
-from helpers import files, skills
-from helpers.api import ApiHandler, Request, Response
-from helpers.skills_import import extract_skills_zip
-from werkzeug.datastructures import FileStorage
-from werkzeug.utils import secure_filename
-
-
-class SkillsScan(ApiHandler):
- """
- Prepare skill scan targets for the Settings > Skills scanner.
- """
-
- async def process(self, input: dict[str, Any], request: Request) -> dict[str, Any] | Response:
- if "skills_file" in request.files:
- return self._prepare_uploaded_archive(request.files["skills_file"])
-
- action = str(input.get("action") or "targets").strip().lower()
- if action == "targets":
- return self._list_installed_targets()
-
- return {"success": False, "error": "Invalid action"}
-
- def _list_installed_targets(self) -> dict[str, Any]:
- targets: list[dict[str, Any]] = []
- seen: set[str] = set()
- total_skills = 0
-
- for raw_root in skills.get_skill_roots():
- root = Path(raw_root)
- if not root.is_dir():
- continue
-
- skill_files = skills.discover_skill_md_files(root)
- if not skill_files:
- continue
-
- key = str(root.resolve())
- if key in seen:
- continue
- seen.add(key)
-
- skill_count = len(skill_files)
- total_skills += skill_count
- targets.append(
- {
- "path": str(root),
- "display_path": files.normalize_a0_path(str(root)),
- "skill_count": skill_count,
- }
- )
-
- targets.sort(key=lambda item: item["path"])
- return {
- "success": True,
- "target_type": "installed",
- "target_label": "Installed Agent Zero skills",
- "targets": targets,
- "paths": [item["path"] for item in targets],
- "skill_count": total_skills,
- }
-
- def _prepare_uploaded_archive(self, skills_file: FileStorage) -> dict[str, Any]:
- if not skills_file.filename:
- return {"success": False, "error": "No file selected"}
-
- base = secure_filename(skills_file.filename) # type: ignore[arg-type]
- if not base.lower().endswith(".zip"):
- return {"success": False, "error": "Skill scan uploads must be .zip files"}
-
- tmp_dir = Path(files.get_abs_path("tmp", "uploads"))
- tmp_dir.mkdir(parents=True, exist_ok=True)
- unique = uuid.uuid4().hex[:8]
- stamp = time.strftime("%Y%m%d_%H%M%S")
- tmp_path = tmp_dir / f"skills_scan_{stamp}_{unique}_{base}"
- skills_file.save(str(tmp_path))
-
- cleanup_root: Path | None = None
- try:
- scan_root, cleanup_root = extract_skills_zip(
- tmp_path,
- tmp_subdir="skill_scans",
- prefix=f"scan_{unique}",
- )
- skill_files = skills.discover_skill_md_files(scan_root)
- skill_entries = [
- {
- "path": str(skill_md.parent),
- "relative_path": str(skill_md.parent.relative_to(scan_root)),
- }
- for skill_md in skill_files
- ]
- warnings = []
- if not skill_entries:
- warnings.append("No SKILL.md files were found in the uploaded archive.")
-
- return {
- "success": True,
- "target_type": "uploaded_archive",
- "target_label": base,
- "paths": [str(scan_root)],
- "scan_path": str(scan_root),
- "display_path": files.normalize_a0_path(str(scan_root)),
- "cleanup_paths": [str(cleanup_root)],
- "skill_count": len(skill_entries),
- "skills": skill_entries,
- "warnings": warnings,
- }
- except Exception as exc:
- if cleanup_root:
- shutil.rmtree(cleanup_root, ignore_errors=True)
- return {"success": False, "error": f"Failed to prepare skill scan: {exc}"}
- finally:
- try:
- tmp_path.unlink(missing_ok=True) # type: ignore[arg-type]
- except Exception:
- pass
diff --git a/api/skills_scan.py.dox.md b/api/skills_scan.py.dox.md
deleted file mode 100644
index b277ae73e..000000000
--- a/api/skills_scan.py.dox.md
+++ /dev/null
@@ -1,46 +0,0 @@
-# skills_scan.py DOX
-
-## Purpose
-
-- Own the `skills_scan.py` API endpoint.
-- Provide scan target discovery and uploaded skills archive preparation for the Settings > Skills scanner.
-- Keep this file-level DOX profile synchronized with `skills_scan.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `skills_scan.py` owns the runtime implementation.
-- `skills_scan.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `SkillsScan` (`ApiHandler`)
- - `async process(self, input: dict[str, Any], request: Request) -> dict[str, Any] | Response`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- The JSON request action `targets` returns existing installed skill roots that contain at least one `SKILL.md`.
-- Multipart requests with `skills_file` accept only `.zip` uploads, extract them into `tmp/skill_scans`, discover contained `SKILL.md` folders, and return `paths` plus `cleanup_paths` for the scanner prompt.
-- Uploaded archives are not imported, installed, or executed by this endpoint.
-- Temporary uploaded zip files under `tmp/uploads` are deleted after extraction or failure.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- Observed side-effect areas: filesystem reads, filesystem writes, filesystem deletion.
-- Imported dependency areas include: `__future__`, `helpers`, `helpers.api`, `helpers.skills_import`, `pathlib`, `shutil`, `time`, `typing`, `uuid`, `werkzeug.datastructures`, `werkzeug.utils`.
-
-## Key Concepts
-
-- Installed target discovery uses `helpers.skills.get_skill_roots()` and filters to roots where `discover_skill_md_files()` finds skills.
-- Uploaded zip preparation uses `extract_skills_zip()` so zip entries remain bounded to the temp extraction root.
-- Response paths are local absolute paths for the scanner agent, while `display_path` provides normalized `/a0/...` style display when possible.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Do not execute uploaded files or scan targets in this endpoint.
-- Keep temp extraction paths explicit so the LLM-driven scan prompt can clean them up.
-
-## Verification
-
-- Run endpoint-specific or API tests for changed behavior; smoke-test uploaded zip and installed-skill scan modal flows when practical.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/subagents.py.dox.md b/api/subagents.py.dox.md
deleted file mode 100644
index 85777218f..000000000
--- a/api/subagents.py.dox.md
+++ /dev/null
@@ -1,49 +0,0 @@
-# subagents.py DOX
-
-## Purpose
-
-- Own the `subagents.py` API endpoint.
-- This module returns subordinate agent profile data for UI and delegation flows.
-- Keep this file-level DOX profile synchronized with `subagents.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `subagents.py` owns the runtime implementation.
-- `subagents.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `Subagents` (`ApiHandler`)
- - `async process(self, input: Input, request: Request) -> Output`
- - `get_subagents_list(self)`
- - `load_agent(self, name: str | None)`
- - `save_agent(self, name: str | None, data: dict | None)`
- - `delete_agent(self, name: str | None)`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `Subagents` is an `ApiHandler`.
-- `Subagents` defines `process(...)`.
-- Observed side-effect areas: filesystem writes, filesystem deletion.
-- Imported dependency areas include: `helpers`, `helpers.api`, `typing`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `subagents.get_agents_list`, `subagents.load_agent_data`, `subagents.SubAgent`, `subagents.save_agent_data`, `subagents.delete_agent_data`, `self.use_context`, `Exception`, `self.get_subagents_list`, `self.load_agent`, `self.save_agent`, `self.delete_agent`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- Related tests observed by source search:
- - `tests/test_skills_runtime.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/tunnel.py.dox.md b/api/tunnel.py.dox.md
deleted file mode 100644
index 3618edc6e..000000000
--- a/api/tunnel.py.dox.md
+++ /dev/null
@@ -1,48 +0,0 @@
-# tunnel.py DOX
-
-## Purpose
-
-- Own the `tunnel.py` API endpoint.
-- This module manages tunnel provider status, start, and stop actions.
-- Keep this file-level DOX profile synchronized with `tunnel.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `tunnel.py` owns the runtime implementation.
-- `tunnel.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `Tunnel` (`ApiHandler`)
- - `async process(self, input: dict, request: Request) -> dict | Response`
-- Top-level functions:
-- `async process(input: dict) -> dict | Response`
-- `stop()`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `Tunnel` is an `ApiHandler`.
-- `Tunnel` defines `process(...)`.
-- Observed side-effect areas: tunnel state.
-- Imported dependency areas include: `helpers`, `helpers.api`, `helpers.tunnel_manager`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `TunnelManager.get_instance`, `tunnel_manager.stop_tunnel`, `runtime.get_web_ui_port`, `tunnel_manager.start_tunnel`, `tunnel_manager.get_last_error`, `process`, `tunnel_manager.get_notifications`, `stop`, `tunnel_manager.get_tunnel_url`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- Related tests observed by source search:
- - `tests/test_tunnel_remote_link.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/tunnel_proxy.py.dox.md b/api/tunnel_proxy.py.dox.md
deleted file mode 100644
index b713482c9..000000000
--- a/api/tunnel_proxy.py.dox.md
+++ /dev/null
@@ -1,46 +0,0 @@
-# tunnel_proxy.py DOX
-
-## Purpose
-
-- Own the `tunnel_proxy.py` API endpoint.
-- This module proxies tunnel-related HTTP traffic through the configured tunnel provider.
-- Keep this file-level DOX profile synchronized with `tunnel_proxy.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `tunnel_proxy.py` owns the runtime implementation.
-- `tunnel_proxy.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `TunnelProxy` (`ApiHandler`)
- - `async process(self, input: dict, request: Request) -> dict | Response`
-- Top-level functions:
-- `async process(input: dict) -> dict | Response`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `TunnelProxy` is an `ApiHandler`.
-- `TunnelProxy` defines `process(...)`.
-- Observed side-effect areas: network calls, settings/state persistence, tunnel state.
-- Imported dependency areas include: `helpers`, `helpers.api`, `helpers.tunnel_manager`, `requests`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `runtime.get_arg`, `requests.post`, `process`, `dotenv.get_dotenv_value`, `response.json`, `local_process`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/upload.py.dox.md b/api/upload.py.dox.md
deleted file mode 100644
index 9fccb571f..000000000
--- a/api/upload.py.dox.md
+++ /dev/null
@@ -1,47 +0,0 @@
-# upload.py DOX
-
-## Purpose
-
-- Own the `upload.py` API endpoint.
-- This module accepts general uploads into runtime upload storage.
-- Keep this file-level DOX profile synchronized with `upload.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `upload.py` owns the runtime implementation.
-- `upload.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `UploadFile` (`ApiHandler`)
- - `async process(self, input: dict, request: Request) -> dict | Response`
- - `allowed_file(self, filename)`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `UploadFile` is an `ApiHandler`.
-- `UploadFile` defines `process(...)`.
-- Observed side-effect areas: filesystem reads, settings/state persistence.
-- Imported dependency areas include: `helpers`, `helpers.api`, `helpers.security`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `request.files.getlist`, `Exception`, `self.allowed_file`, `safe_filename`, `file.save`, `files.get_abs_path`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- Related tests observed by source search:
- - `tests/test_browser_agent_regressions.py`
- - `tests/test_image_get_security.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/upload_work_dir_files.py.dox.md b/api/upload_work_dir_files.py.dox.md
deleted file mode 100644
index 6cf9370ae..000000000
--- a/api/upload_work_dir_files.py.dox.md
+++ /dev/null
@@ -1,47 +0,0 @@
-# upload_work_dir_files.py DOX
-
-## Purpose
-
-- Own the `upload_work_dir_files.py` API endpoint.
-- This module handles workdir file operations for upload work dir files.
-- Keep this file-level DOX profile synchronized with `upload_work_dir_files.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `upload_work_dir_files.py` owns the runtime implementation.
-- `upload_work_dir_files.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `UploadWorkDirFiles` (`ApiHandler`)
- - `async process(self, input: dict, request: Request) -> dict | Response`
-- Top-level functions:
-- `async upload_files(uploaded_files: list[FileStorage], current_path: str)`
-- `async upload_file(current_path: str, filename: str, base64_content: str)`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `UploadWorkDirFiles` is an `ApiHandler`.
-- `UploadWorkDirFiles` defines `process(...)`.
-- Observed side-effect areas: filesystem writes.
-- Imported dependency areas include: `api`, `base64`, `helpers`, `helpers.api`, `helpers.file_browser`, `os`, `posixpath`, `werkzeug.datastructures`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `runtime.is_development`, `FileBrowser`, `browser.save_file_b64`, `request.files.getlist`, `browser.save_files`, `Exception`, `upload_files`, `runtime.call_development_function`, `file.stream.read`, `base64.b64encode.decode`, `extension.call_extensions_async`, `base64.b64encode`, `posixpath.join`, `str.rstrip`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/ws_dev_test.py.dox.md b/api/ws_dev_test.py.dox.md
deleted file mode 100644
index aaad42608..000000000
--- a/api/ws_dev_test.py.dox.md
+++ /dev/null
@@ -1,44 +0,0 @@
-# ws_dev_test.py DOX
-
-## Purpose
-
-- Own the `ws_dev_test.py` API endpoint.
-- This module provides a development WebSocket test namespace.
-- Keep this file-level DOX profile synchronized with `ws_dev_test.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `ws_dev_test.py` owns the runtime implementation.
-- `ws_dev_test.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `WsDevTest` (`WsHandler`)
- - `async process(self, event: str, data: dict, sid: str) -> dict[str, Any] | WsResult | None`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `WsDevTest` is a `WsHandler`.
-- `WsDevTest` defines `process(...)`.
-- Observed side-effect areas: filesystem writes, network calls, WebSocket state.
-- Imported dependency areas include: `asyncio`, `helpers`, `helpers.print_style`, `helpers.ws`, `helpers.ws_manager`, `typing`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `event.startswith`, `self.manager.register_diagnostic_watcher`, `self.manager.unregister_diagnostic_watcher`, `PrintStyle.info`, `PrintStyle.debug`, `PrintStyle.warning`, `runtime.is_development`, `WsResult.error`, `self.broadcast`, `asyncio.sleep`, `self.emit_to`, `self.dispatch_to_all_sids`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/ws_hello.py.dox.md b/api/ws_hello.py.dox.md
deleted file mode 100644
index 863b99167..000000000
--- a/api/ws_hello.py.dox.md
+++ /dev/null
@@ -1,44 +0,0 @@
-# ws_hello.py DOX
-
-## Purpose
-
-- Own the `ws_hello.py` API endpoint.
-- This module provides a small WebSocket hello/test namespace.
-- Keep this file-level DOX profile synchronized with `ws_hello.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `ws_hello.py` owns the runtime implementation.
-- `ws_hello.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `WsHello` (`WsHandler`)
- - `async process(self, event: str, data: dict, sid: str) -> dict | None`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `WsHello` is a `WsHandler`.
-- `WsHello` defines `process(...)`.
-- Observed side-effect areas: WebSocket state.
-- Imported dependency areas include: `helpers.print_style`, `helpers.ws`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `PrintStyle.info`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/api/ws_webui.py.dox.md b/api/ws_webui.py.dox.md
deleted file mode 100644
index a9cebf317..000000000
--- a/api/ws_webui.py.dox.md
+++ /dev/null
@@ -1,49 +0,0 @@
-# ws_webui.py DOX
-
-## Purpose
-
-- Own the `ws_webui.py` API endpoint.
-- This module owns the primary WebUI WebSocket namespace and event bridge.
-- Keep this file-level DOX profile synchronized with `ws_webui.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `ws_webui.py` owns the runtime implementation.
-- `ws_webui.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `WsWebui` (`WsHandler`)
- - `async on_connect(self, sid: str) -> None`
- - `async on_disconnect(self, sid: str) -> None`
- - `async process(self, event: str, data: dict, sid: str) -> dict | None`
-
-## Runtime Contracts
-
-- HTTP handlers must derive from `helpers.api.ApiHandler`; WebSocket handlers must derive from `helpers.ws.WsHandler`.
-- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
-- `WsWebui` is a `WsHandler`.
-- `WsWebui` defines `process(...)`.
-- Observed side-effect areas: network calls, WebSocket state, settings/state persistence.
-- Imported dependency areas include: `helpers`, `helpers.ws`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `extension.call_extensions_async`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve authentication, CSRF, loopback, and API-key checks unless the endpoint contract explicitly changes.
-- Update frontend callers, plugin callers, and tests together when payload shape changes.
-- Use `helpers.api.Response` for non-JSON responses, files, redirects, or status-specific replies.
-
-## Verification
-
-- Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists.
-- Related tests observed by source search:
- - `tests/test_state_sync_handler.py`
- - `tests/test_state_sync_welcome_screen.py`
- - `tests/test_ws_handlers.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/conf/model_providers.yaml b/conf/model_providers.yaml
index 5e90efa3b..12235e58f 100644
--- a/conf/model_providers.yaml
+++ b/conf/model_providers.yaml
@@ -14,7 +14,7 @@
#
# Optional fields:
# kwargs: A dictionary of extra parameters to pass to LiteLLM.
-# This is useful for `api_base`, `extra_headers`, non-secret local placeholders, etc.
+# This is useful for `api_base`, `extra_headers`, etc.
#
# Optional model listing fields (used by the Model Configuration plugin):
# models_list:
@@ -83,20 +83,6 @@ chat:
models_list:
endpoint_url: "/v1/models"
default_base: "http://host.docker.internal:1234"
- kwargs:
- a0_api_mode: chat
- api_base: "http://host.docker.internal:1234/v1"
- api_key: "lm-studio"
- llama_cpp:
- name: llama.cpp
- litellm_provider: hosted_vllm
- models_list:
- endpoint_url: "/v1/models"
- default_base: "http://host.docker.internal:8080"
- kwargs:
- a0_api_mode: chat
- api_base: "http://host.docker.internal:8080/v1"
- api_key: "llama-cpp"
mistral:
name: Mistral AI
litellm_provider: mistral
@@ -114,11 +100,6 @@ chat:
endpoint_url: "/models"
kwargs:
api_base: https://api.tokenfactory.nebius.com/v1
- nvidia_nim:
- name: NVIDIA NIM
- litellm_provider: nvidia_nim
- models_list:
- endpoint_url: "https://integrate.api.nvidia.com/v1/models"
ollama:
name: Ollama
litellm_provider: ollama
@@ -126,26 +107,12 @@ chat:
endpoint_url: "/api/tags"
format: "ollama"
default_base: "http://host.docker.internal:11434"
- kwargs:
- a0_api_mode: chat
- api_base: "http://host.docker.internal:11434"
- omlx:
- name: oMLX
- litellm_provider: hosted_vllm
- models_list:
- endpoint_url: "/v1/models"
- default_base: "http://host.docker.internal:8000"
- kwargs:
- a0_api_mode: chat
- api_base: "http://host.docker.internal:8000/v1"
- api_key: "omlx"
ollama_cloud:
name: Ollama Cloud
litellm_provider: openai
models_list:
endpoint_url: "/models"
kwargs:
- a0_api_mode: chat
api_base: https://ollama.com/v1
openai:
name: OpenAI
@@ -183,20 +150,9 @@ chat:
models_list:
endpoint_url: "https://api.venice.ai/api/v1/models"
kwargs:
- a0_api_mode: chat
api_base: https://api.venice.ai/api/v1
venice_parameters:
include_venice_system_prompt: false
- vllm:
- name: vLLM
- litellm_provider: hosted_vllm
- models_list:
- endpoint_url: "/v1/models"
- default_base: "http://host.docker.internal:8000"
- kwargs:
- a0_api_mode: chat
- api_base: "http://host.docker.internal:8000/v1"
- api_key: "vllm"
xai:
name: xAI
litellm_provider: xai
@@ -219,8 +175,6 @@ chat:
other:
name: Other OpenAI compatible
litellm_provider: openai
- kwargs:
- a0_api_mode: chat
embedding:
huggingface:
@@ -232,34 +186,12 @@ embedding:
lm_studio:
name: LM Studio
litellm_provider: lm_studio
- kwargs:
- api_base: "http://host.docker.internal:1234/v1"
- api_key: "lm-studio"
- llama_cpp:
- name: llama.cpp
- litellm_provider: hosted_vllm
- kwargs:
- api_base: "http://host.docker.internal:8080/v1"
- api_key: "llama-cpp"
mistral:
name: Mistral AI
litellm_provider: mistral
- nvidia_nim:
- name: NVIDIA NIM
- litellm_provider: nvidia_nim
- models_list:
- endpoint_url: "https://integrate.api.nvidia.com/v1/models"
ollama:
name: Ollama
litellm_provider: ollama
- kwargs:
- api_base: "http://host.docker.internal:11434"
- omlx:
- name: oMLX
- litellm_provider: hosted_vllm
- kwargs:
- api_base: "http://host.docker.internal:8000/v1"
- api_key: "omlx"
openai:
name: OpenAI
litellm_provider: openai
@@ -293,12 +225,6 @@ embedding:
endpoint_url: "https://api.venice.ai/api/v1/models"
kwargs:
api_base: https://api.venice.ai/api/v1
- vllm:
- name: vLLM
- litellm_provider: hosted_vllm
- kwargs:
- api_base: "http://host.docker.internal:8000/v1"
- api_key: "vllm"
other:
name: Other OpenAI compatible
litellm_provider: openai
diff --git a/docker/AGENTS.md b/docker/AGENTS.md
index 8fafcaaf5..0b58f32e8 100644
--- a/docker/AGENTS.md
+++ b/docker/AGENTS.md
@@ -13,8 +13,7 @@
## Local Contracts
-- Preserve the two-runtime model: the Python 3.12 framework runtime under `/opt/venv-a0` runs the WebUI, APIs, scheduler, framework imports, and plugin hooks; the Python 3.13 agent execution runtime under `/opt/venv` runs agent terminal tasks and user code.
-- Verify backend imports and plugin hooks with `/opt/venv-a0`; packages installed into `/opt/venv` do not prove framework compatibility.
+- Preserve the two-runtime model documented in the root contract: framework runtime under `/opt/venv-a0` and agent execution runtime under `/opt/venv`.
- Do not bake secrets, local `.env` values, or user data into images.
- Keep compose mounts aligned with `usr/`, `logs/`, and other runtime-state expectations.
- Image changes that affect GitHub publishing must stay synchronized with `.github/workflows/docker-publish.yml`.
@@ -32,9 +31,4 @@
## Child DOX Index
-Direct child DOX files:
-
-| Child | Scope |
-| --- | --- |
-| [base/AGENTS.md](base/AGENTS.md) | Base image Dockerfile, copied filesystem, and installation scripts. |
-| [run/AGENTS.md](run/AGENTS.md) | Runnable image Dockerfile, compose example, entrypoints, and install scripts. |
+No child DOX files.
diff --git a/docker/base/AGENTS.md b/docker/base/AGENTS.md
deleted file mode 100644
index bcfb986d7..000000000
--- a/docker/base/AGENTS.md
+++ /dev/null
@@ -1,34 +0,0 @@
-# Docker Base Image DOX
-
-## Purpose
-
-- Own the Agent Zero base image build context.
-- Build the operating system, package, Python, SearXNG, SSH, and bootstrap layers reused by runnable images.
-
-## Ownership
-
-- `Dockerfile` owns base image layering and installation order.
-- `build.txt` owns maintainer build and push command notes.
-- `fs/ins/` owns installation scripts copied into the image.
-- Files under `fs/` are copied to container root during the base build.
-
-## Local Contracts
-
-- Preserve cache-friendly package and runtime installation stages.
-- Keep locale and timezone defaults compatible with the root Docker contract.
-- Do not add secrets, user data, or local environment files to the image context.
-- Installation scripts must be noninteractive and suitable for multi-architecture buildx runs.
-
-## Work Guidance
-
-- Keep base dependencies here only when they are common to runnable Agent Zero images.
-- Coordinate Python runtime changes with root Docker documentation and runnable image setup.
-
-## Verification
-
-- Build `docker/base` when changing Dockerfile or install scripts.
-- Run a runnable image smoke check when base runtime behavior changes.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/docker/run/AGENTS.md b/docker/run/AGENTS.md
deleted file mode 100644
index bc07f5f5a..000000000
--- a/docker/run/AGENTS.md
+++ /dev/null
@@ -1,41 +0,0 @@
-# Docker Runtime Image DOX
-
-## Purpose
-
-- Own the runnable Agent Zero image context and local compose example.
-- Install Agent Zero from a selected branch onto the base image and prepare runtime entrypoints.
-
-## Ownership
-
-- `Dockerfile` owns branch-based image assembly, exposed ports, and container startup command.
-- `docker-compose.yml` owns the local compose service example.
-- `build.txt` owns maintainer build and push command notes.
-- `fs/exe/` owns runtime entrypoint, supervisor, self-update, Node eval, and service scripts.
-- `fs/ins/` owns preinstall, installation, virtualenv, Playwright, SSH, and postinstall scripts.
-- Files under `fs/` are copied to container root during the runtime build.
-
-## Local Contracts
-
-- `BRANCH` is required for branch-based Docker builds.
-- Preserve exposed ports for SSH, HTTP, and tunneled services unless docs and workflows are updated together.
-- Keep the two-runtime Python model aligned with the root contract.
-- Do not bake secrets, local `.env` values, or user data into the image.
-- Runtime startup must ensure `/a0/usr/uploads` exists before supervised services start.
-- Runtime startup raises the soft open-file limit toward `A0_NOFILE_LIMIT` (default `65535`) before supervisord starts, bounded by the container hard limit.
-- Self-update user-data backups skip Time Travel shadow history under `usr/.time_travel/` and transient Desktop agent state.
-- Self-update waits up to 180 seconds for the updated or restored WebUI health check by default; `A0_SELF_UPDATE_HEALTH_TIMEOUT_SECONDS` may override it.
-- Successful or already-current self-updates refresh an installed Codex CLI with npm on a best-effort basis; missing CLIs and registry failures must not block Agent Zero startup.
-
-## Work Guidance
-
-- Keep startup scripts explicit about framework runtime versus execution runtime.
-- Coordinate tag, branch, and publishing changes with GitHub workflow automation.
-
-## Verification
-
-- Build `docker/run` when changing Dockerfile or install scripts.
-- Smoke-test container startup after entrypoint, supervisor, port, or compose changes.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/docker/run/docker-compose.yml b/docker/run/docker-compose.yml
index b80da0272..cc48f3f1b 100644
--- a/docker/run/docker-compose.yml
+++ b/docker/run/docker-compose.yml
@@ -5,10 +5,4 @@ services:
volumes:
- ./agent-zero:/a0
ports:
- - "50080:80"
- ulimits:
- nofile:
- soft: 65535
- hard: 65535
- extra_hosts:
- - "host.docker.internal:host-gateway"
+ - "50080:80"
\ No newline at end of file
diff --git a/docker/run/fs/exe/initialize.sh b/docker/run/fs/exe/initialize.sh
index f4c4dcd25..8c329bb30 100644
--- a/docker/run/fs/exe/initialize.sh
+++ b/docker/run/fs/exe/initialize.sh
@@ -9,49 +9,9 @@ if [ -z "$1" ]; then
fi
BRANCH="$1"
-raise_open_file_limit() {
- local requested="${A0_NOFILE_LIMIT:-65535}"
- local soft
- local hard
- local target
-
- if ! [[ "$requested" =~ ^[0-9]+$ ]] || [ "$requested" -lt 1 ]; then
- echo "Warning: invalid A0_NOFILE_LIMIT='$requested'; keeping open file limit at $(ulimit -S -n)." >&2
- return
- fi
-
- soft="$(ulimit -S -n)"
- hard="$(ulimit -H -n)"
-
- if [ "$soft" = "unlimited" ]; then
- echo "Open file limit is already unlimited."
- return
- fi
-
- target="$requested"
- if [ "$hard" != "unlimited" ] && [ "$target" -gt "$hard" ]; then
- target="$hard"
- fi
-
- if [ "$target" -gt "$soft" ]; then
- if ulimit -S -n "$target"; then
- echo "Raised open file soft limit from $soft to $(ulimit -S -n) (hard: $hard)."
- else
- echo "Warning: failed to raise open file soft limit from $soft to $target (hard: $hard)." >&2
- fi
- else
- echo "Open file soft limit is $soft (target: $requested, hard: $hard)."
- fi
-}
-
-raise_open_file_limit
-
# Copy all contents from persistent /per to root directory (/) without overwriting
cp -r --no-preserve=ownership,mode /per/* /
-# Ensure upload storage exists before API and connector callers can reference it.
-mkdir -p /a0/usr/uploads
-
# allow execution of /root/.bashrc and /root/.profile
chmod 444 /root/.bashrc
chmod 444 /root/.profile
diff --git a/docker/run/fs/exe/self_update_manager.py b/docker/run/fs/exe/self_update_manager.py
index c0d008831..abf4e595f 100644
--- a/docker/run/fs/exe/self_update_manager.py
+++ b/docker/run/fs/exe/self_update_manager.py
@@ -36,7 +36,7 @@ DEFAULT_HEALTH_URL = os.environ.get(
"http://127.0.0.1:80/api/health",
)
DEFAULT_HEALTH_TIMEOUT_SECONDS = int(
- os.environ.get("A0_SELF_UPDATE_HEALTH_TIMEOUT_SECONDS", "180")
+ os.environ.get("A0_SELF_UPDATE_HEALTH_TIMEOUT_SECONDS", "120")
)
DEFAULT_HEALTH_POLL_INTERVAL_SECONDS = float(
os.environ.get("A0_SELF_UPDATE_HEALTH_POLL_INTERVAL_SECONDS", "2")
@@ -405,11 +405,6 @@ def should_exclude_from_usr_backup(
logger: AttemptLogger,
) -> bool:
parts = relative_dir.parts
- if parts and parts[0] == ".time_travel":
- logger.log(
- f"Skipping Time Travel history during usr backup: {Path('usr') / relative_dir}"
- )
- return True
if (
len(parts) >= 6
and parts[0] == "plugins"
@@ -635,29 +630,6 @@ def clean_uv_cache(logger: AttemptLogger) -> None:
logger.log(f"uv cache clean skipped after error: {exc}")
-def refresh_codex_cli(logger: AttemptLogger) -> None:
- codex_path = shutil.which("codex")
- if not codex_path:
- logger.log("Codex CLI not installed, skipping Codex refresh.")
- return
-
- npm_path = shutil.which("npm")
- if not npm_path:
- logger.log("npm executable not found, skipping Codex refresh.")
- return
-
- logger.log("Refreshing the installed Codex CLI after self-update.")
- try:
- run_command(
- [npm_path, "install", "--global", "@openai/codex@latest"],
- cwd=None,
- logger=logger,
- error_message="Failed to refresh the installed Codex CLI.",
- )
- except Exception as exc:
- logger.log(f"Codex CLI refresh skipped after error: {exc}")
-
-
def has_local_rollback_changes(repo_dir: Path) -> bool:
status = git_output(repo_dir, "status", "--porcelain=v1", "--untracked-files=all")
return bool(status.strip())
@@ -1171,7 +1143,6 @@ def execute_pending_update(
logger=logger,
)
if healthy:
- refresh_codex_cli(logger)
record_result(
status="success",
message=f"Updated Agent Zero to branch {branch}, {resolved_target['target_description']}.",
@@ -1449,7 +1420,6 @@ def docker_run_ui() -> int:
logger.log(
"Requested tag already matches the installed version, skipping file replacement."
)
- refresh_codex_cli(logger)
record_result(
status="skipped",
message="Requested tag already matches the installed version.",
@@ -1493,11 +1463,8 @@ def main(argv: list[str] | None = None) -> int:
return docker_run_ui()
if args[0] == "trigger-update":
return trigger_update_command(args[1:])
- if args[0] == "refresh-codex":
- refresh_codex_cli(AttemptLogger(LOG_FILE))
- return 0
if args[0] in {"-h", "--help"}:
- print("Usage: self_update_manager.py [docker-run-ui | trigger-update ... | refresh-codex]")
+ print("Usage: self_update_manager.py [docker-run-ui | trigger-update ...]")
return 0
print(f"Unknown command: {args[0]}", file=sys.stderr)
return 1
diff --git a/docker/run/fs/ins/install_A0.sh b/docker/run/fs/ins/install_A0.sh
index 7b5d0d807..0aeaf13ff 100644
--- a/docker/run/fs/ins/install_A0.sh
+++ b/docker/run/fs/ins/install_A0.sh
@@ -36,6 +36,8 @@ fi
# Install remaining A0 python packages
uv pip install -r /git/agent-zero/requirements.txt
+# override for packages that have unnecessarily strict dependencies
+uv pip install -r /git/agent-zero/requirements2.txt
# install playwright
bash /ins/install_playwright.sh "$@"
diff --git a/docker/run/fs/ins/install_additional.sh b/docker/run/fs/ins/install_additional.sh
index e9c844be7..31546deaa 100644
--- a/docker/run/fs/ins/install_additional.sh
+++ b/docker/run/fs/ins/install_additional.sh
@@ -46,11 +46,16 @@ install_xpra_repo() {
apt-get update
if ! xpra_install_check; then
- echo "xpra packages are not installable from ${uri} ${suite} for ${arch}; falling back to https://xpra.org trixie"
- XPRA_PACKAGES=(xpra-server xpra-x11 xpra-html5)
- configure_xpra_repo "https://xpra.org" "trixie" "$arch"
- apt-get update
- if ! xpra_install_check; then
+ if [ "$arch" != "amd64" ]; then
+ echo "xpra packages are not installable from ${uri} ${suite} for ${arch}; falling back to https://xpra.org trixie"
+ XPRA_PACKAGES=(xpra-server xpra-x11 xpra-html5)
+ configure_xpra_repo "https://xpra.org" "trixie" "$arch"
+ apt-get update
+ if ! xpra_install_check; then
+ cat /tmp/xpra-install-check.log
+ exit 1
+ fi
+ else
cat /tmp/xpra-install-check.log
exit 1
fi
diff --git a/docs/AGENTS.md b/docs/AGENTS.md
index c81fb55bd..3f3002b01 100644
--- a/docs/AGENTS.md
+++ b/docs/AGENTS.md
@@ -9,7 +9,6 @@
- `README.md`, `quickstart.md`, `guides/`, and `setup/` cover user-facing setup and workflows.
- `developer/` covers compact developer references and source handoffs.
-- `plans/` covers implementation plans, migration notes, and staged technical roadmaps.
- `res/` contains documentation images and other documentation assets.
## Local Contracts
diff --git a/docs/README.md b/docs/README.md
index 5c668f029..655dc0e44 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -12,9 +12,8 @@ docs focus on practical setup, screenshots, and user workflows.
## Quick Start
- **[Quickstart Guide](quickstart.md):** Get up and running in 5 minutes with Agent Zero.
-- **[Agent Zero Launcher](guides/launcher.md):** Use the desktop app to set up Docker, install Agent Zero, open Instances, or connect a remote Instance.
-- **[First-Run Onboarding](guides/onboarding.md):** Choose Cloud, AI account, or Local access, then select main and utility models.
-- **[Installation Guide](setup/installation.md):** A0 Launcher downloads, A0 Install, direct Docker, updates, and advanced Docker setup (includes [How to Update](setup/installation.md#how-to-update-agent-zero)).
+- **[First-Run Onboarding](guides/onboarding.md):** Choose Cloud or Local, add a provider key, and select main and utility models.
+- **[Installation Guide](setup/installation.md):** Install scripts, updates, and advanced Docker setup (includes [How to Update](setup/installation.md#how-to-update-agent-zero)).
- **[A0 CLI Connector](guides/a0-cli-connector.md):** Install the host connector for a running Agent Zero instance, use the command palette, and switch Browser modes.
- **[Self Update](guides/self-update.md):** How the in-app updater works (technical reference).
- **[VPS Deployment](setup/vps-deployment.md):** Deploy Agent Zero on a remote server.
@@ -23,13 +22,12 @@ docs focus on practical setup, screenshots, and user workflows.
## User Guides
- **[Usage Guide](guides/usage.md):** Practical tour of Agent Zero's main workflows.
-- **[Agent Zero Launcher](guides/launcher.md):** Fresh-machine Launcher walkthrough, Docker setup gate, Installs, Instances, and docs screenshot capture with Playwright/Electron.
- **[First-Run Onboarding](guides/onboarding.md):** Set up OpenRouter, our proxy API or another provider with the guided wizard.
- **[Browser Guide](guides/browser.md):** Use the built-in Browser, live Canvas surface, annotations, screenshots, host browser mode, and extensions.
- **[Desktop Guide](guides/desktop.md):** Use the built-in Linux desktop, GUI apps, and LibreOffice Writer/Calc/Impress Cowork.
- **[A0 CLI Connector](guides/a0-cli-connector.md):** Terminal-first host connector for Agent Zero, with screenshots of the host picker, connected shell, command palette, and Browser modes.
- **[Create a Small Plugin](guides/create-plugin.md):** Build and review a tiny Web UI plugin that adds an unread dot to the chat list.
-- **[Skills Guide](guides/skills.md):** Open the Skills selector, add active skills, and remove prompt protocol entries you no longer need.
+- **[Skills Guide](guides/skills.md):** Open the Skills selector, add active skills, and remove prompt extras you no longer need.
- **[Agent Profiles](guides/agent-profiles.md):** Switch the current chat profile or create a new guided profile from the chat input.
- **[Model Presets](guides/model-presets.md):** Create simple named shortcuts for model setups.
- **[Memory Guide](guides/memory.md):** Search, edit, delete, and curate memories so useful context does not become stale noise.
@@ -65,7 +63,6 @@ docs focus on practical setup, screenshots, and user workflows.
- [Quick Start](#quick-start)
- [Quickstart Guide](quickstart.md)
- - [Agent Zero Launcher](guides/launcher.md)
- [First-Run Onboarding](guides/onboarding.md)
- [Installation Guide](setup/installation.md)
- [How to Update Agent Zero](setup/installation.md#how-to-update-agent-zero)
@@ -116,7 +113,6 @@ docs focus on practical setup, screenshots, and user workflows.
- [File Browser](guides/usage.md#file-browser)
- [Memory Management](guides/usage.md#memory-management)
- [Backup And Restore](guides/usage.md#backup-and-restore)
- - [Agent Zero Launcher](guides/launcher.md)
- [Browser Guide](guides/browser.md)
- [Desktop Guide](guides/desktop.md)
- [A0 CLI Connector](guides/a0-cli-connector.md)
diff --git a/docs/guides/a0-cli-connector.md b/docs/guides/a0-cli-connector.md
index f0812efc8..6dd4c2edc 100644
--- a/docs/guides/a0-cli-connector.md
+++ b/docs/guides/a0-cli-connector.md
@@ -132,22 +132,15 @@ machine.
- [ ] Keep A0 CLI connected to the Agent Zero chat.
- [ ] In Agent Zero Web UI, open Browser plugin settings and choose **Bring Your
Own Browser**.
-- [ ] If you want Agent Zero to use an already-open personal browser window,
- open that browser first.
-- [ ] In that browser, open its remote debugging page.
+- [ ] If you want Agent Zero to use an already-open personal Chrome window, open
+ that browser first.
+- [ ] In that browser, go to `chrome://inspect/#remote-debugging`.
- [ ] Enable **Allow remote debugging for this browser instance**.
-Remote debugging pages:
-
-| Browser | Page |
-| --- | --- |
-| Chrome, Edge, Brave, Vivaldi, Chromium | `chrome://inspect/#remote-debugging` |
-| Opera | `opera://inspect/#remote-debugging` |
-

When Agent Zero performs its first Browser action against that host browser,
-the browser asks for confirmation. Click **Allow** if you trust this Agent Zero
+Chrome asks for confirmation. Click **Allow** if you trust this Agent Zero
instance and A0 CLI connection.

@@ -156,30 +149,11 @@ A0 CLI does not take over the browser while it is only checking status. Browser
control starts when Agent Zero actually needs to use the browser.
> [!IMPORTANT]
-> Remote debugging gives the connected app full control of that browser session,
+> Remote debugging gives the connected app full control of that Chrome session,
> including access to saved data, cookies, site data, and navigation. Use it only
> with trusted Agent Zero instances and browser windows you intend the agent to
> control.
-The **Host browser** list in Browser settings comes from the connected local A0
-CLI, not from the Agent Zero Web UI server. It shows Automatic, currently
-advertised debug endpoints, and **Custom endpoint**. If a newly authorized
-browser does not appear, restart or reconnect A0 CLI.
-
-If the inspect checkbox is not enough for your browser build, launch it with an
-explicit remote debugging port and a separate profile:
-
-```bash
-opera --remote-debugging-port=9222 --user-data-dir="$HOME/.config/a0-opera-debug"
-```
-
-Then choose **Custom endpoint** in Browser settings, run `/browser ws://...` in
-A0 CLI, or pass the full DevTools websocket endpoint to A0 CLI:
-
-```bash
-export A0_HOST_BROWSER_REMOTE_DEBUGGING_ENDPOINTS="ws://127.0.0.1:9222/devtools/browser/..."
-```
-
### Browser Profiles
```bash
diff --git a/docs/guides/agent-profiles.md b/docs/guides/agent-profiles.md
index b3e9d9751..41b885fd2 100644
--- a/docs/guides/agent-profiles.md
+++ b/docs/guides/agent-profiles.md
@@ -66,10 +66,6 @@ These controls are related, but they solve different problems.
| **Project** | Files, workspace, memories, instructions, secrets, and long-running context. |
| **Model Preset** | Which models are used for the chat. |
-For small local models that narrate instead of calling tools, use the bundled
-**Tiny Local** profile or the project-scoped Prompt Include recipe in
-[Local Model Tool Use](local-model-tool-use.md).
-
Example:
- use a **Project** for a client repository;
diff --git a/docs/guides/browser.md b/docs/guides/browser.md
index 33d1b719f..ed406327a 100644
--- a/docs/guides/browser.md
+++ b/docs/guides/browser.md
@@ -137,8 +137,8 @@ development, Agent Zero can install it the first time it is needed.
## Bring Your Own Browser
-Bring Your Own Browser lets Agent Zero use Chrome, Edge, Brave, Opera, Vivaldi,
-or Chromium on your own computer through A0 CLI.
+Bring Your Own Browser lets Agent Zero use Chrome, Edge, or Chromium on your own
+computer through A0 CLI.
Use it when the page, login, or browser profile should stay on your machine.
@@ -146,35 +146,8 @@ Requirements:
- [ ] Keep A0 CLI connected to the Agent Zero chat.
- [ ] Choose **Bring Your Own Browser** in Browser settings.
-- [ ] Use a Chromium-family browser on the host: Chrome, Edge, Brave, Opera, Vivaldi, or Chromium.
-- [ ] For an already-open browser, open its remote debugging page and enable **Allow remote debugging for this browser instance**.
-
-Remote debugging pages:
-
-| Browser | Page |
-| --- | --- |
-| Chrome, Edge, Brave, Vivaldi, Chromium | `chrome://inspect/#remote-debugging` |
-| Opera | `opera://inspect/#remote-debugging` |
-
-The **Host browser** list shows Automatic, currently advertised debug endpoints,
-and **Custom endpoint**. If a browser does not appear after enabling remote
-debugging, restart or reconnect the local A0 CLI. Restarting only the Agent Zero
-Web UI server does not refresh the browser inventory; the list comes from the
-connected CLI.
-
-As a fallback, launch the browser with an explicit debugging port and profile
-directory:
-
-```bash
-opera --remote-debugging-port=9222 --user-data-dir="$HOME/.config/a0-opera-debug"
-```
-
-Then choose **Custom endpoint** in Browser settings, or pass the full DevTools
-websocket endpoint to the CLI:
-
-```bash
-export A0_HOST_BROWSER_REMOTE_DEBUGGING_ENDPOINTS="ws://127.0.0.1:9222/devtools/browser/..."
-```
+- [ ] Use Chrome, Edge, or Chromium on the host.
+- [ ] For personal Chrome remote debugging, open the host browser first, go to `chrome://inspect/#remote-debugging`, and enable **Allow remote debugging for this browser instance**.

diff --git a/docs/guides/launcher.md b/docs/guides/launcher.md
deleted file mode 100644
index 2036eb88e..000000000
--- a/docs/guides/launcher.md
+++ /dev/null
@@ -1,177 +0,0 @@
-# Agent Zero Launcher
-
-Agent Zero Launcher is the desktop app for installing, running, switching, and
-opening Dockerized Agent Zero Instances without starting from Docker commands.
-
-Use it when you are setting up a new machine, when you want a quiet inventory of
-installed Agent Zero images, or when you want one place to open local and remote
-Instances.
-
-## Start Fresh On A New Machine
-
-1. Download Agent Zero Launcher from the
- [A0 Launcher releases](https://github.com/agent0ai/a0-launcher/releases).
-2. Open the app.
-3. If the launcher cannot reach Docker yet, follow the setup dialog.
-4. If Agent Zero is already hosted on another computer or VPS, click
- **Add remote Instance** instead of setting up local Docker.
-
-
-
-The first setup dialog keeps the choice simple:
-
-- **Continue** starts the local runtime setup or refreshes the runtime state.
-- **Refresh** checks again after you start Docker yourself.
-- **Add remote Instance** saves an existing Agent Zero URL and lets you use the
- Launcher without local Docker.
-
-## Installs
-
-When Docker is ready, Launcher opens to **Installs**. This page shows official
-Agent Zero release lines and local images.
-
-
-
-Cards usually mean:
-
-- **latest** tracks the newest published Agent Zero release image.
-- **ready** tracks the development-ready image when you intentionally work from
- that branch.
-- Version cards such as **1.20**, **1.19**, or **1.18** are pinned release
- images.
-- **Install** downloads an image.
-- **Run** starts an installed image as a local Instance.
-
-## Instances
-
-Open **Instances** after you run Agent Zero. This is where local containers and
-saved remote Instances live.
-
-Use the Instance card to:
-
-- open the Web UI;
-- start, stop, rename, or delete the container;
-- open logs;
-- use **Backup `/a0/usr`** to download the same user-data backup you can create
- from Agent Zero Core;
-- use **Restore `/a0/usr`** to restore that backup zip into the selected
- Instance;
-- open A0 CLI when the host connector is installed.
-
-Launcher keeps local Instances and remote Instances separate, so deleting a
-container is not the same as deleting a saved remote URL or a workspace backup.
-
-## Updating With Launcher
-
-For same-major Agent Zero updates, the Web UI **Self Update** is still the
-normal path.
-
-For a major image jump such as v1.20 -> v2.0, use Launcher or Docker to start a
-new v2.0 Instance, then restore a backup from the old Instance. In Launcher, the
-flow is: **Instances -> Backup `/a0/usr`** on the old v1.20 Instance, **Installs
--> latest -> Install/Run**, then **Instances -> Restore `/a0/usr`** on the new
-v2.0 Instance. This avoids mixing an old root install with a new Docker image.
-
-See [Updating from v1.20 to v2.0](../setup/installation.md#updating-from-v120-to-v20).
-
-## Capture Launcher Screenshots With Playwright
-
-Launcher is an Electron app, so browser-only Playwright commands are not enough.
-Use Playwright's Electron bridge and the local Electron binary from the Launcher
-repo.
-
-The pattern below installs Playwright into a temporary folder outside the repo,
-launches local Launcher content, waits for the `a0app://content/` window, and
-saves a screenshot.
-
-```bash
-mkdir -p /tmp/a0-launcher-playwright
-npm install --prefix /tmp/a0-launcher-playwright playwright
-```
-
-```bash
-NODE_PATH=/tmp/a0-launcher-playwright/node_modules node <<'JS'
-const { _electron: electron } = require("playwright");
-
-const launcher = "/home/eclypso/a0/a0-launcher";
-const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
-
-(async () => {
- const app = await electron.launch({
- executablePath: `${launcher}/node_modules/electron/dist/electron`,
- args: [launcher],
- env: {
- ...process.env,
- A0_LAUNCHER_LOCAL_REPO: launcher,
- ELECTRON_DISABLE_SECURITY_WARNINGS: "true",
- },
- });
-
- const windows = [];
- app.on("window", (page) => windows.push(page));
- windows.push(await app.firstWindow());
-
- let page = null;
- const deadline = Date.now() + 45000;
- while (Date.now() < deadline && !page) {
- page = windows.find((item) =>
- item && !item.isClosed() && item.url().startsWith("a0app://content/")
- ) || null;
- if (!page) {
- await app.waitForEvent("window", { timeout: 1000 })
- .then((item) => windows.push(item))
- .catch(() => null);
- await sleep(250);
- }
- }
-
- if (!page) throw new Error("Launcher content window did not open");
-
- await page.waitForLoadState("networkidle", { timeout: 10000 }).catch(() => null);
- await sleep(3000);
- await page.screenshot({
- path: `${launcher}/output/playwright/launcher-installs.png`,
- fullPage: false,
- });
- await app.close();
-})();
-JS
-```
-
-For docs screenshots that show the first-run runtime gate without changing the
-real machine state, open the real Launcher page and render the real runtime-gate
-component with a minimal demo state:
-
-```js
-await page.evaluate(async () => {
- const { renderRuntimeGate } = await import(
- "a0app://content/components/docker-manager/runtime-gate/runtime-gate.js"
- );
-
- renderRuntimeGate({
- stateLoaded: true,
- dockerAvailable: false,
- runtime: {
- platform: "linux",
- state: "not_provisioned",
- action: "install",
- canProvision: true,
- setupActionLabel: "Setup Agent Zero",
- detail: "No local container runtime was found.",
- },
- versions: [{ id: "latest", availability: "available" }],
- images: [],
- containers: [],
- remoteInstances: [],
- }, {
- refresh() {},
- provisionRuntime() {},
- openDockerDownload() {},
- addRemoteInstance() {},
- });
-});
-
-await page.locator(".dm-runtime-gate").screenshot({
- path: "/home/eclypso/a0/a0-launcher/output/playwright/launcher-runtime-setup.png",
-});
-```
diff --git a/docs/guides/local-model-tool-use.md b/docs/guides/local-model-tool-use.md
deleted file mode 100644
index 803583b77..000000000
--- a/docs/guides/local-model-tool-use.md
+++ /dev/null
@@ -1,65 +0,0 @@
-# Local Model Tool Use
-
-Small local models can struggle with Agent Zero's full default communication shape. The safest first fix is prompt/profile/plugin-only: use a smaller behavior contract while leaving Agent Zero's core parser and execution code unchanged.
-
-Use this guide for Ollama, LM Studio, Qwen, and similar local chat models when the model explains commands instead of calling tools.
-
-## Use The Tiny Local Profile
-
-Choose the **Tiny Local** profile when starting or switching a chat that uses a small local model.
-
-The bundled profile lives at:
-
-```text
-agents/tiny-local/
-```
-
-Tiny Local keeps the normal Agent Zero tool-call shape, but removes visible reasoning fields from the communication prompt. It tells the model to emit one executable JSON object with `tool_name` and `tool_args`.
-
-## Use A Project Prompt Include
-
-If you want to keep your current profile, create a project-local file that matches the Prompt Include plugin pattern (`*.promptinclude.md`):
-
-```text
-local-model-tool-use.promptinclude.md
-```
-
-Put this content in that file:
-
-```markdown
-## Local model tool-use discipline
-
-You are Agent Zero. Act on the user's behalf.
-
-When the user asks you to do something, do it directly. Do not explain how the user could do it themselves.
-
-Your visible assistant message must be exactly one valid JSON object.
-
-Use exactly these top-level fields: `tool_name` and `tool_args`.
-
-Do not include markdown fences, prose before the JSON, prose after the JSON, hidden reasoning, analysis, thoughts, or headlines.
-
-Choose a tool from the tools listed in the system prompt. Do not invent tool names, action names, or generic names such as `read`, `write`, `terminal`, or `multi`.
-
-For a final user-facing answer, use the `response` tool:
-
-`{"tool_name":"response","tool_args":{"text":"Done."}}`
-
-Use `response` only when the work is complete, blocked, or no tool is needed. If the user says "proceed", "continue", "go ahead", or similar after the agent named a next step, call the next appropriate tool instead of replying with a promise or status update.
-
-For work that requires a command, file action, browser action, or any other available tool, call the appropriate tool immediately.
-
-If the framework warns that your prior message was malformed, repeated, or reasoning-only, output a corrected JSON tool request immediately without explaining the warning.
-```
-
-## Keep This Prompt-Only
-
-Do not change `agent.py` for this workflow.
-
-Do not change `helpers/extract_tools.py` for this workflow.
-
-Do not create parser repair code for this workflow.
-
-Do not add duplicate execution suppression, LiteLLM transport changes, memory runtime changes, or text-editor file operation changes for this workflow.
-
-If a specific local model still cannot follow the prompt/profile/plugin-only contract, capture the exact model, prompt, response, and tool warning before considering deeper framework changes.
diff --git a/docs/guides/model-presets.md b/docs/guides/model-presets.md
index ff23a06fd..18f3bc0f2 100644
--- a/docs/guides/model-presets.md
+++ b/docs/guides/model-presets.md
@@ -83,7 +83,7 @@ You can always rename them later.
| **Model Preset** | Which models power the chat. |
| **Agent Profile** | The agent's role, tone, and prompt behavior. |
| **Project** | Workspace, files, memory, secrets, and project instructions. |
-| **Skill** | A specific procedure added to prompt protocol. |
+| **Skill** | A specific procedure added to prompt extras. |
For example, you can use the same "Researcher" Agent Profile with a cheaper
preset for simple questions and a stronger preset for difficult investigations.
diff --git a/docs/guides/onboarding.md b/docs/guides/onboarding.md
index 1b9879190..012e56c5c 100644
--- a/docs/guides/onboarding.md
+++ b/docs/guides/onboarding.md
@@ -1,59 +1,62 @@
# First-Run Onboarding
Use onboarding the first time you open Agent Zero, or any time the Web UI says
-your models still need setup. The wizard helps you pick Cloud, AI account, or
-Local access, configure a main model, choose a utility model, and start
-chatting.
+your models still need setup. The wizard helps you pick a provider, add the
+needed key or connection, choose the main model, choose the utility model, and
+start chatting.
-This example uses **OpenRouter** with a masked demo key. Replace the demo key
-with your own key.
+This example uses **Agent Zero API** with a fake demo key and
+`claude-opus-4-6`. Replace the demo key with your own key.
-## Open Onboarding
+## Choose Cloud Or Local
-Open the Web UI. The welcome screen can show account shortcuts, and the message
-composer is ready immediately.
+Open the Web UI and click **Start Onboarding** from the welcome banner. On the
+first screen, choose whether Agent Zero should use a hosted provider or a local
+model server.
-
+
-If you send a message before models are configured, Agent Zero creates the chat,
-holds the message, and shows the model gate inside the conversation. Choose
-**Cloud provider**, **AI account**, or **Local model** to open onboarding.
+Choose **Cloud** when you want to use Agent Zero API, OpenRouter, Anthropic,
+OpenAI, Google, Venice, or another hosted provider. Choose **Local** when you
+want to connect to Ollama, LM Studio, or another model server running on your
+machine.
-
+## Pick Agent Zero API
-## Choose A Provider Path
+On the Cloud provider screen, click **Agent Zero API**.
-Choose **Cloud** when you want to paste an API key for OpenRouter, OpenAI,
-Anthropic, Google, Venice, or another hosted provider.
+
-
+The ChatGPT/Codex account option is also available on this Cloud screen if you
+want to connect by device code instead of pasting a provider key.
-Choose **Account** when you want to sign in with Codex/ChatGPT, GitHub Copilot,
-Google Cloud Gemini, or xAI Grok instead of pasting a provider key.
+## Add The Key And Main Model
-
+Paste your Agent Zero API key in **API key**. The screenshot uses a fake example
+key, and the field is masked.
-Choose **Local** when you want to connect to Ollama, LM Studio, oMLX, llama.cpp,
-vLLM, or another model server running on your machine.
+Click the magnifier in **Main model** to open the model list, then choose
+`claude-opus-4-6`.
-
+
-## Configure The Main Model
+After the model is selected, click **Choose utility model**.
-For API-key providers, choose the main model and paste the provider key. The
-screenshot uses a masked demo key.
-
-
-
-After the main model is selected, click **Choose utility model**.
+
## Choose The Utility Model
-The utility model handles quick internal tasks such as summaries, naming, and
-memory. A small, fast, cheap model usually works best here. The wizard may
-prefill a utility provider and model, but it remains an explicit choice.
+The utility model handles supporting work such as summarizing, organizing
+memory, and other background tasks.
-
+Leave **Use same as Main Model** checked if you want both roles to use
+`claude-opus-4-6`.
+
+
+
+You can also uncheck **Use same as Main Model** and choose a different utility
+provider and model. For example, you might keep a strong main model for chat and
+use a faster or cheaper model for utility tasks.
Click **Finish setup** when the utility model looks right.
diff --git a/docs/guides/self-update.md b/docs/guides/self-update.md
index e52b6a438..22e490a09 100644
--- a/docs/guides/self-update.md
+++ b/docs/guides/self-update.md
@@ -10,8 +10,6 @@ For day-to-day upgrades inside a running instance:
The UI will tell you when a new A0 update is available for download. Backups are automatically managed internally during the update process.
-
-
---
## Technical reference
@@ -73,24 +71,6 @@ Self-update is intentionally limited to changes within the same major line.
If a newer major line exists, the UI points you to the Docker setup guide because those upgrades require downloading a new Docker image. They can include operating system level changes or other breaking changes outside the repository checkout.
-## v1.20 to v2.0
-
-Use the Docker image update path for v1.20 -> v2.0. Self Update can show that a
-newer major release line exists, but it intentionally keeps the version selector
-inside the current major line.
-
-
-
-The important part is moving a backup zip into a fresh v2.0 container:
-
-1. In the old v1.20 Web UI, create a backup from **Settings -> Check for Updates -> Backup & Restore -> Create Backup**.
-2. Pull `agent0ai/agent-zero:latest` in Docker Desktop or Docker CLI. For the v2.0 release, `latest` is the v2.0 image.
-3. Start a new container from that image, or use the **latest** card in **Agent Zero Launcher**.
-4. Restore the downloaded backup zip into the new v2.0 Instance.
-5. Verify the new Instance before deleting the old v1.20 container.
-
-For command examples, see [Updating from v1.20 to v2.0](../setup/installation.md#updating-from-v120-to-v20).
-
## Safety notes
- Gitignored paths are preserved during update
diff --git a/docs/guides/skills.md b/docs/guides/skills.md
index 3a959d157..215d87586 100644
--- a/docs/guides/skills.md
+++ b/docs/guides/skills.md
@@ -29,7 +29,7 @@ Click a skill to add it. Active skills are shown at the top of the selector.
To remove a skill, use the remove button in **Active skills** or uncheck it in
the list.
-Active skills are added to the **Protocol** part of the prompt. That means
+Active skills are added to the **Extras** part of the system prompt. That means
Agent Zero sees them every turn while they are active.
> [!TIP]
@@ -56,7 +56,7 @@ usually easier for the agent to follow.
| Control | What it changes |
| --- | --- |
-| **Skills** | Adds a specific procedure to the current prompt protocol. |
+| **Skills** | Adds a specific procedure to the current prompt extras. |
| **Agent Profiles** | Changes the broader role and behavior of the chat. |
| **Projects** | Adds workspace, files, memory, secrets, and project instructions. |
diff --git a/docs/guides/troubleshooting.md b/docs/guides/troubleshooting.md
index 21f8e979e..e41a7141a 100644
--- a/docs/guides/troubleshooting.md
+++ b/docs/guides/troubleshooting.md
@@ -41,7 +41,6 @@ PLAYWRIGHT_BROWSERS_PATH=tmp/playwright playwright install chromium
If **Bring Your Own Browser** mode fails:
- keep A0 CLI connected to the chat;
-- restart or reconnect A0 CLI after enabling remote debugging in a browser;
- run `/browser status` in A0 CLI;
- check that Browser settings still say **Bring Your Own Browser**;
- check **Page content access** if page text or screenshots are blocked.
diff --git a/docs/guides/usage.md b/docs/guides/usage.md
index 76dda859c..d5bc931e5 100644
--- a/docs/guides/usage.md
+++ b/docs/guides/usage.md
@@ -74,7 +74,7 @@ Use the selector to add or remove active skills.

-Active skills are added to the **Protocol** part of the prompt, so keep the
+Active skills are added to the **Extras** part of the system prompt, so keep the
list short and intentional. See the [Skills guide](skills.md).
### Agent Profiles
diff --git a/docs/quickstart.md b/docs/quickstart.md
index bdc446cf3..59c569fe3 100644
--- a/docs/quickstart.md
+++ b/docs/quickstart.md
@@ -5,13 +5,7 @@ Agent Zero, add a model or API key, open the Web UI, and give it a concrete job.
## Installation (recommended)
-Choose the path that matches your machine:
-
-- Use [A0 Launcher](guides/launcher.md) if you want a desktop app on a fresh
- machine. It can set up the local runtime, download Agent Zero, open
- Instances, or save a remote Instance URL.
-- Use A0 Install if you want the terminal path. The script handles Docker
- detection, image pull, and container setup.
+Run one command; the script handles Docker, image pull, and container setup.
**macOS / Linux:**
```bash
@@ -29,7 +23,7 @@ Follow the CLI prompts for port and authentication, complete onboarding, then op
> To update later, open **Settings UI -> Update tab -> Open Self Update** (see [How to Update](setup/installation.md#how-to-update-agent-zero)). Backups are automatically managed internally.
> [!NOTE]
-> For Launcher downloads, headless installer flags, direct Docker, manual Docker Desktop setup, volume mapping, and platform-specific detail, see the [Installation Guide](setup/installation.md).
+> For manual Docker Desktop setup, volume mapping, and platform-specific detail, see the [Installation Guide](setup/installation.md#manual-installation-advanced).
## Use Agent Zero on your real local files
@@ -62,19 +56,18 @@ For the full setup flow, host picker screenshots, command palette guidance, Brow
### Open the Web UI and complete onboarding
Open your browser and navigate to `http://localhost:`. The Web UI will
-open on the welcome screen. If models still need setup, send a message or use
-the setup shortcuts to choose Cloud, AI account, or Local access, then select
-your main and utility models.
+show the onboarding banner. Click **Start Onboarding** to choose Cloud or
+Local, add a provider key or account connection, and select your main and
+utility models.

-For a screenshot walkthrough using OpenRouter, see the
-[First-Run Onboarding guide](guides/onboarding.md).
+For a screenshot walkthrough using **Agent Zero API** with
+`claude-opus-4-6`, see the [First-Run Onboarding guide](guides/onboarding.md).
> [!NOTE]
-> Agent Zero supports hosted providers, account-backed providers, and local
-> models. Choose a strong main model for chat and a fast utility model for
-> internal tasks.
+> Agent Zero supports hosted providers and local models. You can use the same
+> provider for main and utility work, or choose separate providers for each.
### Start your first chat
@@ -148,7 +141,7 @@ Explains how to search, edit, delete, export, and curate memories before stale c
### [Open A0 Skills Guide](guides/skills.md)
-Shows the chat input **+** menu, the Skills selector, and how active skills are added to prompt protocol.
+Shows the chat input **+** menu, the Skills selector, and how active skills are added to prompt extras.
### [Open A0 Agent Profiles Guide](guides/agent-profiles.md)
diff --git a/docs/res/usage/launcher/launcher-installs.png b/docs/res/usage/launcher/launcher-installs.png
deleted file mode 100644
index 01f90fe78..000000000
Binary files a/docs/res/usage/launcher/launcher-installs.png and /dev/null differ
diff --git a/docs/res/usage/launcher/launcher-runtime-setup.png b/docs/res/usage/launcher/launcher-runtime-setup.png
deleted file mode 100644
index fc1029bec..000000000
Binary files a/docs/res/usage/launcher/launcher-runtime-setup.png and /dev/null differ
diff --git a/docs/res/usage/onboarding/onboarding-account-provider.png b/docs/res/usage/onboarding/onboarding-account-provider.png
deleted file mode 100644
index 803b1ea46..000000000
Binary files a/docs/res/usage/onboarding/onboarding-account-provider.png and /dev/null differ
diff --git a/docs/res/usage/onboarding/onboarding-agent-zero-api-key-model.png b/docs/res/usage/onboarding/onboarding-agent-zero-api-key-model.png
index 28aeebef7..c46498169 100644
Binary files a/docs/res/usage/onboarding/onboarding-agent-zero-api-key-model.png and b/docs/res/usage/onboarding/onboarding-agent-zero-api-key-model.png differ
diff --git a/docs/res/usage/onboarding/onboarding-agent-zero-api-model-dropdown.png b/docs/res/usage/onboarding/onboarding-agent-zero-api-model-dropdown.png
index 28aeebef7..b899b35ad 100644
Binary files a/docs/res/usage/onboarding/onboarding-agent-zero-api-model-dropdown.png and b/docs/res/usage/onboarding/onboarding-agent-zero-api-model-dropdown.png differ
diff --git a/docs/res/usage/onboarding/onboarding-cloud-provider.png b/docs/res/usage/onboarding/onboarding-cloud-provider.png
index dcdbcc481..efcb181fd 100644
Binary files a/docs/res/usage/onboarding/onboarding-cloud-provider.png and b/docs/res/usage/onboarding/onboarding-cloud-provider.png differ
diff --git a/docs/res/usage/onboarding/onboarding-local-ollama-main.png b/docs/res/usage/onboarding/onboarding-local-ollama-main.png
deleted file mode 100644
index 3c7891c44..000000000
Binary files a/docs/res/usage/onboarding/onboarding-local-ollama-main.png and /dev/null differ
diff --git a/docs/res/usage/onboarding/onboarding-model-gate.png b/docs/res/usage/onboarding/onboarding-model-gate.png
deleted file mode 100644
index 457939151..000000000
Binary files a/docs/res/usage/onboarding/onboarding-model-gate.png and /dev/null differ
diff --git a/docs/res/usage/onboarding/onboarding-ready.png b/docs/res/usage/onboarding/onboarding-ready.png
index c11a6ce38..81b215f68 100644
Binary files a/docs/res/usage/onboarding/onboarding-ready.png and b/docs/res/usage/onboarding/onboarding-ready.png differ
diff --git a/docs/res/usage/onboarding/onboarding-start.png b/docs/res/usage/onboarding/onboarding-start.png
index eb618ed4d..5102833d1 100644
Binary files a/docs/res/usage/onboarding/onboarding-start.png and b/docs/res/usage/onboarding/onboarding-start.png differ
diff --git a/docs/res/usage/onboarding/onboarding-utility-same-model.png b/docs/res/usage/onboarding/onboarding-utility-same-model.png
index f0c7048d9..992497535 100644
Binary files a/docs/res/usage/onboarding/onboarding-utility-same-model.png and b/docs/res/usage/onboarding/onboarding-utility-same-model.png differ
diff --git a/docs/res/usage/updating/self-update-v1-to-v2-warning.png b/docs/res/usage/updating/self-update-v1-to-v2-warning.png
deleted file mode 100644
index 1454ea2f8..000000000
Binary files a/docs/res/usage/updating/self-update-v1-to-v2-warning.png and /dev/null differ
diff --git a/docs/res/usage/updating/self-update-v2-current.png b/docs/res/usage/updating/self-update-v2-current.png
deleted file mode 100644
index f593adf5d..000000000
Binary files a/docs/res/usage/updating/self-update-v2-current.png and /dev/null differ
diff --git a/docs/setup/installation.md b/docs/setup/installation.md
index 3571d0f99..a966b9191 100644
--- a/docs/setup/installation.md
+++ b/docs/setup/installation.md
@@ -6,77 +6,24 @@
## Quick Start (Recommended)
-Agent Zero runs as a Docker container, and you now have two friendly ways to get
-there:
+The install script is the fastest way to get Agent Zero running. It handles Docker, image pulling, and container setup automatically.
-- **A0 Launcher** is the desktop app. It can download Agent Zero, create and
- manage Instances, and help set up the local container runtime when needed.
-- **A0 Install** is the terminal installer. It is best for SSH sessions,
- servers, scripted setup, recovery shells, or users who prefer commands.
-
-If Docker is already installed and running, you can also start the container
-directly.
-
-### A0 Launcher
-
-Use **A0 Launcher** when you want the guided desktop path. Download the app for
-your platform, open it, and let it check Docker or set up a runtime before it
-downloads Agent Zero.
-
-#### Downloads
-
-| Architecture | macOS | Linux | Windows |
-| --- | --- | --- | --- |
-| x86 | [Mac Intel](https://github.com/agent0ai/a0-launcher/releases/download/v0.9/a0-launcher-0.9-macos-x64.dmg) | [Linux x86](https://github.com/agent0ai/a0-launcher/releases/download/v0.9/a0-launcher-0.9-linux-x64.AppImage) | [Windows x86](https://github.com/agent0ai/a0-launcher/releases/download/v0.9/a0-launcher-0.9-windows-x64.exe) |
-| ARM64 | [Mac Apple Silicon](https://github.com/agent0ai/a0-launcher/releases/download/v0.9/a0-launcher-0.9-macos-arm64.dmg) | [Linux ARM64](https://github.com/agent0ai/a0-launcher/releases/download/v0.9/a0-launcher-0.9-linux-arm64.AppImage) | [Windows ARM64](https://github.com/agent0ai/a0-launcher/releases/download/v0.9/a0-launcher-0.9-windows-arm64.exe) |
-
-See the [A0 Launcher v0.9 release](https://github.com/agent0ai/a0-launcher/releases/tag/v0.9)
-for release notes and updater metadata. See the
-[Launcher guide](../guides/launcher.md) for the first-run walkthrough.
-
-### A0 Install
-
-Use **A0 Install** when you want the command-line path. The installer creates a
-Dockerized Agent Zero instance, mounts user data to `/a0/usr`, and tries to
-reuse an existing Docker-compatible runtime before setting one up.
-
-#### macOS / Linux
+**macOS / Linux:**
```bash
curl -fsSL https://bash.agent-zero.ai | bash
```
-#### Windows PowerShell
+**Windows (PowerShell):**
```powershell
irm https://ps.agent-zero.ai | iex
```
-#### Headless / scripted
-
-For servers and automation, Quick Start mode creates one instance and exits
-without opening menus:
-
+**Docker (run directly):**
```bash
-curl -fsSL https://bash.agent-zero.ai | bash -s -- --quick-start --name agent-zero --port 5080
+docker run -p 80:80 agent0ai/agent-zero
```
-```powershell
-& ([scriptblock]::Create((irm https://ps.agent-zero.ai))) -QuickStart -Name agent-zero -Port 5080
-```
-
-Use `--skip-runtime-setup` / `-SkipRuntimeSetup` when Docker must already be
-working and the installer should not try to set up a runtime. See the
-[A0 Install repository](https://github.com/agent0ai/a0-install) for all
-installer flags.
-
-### Docker already installed? Run this directly
-
-```bash
-docker run -p 80:80 -v a0_usr:/a0/usr agent0ai/agent-zero
-```
-
-Once the install completes, open the URL shown in your terminal or Launcher to
-access the Web UI. Complete onboarding, add your model provider or API key, then
-continue to [Step 3: Configure Agent Zero](#step-3-configure-agent-zero).
+Once the install completes, open the URL shown in your terminal to access the Web UI. Follow the prompts in the CLI to set your port and authentication, complete onboarding, add your API key, then continue to [Step 3: Configure Agent Zero](#step-3-configure-agent-zero).
> [!TIP]
> Need Agent Zero to reach host-machine files, shell, or a host browser? Install the optional [A0 CLI Connector](../guides/a0-cli-connector.md), then run `a0` to connect your terminal to this Agent Zero instance.
@@ -97,54 +44,6 @@ You'll also be prompted through the UI when a new A0 version is released. Backup
For technical details of the updater, see [Self Update](../guides/self-update.md).
-### Updating from v1.20 to v2.0
-
-Agent Zero v2.0 starts a new major release line. If your instance is on v1.20,
-the in-app Self Update can show the newer v2.x line, but it will not apply that
-jump inside the existing v1 Docker image. The safe path is:
-
-1. Create a backup zip from the old v1.20 instance.
-2. Pull the new `agent0ai/agent-zero:latest` Docker image. For the v2.0 release,
- `latest` is the v2.0 image.
-3. Start a new container from that image.
-4. Restore the backup zip into the new v2.0 instance.
-
-
-
-#### Without Agent Zero Launcher
-
-Use this path if you manage Agent Zero directly from Docker Desktop or Docker
-CLI.
-
-1. Open your old v1.20 Web UI and create a backup from **Settings -> Check for Updates -> Backup & Restore -> Create Backup**. Keep the downloaded `.zip` file.
-2. Pull the v2.0 image. In **Docker Desktop**, search for `agent0ai/agent-zero:latest` and pull that image. In **Docker CLI**, run:
- ```bash
- docker pull agent0ai/agent-zero:latest
- ```
-3. Start a new v2.0 container on a different host port so the old instance stays available:
- ```bash
- docker run -d -p 50081:80 --name agent-zero-v2 -v a0_v2_usr:/a0/usr agent0ai/agent-zero:latest
- ```
-4. Open the new v2.0 instance, complete any first-run prompts, then restore the downloaded `.zip` from **Settings -> Check for Updates -> Backup & Restore -> Restore Backup**.
-5. Verify chats, projects, memory, settings, and custom plugins before removing the old v1.20 container.
-
-#### With Agent Zero Launcher
-
-Launcher gives you the same backup/restore idea from the **Instances** page.
-
-1. Open **Instances**, choose the old v1.20 Instance, and use **Backup `/a0/usr`**.
-2. Open **Installs**, use the **latest** card, then **Install** or **Run** the image. For the v2.0 release, **latest** is the v2.0 image.
-3. Return to **Instances**, choose the new v2.0 Instance, and use **Restore `/a0/usr`** with the backup zip.
-4. Open the new Instance and verify it before deleting or stopping the old v1.20 container.
-
-Launcher keeps old and new Instances visible separately, which makes it easier
-to compare them before cleanup.
-
-> [!CAUTION]
-> Do not try to solve the v1.20 -> v2.0 jump by bind-mounting the whole old
-> `/a0` directory into a new container. Keep user data under `/a0/usr`, use the
-> backup/restore flow, and let the new image provide the v2.0 system files.
-
### Updating from Pre-v0.9.8
If you are upgrading from Agent Zero v0.9.8 or earlier to v1.1 or newer, use the migration path below. Older installs were laid out differently, so the in-app Self Update is not the right tool for that jump.
@@ -162,9 +61,9 @@ If you are upgrading from Agent Zero v0.9.8 or earlier to v1.1 or newer, use the
1. Keep the current container running
2. `docker pull agent0ai/agent-zero:latest`
-3. Start a **new** container on a different host port, for example: `docker run -d -p 50081:80 --name agent-zero-new agent0ai/agent-zero:latest`
-4. On the **old** instance: **Settings -> Check for Updates -> Backup & Restore -> Create Backup**
-5. On the **new** instance: restore the downloaded backup zip
+3. Start a **new** container on a different host port, for example: `docker run -d -p 50081:80 --name agent-zero-new agent0ai/agent-zero`
+4. On the **old** instance: **Settings -> Backup & Restore -> Create Backup**
+5. On the **new** instance: **Restore** the backup
6. Verify chats and data, then remove the old container
> [!CAUTION]
@@ -401,10 +300,10 @@ docker run -p 0:80 -v /path/to/your/work_dir:/a0/usr agent0ai/agent-zero
## Step 3: Configure Agent Zero
-The UI opens on the welcome screen. If model setup is missing, send a message
-or use the setup shortcuts to choose Cloud, AI account, or Local access, then
-select your main and utility models. For the screenshot walkthrough, see the
-[First-Run Onboarding guide](../guides/onboarding.md).
+The UI will show a welcome banner when model setup is missing. Click
+**Start Onboarding** to choose Cloud or Local, add a provider key or account
+connection, and select your main and utility models. For the screenshot
+walkthrough, see the [First-Run Onboarding guide](../guides/onboarding.md).
### Settings Configuration
@@ -539,19 +438,10 @@ Use the naming format required by your selected provider:
| OpenAI | Model name only | `claude-sonnet-4-5` |
| OpenRouter | Provider prefix mostly required | `anthropic/claude-sonnet-4-5` |
| Ollama | Model name only | `gpt-oss:20b` |
-| oMLX | API-visible model name from `/v1/models` | `Qwen3-0.6B-4bit` |
-| llama.cpp | API-visible model name from `/v1/models` or `--alias` | `local-gguf` |
-| vLLM | Hugging Face model ID or served model alias | `Qwen/Qwen2.5-1.5B-Instruct` |
> [!TIP]
> If you see "Invalid model ID," verify the provider and naming format on the provider website, or search the web for " model naming".
-#### Local Model Server Addresses From Docker
-
-When Agent Zero runs in Docker, `localhost` and `127.0.0.1` inside an API base URL mean the Agent Zero container, not your host machine. For a model server running on the host, use `http://host.docker.internal:` when available, or the Docker host gateway address such as `http://172.17.0.1:` on the default Linux bridge.
-
-If the model server only listens on host loopback, for example `127.0.0.1:`, the container still cannot reach it through the gateway. Configure the local server to listen on a Docker-reachable address such as `0.0.0.0`, and keep that port limited to trusted clients.
-
#### Context Window & Memory Split
- Set the **total context window** (e.g., 100k) first.
@@ -571,113 +461,6 @@ If the model server only listens on host loopback, for example `127.0.0.1:
---
-## Installing and Using oMLX (Apple Silicon Local Models)
-
-oMLX is a local inference server for Apple Silicon Macs. It serves MLX models through an OpenAI-compatible API and supports chat, embeddings, and model listing endpoints.
-
-> [!NOTE]
-> oMLX requires Apple Silicon and macOS 15+. On 16 GB machines, start with small quantized MLX models.
-
-### macOS oMLX Installation
-
-**Using Homebrew:**
-
-```bash
-brew tap jundot/omlx https://github.com/jundot/omlx
-brew install omlx
-omlx start
-```
-
-**Using the macOS App:**
-
-Download the oMLX app from the [official website](https://omlx.ai/) and follow the welcome flow to choose a model directory, start the server, and download or discover models.
-
-By default, oMLX serves its OpenAI-compatible API at `http://localhost:8000/v1`.
-
-To run a foreground server with oMLX's paged SSD cache enabled:
-
-```bash
-omlx serve --model-dir ~/.omlx/models --paged-ssd-cache-dir ~/.omlx/cache
-```
-
-### Configuring oMLX in Agent Zero
-
-1. Start oMLX and make sure at least one model is available in the oMLX dashboard or model directory.
-2. In Agent Zero Settings, choose **oMLX** as the Chat model, Utility model, or Embedding model provider.
-3. Use the model name shown by oMLX's model list or dashboard.
-4. Agent Zero includes Docker-friendly defaults for oMLX on the host at `http://host.docker.internal:8000/v1`. Override the API base URL only if your oMLX server runs somewhere else.
-5. Click `Save` to confirm your settings.
-
-> [!NOTE]
-> If Agent Zero runs in Docker and oMLX runs on the Mac host, ensure port **8000** is reachable from the container. The shipped Docker Compose file maps `host.docker.internal` to the host gateway for Linux Docker. Docker Desktop for macOS provides this hostname automatically.
-
----
-
-## Installing and Using llama.cpp (GGUF Local Models)
-
-llama.cpp provides `llama-server`, a lightweight OpenAI-compatible HTTP server for GGUF models. Agent Zero talks to it through the same `/v1` API used by OpenAI-compatible clients.
-
-### macOS llama.cpp Installation
-
-**Using Homebrew:**
-
-```bash
-brew install llama.cpp
-```
-
-Start a server with a downloaded GGUF model:
-
-```bash
-llama-server -m ~/models/model.gguf --port 8080 --alias local-gguf
-```
-
-By default, Agent Zero expects llama.cpp at `http://host.docker.internal:8080/v1`. The model name can be the model path returned by `/v1/models`, but using `--alias` gives you a short stable name such as `local-gguf`.
-
-### Configuring llama.cpp in Agent Zero
-
-1. Start `llama-server` and confirm `http://localhost:8080/v1/models` returns your model.
-2. In Agent Zero Settings, choose **llama.cpp** as the Chat model, Utility model, or Embedding model provider.
-3. Use the model ID shown by `/v1/models`, or the alias you passed with `--alias`.
-4. Override the API base URL only if you started `llama-server` on another host or port.
-5. Click `Save` to confirm your settings.
-
-> [!NOTE]
-> If Agent Zero runs in Docker and cannot reach a host-side `llama-server`, start the server on an address Docker can reach, for example `--host 0.0.0.0`, and keep the port firewalled to trusted clients.
-
----
-
-## Installing and Using vLLM (Local OpenAI-Compatible Serving)
-
-vLLM is a high-throughput local inference server with an OpenAI-compatible API. It is most common on Linux GPU hosts, and can also run on Apple Silicon through the vLLM Apple Silicon path or vLLM-Metal.
-
-For Apple Silicon Macs, install and activate vLLM-Metal:
-
-```bash
-curl -fsSL https://raw.githubusercontent.com/vllm-project/vllm-metal/main/install.sh | bash
-source ~/.venv-vllm-metal/bin/activate
-```
-
-Start a basic OpenAI-compatible server:
-
-```bash
-vllm serve Qwen/Qwen2.5-1.5B-Instruct --host 0.0.0.0 --port 8000
-```
-
-By default, Agent Zero expects vLLM at `http://host.docker.internal:8000/v1`, matching vLLM's default HTTP port. If another local provider already uses port 8000, start vLLM on another port and update Agent Zero's API base, for example `http://host.docker.internal:8001/v1`.
-
-### Configuring vLLM in Agent Zero
-
-1. Start vLLM and confirm `http://localhost:8000/v1/models` returns the served model.
-2. In Agent Zero Settings, choose **vLLM** as the Chat model, Utility model, or Embedding model provider.
-3. Use the model ID returned by vLLM's model list endpoint.
-4. If you started vLLM with `--api-key`, enter the same key in the advanced provider settings or environment.
-5. Click `Save` to confirm your settings.
-
-> [!NOTE]
-> vLLM serves one model at a time by default. Use a generation model for Chat and Utility slots, and a separate embedding-capable vLLM server if you want vLLM embeddings.
-
----
-
## Installing and Using Ollama (Local Models)
Ollama is a powerful tool that allows you to run various large language models locally.
@@ -744,13 +527,13 @@ Replace `` with the name of the model you want to use. For example:
1. Once you've downloaded your model(s), select it in the Settings page of the GUI.
2. Within the Chat model, Utility model, or Embedding model section, choose **Ollama** as provider.
3. Write your model code as expected by Ollama, in the format `llama3.2` or `qwen2.5:7b`
-4. Agent Zero includes Docker-friendly defaults for Ollama on the host at `http://host.docker.internal:11434`. Override the API base URL only if your Ollama server runs somewhere else.
+4. Provide your API base URL to your Ollama API endpoint, usually `http://host.docker.internal:11434`
5. Click `Save` to confirm your settings.

> [!NOTE]
-> If Agent Zero runs in Docker and Ollama runs on the host, ensure port **11434** is reachable from the container. The shipped Docker Compose file maps `host.docker.internal` to the host gateway for Linux Docker. If both services are in the same Docker network, you can use `http://:11434` instead of `host.docker.internal`.
+> If Agent Zero runs in Docker and Ollama runs on the host, ensure port **11434** is reachable from the container. If both services are in the same Docker network, you can use `http://:11434` instead of `host.docker.internal`.
### Managing Downloaded Models
diff --git a/extensions/python/AGENTS.md b/extensions/python/AGENTS.md
index 079c0ca5b..53538a8e1 100644
--- a/extensions/python/AGENTS.md
+++ b/extensions/python/AGENTS.md
@@ -31,37 +31,4 @@
## Child DOX Index
-Direct child DOX files:
-
-| Child | Scope |
-| --- | --- |
-| [_functions/AGENTS.md](_functions/AGENTS.md) | Implicit `@extensible` backend hook implementations. |
-| [agent_init/AGENTS.md](agent_init/AGENTS.md) | Agent context initialization hooks. |
-| [banners/AGENTS.md](banners/AGENTS.md) | Backend banner and discovery-card contributions. |
-| [before_main_llm_call/AGENTS.md](before_main_llm_call/AGENTS.md) | Pre-main-model-call behavior. |
-| [error_format/AGENTS.md](error_format/AGENTS.md) | Error formatting and masking behavior. |
-| [hist_add_before/AGENTS.md](hist_add_before/AGENTS.md) | Pre-history-insertion masking behavior. |
-| [hist_add_tool_result/AGENTS.md](hist_add_tool_result/AGENTS.md) | Tool-result history side effects. |
-| [job_loop/AGENTS.md](job_loop/AGENTS.md) | Periodic backend maintenance jobs. |
-| [message_loop_end/AGENTS.md](message_loop_end/AGENTS.md) | End-of-message-loop history and persistence behavior. |
-| [message_loop_prompts_after/AGENTS.md](message_loop_prompts_after/AGENTS.md) | Prompt protocol and extras assembled around message-loop prompt construction. |
-| [message_loop_prompts_before/AGENTS.md](message_loop_prompts_before/AGENTS.md) | Pre-prompt-construction message-loop gates. |
-| [message_loop_start/AGENTS.md](message_loop_start/AGENTS.md) | Start-of-message-loop iteration state. |
-| [monologue_end/AGENTS.md](monologue_end/AGENTS.md) | End-of-monologue UI and cleanup behavior. |
-| [monologue_start/AGENTS.md](monologue_start/AGENTS.md) | Start-of-monologue behavior such as chat renaming. |
-| [process_chain_end/AGENTS.md](process_chain_end/AGENTS.md) | Process-chain completion and queued-message handling. |
-| [reasoning_stream/AGENTS.md](reasoning_stream/AGENTS.md) | Full reasoning stream handling. |
-| [reasoning_stream_chunk/AGENTS.md](reasoning_stream_chunk/AGENTS.md) | Reasoning stream chunk masking. |
-| [reasoning_stream_end/AGENTS.md](reasoning_stream_end/AGENTS.md) | Reasoning stream finalization. |
-| [response_stream/AGENTS.md](response_stream/AGENTS.md) | Full assistant response stream handling. |
-| [response_stream_chunk/AGENTS.md](response_stream_chunk/AGENTS.md) | Assistant response chunk masking. |
-| [response_stream_end/AGENTS.md](response_stream_end/AGENTS.md) | Assistant response stream finalization. |
-| [startup_migration/AGENTS.md](startup_migration/AGENTS.md) | Startup migrations. |
-| [system_prompt/AGENTS.md](system_prompt/AGENTS.md) | Core system prompt section construction. |
-| [tool_execute_after/AGENTS.md](tool_execute_after/AGENTS.md) | Post-tool-execution processing. |
-| [tool_execute_before/AGENTS.md](tool_execute_before/AGENTS.md) | Pre-tool-execution processing. |
-| [user_message_ui/AGENTS.md](user_message_ui/AGENTS.md) | User-visible UI message hooks. |
-| [util_model_call_before/AGENTS.md](util_model_call_before/AGENTS.md) | Pre-utility-model-call masking. |
-| [webui_ws_connect/AGENTS.md](webui_ws_connect/AGENTS.md) | WebUI WebSocket connect behavior. |
-| [webui_ws_disconnect/AGENTS.md](webui_ws_disconnect/AGENTS.md) | WebUI WebSocket disconnect behavior. |
-| [webui_ws_event/AGENTS.md](webui_ws_event/AGENTS.md) | Incoming WebUI WebSocket event behavior. |
+No child DOX files.
diff --git a/extensions/python/_functions/AGENTS.md b/extensions/python/_functions/AGENTS.md
deleted file mode 100644
index db75fd4af..000000000
--- a/extensions/python/_functions/AGENTS.md
+++ /dev/null
@@ -1,31 +0,0 @@
-# Python Function Extensions DOX
-
-## Purpose
-
-- Own implicit `@extensible` backend hook implementations.
-- Preserve nested module, class/function, method, and `start`/`end` extension layout.
-
-## Ownership
-
-- Each nested path mirrors a Python module and qualname segment.
-- Leaf `start/` and `end/` directories own ordered extension files for that extensible function point.
-
-## Local Contracts
-
-- Do not flatten nested qualname paths into retired legacy folder names.
-- Extension functions must match the implicit hook's supplied arguments.
-- Preserve ordering prefixes where exception handling, watchdog registration, or cleanup depends on them.
-- Hooks that mirror persisted AI responses into UI logs must reuse existing stream log items and avoid duplicating live response-tool logs.
-- Recovery-loop circuit breakers must stop at the General Settings limit and render their user-visible cost warning from a core framework prompt.
-
-## Work Guidance
-
-- Keep implicit hook extensions narrow and colocated with the exact function point they extend.
-
-## Verification
-
-- Run targeted tests for the affected function point after changes.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/extensions/python/_functions/agent/Agent/hist_add_ai_response/end/_10_log_plain_responses.py b/extensions/python/_functions/agent/Agent/hist_add_ai_response/end/_10_log_plain_responses.py
deleted file mode 100644
index be026d7e2..000000000
--- a/extensions/python/_functions/agent/Agent/hist_add_ai_response/end/_10_log_plain_responses.py
+++ /dev/null
@@ -1,44 +0,0 @@
-from typing import Any
-
-from helpers import extract_tools
-from helpers.extension import Extension
-
-
-class LogPlainResponses(Extension):
- def execute(self, data: dict[str, Any] | None = None, **kwargs):
- if not self.agent or not isinstance(data, dict):
- return
-
- call_kwargs = data.get("kwargs")
- if not isinstance(call_kwargs, dict):
- call_kwargs = {}
-
- llm_result = call_kwargs.get("llm_result")
- if getattr(llm_result, "mode", "") != "responses":
- return
-
- message = call_kwargs.get("message")
- call_args = data.get("args")
- if message is None and isinstance(call_args, tuple) and len(call_args) > 1:
- message = call_args[1]
- if not isinstance(message, str) or not message:
- return
- if extract_tools.json_parse_dirty(message) is not None:
- return
-
- params = getattr(getattr(self.agent, "loop_data", None), "params_temporary", None)
- if not isinstance(params, dict) or "log_item_response" in params:
- return
-
- log_item = params.get("log_item_generating")
- if log_item is None:
- return
-
- params["log_item_response"] = log_item
- log_item.update(
- type="response",
- heading="",
- content=message,
- finished=True,
- update_progress="none",
- )
diff --git a/extensions/python/_functions/agent/Agent/hist_add_warning/end/_90_stop_unusable_response_loop.py b/extensions/python/_functions/agent/Agent/hist_add_warning/end/_90_stop_unusable_response_loop.py
deleted file mode 100644
index 2c44a910c..000000000
--- a/extensions/python/_functions/agent/Agent/hist_add_warning/end/_90_stop_unusable_response_loop.py
+++ /dev/null
@@ -1,55 +0,0 @@
-from helpers.errors import HandledException
-from helpers.extension import Extension
-from helpers.settings import get_settings
-
-
-STATE_KEY = "_unusable_response_failures"
-
-
-class StopUnusableResponseLoop(Extension):
- def execute(self, data: dict | None = None, **kwargs):
- if not self.agent or not isinstance(data, dict):
- return
-
- call_kwargs = data.get("kwargs")
- message = call_kwargs.get("message") if isinstance(call_kwargs, dict) else None
- call_args = data.get("args")
- if message is None and isinstance(call_args, tuple) and len(call_args) > 1:
- message = call_args[1]
-
- if not isinstance(message, str):
- return
- if message not in {
- self.agent.read_prompt("fw.msg_misformat.md"),
- self.agent.read_prompt("fw.msg_repeat.md"),
- }:
- return
-
- loop_data = getattr(self.agent, "loop_data", None)
- state = getattr(loop_data, "params_persistent", None)
- iteration = getattr(loop_data, "iteration", None)
- if not isinstance(state, dict) or not isinstance(iteration, int):
- return
-
- previous = state.get(STATE_KEY, {})
- if not isinstance(previous, dict):
- previous = {}
- previous_iteration = previous.get("iteration")
- if previous_iteration == iteration:
- return
-
- count = (
- previous.get("count", 0) + 1
- if previous_iteration == iteration - 1
- else 1
- )
- state[STATE_KEY] = {"iteration": iteration, "count": count}
- limit = get_settings()["max_consecutive_unusable_responses"]
- if count < limit:
- return
-
- stop_message = self.agent.read_prompt(
- "fw.msg_unusable_response_limit.md", limit=limit
- )
- self.agent.context.log.log(type="warning", content=stop_message)
- data["exception"] = HandledException(stop_message)
diff --git a/extensions/python/agent_init/AGENTS.md b/extensions/python/agent_init/AGENTS.md
deleted file mode 100644
index 3121c8244..000000000
--- a/extensions/python/agent_init/AGENTS.md
+++ /dev/null
@@ -1,26 +0,0 @@
-# Agent Init Extensions DOX
-
-## Purpose
-
-- Own backend extensions that run when an agent context initializes.
-
-## Ownership
-
-- Ordered Python files own initial UI message setup and profile settings load behavior.
-
-## Local Contracts
-
-- Keep initialization idempotent for contexts that may be restored or reloaded.
-- Preserve ordering between initial message creation and profile settings loading.
-
-## Work Guidance
-
-- Coordinate changes with profile loading, settings resolution, and startup smoke checks.
-
-## Verification
-
-- Smoke-test new chat/context initialization after changes.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/extensions/python/banners/AGENTS.md b/extensions/python/banners/AGENTS.md
deleted file mode 100644
index f2ce20f10..000000000
--- a/extensions/python/banners/AGENTS.md
+++ /dev/null
@@ -1,28 +0,0 @@
-# Banner Extensions DOX
-
-## Purpose
-
-- Own backend banner and discovery-card contributions.
-
-## Ownership
-
-- Ordered Python files append alert banners or discovery cards to the mutable `banners` list.
-
-## Local Contracts
-
-- Banner IDs must be unique and stable.
-- Use supported banner/card fields and types only.
-- Banner HTML links that trigger WebUI behavior should use supported structured actions, such as `data-banner-action`, instead of inline JavaScript handlers.
-- Do not expose secrets, local paths, or raw system diagnostics in banner text.
-
-## Work Guidance
-
-- Gate setup or warning banners on current configuration/status where possible.
-
-## Verification
-
-- Smoke-test welcome-screen banner rendering, ordering, dismissal, and CTA behavior after changes.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/extensions/python/banners/_10_unsecured_connection.py b/extensions/python/banners/_10_unsecured_connection.py
index 19145fef3..3b05b21b3 100644
--- a/extensions/python/banners/_10_unsecured_connection.py
+++ b/extensions/python/banners/_10_unsecured_connection.py
@@ -24,7 +24,7 @@ class UnsecuredConnectionCheck(Extension):
"priority": 80,
"title": "Unsecured Connection",
"html": """You are accessing Agent Zero from a non-local address without authentication.
-
+
Configure credentials in Settings → External Services → Authentication.""",
"dismissible": True,
"source": "backend"
diff --git a/extensions/python/banners/_30_system_resources.py b/extensions/python/banners/_30_system_resources.py
index 3c1949f4f..25c65a670 100644
--- a/extensions/python/banners/_30_system_resources.py
+++ b/extensions/python/banners/_30_system_resources.py
@@ -69,37 +69,34 @@ class SystemResourcesCheck(Extension):
"priority": 10,
"title": "System Resources",
"html": (
- "
"
- "
"
- "
"
- "
CPU
"
- f"
{cpu_value}
"
+ "
"
+ "
"
+ "
"
+ "
"
+ "
CPU
"
+ f"
{cpu_value}
"
"
"
f"{cpu_bar}"
"
"
- "
"
- "
"
- "
RAM
"
- f"
{ram_value}
"
+ "
"
+ "
"
+ "
RAM
"
+ f"
{ram_value}
"
"
"
f"{ram_bar}"
"
"
- "
"
- "
"
- "
Disk
"
- f"
{disk_value}
"
+ "
"
+ "
"
+ "
Disk
"
+ f"
{disk_value}
"
"
"
f"{disk_bar}"
"
"
- ""
),
@@ -120,8 +117,9 @@ class SystemResourcesCheck(Extension):
color = "#22c55e"
return (
- "
"
- f""
+ "
"
+ f""
"
"
)
diff --git a/extensions/python/before_main_llm_call/AGENTS.md b/extensions/python/before_main_llm_call/AGENTS.md
deleted file mode 100644
index 36214e3b3..000000000
--- a/extensions/python/before_main_llm_call/AGENTS.md
+++ /dev/null
@@ -1,26 +0,0 @@
-# Before Main LLM Call Extensions DOX
-
-## Purpose
-
-- Own backend behavior that runs immediately before the main LLM call.
-
-## Ownership
-
-- Ordered Python files own pre-call logging or context preparation for streaming/model execution.
-
-## Local Contracts
-
-- Do not mutate prompt or history data unless the hook contract explicitly passes mutable state for that purpose.
-- Avoid logging secrets, hidden prompt sections, or private user data.
-
-## Work Guidance
-
-- Keep this hook light because it sits on the main model hot path.
-
-## Verification
-
-- Run targeted model-call or streaming tests after behavior changes.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/extensions/python/error_format/AGENTS.md b/extensions/python/error_format/AGENTS.md
deleted file mode 100644
index 6dd136a73..000000000
--- a/extensions/python/error_format/AGENTS.md
+++ /dev/null
@@ -1,26 +0,0 @@
-# Error Format Extensions DOX
-
-## Purpose
-
-- Own backend error formatting and masking before errors are shown or logged.
-
-## Ownership
-
-- Ordered Python files own mutation of error-format data passed by the hook.
-
-## Local Contracts
-
-- Preserve secret masking and safe user-facing error messages.
-- Do not expose raw tokens, credentials, hidden prompts, or private payloads.
-
-## Work Guidance
-
-- Keep masking rules conservative and synchronized with tool/model error surfaces.
-
-## Verification
-
-- Test or inspect representative masked and unmasked error paths after changes.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/extensions/python/hist_add_before/AGENTS.md b/extensions/python/hist_add_before/AGENTS.md
deleted file mode 100644
index 791b47294..000000000
--- a/extensions/python/hist_add_before/AGENTS.md
+++ /dev/null
@@ -1,26 +0,0 @@
-# History Add Before Extensions DOX
-
-## Purpose
-
-- Own preprocessing before messages are added to agent history.
-
-## Ownership
-
-- Ordered Python files own history content masking before persistence or model reuse.
-
-## Local Contracts
-
-- Preserve secret and sensitive-content masking before history storage.
-- Do not remove fields required by downstream history organization or replay.
-
-## Work Guidance
-
-- Coordinate history mutation changes with message persistence and plugin-owned history consumers.
-
-## Verification
-
-- Test message history insertion for masked content after changes.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/extensions/python/hist_add_tool_result/AGENTS.md b/extensions/python/hist_add_tool_result/AGENTS.md
deleted file mode 100644
index c1dd0ecf2..000000000
--- a/extensions/python/hist_add_tool_result/AGENTS.md
+++ /dev/null
@@ -1,27 +0,0 @@
-# History Tool Result Extensions DOX
-
-## Purpose
-
-- Own processing after tool results are added to history.
-
-## Ownership
-
-- Ordered Python files own tool-call file persistence and related history side effects.
-
-## Local Contracts
-
-- Preserve tool result traceability without leaking secrets.
-- Keep file artifacts inside expected runtime/user-owned paths.
-- Skip BACKGROUND contexts; background workers must remain ephemeral and must not create chat message files.
-
-## Work Guidance
-
-- Coordinate changes with tool output storage and chat persistence behavior.
-
-## Verification
-
-- Smoke-test a tool call that produces persisted output after changes.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/extensions/python/hist_add_tool_result/_90_save_tool_call_file.py b/extensions/python/hist_add_tool_result/_90_save_tool_call_file.py
index fd5a32854..21d6ea0ee 100644
--- a/extensions/python/hist_add_tool_result/_90_save_tool_call_file.py
+++ b/extensions/python/hist_add_tool_result/_90_save_tool_call_file.py
@@ -1,5 +1,4 @@
from typing import Any
-from agent import AgentContextType
from helpers.extension import Extension
from helpers import files, persist_chat
import os, re
@@ -10,9 +9,6 @@ class SaveToolCallFile(Extension):
def execute(self, data: dict[str, Any] | None = None, **kwargs):
if not self.agent:
return
-
- if self.agent.context.type == AgentContextType.BACKGROUND:
- return
if not data:
return
diff --git a/extensions/python/job_loop/AGENTS.md b/extensions/python/job_loop/AGENTS.md
deleted file mode 100644
index afa7597a3..000000000
--- a/extensions/python/job_loop/AGENTS.md
+++ /dev/null
@@ -1,26 +0,0 @@
-# Job Loop Extensions DOX
-
-## Purpose
-
-- Own periodic backend maintenance jobs.
-
-## Ownership
-
-- Ordered Python files own cleanup of expired API chats, cache trimming, and future job-loop tasks.
-
-## Local Contracts
-
-- Jobs must be idempotent and safe to run repeatedly.
-- Keep cleanup scoped to owned caches, temporary contexts, or documented runtime state.
-
-## Work Guidance
-
-- Avoid expensive work on every loop; use timestamps or thresholds when practical.
-
-## Verification
-
-- Run targeted cleanup/cache tests or smoke-test job-loop startup after changes.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/extensions/python/message_loop_end/AGENTS.md b/extensions/python/message_loop_end/AGENTS.md
deleted file mode 100644
index 2200c4aa3..000000000
--- a/extensions/python/message_loop_end/AGENTS.md
+++ /dev/null
@@ -1,27 +0,0 @@
-# Message Loop End Extensions DOX
-
-## Purpose
-
-- Own backend behavior that runs after a message loop completes.
-
-## Ownership
-
-- Ordered Python files own history organization and chat persistence at loop end.
-
-## Local Contracts
-
-- Preserve history consistency before saving chats.
-- Do not skip persistence for successful loops unless the hook contract explicitly permits it.
-- History compression that rewrites local history must clear the active Responses provider continuation while preserving stored response IDs for cleanup.
-
-## Work Guidance
-
-- Coordinate changes with chat serialization, history organization, and WebUI refresh behavior.
-
-## Verification
-
-- Smoke-test sending a message, reloading the chat, and checking persisted history after changes.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/extensions/python/message_loop_end/_10_organize_history.py b/extensions/python/message_loop_end/_10_organize_history.py
index 0cc205bd6..d91c9ef80 100644
--- a/extensions/python/message_loop_end/_10_organize_history.py
+++ b/extensions/python/message_loop_end/_10_organize_history.py
@@ -1,18 +1,10 @@
from helpers.extension import Extension
from agent import LoopData
from helpers.defer import DeferredTask, THREAD_BACKGROUND
-from helpers.history import clear_responses_provider_state
DATA_NAME_TASK = "_organize_history_task"
-async def compress_history(agent) -> bool:
- compressed = bool(await agent.history.compress())
- if compressed:
- clear_responses_provider_state(agent)
- return compressed
-
-
class OrganizeHistory(Extension):
async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
if not self.agent:
@@ -25,6 +17,6 @@ class OrganizeHistory(Extension):
# start task
task = DeferredTask(thread_name=THREAD_BACKGROUND)
- task.start_task(compress_history, self.agent)
+ task.start_task(self.agent.history.compress)
# set to agent to be able to wait for it
self.agent.set_data(DATA_NAME_TASK, task)
diff --git a/extensions/python/message_loop_prompts_after/AGENTS.md b/extensions/python/message_loop_prompts_after/AGENTS.md
deleted file mode 100644
index d18b1c5a1..000000000
--- a/extensions/python/message_loop_prompts_after/AGENTS.md
+++ /dev/null
@@ -1,31 +0,0 @@
-# Message Loop Prompts After Extensions DOX
-
-## Purpose
-
-- Own prompt protocol, prompt extras, and history reattachment around primary message-loop prompt construction.
-
-## Ownership
-
-- Ordered Python files own current datetime, relevant-skill hints, loaded-skill history reattachment, agent info, parallel job status, and workdir extras injection.
-- Explicitly loaded skill bodies belong in tool-result history with metadata so they can survive persistence and be reattached after compaction.
-- Explicitly loaded skill IDs are chat-wide context data, not agent-local state.
-- Skills must not write selected or loaded skill bodies into protocol or extras.
-
-## Local Contracts
-
-- Keep injected content bounded and clearly attributed.
-- Preserve ordering where later prompt extras depend on earlier recall or load results.
-- Do not expose secrets or private files from workdir extras.
-- Relevant-skill recall should search the raw user message when available, not the rendered history wrapper.
-
-## Work Guidance
-
-- Coordinate prompt protocol, history-reattachment, and prompt-extra changes with skill, workdir, and profile contracts.
-
-## Verification
-
-- Inspect rendered prompt protocol/history/extras or run prompt-construction tests after changes.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/extensions/python/message_loop_prompts_after/_63_recall_relevant_skills.py b/extensions/python/message_loop_prompts_after/_63_recall_relevant_skills.py
index a810cece4..c4b2a45e8 100644
--- a/extensions/python/message_loop_prompts_after/_63_recall_relevant_skills.py
+++ b/extensions/python/message_loop_prompts_after/_63_recall_relevant_skills.py
@@ -8,13 +8,9 @@ class RecallRelevantSkills(Extension):
if not self.agent or loop_data.iteration != 0:
return
- content = loop_data.user_message.content if loop_data.user_message else ""
- if isinstance(content, dict):
- user_instruction = str(content.get("user_message") or "").strip()
- else:
- user_instruction = (
- loop_data.user_message.output_text() if loop_data.user_message else ""
- ).strip()
+ user_instruction = (
+ loop_data.user_message.output_text() if loop_data.user_message else ""
+ ).strip()
if len(user_instruction) < 8:
return
diff --git a/extensions/python/message_loop_prompts_after/_65_include_loaded_skills.py b/extensions/python/message_loop_prompts_after/_65_include_loaded_skills.py
index 6c604005a..b3f88f988 100644
--- a/extensions/python/message_loop_prompts_after/_65_include_loaded_skills.py
+++ b/extensions/python/message_loop_prompts_after/_65_include_loaded_skills.py
@@ -1,78 +1,38 @@
from helpers.extension import Extension
-from helpers import skills, tokens
+from helpers import skills
+from tools.skills_tool import DATA_NAME_LOADED_SKILLS
from agent import LoopData
-SKILL_REATTACHMENT_TOKEN_BUDGET = 12_000
-SKILL_REATTACHMENT_HEADER = (
- "Reattached loaded skill instructions after history compaction."
-)
-
-
class IncludeLoadedSkills(Extension):
async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
if not self.agent:
return
- skill_names = skills.get_loaded_skill_names(self.agent)
+ extras = loop_data.extras_persistent
+
+ # Get loaded skills names
+ skill_names = self.agent.data.get(DATA_NAME_LOADED_SKILLS)
if not skill_names:
return
- # Loaded skill bodies live in tool-result history. This hook only keeps
- # the ledger clean and restores bodies that compaction hid.
+ # load skill text here
+ content = ""
visible_skill_names = []
- loaded_skills = []
for skill_name in skill_names:
- skill = skills.find_skill(skill_name, agent=self.agent)
- if not skill:
+ if not skills.find_skill(skill_name, agent=self.agent):
continue
- visible_skill_names.append(skill.name)
- loaded_skills.append(skill)
- skills.set_loaded_skill_names(self.agent, visible_skill_names)
-
- self._reattach_missing_skill_bodies(loop_data, loaded_skills)
-
- def _reattach_missing_skill_bodies(self, loop_data: LoopData, loaded_skills):
- if not self.agent or not loaded_skills:
+ visible_skill_names.append(skill_name)
+ skill_data = skills.load_skill_for_agent(skill_name=skill_name, agent=self.agent)
+ content += "\n\n" + skill_data
+ self.agent.data[DATA_NAME_LOADED_SKILLS] = visible_skill_names
+ content = content.strip()
+ if not content:
return
- visible_skill_names = _visible_skill_names(loop_data.history_output)
- selected = []
- used_tokens = 0
- for skill in reversed(loaded_skills):
- if skill.name in visible_skill_names:
- continue
-
- skill_data = skills.load_skill_for_agent(
- skill_name=skill.name,
- agent=self.agent,
- )
- message = f"{SKILL_REATTACHMENT_HEADER}\n\n{skill_data}"
- message_tokens = tokens.approximate_tokens(message)
- if used_tokens + message_tokens > SKILL_REATTACHMENT_TOKEN_BUDGET:
- continue
-
- selected.append((skill, message))
- used_tokens += message_tokens
-
- for skill, message in reversed(selected):
- history_message = self.agent.hist_add_tool_result(
- "skills_tool",
- message,
- skill_instructions={
- "name": skill.name,
- "path": str(skill.path),
- "source": "skills_tool:reattach",
- "content_included": True,
- },
- )
- loop_data.history_output.extend(history_message.output())
-
-
-def _visible_skill_names(history_output) -> set[str]:
- return {
- name
- for message in history_output or []
- if (name := skills.skill_instruction_name(message))
- }
+ # Inject into extras
+ extras["loaded_skills"] = self.agent.read_prompt(
+ "agent.system.skills.loaded.md",
+ skills=content,
+ )
diff --git a/extensions/python/message_loop_prompts_after/_72_include_parallel_jobs.py b/extensions/python/message_loop_prompts_after/_72_include_parallel_jobs.py
deleted file mode 100644
index 70bc0ce4e..000000000
--- a/extensions/python/message_loop_prompts_after/_72_include_parallel_jobs.py
+++ /dev/null
@@ -1,13 +0,0 @@
-from helpers.extension import Extension
-from agent import LoopData
-from helpers import parallel_tools
-
-
-class IncludeParallelJobs(Extension):
- async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
- if not self.agent:
- return
-
- extras = await parallel_tools.build_parallel_jobs_extras(self.agent)
- if extras:
- loop_data.extras_temporary["parallel_jobs"] = extras
diff --git a/extensions/python/message_loop_prompts_before/AGENTS.md b/extensions/python/message_loop_prompts_before/AGENTS.md
deleted file mode 100644
index 3483592c7..000000000
--- a/extensions/python/message_loop_prompts_before/AGENTS.md
+++ /dev/null
@@ -1,26 +0,0 @@
-# Message Loop Prompts Before Extensions DOX
-
-## Purpose
-
-- Own preprocessing before message-loop prompt construction.
-
-## Ownership
-
-- Ordered Python files own history organization waits and related prompt-preparation gates.
-
-## Local Contracts
-
-- Preserve history consistency before prompts are assembled.
-- Avoid blocking indefinitely on background organization tasks.
-
-## Work Guidance
-
-- Keep waiting behavior observable and bounded.
-
-## Verification
-
-- Smoke-test prompt construction after chats with pending history organization.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/extensions/python/message_loop_prompts_before/_90_organize_history_wait.py b/extensions/python/message_loop_prompts_before/_90_organize_history_wait.py
index 08966d2b1..b2c925fe8 100644
--- a/extensions/python/message_loop_prompts_before/_90_organize_history_wait.py
+++ b/extensions/python/message_loop_prompts_before/_90_organize_history_wait.py
@@ -1,9 +1,6 @@
from helpers.extension import Extension
from agent import LoopData
-from extensions.python.message_loop_end._10_organize_history import (
- DATA_NAME_TASK,
- compress_history,
-)
+from extensions.python.message_loop_end._10_organize_history import DATA_NAME_TASK
from helpers.defer import DeferredTask, THREAD_BACKGROUND
MAX_SYNC_COMPRESSION_PASSES = 64
@@ -36,7 +33,7 @@ class OrganizeHistoryWait(Extension):
else:
# no task was running, start and wait
self.agent.context.log.set_progress("Compressing history...")
- compressed = await compress_history(self.agent)
+ compressed = await self.agent.history.compress()
after_tokens = self.agent.history.get_tokens()
if not compressed or after_tokens >= before_tokens:
diff --git a/extensions/python/message_loop_start/AGENTS.md b/extensions/python/message_loop_start/AGENTS.md
deleted file mode 100644
index a0c6c1790..000000000
--- a/extensions/python/message_loop_start/AGENTS.md
+++ /dev/null
@@ -1,26 +0,0 @@
-# Message Loop Start Extensions DOX
-
-## Purpose
-
-- Own backend behavior that runs at the start of each message loop iteration.
-
-## Ownership
-
-- Ordered Python files own iteration counters and future loop-start state setup.
-
-## Local Contracts
-
-- Keep per-loop counters deterministic and scoped to the active context.
-- Do not reset state owned by monologue-level hooks.
-
-## Work Guidance
-
-- Coordinate loop-start state changes with logging, streaming, and process-chain behavior.
-
-## Verification
-
-- Smoke-test multi-turn conversations after changes.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/extensions/python/monologue_end/AGENTS.md b/extensions/python/monologue_end/AGENTS.md
deleted file mode 100644
index b842ba071..000000000
--- a/extensions/python/monologue_end/AGENTS.md
+++ /dev/null
@@ -1,26 +0,0 @@
-# Monologue End Extensions DOX
-
-## Purpose
-
-- Own backend behavior that runs when a monologue ends.
-
-## Ownership
-
-- Ordered Python files own waiting-for-input UI message behavior and future monologue-end cleanup.
-
-## Local Contracts
-
-- Preserve clear UI state when the agent stops for user input.
-- Do not leave loading/processing indicators stale.
-
-## Work Guidance
-
-- Coordinate changes with WebUI loading state and message-loop persistence.
-
-## Verification
-
-- Smoke-test an agent response that returns to waiting-for-input state.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/extensions/python/monologue_start/AGENTS.md b/extensions/python/monologue_start/AGENTS.md
deleted file mode 100644
index 3c4b32c48..000000000
--- a/extensions/python/monologue_start/AGENTS.md
+++ /dev/null
@@ -1,27 +0,0 @@
-# Monologue Start Extensions DOX
-
-## Purpose
-
-- Own backend behavior that runs when a monologue starts.
-
-## Ownership
-
-- Ordered Python files own automatic chat renaming and future monologue-start setup.
-
-## Local Contracts
-
-- Keep automatic rename behavior bounded and non-destructive.
-- Do not override explicit user chat names without the intended guard conditions.
-- Surface Utility Model rename failures with one scoped error notification per chat.
-
-## Work Guidance
-
-- Coordinate rename behavior with chat persistence and WebUI refresh after successful saves.
-
-## Verification
-
-- Smoke-test new chat naming and existing named chat behavior after changes.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/extensions/python/monologue_start/_60_rename_chat.py b/extensions/python/monologue_start/_60_rename_chat.py
index ee2361cf6..c23486b94 100644
--- a/extensions/python/monologue_start/_60_rename_chat.py
+++ b/extensions/python/monologue_start/_60_rename_chat.py
@@ -1,7 +1,5 @@
from helpers import persist_chat, tokens
from helpers.extension import Extension
-from helpers.notification import NotificationManager, NotificationPriority, NotificationType
-from helpers.state_monitor_integration import mark_dirty_all
from agent import LoopData
import asyncio
@@ -31,35 +29,16 @@ class RenameChat(Extension):
"fw.rename_chat.msg.md", current_name=current_name, history=history_text
)
# call utility model
- try:
- new_name = await self.agent.call_utility_model(
- system=system, message=message, background=True
- )
- except Exception:
- NotificationManager.send_notification(
- type=NotificationType.ERROR,
- priority=NotificationPriority.NORMAL,
- title="Chat Rename Failed",
- message="Automatic chat renaming failed because the Utility Model was not reachable.",
- detail=(
- "Automatic chat renaming uses the Utility Model. Check Settings > Models > "
- "Utility Model, provider/API key, and network reachability."
- ),
- display_time=10,
- group="chat_rename",
- id=f"chat_rename_failed_{self.agent.context.id}",
- )
- return
+ new_name = await self.agent.call_utility_model(
+ system=system, message=message, background=True
+ )
# update name
if new_name:
- new_name = " ".join(str(new_name).split())
+ # trim name to max length if needed
if len(new_name) > 40:
new_name = new_name[:40] + "..."
- if not new_name:
- return
# apply to context and save
self.agent.context.name = new_name
persist_chat.save_tmp_chat(self.agent.context)
- mark_dirty_all(reason="monologue_start.RenameChat.change_name")
- except Exception:
+ except Exception as e:
pass # non-critical
diff --git a/extensions/python/process_chain_end/AGENTS.md b/extensions/python/process_chain_end/AGENTS.md
deleted file mode 100644
index c70340417..000000000
--- a/extensions/python/process_chain_end/AGENTS.md
+++ /dev/null
@@ -1,26 +0,0 @@
-# Process Chain End Extensions DOX
-
-## Purpose
-
-- Own backend behavior after process-chain execution completes.
-
-## Ownership
-
-- Ordered Python files own queued-message processing and future process-chain completion behavior.
-
-## Local Contracts
-
-- Preserve queue ordering and avoid duplicate message processing.
-- Keep queue side effects synchronized with chat persistence and WebUI state.
-
-## Work Guidance
-
-- Coordinate changes with message queue components and external integration plugins.
-
-## Verification
-
-- Smoke-test queued messages and final response handling after changes.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/extensions/python/reasoning_stream/AGENTS.md b/extensions/python/reasoning_stream/AGENTS.md
deleted file mode 100644
index 6cd3e6b2e..000000000
--- a/extensions/python/reasoning_stream/AGENTS.md
+++ /dev/null
@@ -1,26 +0,0 @@
-# Reasoning Stream Extensions DOX
-
-## Purpose
-
-- Own handling of full reasoning stream updates.
-
-## Ownership
-
-- Ordered Python files own logging reasoning content from stream state.
-
-## Local Contracts
-
-- Preserve masking and privacy rules for reasoning content.
-- Keep stream logging compatible with chunk and end hooks.
-
-## Work Guidance
-
-- Coordinate reasoning stream changes with UI log rendering and hidden-content policy.
-
-## Verification
-
-- Smoke-test reasoning stream display/logging when the active model provides reasoning.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/extensions/python/reasoning_stream_chunk/AGENTS.md b/extensions/python/reasoning_stream_chunk/AGENTS.md
deleted file mode 100644
index 4e7461224..000000000
--- a/extensions/python/reasoning_stream_chunk/AGENTS.md
+++ /dev/null
@@ -1,26 +0,0 @@
-# Reasoning Stream Chunk Extensions DOX
-
-## Purpose
-
-- Own handling of incremental reasoning stream chunks.
-
-## Ownership
-
-- Ordered Python files own chunk-level masking before reasoning is displayed or stored.
-
-## Local Contracts
-
-- Mask secrets and sensitive content before chunk data reaches logs or UI.
-- Keep chunk mutation compatible with final stream-end masking.
-
-## Work Guidance
-
-- Keep chunk processing lightweight for streaming performance.
-
-## Verification
-
-- Smoke-test streamed reasoning with representative sensitive patterns after changes.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/extensions/python/reasoning_stream_end/AGENTS.md b/extensions/python/reasoning_stream_end/AGENTS.md
deleted file mode 100644
index d702b22ad..000000000
--- a/extensions/python/reasoning_stream_end/AGENTS.md
+++ /dev/null
@@ -1,26 +0,0 @@
-# Reasoning Stream End Extensions DOX
-
-## Purpose
-
-- Own finalization of reasoning stream content.
-
-## Ownership
-
-- Ordered Python files own final reasoning masking and end-of-stream cleanup.
-
-## Local Contracts
-
-- Preserve final masking even if earlier chunk masking missed content.
-- Keep end-state consistent with reasoning stream log entries.
-
-## Work Guidance
-
-- Coordinate final masking changes with chunk masking and UI rendering.
-
-## Verification
-
-- Smoke-test reasoning stream completion with sensitive-content cases.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/extensions/python/response_stream/AGENTS.md b/extensions/python/response_stream/AGENTS.md
deleted file mode 100644
index 53a42df09..000000000
--- a/extensions/python/response_stream/AGENTS.md
+++ /dev/null
@@ -1,27 +0,0 @@
-# Response Stream Extensions DOX
-
-## Purpose
-
-- Own handling of full assistant response stream updates.
-
-## Ownership
-
-- Ordered Python files own response logging, include-alias replacement, and live response updates.
-
-## Local Contracts
-
-- Keep streaming output synchronized with UI log items.
-- Preserve include-alias replacement semantics where prompts/tools rely on them.
-- Do not expose unmasked secrets in live responses.
-
-## Work Guidance
-
-- Coordinate stream changes with chunk/end hooks and message rendering.
-
-## Verification
-
-- Smoke-test streamed responses, live updates, and include alias replacement after changes.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/extensions/python/response_stream_chunk/AGENTS.md b/extensions/python/response_stream_chunk/AGENTS.md
deleted file mode 100644
index 9ceff97a6..000000000
--- a/extensions/python/response_stream_chunk/AGENTS.md
+++ /dev/null
@@ -1,26 +0,0 @@
-# Response Stream Chunk Extensions DOX
-
-## Purpose
-
-- Own handling of incremental assistant response chunks.
-
-## Ownership
-
-- Ordered Python files own chunk-level response masking.
-
-## Local Contracts
-
-- Mask secrets before response chunks reach UI or persisted logs.
-- Keep chunk processing compatible with final response stream masking.
-
-## Work Guidance
-
-- Keep per-chunk work lightweight for streaming responsiveness.
-
-## Verification
-
-- Smoke-test streaming responses with sensitive patterns after changes.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/extensions/python/response_stream_end/AGENTS.md b/extensions/python/response_stream_end/AGENTS.md
deleted file mode 100644
index 963620e5d..000000000
--- a/extensions/python/response_stream_end/AGENTS.md
+++ /dev/null
@@ -1,26 +0,0 @@
-# Response Stream End Extensions DOX
-
-## Purpose
-
-- Own finalization of assistant response stream content.
-
-## Ownership
-
-- Ordered Python files own final masking and stream-end log updates.
-
-## Local Contracts
-
-- Preserve final masking before response content is considered complete.
-- Keep log state consistent with streamed chunks and final response text.
-
-## Work Guidance
-
-- Coordinate finalization changes with live response and message rendering behavior.
-
-## Verification
-
-- Smoke-test response completion and persisted message display after changes.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/extensions/python/startup_migration/AGENTS.md b/extensions/python/startup_migration/AGENTS.md
deleted file mode 100644
index d3ffc39a0..000000000
--- a/extensions/python/startup_migration/AGENTS.md
+++ /dev/null
@@ -1,29 +0,0 @@
-# Startup Migration Extensions DOX
-
-## Purpose
-
-- Own backend startup migrations.
-
-## Ownership
-
-- Ordered Python files in this folder own idempotent migration steps that run during startup.
-
-## Local Contracts
-
-- Migrations must be safe to run repeatedly.
-- Preserve user data and create backups or reversible paths when changing durable state.
-- Keep long-running work bounded and observable.
-- `_10_self_update_manager.py` may replace `/exe/self_update_manager.py` from the repository copy when the installed runtime updater is stale; it must validate required safety markers and keep a backup before replacement.
-- After synchronizing a stale self-update manager, `_10_self_update_manager.py` starts that manager's best-effort Codex CLI refresh in the background so the update that introduces the hook does not need a second restart.
-
-## Work Guidance
-
-- Add migrations only for durable state changes that cannot be handled lazily elsewhere.
-
-## Verification
-
-- Smoke-test startup on a clean checkout and on representative existing user state when practical.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/extensions/python/startup_migration/_10_self_update_manager.py b/extensions/python/startup_migration/_10_self_update_manager.py
deleted file mode 100644
index fff24cc1c..000000000
--- a/extensions/python/startup_migration/_10_self_update_manager.py
+++ /dev/null
@@ -1,146 +0,0 @@
-from __future__ import annotations
-
-import os
-import shutil
-import stat
-import subprocess
-import sys
-from pathlib import Path
-from typing import Any
-
-from helpers.extension import Extension
-from helpers.print_style import PrintStyle
-
-
-SELF_UPDATE_MANAGER_PATH = Path(
- os.environ.get("A0_SELF_UPDATE_MANAGER_PATH", "/exe/self_update_manager.py")
-)
-SELF_UPDATE_MANAGER_SOURCE_PATH = Path(
- os.environ.get(
- "A0_SELF_UPDATE_MANAGER_SOURCE_PATH",
- "/a0/docker/run/fs/exe/self_update_manager.py",
- )
-)
-BACKUP_SUFFIX = ".startup-migration-backup"
-REQUIRED_RUNTIME_MARKERS = (
- "def should_include_usr_backup_entry(",
- "Skipping non-regular usr backup entry",
- "def clean_transient_desktop_agent_state(",
- "clean_transient_desktop_agent_state(REPO_DIR, logger)",
- "def refresh_codex_cli(",
- "refresh_codex_cli(logger)",
-)
-
-
-class SelfUpdateManagerRuntimeSync(Extension):
- def execute(self, **kwargs):
- result = ensure_self_update_manager_runtime_current()
- if result.get("updated"):
- PrintStyle.info("Self-update manager runtime synchronized:", result["target"])
- warning = start_codex_cli_refresh(result["target"])
- if warning:
- PrintStyle.warning("Codex CLI refresh could not be started:", warning)
- elif result.get("warning"):
- PrintStyle.warning("Self-update manager runtime sync skipped:", result["warning"])
-
-
-def ensure_self_update_manager_runtime_current(
- *,
- target_path: Path | str | None = None,
- source_path: Path | str | None = None,
-) -> dict[str, Any]:
- target = Path(target_path) if target_path is not None else SELF_UPDATE_MANAGER_PATH
- source = Path(source_path) if source_path is not None else SELF_UPDATE_MANAGER_SOURCE_PATH
-
- target_text, target_warning = _read_regular_text(target, role="runtime self-update manager")
- if target_text is None:
- return {"ok": True, "updated": False, "reason": target_warning}
-
- source_text, source_warning = _read_regular_text(source, role="source self-update manager")
- if source_text is None:
- return {"ok": False, "updated": False, "warning": source_warning}
-
- missing_source_markers = _missing_required_markers(source_text)
- if missing_source_markers:
- return {
- "ok": False,
- "updated": False,
- "warning": (
- "source self-update manager is missing required safety markers: "
- + ", ".join(missing_source_markers)
- ),
- }
-
- if not _missing_required_markers(target_text):
- return {"ok": True, "updated": False, "reason": "already-current"}
-
- try:
- backup = _replace_runtime_manager(target, source_text)
- except OSError as exc:
- return {
- "ok": False,
- "updated": False,
- "warning": f"could not update {target}: {exc}",
- }
-
- return {
- "ok": True,
- "updated": True,
- "target": str(target),
- "backup": str(backup),
- }
-
-
-def start_codex_cli_refresh(manager_path: Path | str) -> str:
- try:
- subprocess.Popen(
- [sys.executable, str(manager_path), "refresh-codex"],
- stdin=subprocess.DEVNULL,
- stdout=subprocess.DEVNULL,
- stderr=subprocess.DEVNULL,
- start_new_session=True,
- )
- except OSError as exc:
- return str(exc)
- return ""
-
-
-def _missing_required_markers(text: str) -> list[str]:
- return [marker for marker in REQUIRED_RUNTIME_MARKERS if marker not in text]
-
-
-def _read_regular_text(path: Path, *, role: str) -> tuple[str | None, str]:
- try:
- path_stat = path.lstat()
- except FileNotFoundError:
- return None, f"{role} not found: {path}"
- except OSError as exc:
- return None, f"{role} could not be inspected: {path}: {exc}"
-
- if not stat.S_ISREG(path_stat.st_mode):
- return None, f"{role} is not a regular file: {path}"
-
- try:
- return path.read_text(encoding="utf-8"), ""
- except OSError as exc:
- return None, f"{role} could not be read: {path}: {exc}"
-
-
-def _replace_runtime_manager(target: Path, source_text: str) -> Path:
- target_stat = target.stat()
- backup = _ensure_backup(target)
- temp_path = target.with_name(f".{target.name}.{os.getpid()}.tmp")
- try:
- temp_path.write_text(source_text, encoding="utf-8")
- os.chmod(temp_path, stat.S_IMODE(target_stat.st_mode))
- os.replace(temp_path, target)
- finally:
- temp_path.unlink(missing_ok=True)
- return backup
-
-
-def _ensure_backup(target: Path) -> Path:
- backup = target.with_name(f"{target.name}{BACKUP_SUFFIX}")
- if not backup.exists():
- shutil.copy2(target, backup)
- return backup
diff --git a/extensions/python/system_prompt/AGENTS.md b/extensions/python/system_prompt/AGENTS.md
deleted file mode 100644
index c4aa1bfd8..000000000
--- a/extensions/python/system_prompt/AGENTS.md
+++ /dev/null
@@ -1,28 +0,0 @@
-# System Prompt Extensions DOX
-
-## Purpose
-
-- Own construction of core system prompt sections.
-
-## Ownership
-
-- Ordered Python files own main, tools, MCP, secrets, skills, and project prompt sections.
-- Active project instruction bodies and active-project AGENTS.md path-chain guidance are moved into prompt protocol; the system prompt keeps project metadata and stable project rules.
-
-## Local Contracts
-
-- Preserve ordering where sections depend on earlier context.
-- Keep secret-related prompt sections masked and scoped.
-- Prompt additions must be bounded and compatible with tool-call contracts.
-
-## Work Guidance
-
-- Coordinate broad system prompt changes with profiles, skills, tools, plugins, and prompt tests.
-
-## Verification
-
-- Inspect rendered system prompts or run prompt-construction tests after changes.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/extensions/python/system_prompt/_12_mcp_prompt.py b/extensions/python/system_prompt/_12_mcp_prompt.py
index 44c760a7b..30ca34454 100644
--- a/extensions/python/system_prompt/_12_mcp_prompt.py
+++ b/extensions/python/system_prompt/_12_mcp_prompt.py
@@ -22,7 +22,7 @@ class MCPToolsPrompt(Extension):
@extensible
async def build_prompt(agent: Agent) -> str:
- mcp_config = MCPConfig.get_for_agent(agent)
+ mcp_config = MCPConfig.get_instance()
if not mcp_config.servers:
return ""
diff --git a/extensions/python/system_prompt/_14_project_prompt.py b/extensions/python/system_prompt/_14_project_prompt.py
index 4afa620cd..41948f5ed 100644
--- a/extensions/python/system_prompt/_14_project_prompt.py
+++ b/extensions/python/system_prompt/_14_project_prompt.py
@@ -15,31 +15,17 @@ class ProjectPrompt(Extension):
):
if not self.agent:
return
- prompt = await build_prompt(self.agent, loop_data=loop_data)
+ prompt = await build_prompt(self.agent)
if prompt:
system_prompt.append(prompt)
@extensible
-async def build_prompt(agent: Agent, loop_data: LoopData | None = None) -> str:
+async def build_prompt(agent: Agent) -> str:
result = agent.read_prompt("agent.system.projects.main.md")
project_name = agent.context.get_data(projects.CONTEXT_DATA_KEY_PROJECT)
- if loop_data:
- loop_data.protocol_persistent.pop("agents_md_instructions", None)
- loop_data.protocol_persistent.pop("project_instructions", None)
if project_name:
project_vars = projects.build_system_prompt_vars(project_name)
- if loop_data and project_vars.get("include_agents_md", True):
- agents_md_protocol = projects.build_agents_md_protocol(project_name)
- if agents_md_protocol:
- loop_data.protocol_persistent["agents_md_instructions"] = (
- agents_md_protocol
- )
- if loop_data and project_vars.get("project_instructions"):
- loop_data.protocol_persistent["project_instructions"] = agent.read_prompt(
- "agent.protocol.projects.instructions.md",
- **project_vars,
- )
result += "\n\n" + agent.read_prompt(
"agent.system.projects.active.md", **project_vars
)
diff --git a/extensions/python/tool_execute_after/AGENTS.md b/extensions/python/tool_execute_after/AGENTS.md
deleted file mode 100644
index 54c4557c5..000000000
--- a/extensions/python/tool_execute_after/AGENTS.md
+++ /dev/null
@@ -1,26 +0,0 @@
-# Tool Execute After Extensions DOX
-
-## Purpose
-
-- Own backend processing immediately after tool execution.
-
-## Ownership
-
-- Ordered Python files own post-tool secret masking and future tool-result postprocessing.
-
-## Local Contracts
-
-- Mask secrets before tool results reach history, UI, or model-visible context.
-- Do not alter tool `break_loop` or response semantics unless the hook contract owns that behavior.
-
-## Work Guidance
-
-- Coordinate with tool implementations and history hooks when changing tool result data.
-
-## Verification
-
-- Smoke-test tool execution with sensitive output after changes.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/extensions/python/tool_execute_before/AGENTS.md b/extensions/python/tool_execute_before/AGENTS.md
deleted file mode 100644
index e9f11e78a..000000000
--- a/extensions/python/tool_execute_before/AGENTS.md
+++ /dev/null
@@ -1,27 +0,0 @@
-# Tool Execute Before Extensions DOX
-
-## Purpose
-
-- Own backend processing immediately before tool execution.
-
-## Ownership
-
-- Ordered Python files own prior tool-output replacement, parallel recursion guards, and secret unmasking before execution.
-
-## Local Contracts
-
-- Unmask only values required by the target tool.
-- Preserve safety checks and do not expose secrets to logs or unrelated tools.
-- Keep ordering stable where replacement must occur before unmasking or execution.
-
-## Work Guidance
-
-- Coordinate with secret handling, tool argument preparation, and plugin tool gates.
-
-## Verification
-
-- Smoke-test tool execution with masked secret arguments and prior-output references after changes.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/extensions/python/tool_execute_before/_20_block_parallel_recursion.py b/extensions/python/tool_execute_before/_20_block_parallel_recursion.py
deleted file mode 100644
index a5547ed8f..000000000
--- a/extensions/python/tool_execute_before/_20_block_parallel_recursion.py
+++ /dev/null
@@ -1,15 +0,0 @@
-from helpers.extension import Extension
-from helpers.errors import RepairableException
-from helpers import parallel_tools
-
-
-class BlockParallelRecursion(Extension):
- async def execute(self, tool_name: str = "", **kwargs) -> None:
- if tool_name != "parallel":
- return
- if not parallel_tools.is_parallel_worker(self.agent):
- return
- raise RepairableException(
- "The `parallel` tool cannot be used inside a parallel worker. "
- "Finish the current worker task sequentially and return its result."
- )
diff --git a/extensions/python/user_message_ui/AGENTS.md b/extensions/python/user_message_ui/AGENTS.md
deleted file mode 100644
index 55428a4d7..000000000
--- a/extensions/python/user_message_ui/AGENTS.md
+++ /dev/null
@@ -1,26 +0,0 @@
-# User Message UI Extensions DOX
-
-## Purpose
-
-- Own backend behavior triggered around user-visible UI messages.
-
-## Ownership
-
-- Ordered Python files own update-check messaging and future user-message UI hooks.
-
-## Local Contracts
-
-- Keep proactive UI messages relevant, non-spammy, and safe for display.
-- Do not expose local diagnostics or update data that should stay internal.
-
-## Work Guidance
-
-- Gate recurring messages so they do not repeat unnecessarily across chats or tabs.
-
-## Verification
-
-- Smoke-test UI message rendering after changes.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/extensions/python/util_model_call_before/AGENTS.md b/extensions/python/util_model_call_before/AGENTS.md
deleted file mode 100644
index dc0bd77f8..000000000
--- a/extensions/python/util_model_call_before/AGENTS.md
+++ /dev/null
@@ -1,26 +0,0 @@
-# Utility Model Call Before Extensions DOX
-
-## Purpose
-
-- Own preprocessing before utility model calls.
-
-## Ownership
-
-- Ordered Python files own secret masking and future utility-call preparation.
-
-## Local Contracts
-
-- Mask secrets before utility prompts leave the framework.
-- Keep utility model inputs compatible with callers expecting structured outputs.
-
-## Work Guidance
-
-- Coordinate masking changes with main model call and error-format masking behavior.
-
-## Verification
-
-- Test utility model calls that include masked secret patterns after changes.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/extensions/python/webui_ws_connect/AGENTS.md b/extensions/python/webui_ws_connect/AGENTS.md
deleted file mode 100644
index f722ae7b2..000000000
--- a/extensions/python/webui_ws_connect/AGENTS.md
+++ /dev/null
@@ -1,26 +0,0 @@
-# WebUI WebSocket Connect Extensions DOX
-
-## Purpose
-
-- Own backend behavior when a WebUI WebSocket client connects.
-
-## Ownership
-
-- Ordered Python files own state-sync behavior for new WebSocket connections.
-
-## Local Contracts
-
-- Preserve WebSocket auth/session assumptions.
-- Send only state the connected client is allowed to receive.
-
-## Work Guidance
-
-- Coordinate connect behavior with frontend WebSocket client and sync store.
-
-## Verification
-
-- Smoke-test WebUI connection and initial state sync after changes.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/extensions/python/webui_ws_disconnect/AGENTS.md b/extensions/python/webui_ws_disconnect/AGENTS.md
deleted file mode 100644
index 703652891..000000000
--- a/extensions/python/webui_ws_disconnect/AGENTS.md
+++ /dev/null
@@ -1,26 +0,0 @@
-# WebUI WebSocket Disconnect Extensions DOX
-
-## Purpose
-
-- Own backend behavior when a WebUI WebSocket client disconnects.
-
-## Ownership
-
-- Ordered Python files own state-sync cleanup for disconnect events.
-
-## Local Contracts
-
-- Cleanup must be idempotent and safe for repeated disconnect events.
-- Do not remove shared state still needed by other active clients.
-
-## Work Guidance
-
-- Coordinate disconnect behavior with frontend reconnect and sync indicators.
-
-## Verification
-
-- Smoke-test disconnect and reconnect behavior after changes.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/extensions/python/webui_ws_event/AGENTS.md b/extensions/python/webui_ws_event/AGENTS.md
deleted file mode 100644
index a47e147b2..000000000
--- a/extensions/python/webui_ws_event/AGENTS.md
+++ /dev/null
@@ -1,26 +0,0 @@
-# WebUI WebSocket Event Extensions DOX
-
-## Purpose
-
-- Own backend behavior for incoming WebUI WebSocket events.
-
-## Ownership
-
-- Ordered Python files own state-sync event handling and future WebSocket event extensions.
-
-## Local Contracts
-
-- Validate event names and payloads before acting on them.
-- Preserve auth/session boundaries for all WebSocket events.
-
-## Work Guidance
-
-- Coordinate event changes with frontend WebSocket client and sync store.
-
-## Verification
-
-- Smoke-test relevant WebSocket events after changes.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/extensions/webui/AGENTS.md b/extensions/webui/AGENTS.md
index f2c530b90..11f949ca5 100644
--- a/extensions/webui/AGENTS.md
+++ b/extensions/webui/AGENTS.md
@@ -31,18 +31,4 @@
## Child DOX Index
-Direct child DOX files:
-
-| Child | Scope |
-| --- | --- |
-| [fetch_api_call_after/AGENTS.md](fetch_api_call_after/AGENTS.md) | Frontend hooks after raw `fetchApi()` calls. |
-| [fetch_api_call_before/AGENTS.md](fetch_api_call_before/AGENTS.md) | Frontend hooks before raw `fetchApi()` calls. |
-| [get_message_handler/AGENTS.md](get_message_handler/AGENTS.md) | Message rendering handler extensions. |
-| [initFw_end/AGENTS.md](initFw_end/AGENTS.md) | Post-WebUI-framework-initialization extensions. |
-| [json_api_call_after/AGENTS.md](json_api_call_after/AGENTS.md) | Frontend hooks after `callJsonApi()` calls. |
-| [json_api_call_before/AGENTS.md](json_api_call_before/AGENTS.md) | Frontend hooks before `callJsonApi()` calls. |
-| [right-canvas-panels/AGENTS.md](right-canvas-panels/AGENTS.md) | Built-in right-canvas panel HTML contributions. |
-| [right_canvas_register_surfaces/AGENTS.md](right_canvas_register_surfaces/AGENTS.md) | Built-in right-canvas surface registrations. |
-| [set_messages_after_loop/AGENTS.md](set_messages_after_loop/AGENTS.md) | Frontend hooks after message DOM updates. |
-| [set_messages_before_loop/AGENTS.md](set_messages_before_loop/AGENTS.md) | Frontend hooks before message DOM updates. |
-| [webui_ws_push/AGENTS.md](webui_ws_push/AGENTS.md) | WebUI WebSocket push-event behavior. |
+No child DOX files.
diff --git a/extensions/webui/fetch_api_call_after/AGENTS.md b/extensions/webui/fetch_api_call_after/AGENTS.md
deleted file mode 100644
index 963be5f11..000000000
--- a/extensions/webui/fetch_api_call_after/AGENTS.md
+++ /dev/null
@@ -1,26 +0,0 @@
-# Fetch API Call After Extensions DOX
-
-## Purpose
-
-- Own frontend extension hooks that run after raw `fetchApi()` calls.
-
-## Ownership
-
-- Files in this folder own after-call behavior for CSRF-aware raw fetch flows.
-
-## Local Contracts
-
-- JavaScript modules must export a default function when present.
-- Do not consume response bodies unless the hook contract explicitly passes a clone or mutable context for that purpose.
-
-## Work Guidance
-
-- Keep after-call extensions lightweight and safe for all fetch callers.
-
-## Verification
-
-- Smoke-test frontend API calls after adding behavior here.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/extensions/webui/fetch_api_call_before/AGENTS.md b/extensions/webui/fetch_api_call_before/AGENTS.md
deleted file mode 100644
index c715757d5..000000000
--- a/extensions/webui/fetch_api_call_before/AGENTS.md
+++ /dev/null
@@ -1,26 +0,0 @@
-# Fetch API Call Before Extensions DOX
-
-## Purpose
-
-- Own frontend extension hooks that run before raw `fetchApi()` calls.
-
-## Ownership
-
-- Files in this folder own before-call behavior for CSRF-aware raw fetch flows.
-
-## Local Contracts
-
-- JavaScript modules must export a default function when present.
-- Preserve CSRF, auth, and redirect behavior owned by `/js/api.js`.
-
-## Work Guidance
-
-- Avoid broad request mutation that surprises unrelated API callers.
-
-## Verification
-
-- Smoke-test affected frontend API calls after changes.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/extensions/webui/get_message_handler/AGENTS.md b/extensions/webui/get_message_handler/AGENTS.md
deleted file mode 100644
index ea0a36977..000000000
--- a/extensions/webui/get_message_handler/AGENTS.md
+++ /dev/null
@@ -1,27 +0,0 @@
-# Get Message Handler Extensions DOX
-
-## Purpose
-
-- Own frontend extensions that provide or modify message rendering handlers.
-
-## Ownership
-
-- Files in this folder own handler registration behavior for rendered chat messages.
-
-## Local Contracts
-
-- JavaScript modules must export a default function when present.
-- Preserve mutable context contracts used by `/js/messages.js`.
-- Do not render unsanitized model or user content.
-
-## Work Guidance
-
-- Coordinate handler changes with message components and plugin message extensions.
-
-## Verification
-
-- Smoke-test message rendering for affected message types after changes.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/extensions/webui/initFw_end/AGENTS.md b/extensions/webui/initFw_end/AGENTS.md
deleted file mode 100644
index 7390427de..000000000
--- a/extensions/webui/initFw_end/AGENTS.md
+++ /dev/null
@@ -1,27 +0,0 @@
-# Init Framework End Extensions DOX
-
-## Purpose
-
-- Own frontend extensions that run after WebUI framework initialization.
-
-## Ownership
-
-- JavaScript files own post-bootstrap global setup such as self-update helpers and session-scoped UI restoration hooks.
-
-## Local Contracts
-
-- JavaScript modules must export a default function.
-- Setup must be idempotent across reloads and cache resets.
-- Do not register duplicate global listeners.
-
-## Work Guidance
-
-- Coordinate initialization changes with `/js/initFw.js` and component lifecycle directives.
-
-## Verification
-
-- Smoke-test WebUI startup and browser console after changes.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/extensions/webui/initFw_end/restoreRestorableModals.js b/extensions/webui/initFw_end/restoreRestorableModals.js
deleted file mode 100644
index 51462c2bf..000000000
--- a/extensions/webui/initFw_end/restoreRestorableModals.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import { restoreRestorableModalStack } from "/js/modals.js";
-
-export default function restoreRestorableModals() {
- restoreRestorableModalStack();
-}
diff --git a/extensions/webui/json_api_call_after/AGENTS.md b/extensions/webui/json_api_call_after/AGENTS.md
deleted file mode 100644
index 96f356b8d..000000000
--- a/extensions/webui/json_api_call_after/AGENTS.md
+++ /dev/null
@@ -1,26 +0,0 @@
-# JSON API Call After Extensions DOX
-
-## Purpose
-
-- Own frontend extension hooks that run after `callJsonApi()` calls.
-
-## Ownership
-
-- JavaScript files own after-call behavior such as cache reset handling.
-
-## Local Contracts
-
-- JavaScript modules must export a default function.
-- Preserve JSON API response contracts and avoid hiding errors from callers.
-
-## Work Guidance
-
-- Keep global side effects narrow and tied to explicit API results or mutable contexts.
-
-## Verification
-
-- Smoke-test JSON API callers affected by extension changes.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/extensions/webui/json_api_call_before/AGENTS.md b/extensions/webui/json_api_call_before/AGENTS.md
deleted file mode 100644
index b14f04ea9..000000000
--- a/extensions/webui/json_api_call_before/AGENTS.md
+++ /dev/null
@@ -1,26 +0,0 @@
-# JSON API Call Before Extensions DOX
-
-## Purpose
-
-- Own frontend extension hooks that run before `callJsonApi()` calls.
-
-## Ownership
-
-- Files in this folder own before-call behavior for JSON API requests.
-
-## Local Contracts
-
-- JavaScript modules must export a default function when present.
-- Preserve CSRF/auth behavior and JSON payload shape expected by `/js/api.js`.
-
-## Work Guidance
-
-- Avoid broad request mutation that affects unrelated plugin or core API calls.
-
-## Verification
-
-- Smoke-test affected JSON API calls after changes.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/extensions/webui/right-canvas-panels/AGENTS.md b/extensions/webui/right-canvas-panels/AGENTS.md
deleted file mode 100644
index 014adda27..000000000
--- a/extensions/webui/right-canvas-panels/AGENTS.md
+++ /dev/null
@@ -1,28 +0,0 @@
-# Right Canvas Panel Extensions DOX
-
-## Purpose
-
-- Own built-in HTML panel contributions for the right-canvas surface area.
-
-## Ownership
-
-- `.html` files mount WebUI components into the `right-canvas-panels` extension point.
-- Panel wrappers own `data-surface-id` anchors and active/mounted visibility bindings.
-
-## Local Contracts
-
-- Each panel must correspond to a registered right-canvas surface ID.
-- Use `` for reusable component content instead of duplicating panel implementations.
-- Keep canvas panels compatible with `.right-canvas-surface-panel` layout semantics.
-
-## Work Guidance
-
-- Prefer thin wrappers that delegate lifecycle and state to the owning component store.
-
-## Verification
-
-- Smoke-test opening the matching surface from the right-canvas rail.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/extensions/webui/right-canvas-panels/files-panel.html b/extensions/webui/right-canvas-panels/files-panel.html
deleted file mode 100644
index fc0e7a1e6..000000000
--- a/extensions/webui/right-canvas-panels/files-panel.html
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
diff --git a/extensions/webui/right_canvas_register_surfaces/AGENTS.md b/extensions/webui/right_canvas_register_surfaces/AGENTS.md
deleted file mode 100644
index 1387cda74..000000000
--- a/extensions/webui/right_canvas_register_surfaces/AGENTS.md
+++ /dev/null
@@ -1,27 +0,0 @@
-# Right Canvas Surface Extensions DOX
-
-## Purpose
-
-- Own frontend registration of built-in right-canvas surfaces.
-
-## Ownership
-
-- JavaScript files own registration of remote link, space agent, file browser, and future core canvas surfaces.
-
-## Local Contracts
-
-- JavaScript modules must export a default function.
-- Surface IDs must be unique and stable.
-- Registered surfaces must point to valid components or handlers.
-
-## Work Guidance
-
-- Coordinate surface registration changes with `webui/components/canvas/` and related plugin panels.
-
-## Verification
-
-- Smoke-test opening each registered right-canvas surface after changes.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/extensions/webui/right_canvas_register_surfaces/register-files.js b/extensions/webui/right_canvas_register_surfaces/register-files.js
deleted file mode 100644
index f1cd6bd40..000000000
--- a/extensions/webui/right_canvas_register_surfaces/register-files.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import { store as fileBrowserStore } from "/components/modals/file-browser/file-browser-store.js";
-
-function waitForElement(selector, timeoutMs = 3000) {
- const found = document.querySelector(selector);
- if (found) return Promise.resolve(found);
- return new Promise((resolve) => {
- const timeout = globalThis.setTimeout(() => {
- observer.disconnect();
- resolve(document.querySelector(selector));
- }, timeoutMs);
- const observer = new MutationObserver(() => {
- const element = document.querySelector(selector);
- if (!element) return;
- globalThis.clearTimeout(timeout);
- observer.disconnect();
- resolve(element);
- });
- observer.observe(document.body, { childList: true, subtree: true });
- });
-}
-
-export default async function registerFilesSurface(surfaces) {
- surfaces.registerSurface({
- id: "files",
- title: "Files",
- icon: "folder",
- order: 5,
- modalPath: "modals/file-browser/file-browser.html",
- beginDockHandoff() {
- fileBrowserStore.beginSurfaceHandoff?.();
- },
- finishDockHandoff(payload = {}) {
- fileBrowserStore.finishSurfaceHandoff?.(payload);
- },
- cancelDockHandoff() {
- fileBrowserStore.cancelSurfaceHandoff?.();
- },
- async open(payload = {}) {
- const panel = await waitForElement('[data-surface-id="files"] .file-browser-root');
- if (!panel) throw new Error("Files surface panel did not mount.");
- await fileBrowserStore.openSurface(payload.path || payload.filePath || payload.directory || "");
- },
- });
-}
diff --git a/extensions/webui/set_messages_after_loop/AGENTS.md b/extensions/webui/set_messages_after_loop/AGENTS.md
deleted file mode 100644
index 724987ba7..000000000
--- a/extensions/webui/set_messages_after_loop/AGENTS.md
+++ /dev/null
@@ -1,26 +0,0 @@
-# Set Messages After Loop Extensions DOX
-
-## Purpose
-
-- Own frontend extensions that run after message DOM updates complete.
-
-## Ownership
-
-- Files in this folder own after-render message behavior.
-
-## Local Contracts
-
-- JavaScript modules must export a default function when present.
-- Preserve message DOM stability and avoid duplicate controls on repeated renders.
-
-## Work Guidance
-
-- Use stable markers when injecting controls into message elements.
-
-## Verification
-
-- Smoke-test message rerendering and extension-injected controls after changes.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/extensions/webui/set_messages_before_loop/AGENTS.md b/extensions/webui/set_messages_before_loop/AGENTS.md
deleted file mode 100644
index d417ba566..000000000
--- a/extensions/webui/set_messages_before_loop/AGENTS.md
+++ /dev/null
@@ -1,26 +0,0 @@
-# Set Messages Before Loop Extensions DOX
-
-## Purpose
-
-- Own frontend extensions that run before message DOM updates.
-
-## Ownership
-
-- Files in this folder own pre-render message behavior.
-
-## Local Contracts
-
-- JavaScript modules must export a default function when present.
-- Do not remove DOM state needed by message rendering unless the mutable context owns it.
-
-## Work Guidance
-
-- Coordinate pre-render behavior with `/js/messages.js` and message components.
-
-## Verification
-
-- Smoke-test message updates after changes.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/extensions/webui/webui_ws_push/AGENTS.md b/extensions/webui/webui_ws_push/AGENTS.md
deleted file mode 100644
index 58d838730..000000000
--- a/extensions/webui/webui_ws_push/AGENTS.md
+++ /dev/null
@@ -1,27 +0,0 @@
-# WebUI WebSocket Push Extensions DOX
-
-## Purpose
-
-- Own frontend behavior for WebUI WebSocket push events.
-
-## Ownership
-
-- JavaScript files own push-event side effects such as cache clearing.
-
-## Local Contracts
-
-- JavaScript modules must export a default function.
-- Validate event payload shape before acting.
-- Keep cache or state resets scoped to the event type.
-
-## Work Guidance
-
-- Coordinate push behavior with backend WebSocket event extensions and frontend stores.
-
-## Verification
-
-- Smoke-test relevant WebSocket push events after changes.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/AGENTS.md b/helpers/AGENTS.md
index 39d24d71e..bb70bf289 100644
--- a/helpers/AGENTS.md
+++ b/helpers/AGENTS.md
@@ -15,26 +15,20 @@
- Preserve public helper APIs used by core code and plugins unless all callers, docs, and tests are updated.
- Use structured parsers and serializers for YAML, JSON, paths, and URLs instead of ad hoc string handling.
- Keep path handling constrained to intended roots for user files, uploads, downloads, projects, and workdirs.
-- Project metadata defaults must remain backwards-compatible; missing `include_agents_md` is treated as enabled, project instruction file content is injected with an explicit source path, and active-project AGENTS.md path-chain guidance is assembled into prompt protocol without duplicating the project root AGENTS.md.
+- Project metadata defaults must remain backwards-compatible; missing `include_agents_md` is treated as enabled and project instruction file content is injected with an explicit source path.
- Do not hardcode secrets, provider keys, local absolute paths, or environment-specific values.
- Use `RepairableException` for errors an agent may be able to fix.
-- This directory is a file-documented DOX profile: every direct `*.py` helper module must have a same-directory `*.py.dox.md` file named by appending `.dox.md` to the full Python filename.
-- The `*.py.dox.md` file owns helper purpose, public classes/functions, cross-module contracts, persistence or side effects, path/security assumptions, important dependencies, and verification guidance.
-- When a helper module is added, removed, renamed, or behaviorally changed, update its matching `*.py.dox.md` in the same change.
-- Do not leave stale file-level DOX after helper deletion or rename.
## Work Guidance
- Prefer cohesive helper modules over adding unrelated utilities to large files.
- Keep imports acyclic where possible; defer imports inside functions only when needed to avoid startup cycles.
- For changes touching auth, CSRF, files, plugins, tunnels, WebSockets, or model calls, read the caller and tests before editing.
-- During the DOX pass, verify that every direct `*.py` file has a matching `*.py.dox.md` and that changed helper behavior is described there.
## Verification
- Run targeted tests for changed helper modules.
- Run security regression tests for auth, CSRF, filesystem, WebSocket, tunnel, upload, or image-serving changes.
-- Check file-level documentation coverage with a script or shell loop that verifies each `helpers/*.py` has a matching `helpers/*.py.dox.md`.
## Child DOX Index
diff --git a/helpers/api.py b/helpers/api.py
index 351ba4716..9616528b7 100644
--- a/helpers/api.py
+++ b/helpers/api.py
@@ -1,7 +1,6 @@
from abc import abstractmethod
import json
import threading
-from urllib.parse import urlsplit, unquote
from functools import wraps
from pathlib import Path
from typing import Union, Dict, Any
@@ -103,44 +102,6 @@ class ApiHandler:
from helpers.network import is_loopback_address
-def is_safe_next_url(value: str | None) -> bool:
- """Return True when value is a safe same-origin redirect target."""
- if not value:
- return False
- if "\r" in value or "\n" in value:
- return False
- # Reject raw backslashes (browsers normalize `/\host` to `//host` -> external).
- if "\\" in value:
- return False
-
- # Decode percent-escapes so encoded backslashes (e.g. `%5C`) are caught too.
- decoded = unquote(value)
- if "\\" in decoded:
- return False
-
- parsed = urlsplit(decoded)
- if parsed.scheme or parsed.netloc:
- return False
-
- # Require an absolute path within this origin, but reject protocol-relative URLs.
- return parsed.path.startswith("/") and not parsed.path.startswith("//")
-
-
-def get_safe_next_url(value: str | None, fallback: str | None = None) -> str | None:
- """Return value if it is a safe next URL, otherwise return a safe fallback."""
- if is_safe_next_url(value):
- return value
- if is_safe_next_url(fallback):
- return fallback
- return None
-
-
-def get_current_request_next_url() -> str:
- """Return the current request path/query as a safe relative redirect target."""
- next_url = request.full_path if request.query_string else request.path
- return get_safe_next_url(next_url, url_for("serve_index")) or url_for("serve_index")
-
-
def requires_api_key(f):
@wraps(f)
async def decorated(*args, **kwargs):
@@ -181,7 +142,7 @@ def requires_auth(f):
if not user_pass_hash:
return await f(*args, **kwargs)
if session.get("authentication") != user_pass_hash:
- return redirect(url_for("login_handler", next=get_current_request_next_url()))
+ return redirect(url_for("login_handler"))
return await f(*args, **kwargs)
return decorated
diff --git a/helpers/api.py.dox.md b/helpers/api.py.dox.md
deleted file mode 100644
index d9d6bb546..000000000
--- a/helpers/api.py.dox.md
+++ /dev/null
@@ -1,71 +0,0 @@
-# api.py DOX
-
-## Purpose
-
-- Own the `api.py` helper module.
-- This module defines API handler registration, request security gates, CSRF checks, and watchdog registration.
-- Keep this file-level DOX profile synchronized with `api.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `api.py` owns the runtime implementation.
-- `api.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `ApiHandler` (no explicit base class)
- - `requires_loopback(cls) -> bool`
- - `requires_api_key(cls) -> bool`
- - `requires_auth(cls) -> bool`
- - `get_methods(cls) -> list[str]`
- - `requires_csrf(cls) -> bool`
- - `async process(self, input: Input, request: Request) -> Output`
- - `async handle_request(self, request: Request) -> Response`
- - `use_context(self, ctxid: str, create_if_not_exists: bool=...)`
-- Top-level functions:
-- `requires_api_key(f)`
-- `requires_loopback(f)`
-- `requires_auth(f)`
-- `csrf_protect(f)`
-- `register_api_route(app: Flask, lock: ThreadLockType) -> None`
-- `register_watchdogs()`
-- Notable constants/configuration names: `CACHE_AREA`.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- `ApiHandler` defines `process(...)`.
-- `ApiHandler` defines `get_methods(...)`.
-- `ApiHandler` defines `requires_auth(...)`.
-- `ApiHandler` defines `requires_csrf(...)`.
-- `ApiHandler` defines `requires_api_key(...)`.
-- `ApiHandler` defines `requires_loopback(...)`.
-- Observed side-effect areas: filesystem reads, filesystem writes, filesystem deletion, WebSocket state, plugin state, settings/state persistence, secret handling.
-- Imported dependency areas include: `abc`, `flask`, `functools`, `helpers`, `helpers.errors`, `helpers.network`, `helpers.print_style`, `json`, `pathlib`, `threading`, `typing`, `werkzeug.wrappers.response`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `wraps`, `app.add_url_rule`, `watchdog.add_watchdog`, `cls.requires_auth`, `_use_context`, `login.get_credentials_hash`, `files.get_abs_path`, `handler_cls.requires_csrf`, `handler_cls.requires_api_key`, `handler_cls.requires_auth`, `handler_cls.requires_loopback`, `cache.add`, `PrintStyle.debug`, `cache.clear`, `get_settings`, `f`, `is_loopback_address`, `Response`, `redirect`, `files.is_in_dir`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_api_chat_lifetime.py`
- - `tests/test_browser_agent_regressions.py`
- - `tests/test_download_toast_regressions.py`
- - `tests/test_fasta2a_client.py`
- - `tests/test_fastmcp_openapi_security.py`
- - `tests/test_host_browser_connector.py`
- - `tests/test_image_get_security.py`
- - `tests/test_model_config_api_keys.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/attachment_manager.py.dox.md b/helpers/attachment_manager.py.dox.md
deleted file mode 100644
index 46dd4a930..000000000
--- a/helpers/attachment_manager.py.dox.md
+++ /dev/null
@@ -1,47 +0,0 @@
-# attachment_manager.py DOX
-
-## Purpose
-
-- Own the `attachment_manager.py` helper module.
-- This module tracks uploaded or generated attachments associated with chat contexts.
-- Keep this file-level DOX profile synchronized with `attachment_manager.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `attachment_manager.py` owns the runtime implementation.
-- `attachment_manager.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `AttachmentManager` (no explicit base class)
- - `is_allowed_file(self, filename: str) -> bool`
- - `get_file_type(self, filename: str) -> str`
- - `get_file_extension(filename: str) -> str`
- - `validate_mime_type(self, file) -> bool`
- - `save_file(self, file: FileStorage, name: str) -> Tuple[str, Dict]`
- - `generate_image_preview(self, image_path: str, max_size: int=...) -> Optional[str]`
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem reads, filesystem writes, settings/state persistence.
-- Imported dependency areas include: `PIL`, `base64`, `helpers.print_style`, `helpers.security`, `io`, `os`, `typing`, `werkzeug.datastructures`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `os.makedirs`, `self.get_file_extension`, `set.union`, `filename.rsplit.lower`, `safe_filename`, `os.path.join`, `self.get_file_type`, `file.save`, `ValueError`, `self.generate_image_preview`, `PrintStyle.error`, `img.thumbnail`, `io.BytesIO`, `img.save`, `base64.b64encode.decode`, `mime_type.split`, `img.convert`, `filename.rsplit`, `base64.b64encode`, `buffer.getvalue`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/backup.py b/helpers/backup.py
index 70ad7b082..1aa651893 100644
--- a/helpers/backup.py
+++ b/helpers/backup.py
@@ -63,7 +63,6 @@ class BackupService:
return f"""# User data
# All persistent user data is now centralized in /usr for easier backup and restore
{agent_root}/usr/**
-!{agent_root}/usr/.time_travel/**
"""
def _get_agent_zero_version(self) -> str:
@@ -241,12 +240,8 @@ class BackupService:
return translated_patterns
- async def test_patterns(self, metadata: Dict[str, Any], max_files: Optional[int] = 1000) -> List[Dict[str, Any]]:
- """Test backup patterns and return list of matched files.
-
- Pass max_files=None for internal flows that must process the complete
- match set, such as backup creation and restore cleanup.
- """
+ async def test_patterns(self, metadata: Dict[str, Any], max_files: int = 1000) -> List[Dict[str, Any]]:
+ """Test backup patterns and return list of matched files"""
include_patterns = metadata.get("include_patterns", [])
exclude_patterns = metadata.get("exclude_patterns", [])
include_hidden = metadata.get("include_hidden", True)
@@ -263,7 +258,6 @@ class BackupService:
# Get explicit patterns for hidden file handling
explicit_patterns = self._get_explicit_patterns(include_patterns)
- has_limit = max_files is not None
matched_files = []
processed_count = 0
@@ -291,7 +285,7 @@ class BackupService:
dirs[:] = dirs_to_keep
for file in files_list:
- if has_limit and processed_count >= max_files:
+ if processed_count >= max_files:
break
file_path = os.path.join(root, file)
@@ -323,10 +317,10 @@ class BackupService:
# Skip files we can't access
continue
- if has_limit and processed_count >= max_files:
+ if processed_count >= max_files:
break
- if has_limit and processed_count >= max_files:
+ if processed_count >= max_files:
break
except Exception as e:
@@ -350,10 +344,8 @@ class BackupService:
"include_hidden": include_hidden
}
- # Get the complete matched file set. Preview and dry-run callers may
- # cap their scans for UI responsiveness, but the archive itself must be
- # complete.
- matched_files = await self.test_patterns(metadata, max_files=None)
+ # Get matched files
+ matched_files = await self.test_patterns(metadata, max_files=50000)
if not matched_files:
raise Exception("No files matched the backup patterns")
@@ -832,7 +824,7 @@ class BackupService:
# Find existing files that match the translated user-edited patterns
try:
- existing_files = await self.test_patterns(metadata, max_files=None)
+ existing_files = await self.test_patterns(metadata, max_files=10000)
# Convert to delete operations format
files_to_delete = []
diff --git a/helpers/backup.py.dox.md b/helpers/backup.py.dox.md
deleted file mode 100644
index 5612447ec..000000000
--- a/helpers/backup.py.dox.md
+++ /dev/null
@@ -1,52 +0,0 @@
-# backup.py DOX
-
-## Purpose
-
-- Own the `backup.py` helper module.
-- This module builds, inspects, previews, tests, and restores Agent Zero backup archives.
-- Keep this file-level DOX profile synchronized with `backup.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `backup.py` owns the runtime implementation.
-- `backup.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `BackupService` (no explicit base class)
- - `get_default_backup_metadata(self) -> Dict[str, Any]`
- - `async test_patterns(self, metadata: Dict[str, Any], max_files: Optional[int]=...) -> List[Dict[str, Any]]`
- - `async create_backup(self, include_patterns: List[str], exclude_patterns: List[str], include_hidden: bool=..., backup_name: str=...) -> str`
- - `async inspect_backup(self, backup_file) -> Dict[str, Any]`
- - `async preview_restore(self, backup_file, restore_include_patterns: Optional[List[str]]=..., restore_exclude_patterns: Optional[List[str]]=..., overwrite_policy: str=..., clean_before_restore: bool=..., user_edited_metadata: Optional[Dict[str, Any]]=...) -> Dict[str, Any]`
- - `async restore_backup(self, backup_file, restore_include_patterns: Optional[List[str]]=..., restore_exclude_patterns: Optional[List[str]]=..., overwrite_policy: str=..., clean_before_restore: bool=..., user_edited_metadata: Optional[Dict[str, Any]]=...) -> Dict[str, Any]`
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem reads, filesystem writes, filesystem deletion, settings/state persistence, secret handling.
-- Imported dependency areas include: `datetime`, `helpers`, `helpers.localization`, `helpers.print_style`, `json`, `os`, `pathspec`, `platform`, `tempfile`, `typing`, `zipfile`.
-- `test_patterns(..., max_files=None)` is the unlimited scan mode. UI preview and dry-run callers may pass bounded limits, but real backup creation and restore clean-before-restore must use unlimited matching so archives and cleanup are not silently truncated.
-- Default backup metadata includes persistent `/usr` data but excludes Time Travel shadow history under `usr/.time_travel/**`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `self._get_agent_zero_version`, `files.get_abs_path`, `Localization.get.now_iso`, `self._get_default_patterns`, `self._parse_patterns`, `self.agent_zero_root.rstrip`, `patterns.split`, `join`, `file_path.lstrip`, `backed_up_agent_root.rstrip`, `current_agent_root.rstrip`, `self._patterns_to_string`, `self._get_explicit_patterns`, `tempfile.mkdtemp`, `os.path.join`, `self._translate_patterns`, `git.get_git_info`, `line.strip`, `line.startswith`, `getpass.getuser`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_download_toast_regressions.py`
- - `tests/test_office_document_store.py`
- - `tests/test_self_update_tag_filter.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/browser.py.dox.md b/helpers/browser.py.dox.md
deleted file mode 100644
index 0ac4afcca..000000000
--- a/helpers/browser.py.dox.md
+++ /dev/null
@@ -1,46 +0,0 @@
-# browser.py DOX
-
-## Purpose
-
-- Own the `browser.py` helper module.
-- This module holds shared browser helper state used by browser-facing integrations.
-- Keep this file-level DOX profile synchronized with `browser.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `browser.py` owns the runtime implementation.
-- `browser.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem reads, filesystem deletion.
-
-## Key Concepts
-
-- This module is primarily declarative or delegates behavior through classes/imported objects.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_a0_connector_prompt_gating.py`
- - `tests/test_browser_agent_regressions.py`
- - `tests/test_download_toast_regressions.py`
- - `tests/test_host_browser_connector.py`
- - `tests/test_oauth_gemini_api.py`
- - `tests/test_oauth_providers.py`
- - `tests/test_oauth_static.py`
- - `tests/test_oauth_xai_grok.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/cache.py.dox.md b/helpers/cache.py.dox.md
deleted file mode 100644
index abe865f75..000000000
--- a/helpers/cache.py.dox.md
+++ /dev/null
@@ -1,64 +0,0 @@
-# cache.py DOX
-
-## Purpose
-
-- Own the `cache.py` helper module.
-- This module provides in-process cache areas with optional global and area toggles.
-- Keep this file-level DOX profile synchronized with `cache.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `cache.py` owns the runtime implementation.
-- `cache.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `CacheEntry` (no explicit base class)
-- Top-level functions:
-- `toggle_global(enabled: bool) -> None`
-- `toggle_area(area: str, enabled: bool) -> None`
-- `has(area: str, key: Any) -> bool`
-- `add(area: str, key: Any, data: Any) -> None`
-- `get(area: str, key: Any, default: Any=...) -> Any`
-- `remove(area: str, key: Any) -> None`
-- `clear(area: str) -> None`
-- `trim_cache(area: str, seconds: float=...) -> None`
-- `clear_all() -> None`
-- `_is_enabled(area: str) -> bool`
-- `_create_entry(value: Any) -> CacheEntry`
-- `_touch_entry(entry: CacheEntry) -> None`
-- `_get_matching_areas(area: str) -> list[str]`
-- `determine_cache_key(agent, *additional)`
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem deletion, plugin state, settings/state persistence.
-- Imported dependency areas include: `dataclasses`, `fnmatch`, `threading`, `time`, `typing`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `threading.RLock`, `dataclass`, `CacheEntry`, `time.time`, `_is_enabled`, `_touch_entry`, `_create_entry`, `_cache.pop`, `_get_matching_areas`, `_cache.clear`, `agent.context.get_data`, `area_cache.pop`, `fnmatch.fnmatch`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_browser_agent_regressions.py`
- - `tests/test_file_tree_visualize.py`
- - `tests/test_office_canvas_setup.py`
- - `tests/test_office_document_store.py`
- - `tests/test_self_update_tag_filter.py`
- - `tests/test_time_travel.py`
- - `tests/test_webui_extension_surfaces.py`
- - `tests/test_whatsapp_bridge_manager.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/call_llm.py.dox.md b/helpers/call_llm.py.dox.md
deleted file mode 100644
index 0ba5bd966..000000000
--- a/helpers/call_llm.py.dox.md
+++ /dev/null
@@ -1,43 +0,0 @@
-# call_llm.py DOX
-
-## Purpose
-
-- Own the `call_llm.py` helper module.
-- This module wraps LiteLLM/model calls with examples, streaming, and extension hooks.
-- Keep this file-level DOX profile synchronized with `call_llm.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `call_llm.py` owns the runtime implementation.
-- `call_llm.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `Example` (`TypedDict`)
-- Top-level functions:
-- `async call_llm(system: str, model: BaseChatModel | BaseLLM, message: str, examples: list[Example]=..., callback: Callable[[str], None] | None=...)`
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: model calls.
-- Imported dependency areas include: `langchain.prompts`, `langchain.schema`, `langchain_core.language_models.chat_models`, `langchain_core.language_models.llms`, `langchain_core.messages`, `typing`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `ChatPromptTemplate.from_messages`, `FewShotChatMessagePromptTemplate`, `few_shot_prompt.format`, `chain.astream`, `HumanMessage`, `AIMessage`, `SystemMessage`, `callback`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/chat_media.py.dox.md b/helpers/chat_media.py.dox.md
deleted file mode 100644
index 5aa9af9fb..000000000
--- a/helpers/chat_media.py.dox.md
+++ /dev/null
@@ -1,60 +0,0 @@
-# chat_media.py DOX
-
-## Purpose
-
-- Own the `chat_media.py` helper module.
-- This module materializes, stores, and resolves chat-scoped image/media artifacts.
-- Keep this file-level DOX profile synchronized with `chat_media.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `chat_media.py` owns the runtime implementation.
-- `chat_media.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `ChatImage` (no explicit base class)
-- Top-level functions:
-- `screenshot_dir(context_id: str, source: str) -> Path`
-- `artifact_dir(context_id: str, category: ImageCategory=..., source: str=...) -> Path`
-- `save_image_bytes(context_id: str, payload: bytes, mime_type: str=..., category: ImageCategory=..., source: str=..., preferred_name: str=..., max_bytes: int | None=...) -> ChatImage`
-- `save_image_base64(context_id: str, data: str, mime_type: str=..., category: ImageCategory=..., source: str=..., preferred_name: str=..., max_bytes: int | None=...) -> ChatImage`
-- `save_image_file(context_id: str, path: str | Path, category: ImageCategory=..., source: str=..., preferred_name: str=..., max_bytes: int | None=...) -> ChatImage`
-- `save_image_data_url(context_id: str, data_url: str, category: ImageCategory=..., source: str=..., preferred_name: str=..., max_bytes: int | None=...) -> ChatImage`
-- `materialize_image_ref(context_id: str, url: str, source: str=..., preferred_name: str=..., max_bytes: int | None=...) -> str`
-- `is_chat_scoped_path(context_id: str, path: str | Path) -> bool`
-- `infer_source(value: str=..., preferred_name: str=...) -> str`
-- `category_for_source(source: str) -> ImageCategory`
-- `_guess_image_mime(path: Path) -> str`
-- `_is_data_image_url(value: str) -> bool`
-- `_split_image_data_url(data_url: str) -> tuple[str, str]`
-- Notable constants/configuration names: `DEFAULT_MAX_IMAGE_BYTES`.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem reads, filesystem writes, filesystem deletion.
-- Imported dependency areas include: `__future__`, `dataclasses`, `helpers`, `pathlib`, `time`, `typing`, `uuid`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `dataclass`, `artifact_dir`, `bytes`, `media_artifacts.normalize_mime`, `media_artifacts.guess_extension`, `media_artifacts.safe_filename`, `Path`, `time.strftime`, `path.parent.mkdir`, `path.write_bytes`, `ChatImage`, `media_artifacts.decode_base64_payload`, `save_image_bytes`, `image_path.read_bytes`, `_split_image_data_url`, `save_image_base64`, `str.strip`, `category_for_source`, `_is_data_image_url`, `images.resolve_ref`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_browser_agent_regressions.py`
- - `tests/test_host_browser_connector.py`
- - `tests/test_tool_action_contracts.py`
- - `tests/test_vision_load_image_refs.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/cli_tunnel.py.dox.md b/helpers/cli_tunnel.py.dox.md
deleted file mode 100644
index da8fc4a0c..000000000
--- a/helpers/cli_tunnel.py.dox.md
+++ /dev/null
@@ -1,54 +0,0 @@
-# cli_tunnel.py DOX
-
-## Purpose
-
-- Own the `cli_tunnel.py` helper module.
-- This module provides shared download and install mechanics for CLI-based tunnel providers.
-- Keep this file-level DOX profile synchronized with `cli_tunnel.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `cli_tunnel.py` owns the runtime implementation.
-- `cli_tunnel.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `CliTunnelHelper` (`TunnelHelper`)
- - `start(self)`
- - `stop(self)`
-- Top-level functions:
-- `executable_name(name)`
-- `chmod_executable(path)`
-- `notify_download(notify, message, data=...)`
-- `notify_download_complete(notify, message, data=...)`
-- `download_file(url, destination, notify=...)`
-- `platform_parts()`
-- `extract_named_members_from_tar(archive_path, destination_dir, member_names)`
-- `extract_named_members_from_zip(archive_path, destination_dir, member_names)`
-- Notable constants/configuration names: `RUNTIME_BIN_DIR`.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem reads, filesystem writes, filesystem deletion, network calls, subprocess/runtime control, tunnel state.
-- Imported dependency areas include: `collections`, `flaredantic`, `helpers`, `helpers.tunnel_common`, `os`, `pathlib`, `platform`, `queue`, `shutil`, `subprocess`, `tarfile`, `tempfile`, `threading`, `time`, `urllib.request`, `zipfile`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `Path`, `files.get_abs_path`, `callable`, `destination.parent.mkdir`, `notify_download`, `tempfile.mkstemp`, `os.close`, `platform.system.lower`, `platform.machine.lower`, `destination_dir.mkdir`, `path.chmod`, `notify`, `temp_path.replace`, `notify_download_complete`, `RuntimeError`, `archive.getmembers`, `zipfile.ZipFile`, `archive.infolist`, `super.__init__`, `self.url_pattern.search`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_tunnel_remote_link.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/cloudflare_tunnel.py.dox.md b/helpers/cloudflare_tunnel.py.dox.md
deleted file mode 100644
index e2172dcae..000000000
--- a/helpers/cloudflare_tunnel.py.dox.md
+++ /dev/null
@@ -1,43 +0,0 @@
-# cloudflare_tunnel.py DOX
-
-## Purpose
-
-- Own the `cloudflare_tunnel.py` helper module.
-- This module implements Cloudflare tunnel provider lifecycle behavior.
-- Keep this file-level DOX profile synchronized with `cloudflare_tunnel.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `cloudflare_tunnel.py` owns the runtime implementation.
-- `cloudflare_tunnel.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `CloudflareTunnel` (`FlaredanticTunnelHelper`)
- - `build_tunnel(self)`
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: settings/state persistence, tunnel state.
-- Imported dependency areas include: `flaredantic`, `helpers.tunnel_common`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `FlareConfig`, `FlareTunnel`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_tunnel_remote_link.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/context.py.dox.md b/helpers/context.py.dox.md
deleted file mode 100644
index 8b6e76a51..000000000
--- a/helpers/context.py.dox.md
+++ /dev/null
@@ -1,54 +0,0 @@
-# context.py DOX
-
-## Purpose
-
-- Own the `context.py` helper module.
-- This module provides compatibility helpers for reading and writing `AgentContext` data.
-- Keep this file-level DOX profile synchronized with `context.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `context.py` owns the runtime implementation.
-- `context.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Top-level functions:
-- `_ensure_context() -> Dict[str, Any]`: Make sure a context dict exists, and return it.
-- `set_context_data(key: str, value: Any)`: Set context data for the current async/task context.
-- `delete_context_data(key: str)`: Delete a key from the current async/task context.
-- `get_context_data(key: Optional[str]=..., default: T=...) -> T`: Get a key from the current context, or the full dict if key is None.
-- `clear_context_data()`: Completely clear the context dict.
-- Notable constants/configuration names: `T`.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem deletion, scheduler state.
-- Imported dependency areas include: `contextvars`, `typing`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `TypeVar`, `ContextVar`, `_ensure_context`, `cast`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_a0_connector_prompt_gating.py`
- - `tests/test_api_chat_lifetime.py`
- - `tests/test_browser_agent_regressions.py`
- - `tests/test_document_query_plugin.py`
- - `tests/test_error_retry_plugin.py`
- - `tests/test_extensions_stress.py`
- - `tests/test_file_tree_visualize.py`
- - `tests/test_history_compression_wait.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/context_utils.py.dox.md b/helpers/context_utils.py.dox.md
deleted file mode 100644
index 725340ecd..000000000
--- a/helpers/context_utils.py.dox.md
+++ /dev/null
@@ -1,41 +0,0 @@
-# context_utils.py DOX
-
-## Purpose
-
-- Own the `context_utils.py` helper module.
-- This module provides context managers/utilities for current context binding.
-- Keep this file-level DOX profile synchronized with `context_utils.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `context_utils.py` owns the runtime implementation.
-- `context_utils.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Top-level functions:
-- `use_context(lock: ThreadLockType, ctxid: str, create_if_not_exists: bool=...)`
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: WebSocket state, settings/state persistence.
-- Imported dependency areas include: `threading`, `typing`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `AgentContext.use`, `AgentContext.first`, `AgentContext`, `Exception`, `initialize_agent`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/crypto.py.dox.md b/helpers/crypto.py.dox.md
deleted file mode 100644
index a66e730ca..000000000
--- a/helpers/crypto.py.dox.md
+++ /dev/null
@@ -1,48 +0,0 @@
-# crypto.py DOX
-
-## Purpose
-
-- Own the `crypto.py` helper module.
-- This module hashes, verifies, encrypts, and decrypts data for signed or protected payloads.
-- Keep this file-level DOX profile synchronized with `crypto.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `crypto.py` owns the runtime implementation.
-- `crypto.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Top-level functions:
-- `hash_data(data: str, password: str)`
-- `verify_data(data: str, hash: str, password: str)`
-- `_generate_private_key()`
-- `_generate_public_key(private_key: rsa.RSAPrivateKey)`
-- `_decode_public_key(public_key: str) -> rsa.RSAPublicKey`
-- `encrypt_data(data: str, public_key_pem: str)`
-- `_encrypt_data(data: bytes, public_key: rsa.RSAPublicKey)`
-- `decrypt_data(data: str, private_key: rsa.RSAPrivateKey)`
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: secret handling.
-- Imported dependency areas include: `cryptography.hazmat.primitives`, `cryptography.hazmat.primitives.asymmetric`, `hashlib`, `hmac`, `os`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `hmac.new.hexdigest`, `rsa.generate_private_key`, `private_key.public_key.public_bytes.hex`, `bytes.fromhex`, `serialization.load_pem_public_key`, `_encrypt_data`, `public_key.encrypt`, `b.hex`, `private_key.decrypt`, `b.decode`, `hash_data`, `TypeError`, `data.encode`, `_decode_public_key`, `padding.OAEP`, `hmac.new`, `private_key.public_key.public_bytes`, `password.encode`, `padding.MGF1`, `hashes.SHA256`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/defer.py.dox.md b/helpers/defer.py.dox.md
deleted file mode 100644
index cccc3da9f..000000000
--- a/helpers/defer.py.dox.md
+++ /dev/null
@@ -1,55 +0,0 @@
-# defer.py DOX
-
-## Purpose
-
-- Own the `defer.py` helper module.
-- This module runs deferred or child async tasks on managed event-loop threads.
-- Keep this file-level DOX profile synchronized with `defer.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `defer.py` owns the runtime implementation.
-- `defer.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `EventLoopThread` (no explicit base class)
- - `terminate(self)`
- - `run_coroutine(self, coro)`
-- `ChildTask` (no explicit base class)
-- `DeferredTask` (no explicit base class)
- - `start_task(self, func: Callable[..., Coroutine[Any, Any, Any]], *args, **kwargs)`
- - `is_ready(self) -> bool`
- - `result_sync(self, timeout: Optional[float]=...) -> Any`
- - `async result(self, timeout: Optional[float]=...) -> Any`
- - `kill(self, terminate_thread: bool=...) -> None`
- - `kill_children(self) -> None`
- - `is_alive(self) -> bool`
- - `restart(self, terminate_thread: bool=...) -> None`
-- Notable constants/configuration names: `T`, `THREAD_BACKGROUND`.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: scheduler state.
-- Imported dependency areas include: `asyncio`, `concurrent.futures`, `dataclasses`, `threading`, `typing`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `TypeVar`, `threading.Lock`, `self._start`, `asyncio.set_event_loop`, `self.loop.run_forever`, `loop.is_running`, `asyncio.run_coroutine_threadsafe`, `EventLoopThread`, `self._start_task`, `self.kill`, `self.event_loop_thread.run_coroutine`, `self.kill_children`, `asyncio.get_running_loop`, `func`, `asyncio.iscoroutine`, `Future`, `asyncio.wrap_future`, `asyncio.current_task`, `asyncio.new_event_loop`, `threading.Thread`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_office_document_store.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/dirty_json.py b/helpers/dirty_json.py
index 8e7b21012..8b731bee4 100644
--- a/helpers/dirty_json.py
+++ b/helpers/dirty_json.py
@@ -214,7 +214,7 @@ class DirtyJson:
def _parse_key(self):
self._skip_whitespace()
if self.current_char in ['"', "'"]:
- return self._parse_string(is_key=True)
+ return self._parse_string()
else:
return self._parse_unquoted_key()
@@ -260,18 +260,11 @@ class DirtyJson:
self._pop_stack()
return
- def _parse_string(self, is_key: bool = False):
+ def _parse_string(self):
result = ""
quote_char = self.current_char
self._advance() # Skip opening quote
- while self.current_char is not None:
- if self.current_char == quote_char:
- if self._is_closing_quote(is_key):
- break
- result += self.current_char
- self._advance()
- continue
-
+ while self.current_char is not None and self.current_char != quote_char:
if self.current_char == "\\":
self._advance()
if self.current_char in ['"', "'", "\\", "/", "b", "f", "n", "r", "t"]:
@@ -305,67 +298,6 @@ class DirtyJson:
self._advance() # Skip closing quote
return result
- def _is_closing_quote(self, is_key: bool) -> bool:
- next_index = self._skip_padding_from(self.index + 1)
- if next_index >= len(self.json_string):
- return True
-
- next_char = self.json_string[next_index]
- if is_key:
- return next_char in [":", ",", "}", "]"]
-
- if next_char in [",", "}", "]"]:
- return True
-
- return self._looks_like_missing_comma_before_key(next_index)
-
- def _looks_like_missing_comma_before_key(self, index: int) -> bool:
- if not self.stack or not isinstance(self.stack[-1], dict):
- return False
- if index >= len(self.json_string) or self.json_string[index] not in ['"', "'"]:
- return False
-
- quote_char = self.json_string[index]
- index += 1
- while index < len(self.json_string):
- char = self.json_string[index]
- if char == "\\":
- index += 2
- continue
- if char == quote_char:
- next_index = self._skip_padding_from(index + 1)
- return (
- next_index < len(self.json_string)
- and self.json_string[next_index] == ":"
- )
- if char in ["\n", "\r", "{", "}", "[", "]", ","]:
- return False
- index += 1
-
- return False
-
- def _skip_padding_from(self, index: int) -> int:
- while index < len(self.json_string):
- char = self.json_string[index]
- if char.isspace():
- index += 1
- elif char == "/" and index + 1 < len(self.json_string):
- next_char = self.json_string[index + 1]
- if next_char == "/":
- index += 2
- while index < len(self.json_string) and self.json_string[index] != "\n":
- index += 1
- elif next_char == "*":
- end = self.json_string.find("*/", index + 2)
- if end == -1:
- return len(self.json_string)
- index = end + 2
- else:
- break
- else:
- break
- return index
-
def _parse_multiline_string(self):
result = ""
quote_char = self.current_char
diff --git a/helpers/dirty_json.py.dox.md b/helpers/dirty_json.py.dox.md
deleted file mode 100644
index 030a5c652..000000000
--- a/helpers/dirty_json.py.dox.md
+++ /dev/null
@@ -1,51 +0,0 @@
-# dirty_json.py DOX
-
-## Purpose
-
-- Own the `dirty_json.py` helper module.
-- This module parses and serializes relaxed JSON-like model output.
-- Keep this file-level DOX profile synchronized with `dirty_json.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `dirty_json.py` owns the runtime implementation.
-- `dirty_json.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `DirtyJson` (no explicit base class)
- - `parse_string(json_string)`
- - `parse(self, json_string)`
- - `feed(self, chunk)`
- - `get_start_pos(self, input_str: str) -> int`
-- Top-level functions:
-- `try_parse(json_string: str)`
-- `parse(json_string: str)`
-- `stringify(obj, **kwargs)`
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem writes, settings/state persistence.
-- Imported dependency areas include: `json`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `DirtyJson.parse_string`, `json.dumps`, `json.loads`, `self._reset`, `self.stack.pop`, `DirtyJson`, `parser.parse`, `self.get_start_pos`, `self._parse`, `self._advance`, `self._skip_whitespace`, `self._parse_object_content`, `self._parse_array_content`, `self._skip_padding_from`, `self._looks_like_missing_comma_before_key`, `result.strip`, `self.current_char.isspace`, `self._parse_value`, `self._continue_parsing`, `self._parse_object`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_dirty_json.py`
- - `tests/test_projects.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/docker.py.dox.md b/helpers/docker.py.dox.md
deleted file mode 100644
index e932e7bcc..000000000
--- a/helpers/docker.py.dox.md
+++ /dev/null
@@ -1,53 +0,0 @@
-# docker.py DOX
-
-## Purpose
-
-- Own the `docker.py` helper module.
-- This module manages Docker container operations used by runtime helpers.
-- Keep this file-level DOX profile synchronized with `docker.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `docker.py` owns the runtime implementation.
-- `docker.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `DockerContainerManager` (no explicit base class)
- - `init_docker(self)`
- - `cleanup_container(self) -> None`
- - `get_image_containers(self)`
- - `start_container(self) -> None`
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem reads, filesystem deletion, subprocess/runtime control, WebSocket state.
-- Imported dependency areas include: `atexit`, `docker`, `helpers.errors`, `helpers.files`, `helpers.log`, `helpers.print_style`, `time`, `typing`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `self.init_docker`, `PrintStyle.standard`, `self.client.containers.run`, `time.sleep`, `docker.from_env`, `self.container.stop`, `self.container.remove`, `existing_container.start`, `self.logger.log`, `format_error`, `PrintStyle.error`, `PrintStyle.hint`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_browser_agent_regressions.py`
- - `tests/test_default_prompt_budget.py`
- - `tests/test_docker_release_plan.py`
- - `tests/test_document_query_plugin.py`
- - `tests/test_host_browser_connector.py`
- - `tests/test_model_search.py`
- - `tests/test_office_canvas_setup.py`
- - `tests/test_office_document_store.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/document_query.py.dox.md b/helpers/document_query.py.dox.md
deleted file mode 100644
index 7d205d988..000000000
--- a/helpers/document_query.py.dox.md
+++ /dev/null
@@ -1,41 +0,0 @@
-# document_query.py DOX
-
-## Purpose
-
-- Own the `document_query.py` helper module.
-- This module provides compatibility exports for document query behavior.
-- Keep this file-level DOX profile synchronized with `document_query.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `document_query.py` owns the runtime implementation.
-- `document_query.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: plugin state.
-- Imported dependency areas include: `plugins._document_query.helpers.document_query`.
-
-## Key Concepts
-
-- This module is primarily declarative or delegates behavior through classes/imported objects.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_document_query_fallback.py`
- - `tests/test_document_query_plugin.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/dotenv.py.dox.md b/helpers/dotenv.py.dox.md
deleted file mode 100644
index 2e142d657..000000000
--- a/helpers/dotenv.py.dox.md
+++ /dev/null
@@ -1,48 +0,0 @@
-# dotenv.py DOX
-
-## Purpose
-
-- Own the `dotenv.py` helper module.
-- This module loads and updates `.env` configuration values.
-- Keep this file-level DOX profile synchronized with `dotenv.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `dotenv.py` owns the runtime implementation.
-- `dotenv.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Top-level functions:
-- `load_dotenv()`
-- `get_dotenv_file_path()`
-- `get_dotenv_value(key: str, default: Any=...)`
-- `save_dotenv_value(key: str, value: str)`
-- Notable constants/configuration names: `KEY_AUTH_LOGIN`, `KEY_AUTH_PASSWORD`, `KEY_RFC_PASSWORD`, `KEY_ROOT_PASSWORD`.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem reads, filesystem writes, secret handling.
-- Imported dependency areas include: `dotenv`, `files`, `os`, `re`, `typing`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `_load_dotenv`, `get_abs_path`, `os.getenv`, `get_dotenv_file_path`, `load_dotenv`, `os.path.isfile`, `f.readlines`, `f.seek`, `f.writelines`, `f.truncate`, `f.write`, `re.match`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/email_parser_test.py`
- - `tests/test_model_config_api_keys.py`
- - `tests/test_timezone_regressions.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/duckduckgo_search.py.dox.md b/helpers/duckduckgo_search.py.dox.md
deleted file mode 100644
index 5090f5042..000000000
--- a/helpers/duckduckgo_search.py.dox.md
+++ /dev/null
@@ -1,40 +0,0 @@
-# duckduckgo_search.py DOX
-
-## Purpose
-
-- Own the `duckduckgo_search.py` helper module.
-- This module queries DuckDuckGo search through the configured helper path.
-- Keep this file-level DOX profile synchronized with `duckduckgo_search.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `duckduckgo_search.py` owns the runtime implementation.
-- `duckduckgo_search.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Top-level functions:
-- `search(query: str, results=..., region=..., time=...) -> list[str]`
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Imported dependency areas include: `duckduckgo_search`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `DDGS`, `ddgs.text`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/email_client.py.dox.md b/helpers/email_client.py.dox.md
deleted file mode 100644
index 976e32cd9..000000000
--- a/helpers/email_client.py.dox.md
+++ /dev/null
@@ -1,39 +0,0 @@
-# email_client.py DOX
-
-## Purpose
-
-- Own the `email_client.py` helper module.
-- This module provides shared email client plumbing for mail integrations.
-- Keep this file-level DOX profile synchronized with `email_client.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `email_client.py` owns the runtime implementation.
-- `email_client.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem reads, filesystem writes, filesystem deletion, WebSocket state, settings/state persistence, secret handling.
-
-## Key Concepts
-
-- This module is primarily declarative or delegates behavior through classes/imported objects.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/email_parser_test.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/ephemeral_images.py.dox.md b/helpers/ephemeral_images.py.dox.md
deleted file mode 100644
index 62b7a36a0..000000000
--- a/helpers/ephemeral_images.py.dox.md
+++ /dev/null
@@ -1,58 +0,0 @@
-# ephemeral_images.py DOX
-
-## Purpose
-
-- Own the `ephemeral_images.py` helper module.
-- This module stores temporary image payloads by reference for safe short-lived access.
-- Keep this file-level DOX profile synchronized with `ephemeral_images.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `ephemeral_images.py` owns the runtime implementation.
-- `ephemeral_images.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `EphemeralImage` (no explicit base class)
- - `data_url(self) -> str`
- - `display_name(self) -> str`
-- Top-level functions:
-- `put_image_bytes(context_id: str, mime: str, payload: bytes, name: str=..., ttl_seconds: float=...) -> str`
-- `put_image(context_id: str, mime: str, data: str, name: str=..., ttl_seconds: float=...) -> str`
-- `is_ref(value: object) -> bool`
-- `display_ref(ref: str) -> str`
-- `get_image(ref: str, context_id: str=...) -> EphemeralImage | None`
-- `consume_image(ref: str, context_id: str=...) -> EphemeralImage | None`
-- `delete_image(ref: str) -> None`
-- `clear_context(context_id: str) -> None`
-- `_resolve_image(ref: str, context_id: str=..., consume: bool) -> EphemeralImage | None`
-- `_compact_base64(data: str) -> str`
-- `_normalize_mime(mime: str) -> str`
-- `_prune_expired_locked(now: float | None=...) -> None`
-- Notable constants/configuration names: `REF_PREFIX`, `DEFAULT_TTL_SECONDS`.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem deletion.
-- Imported dependency areas include: `__future__`, `base64`, `dataclasses`, `threading`, `time`, `uuid`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `dataclass`, `threading.RLock`, `base64.b64encode.decode`, `put_image`, `_compact_base64`, `base64.b64decode`, `_normalize_mime`, `time.time`, `EphemeralImage`, `str.strip.startswith`, `str.strip`, `_resolve_image`, `join`, `str.strip.lower`, `ValueError`, `_prune_expired_locked`, `is_ref`, `_store.pop`, `value.startswith`, `display_ref`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_browser_agent_regressions.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/errors.py.dox.md b/helpers/errors.py.dox.md
deleted file mode 100644
index c551fc8d5..000000000
--- a/helpers/errors.py.dox.md
+++ /dev/null
@@ -1,54 +0,0 @@
-# errors.py DOX
-
-## Purpose
-
-- Own the `errors.py` helper module.
-- This module defines framework exceptions and safe error formatting.
-- Keep this file-level DOX profile synchronized with `errors.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `errors.py` owns the runtime implementation.
-- `errors.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `RepairableException` (`Exception`)
-- `InterventionException` (`Exception`)
-- `HandledException` (`Exception`)
-- Top-level functions:
-- `handle_error(e: Exception)`
-- `error_text(e: Exception)`
-- `format_error(e: Exception, start_entries=..., end_entries=..., error_message_position: Literal['top', 'bottom', 'none']=...)`
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Imported dependency areas include: `asyncio`, `re`, `traceback`, `typing`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `join`, `traceback_text.split`, `traceback.format_exception`, `re.match`, `type`, `line.strip.startswith`, `trimmed_lines.strip`, `error_message.strip`, `line.strip`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_browser_agent_regressions.py`
- - `tests/test_fasta2a_client.py`
- - `tests/test_host_browser_connector.py`
- - `tests/test_office_desktop_state.py`
- - `tests/test_office_document_store.py`
- - `tests/test_skills_runtime.py`
- - `tests/test_speech_plugin_split.py`
- - `tests/test_time_travel.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/extension.py.dox.md b/helpers/extension.py.dox.md
deleted file mode 100644
index 4271cc57c..000000000
--- a/helpers/extension.py.dox.md
+++ /dev/null
@@ -1,63 +0,0 @@
-# extension.py DOX
-
-## Purpose
-
-- Own the `extension.py` helper module.
-- This module discovers and dispatches Python and WebUI extension hooks.
-- Keep this file-level DOX profile synchronized with `extension.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `extension.py` owns the runtime implementation.
-- `extension.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `_Unset` (no explicit base class)
-- `Extension` (no explicit base class)
- - `execute(self, **kwargs) -> None | Awaitable[None]`
-- Top-level functions:
-- `_log_extension_call(name: str)`
-- `extensible(func)`: Make a function emit two implicit extension points around its execution.
-- `async call_extensions_async(extension_point: str, agent: 'Agent|None'=..., **kwargs)`
-- `call_extensions_sync(extension_point: str, agent: 'Agent|None'=..., **kwargs)`
-- `get_webui_extensions(agent: 'Agent | None', extension_point: str, filters: list[str] | None=...)`
-- `_get_extension_classes(extension_point: str, agent: 'Agent|None'=..., **kwargs) -> list[Type[Extension]]`
-- `_get_file_from_module(module_name: str) -> str`
-- `_get_extensions(folder: str)`
-- `register_extensions_watchdogs()`
-- Notable constants/configuration names: `DEFAULT_EXTENSIONS_FOLDER`, `USER_EXTENSIONS_FOLDER`, `_EXTENSIONS_CACHE_AREA`, `_CLASSES_CACHE_AREA`, `_UNSET`, `_EXTENSIONS_LOG_COUNTS`.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- `Extension` defines `execute(...)`.
-- Observed side-effect areas: filesystem reads, WebSocket state, plugin state.
-- Imported dependency areas include: `abc`, `functools`, `helpers`, `helpers.print_style`, `inspect`, `os`, `typing`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `_Unset`, `inspect.iscoroutinefunction`, `wraps`, `_log_extension_call`, `_get_extension_classes`, `subagents.get_paths`, `cache.determine_cache_key`, `cache.add`, `files.get_abs_path`, `modules.load_classes_from_folder`, `watchdog.add_watchdog`, `os.path.join`, `_get_agent`, `_prepare_inputs`, `_process_result`, `call_extensions_sync`, `cls.execute`, `files.deabsolute_path`, `_get_file_from_module`, `module_name.split`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_a0_connector_prompt_gating.py`
- - `tests/test_api_chat_lifetime.py`
- - `tests/test_browser_agent_regressions.py`
- - `tests/test_error_retry_plugin.py`
- - `tests/test_extensions_stress.py`
- - `tests/test_history_compression_wait.py`
- - `tests/test_model_config_api_keys.py`
- - `tests/test_oauth_codex.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/extract_tools.py b/helpers/extract_tools.py
index 0c49df968..39e837855 100644
--- a/helpers/extract_tools.py
+++ b/helpers/extract_tools.py
@@ -8,16 +8,16 @@ def json_parse_dirty(json: str) -> dict[str, Any] | None:
if not json or not isinstance(json, str):
return None
- first_data: dict[str, Any] | None = None
- for ext_json in extract_json_root_strings(json.strip()):
- data = _parse_json_root_object(ext_json)
- if data is None:
- continue
- if first_data is None:
- first_data = data
- if _is_tool_request(data):
- return data
- return first_data
+ ext_json = extract_json_object_string(json.strip())
+ if ext_json:
+ try:
+ data = DirtyJson.parse_string(ext_json)
+ if isinstance(data, dict):
+ return data
+ except Exception:
+ # If parsing fails, return None instead of crashing
+ return None
+ return None
def normalize_tool_request(tool_request: Any) -> tuple[str, dict]:
@@ -46,82 +46,26 @@ def normalize_tool_request(tool_request: Any) -> tuple[str, dict]:
def extract_json_root_string(content: str) -> str | None:
- first_root: str | None = None
- for root in extract_json_root_strings(content):
- if first_root is None:
- first_root = root
- data = _parse_json_root_object(root)
- if data is not None and _is_tool_request(data):
- return root
- return first_root
-
-
-def extract_json_root_strings(content: str) -> list[str]:
if not content or not isinstance(content, str):
- return []
+ return None
- if content.lstrip().startswith("["):
- return []
+ start = content.find("{")
+ if start == -1:
+ return None
+ first_array = content.find("[")
+ if first_array != -1 and first_array < start:
+ return None
- roots: list[str] = []
- for start in _json_root_object_starts(content):
- parser = DirtyJson()
- try:
- parser.parse(content[start:])
- except Exception:
- continue
-
- if not parser.completed:
- continue
-
- roots.append(content[start : start + parser.index])
- return roots
-
-
-def _json_root_object_starts(content: str) -> list[int]:
- starts: list[int] = []
- depth = 0
- quote: str | None = None
- escaped = False
-
- for index, char in enumerate(content):
- if quote:
- if escaped:
- escaped = False
- elif char == "\\":
- escaped = True
- elif char == quote:
- quote = None
- continue
-
- if depth and char in ['"', "'", "`"]:
- quote = char
- elif char == "{":
- if depth == 0:
- starts.append(index)
- depth += 1
- elif depth and char == "[":
- depth += 1
- elif depth and char in ["}", "]"]:
- depth -= 1
-
- return starts
-
-
-def _parse_json_root_object(root: str) -> dict[str, Any] | None:
+ parser = DirtyJson()
try:
- data = DirtyJson.parse_string(root)
+ parser.parse(content[start:])
except Exception:
return None
- return data if isinstance(data, dict) else None
+ if not parser.completed:
+ return None
-def _is_tool_request(data: dict[str, Any]) -> bool:
- try:
- normalize_tool_request(data)
- except ValueError:
- return False
- return True
+ return content[start : start + parser.index]
def extract_json_object_string(content):
diff --git a/helpers/extract_tools.py.dox.md b/helpers/extract_tools.py.dox.md
deleted file mode 100644
index d932d79c2..000000000
--- a/helpers/extract_tools.py.dox.md
+++ /dev/null
@@ -1,52 +0,0 @@
-# extract_tools.py DOX
-
-## Purpose
-
-- Own the `extract_tools.py` helper module.
-- This module normalizes and repairs model-emitted tool-call JSON.
-- Keep this file-level DOX profile synchronized with `extract_tools.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `extract_tools.py` owns the runtime implementation.
-- `extract_tools.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Top-level functions:
-- `json_parse_dirty(json: str) -> dict[str, Any] | None`
-- `normalize_tool_request(tool_request: Any) -> tuple[str, dict]`
-- `extract_json_root_string(content: str) -> str | None`
-- `extract_json_root_strings(content: str) -> list[str]`
-- `extract_json_object_string(content)`
-- `extract_json_string(content)`
-- `fix_json_string(json_string)`
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: settings/state persistence.
-- Dirty parsing scans complete JSON object roots in prose and prefers the first object that normalizes as a valid tool request, so a leading text preamble or incidental non-tool object does not force a misformat warning when a valid tool call follows.
-- Streaming tool snapshots use the same valid-tool preference through `extract_json_root_string`, while preserving the first complete object fallback when no valid tool-call object is present.
-- Root extraction ignores objects nested inside an open parent object, so streamed wrapper tools such as `parallel` cannot stop early on the first nested `tool_calls` item.
-- Imported dependency areas include: `dirty_json`, `helpers.modules`, `re`, `regex`, `typing`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `extract_json_object_string`, `content.find`, `DirtyJson`, `content.rfind`, `regex.search`, `re.sub`, `json.strip`, `ValueError`, `tool_name.split`, `parser.parse`, `match.group`, `match.group.replace`, `DirtyJson.parse_string`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_stream_tool_early_stop.py`
- - `tests/test_tool_request_normalization.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/faiss_monkey_patch.py.dox.md b/helpers/faiss_monkey_patch.py.dox.md
deleted file mode 100644
index bfbacf50a..000000000
--- a/helpers/faiss_monkey_patch.py.dox.md
+++ /dev/null
@@ -1,38 +0,0 @@
-# faiss_monkey_patch.py DOX
-
-## Purpose
-
-- Own the `faiss_monkey_patch.py` helper module.
-- This module applies compatibility patches for FAISS behavior.
-- Keep this file-level DOX profile synchronized with `faiss_monkey_patch.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `faiss_monkey_patch.py` owns the runtime implementation.
-- `faiss_monkey_patch.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Imported dependency areas include: `numpy`, `sys`, `types`, `warnings`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `types.ModuleType`, `SimpleNamespace`, `warnings.catch_warnings`, `warnings.simplefilter`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/fasta2a_client.py.dox.md b/helpers/fasta2a_client.py.dox.md
deleted file mode 100644
index e5ef5b6a8..000000000
--- a/helpers/fasta2a_client.py.dox.md
+++ /dev/null
@@ -1,50 +0,0 @@
-# fasta2a_client.py DOX
-
-## Purpose
-
-- Own the `fasta2a_client.py` helper module.
-- This module connects Agent Zero to external A2A agents.
-- Keep this file-level DOX profile synchronized with `fasta2a_client.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `fasta2a_client.py` owns the runtime implementation.
-- `fasta2a_client.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `AgentConnection` (no explicit base class)
- - `async get_agent_card(self) -> Dict[str, Any]`
- - `async send_message(self, message: str, attachments: Optional[List[str]]=..., context_id: Optional[str]=..., metadata: Optional[Dict[str, Any]]=...) -> Dict[str, Any]`
- - `async get_task(self, task_id: str) -> Dict[str, Any]`
- - `async wait_for_completion(self, task_id: str, poll_interval: int=..., max_wait: int=...) -> Dict[str, Any]`
- - `async close(self)`
-- Top-level functions:
-- `async connect_to_agent(agent_url: str, timeout: int=...) -> AgentConnection`: Create a connection to a remote agent.
-- `is_client_available() -> bool`: Check if FastA2A client is available.
-- Notable constants/configuration names: `_PRINTER`.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem writes, network calls, WebSocket state, settings/state persistence, secret handling, scheduler state.
-- Imported dependency areas include: `helpers.print_style`, `typing`, `uuid`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `PrintStyle`, `AgentConnection`, `PrintStyle.warning`, `agent_url.rstrip`, `httpx.AsyncClient`, `A2AClient`, `TimeoutError`, `connection.get_agent_card`, `RuntimeError`, `agent_url.startswith`, `os.getenv`, `self._http_client.aclose`, `self.close`, `response.raise_for_status`, `response.json`, `self.get_agent_card`, `uuid.uuid4`, `self._a2a_client.send_message`, `self._a2a_client.get_task`, `self.get_task`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/fasta2a_server.py b/helpers/fasta2a_server.py
index dfac9da24..89afade1c 100644
--- a/helpers/fasta2a_server.py
+++ b/helpers/fasta2a_server.py
@@ -2,13 +2,11 @@
import asyncio
import uuid
import atexit
-import json
from typing import Any, List
import contextlib
import threading
from helpers import settings, projects
-from starlette.responses import Response as StarletteResponse
from starlette.requests import Request
# Local imports
@@ -63,27 +61,6 @@ except ImportError: # pragma: no cover – library not installed
_PRINTER = PrintStyle(italic=True, font_color="purple", padding=False)
-def _enable_streaming_capability(agent_card_body: bytes) -> bytes:
- """Return an agent-card JSON body with A2A streaming enabled."""
- agent_card = json.loads(agent_card_body)
- capabilities = agent_card.get("capabilities")
- if not isinstance(capabilities, dict):
- capabilities = {}
- agent_card["capabilities"] = capabilities
- capabilities["streaming"] = True
- return json.dumps(agent_card, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
-
-
-class AgentZeroFastA2A(FastA2A): # type: ignore[misc]
- """FastA2A app with Agent Zero defaults layered over library defaults."""
-
- async def _agent_card_endpoint(self, request: Request) -> StarletteResponse:
- response = await super()._agent_card_endpoint(request)
- body = _enable_streaming_capability(response.body)
- self._agent_card_json_schema = body
- return StarletteResponse(content=body, media_type="application/json")
-
-
class AgentZeroWorker(Worker): # type: ignore[misc]
"""Agent Zero implementation of FastA2A Worker."""
@@ -265,7 +242,7 @@ class DynamicA2AProxy:
}
# Create new FastA2A app with proper thread safety
- new_app = AgentZeroFastA2A( # type: ignore
+ new_app = FastA2A( # type: ignore
storage=storage,
broker=broker,
name="Agent Zero",
diff --git a/helpers/fasta2a_server.py.dox.md b/helpers/fasta2a_server.py.dox.md
deleted file mode 100644
index 1f2dcba78..000000000
--- a/helpers/fasta2a_server.py.dox.md
+++ /dev/null
@@ -1,57 +0,0 @@
-# fasta2a_server.py DOX
-
-## Purpose
-
-- Own the `fasta2a_server.py` helper module.
-- This module serves Agent Zero through a dynamic A2A proxy.
-- Keep this file-level DOX profile synchronized with `fasta2a_server.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `fasta2a_server.py` owns the runtime implementation.
-- `fasta2a_server.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `AgentZeroFastA2A` (`FastA2A`)
- - `async _agent_card_endpoint(self, request: Request) -> StarletteResponse`
-- `AgentZeroWorker` (`Worker`)
- - `async run_task(self, params: Any) -> None`
- - `async cancel_task(self, params: Any) -> None`
- - `build_message_history(self, history: List[Any]) -> List[Message]`
- - `build_artifacts(self, result: Any) -> List[Artifact]`
-- `DynamicA2AProxy` (no explicit base class)
- - `get_instance()`
- - `reconfigure(self, token: str)`
-- Top-level functions:
-- `_enable_streaming_capability(agent_card_body: bytes) -> bytes`: Ensure the generated A2A agent card advertises streaming support.
-- `is_available()`: Check if FastA2A is available and properly configured.
-- `get_proxy()`: Get the FastA2A proxy instance.
-- Notable constants/configuration names: `_PRINTER`.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Agent Zero wraps `FastA2A` with `AgentZeroFastA2A` so the generated agent card sets `capabilities.streaming` to `true` by default.
-- Observed side-effect areas: filesystem writes, filesystem deletion, settings/state persistence, secret handling, scheduler state.
-- Imported dependency areas include: `agent`, `asyncio`, `atexit`, `contextlib`, `helpers`, `helpers.persist_chat`, `helpers.print_style`, `initialize`, `json`, `starlette.requests`, `starlette.responses`, `threading`, `typing`, `uuid`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `PrintStyle`, `DynamicA2AProxy.get_instance`, `json.loads`, `json.dumps`, `super.__init__`, `super._agent_card_endpoint`, `join`, `UserMessage`, `threading.Lock`, `atexit.register`, `self._configure`, `settings.get_settings`, `path.startswith`, `self._convert_message`, `initialize_agent`, `AgentContext`, `context.log.log`, `context.communicate`, `context.reset`, `AgentContext.remove`, `remove_chat`, `self.storage.update_task`, `self._register_shutdown`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests:
- - `tests/test_fasta2a_server.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/file_browser.py b/helpers/file_browser.py
index a4a7e936f..4b3ac2746 100644
--- a/helpers/file_browser.py
+++ b/helpers/file_browser.py
@@ -313,10 +313,6 @@ class FileBrowser:
full_path = (self.base_dir / current_path).resolve()
if not str(full_path).startswith(str(self.base_dir)):
raise ValueError("Invalid path")
- if not full_path.exists():
- raise FileNotFoundError("Directory not found")
- if not full_path.is_dir():
- raise NotADirectoryError("Path is not a directory")
# Use ls command instead of os.scandir for better error handling
files, folders = self._get_files_via_ls(full_path)
@@ -346,12 +342,7 @@ class FileBrowser:
except Exception as e:
PrintStyle.error(f"Error reading directory: {e}")
- return {
- "entries": [],
- "current_path": current_path,
- "parent_path": "",
- "error": str(e),
- }
+ return {"entries": [], "current_path": "", "parent_path": ""}
def get_full_path(self, file_path: str, allow_dir: bool = False) -> str:
"""Get full file path if it exists and is within base_dir"""
diff --git a/helpers/file_browser.py.dox.md b/helpers/file_browser.py.dox.md
deleted file mode 100644
index 3f8f2e43a..000000000
--- a/helpers/file_browser.py.dox.md
+++ /dev/null
@@ -1,51 +0,0 @@
-# file_browser.py DOX
-
-## Purpose
-
-- Own the `file_browser.py` helper module.
-- This module builds safe file-browser views over allowed filesystem roots.
-- Keep this file-level DOX profile synchronized with `file_browser.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `file_browser.py` owns the runtime implementation.
-- `file_browser.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `FileBrowser` (no explicit base class)
- - `save_file_b64(self, current_path: str, filename: str, base64_content: str)`
- - `save_files(self, files: List, current_path: str=...) -> Tuple[List[str], List[str]]`
- - `delete_file(self, file_path: str) -> bool`
- - `rename_item(self, file_path: str, new_name: str) -> bool`
- - `create_folder(self, parent_path: str, folder_name: str) -> bool`
- - `save_text_file(self, file_path: str, content: str) -> bool`
- - `get_files(self, current_path: str=...) -> Dict`
- - `get_full_path(self, file_path: str, allow_dir: bool=...) -> str`
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem reads, filesystem writes, filesystem deletion, subprocess/runtime control, settings/state persistence.
-- Imported dependency areas include: `base64`, `datetime`, `helpers`, `helpers.localization`, `helpers.print_style`, `helpers.security`, `os`, `pathlib`, `shutil`, `subprocess`, `typing`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `Path`, `files.get_abs_path`, `self._get_file_extension`, `file.seek`, `file.tell`, `resolve`, `os.makedirs`, `os.path.exists`, `full_path.with_name`, `new_path.exists`, `os.rename`, `target_dir.exists`, `filename.rsplit.lower`, `subprocess.run`, `result.stdout.strip.split`, `self._get_files_via_ls`, `files.exists`, `ValueError`, `str.startswith`, `file.write`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_download_toast_regressions.py`
- - `tests/test_office_document_store.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/file_tree.py.dox.md b/helpers/file_tree.py.dox.md
deleted file mode 100644
index db3625d70..000000000
--- a/helpers/file_tree.py.dox.md
+++ /dev/null
@@ -1,63 +0,0 @@
-# file_tree.py DOX
-
-## Purpose
-
-- Own the `file_tree.py` helper module.
-- This module renders bounded file-tree summaries for prompts and UI surfaces.
-- Keep this file-level DOX profile synchronized with `file_tree.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `file_tree.py` owns the runtime implementation.
-- `file_tree.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `_TreeEntry` (no explicit base class)
- - `as_dict(self) -> dict[str, Any]`
-- Top-level functions:
-- `_from_timestamp(timestamp: float) -> datetime`
-- `file_tree(relative_path: str, max_depth: int=..., max_lines: int=..., folders_first: bool=..., max_folders: int=..., max_files: int=..., sort: tuple[Literal['name', 'created', 'modified'], Literal['asc', 'desc']]=..., ignore: str | None=..., output_mode: Literal['string', 'flat', 'nested']=...) -> str | list[dict]`: Render a directory tree relative to the repository base path.
-- `_normalize_relative_path(path: str) -> str`
-- `_directory_has_visible_entries(directory: str, root_abs_path: str, ignore_spec: PathSpec, cache: dict[str, bool], max_depth_remaining: int) -> bool`
-- `_create_summary_comment(parent: _TreeEntry, noun: str, count: int) -> _TreeEntry`
-- `_create_global_limit_comment(parent: _TreeEntry, hidden_children: Sequence[_TreeEntry]) -> _TreeEntry`
-- `_create_folder_unprocessed_comment(folder_node: _TreeEntry, folder_path: str, abs_root: str, ignore_spec: Optional[PathSpec]) -> Optional[_TreeEntry]`
-- `_prune_to_visible(node: _TreeEntry, visible_ids: set[int]) -> None`
-- `_mark_last_flags(node: _TreeEntry) -> None`
-- `_refresh_render_metadata(node: _TreeEntry) -> None`
-- `_resolve_ignore_patterns(ignore: str | None, root_abs_path: str) -> Optional[PathSpec]`
-- `_list_directory_children(directory: str, root_abs_path: str, ignore_spec: Optional[PathSpec], max_depth_remaining: int, cache: dict[str, bool]) -> tuple[list[os.DirEntry], list[os.DirEntry]]`
-- `_apply_sorting_and_limits(folders: list[_TreeEntry], files: list[_TreeEntry], folders_first: bool, sort: tuple[str, str], max_folders: int | None, max_files: int | None, directory_node: _TreeEntry) -> list[_TreeEntry]`
-- `_format_line(node: _TreeEntry) -> str`
-- `_build_tree_items_flat(items: Sequence[_TreeEntry]) -> list[dict]`
-- `_to_nested_structure(items: Sequence[_TreeEntry]) -> list[dict]`
-- `_iter_depth_first(items: Sequence[_TreeEntry]) -> Iterable[_TreeEntry]`
-- Notable constants/configuration names: `SORT_BY_NAME`, `SORT_BY_CREATED`, `SORT_BY_MODIFIED`, `SORT_ASC`, `SORT_DESC`, `OUTPUT_MODE_STRING`, `OUTPUT_MODE_FLAT`, `OUTPUT_MODE_NESTED`.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem reads, subprocess/runtime control, WebSocket state.
-- Imported dependency areas include: `__future__`, `collections`, `dataclasses`, `datetime`, `helpers`, `helpers.localization`, `os`, `pathspec`, `typing`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `dataclass`, `datetime.fromtimestamp`, `files_helper.get_abs_path`, `files_helper.get_abs_path_dockerized`, `_resolve_ignore_patterns`, `os.stat`, `_TreeEntry`, `deque`, `queue.clear`, `_mark_last_flags`, `_refresh_render_metadata`, `path.replace`, `normalized.startswith`, `join`, `_create_global_limit_comment`, `ignore.startswith`, `PathSpec.from_lines`, `segments.reverse`, `os.path.exists`, `FileNotFoundError`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_file_tree_visualize.py`
- - `tests/test_skills_runtime.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/files.py.dox.md b/helpers/files.py.dox.md
deleted file mode 100644
index b4f153240..000000000
--- a/helpers/files.py.dox.md
+++ /dev/null
@@ -1,82 +0,0 @@
-# files.py DOX
-
-## Purpose
-
-- Own the `files.py` helper module.
-- This module owns repository/user path helpers and file read/write/parsing primitives.
-- Keep this file-level DOX profile synchronized with `files.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `files.py` owns the runtime implementation.
-- `files.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `VariablesPlugin` (`ABC`)
- - `get_variables(self, file: str, backup_dirs: list[str] | None=..., **kwargs) -> dict[str, Any]`
-- Top-level functions:
-- `load_plugin_variables(file: str, backup_dirs: list[str] | None=..., **kwargs) -> dict[str, Any]`
-- `parse_file(_filename: str, _directories: list[str] | None=..., _encoding=..., **kwargs)`
-- `read_prompt_file(_file: str, _directories: list[str] | None=..., _encoding=..., **kwargs)`
-- `evaluate_text_conditions(_content: str, **kwargs)`
-- `read_file(relative_path: str, encoding=...)`
-- `read_file_json(relative_path: str, encoding=...)`
-- `read_file_yaml(relative_path: str, encoding=...)`
-- `read_file_bin(relative_path: str)`
-- `read_file_base64(relative_path)`
-- `is_probably_binary_bytes(data: bytes, threshold: float=...) -> bool`: Binary detection.
-- `is_probably_binary_file(file_path: str, sample_size: int=..., threshold: float=...) -> bool`: Binary detection by reading only the first ~sample_size bytes of a file.
-- `replace_placeholders_text(_content: str, **kwargs)`
-- `replace_placeholders_json(_content: str, **kwargs)`
-- `replace_placeholders_dict(_content: dict, **kwargs)`
-- `process_includes(_content: str, _directories: list[str], _source_file: str=..., _source_dir: str=..., **kwargs)`
-- `_get_dirs_after(_directories: list[str], _source_dir: str) -> list[str]`: Return directories after _source_dir in the priority list.
-- `find_file_in_dirs(_filename: str, _directories: list[str])`: This function searches for a filename in a list of directories in order.
-- `get_unique_filenames_in_dirs(dir_paths: list[str], pattern: str=..., type: Literal['file', 'dir', 'any']=...)`
-- `find_existing_paths_by_pattern(pattern: str)`
-- `remove_code_fences(text)`
-- `is_full_json_template(text)`
-- `write_file(relative_path: str, content: str, encoding: str=...)`
-- `delete_file(relative_path: str)`
-- `write_file_bin(relative_path: str, content: bytes)`
-- `write_file_base64(relative_path: str, content: str)`
-- `delete_dir(relative_path: str)`
-- `move_dir(old_path: str, new_path: str)`
-- `move_dir_safe(src, dst, rename_format=...)`
-- `create_dir_safe(dst, rename_format=...)`
-- `create_dir(relative_path: str)`
-- Notable constants/configuration names: `AGENTS_DIR`, `PLUGINS_DIR`, `PROJECTS_DIR`, `EXTENSIONS_DIR`, `USER_DIR`, `TEMP_DIR`, `API_DIR`.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem reads, filesystem writes, filesystem deletion, subprocess/runtime control, plugin state, settings/state persistence, secret handling.
-- Imported dependency areas include: `abc`, `base64`, `fnmatch`, `glob`, `helpers`, `helpers.strings`, `json`, `mimetypes`, `os`, `re`, `shutil`, `simpleeval`, `tempfile`, `typing`, `zipfile`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `os.path.dirname`, `os.path.abspath`, `find_file_in_dirs`, `is_full_json_template`, `remove_code_fences`, `evaluate_text_conditions`, `replace_placeholders_text`, `process_includes`, `re.compile`, `_process`, `get_abs_path`, `is_probably_binary_bytes`, `replace_value`, `re.sub`, `os.path.normpath`, `FileNotFoundError`, `result.sort`, `glob.glob`, `matches.sort`, `re.fullmatch`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_a0_connector_prompt_gating.py`
- - `tests/test_browser_agent_regressions.py`
- - `tests/test_default_prompt_budget.py`
- - `tests/test_document_query_plugin.py`
- - `tests/test_download_toast_regressions.py`
- - `tests/test_file_tree_visualize.py`
- - `tests/test_host_browser_connector.py`
- - `tests/test_image_get_security.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/functions.py.dox.md b/helpers/functions.py.dox.md
deleted file mode 100644
index c2e00daac..000000000
--- a/helpers/functions.py.dox.md
+++ /dev/null
@@ -1,45 +0,0 @@
-# functions.py DOX
-
-## Purpose
-
-- Own the `functions.py` helper module.
-- This module provides safe generic callable execution helpers.
-- Keep this file-level DOX profile synchronized with `functions.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `functions.py` owns the runtime implementation.
-- `functions.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Top-level functions:
-- `safe_call(func, *args, **kwargs)`
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Imported dependency areas include: `inspect`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `inspect.signature`, `func`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_a0_connector_prompt_gating.py`
- - `tests/test_browser_agent_regressions.py`
- - `tests/test_error_retry_plugin.py`
- - `tests/test_oauth_codex.py`
- - `tests/test_oauth_providers.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/git.py.dox.md b/helpers/git.py.dox.md
deleted file mode 100644
index 857c3966c..000000000
--- a/helpers/git.py.dox.md
+++ /dev/null
@@ -1,70 +0,0 @@
-# git.py DOX
-
-## Purpose
-
-- Own the `git.py` helper module.
-- This module reads local and remote Git release/commit metadata safely.
-- Keep this file-level DOX profile synchronized with `git.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `git.py` owns the runtime implementation.
-- `git.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `GitHeadInfo` (no explicit base class)
-- `GitReleaseInfo` (no explicit base class)
-- `GitRemoteReleaseInfo` (no explicit base class)
-- `GitRemoteReleasesResult` (no explicit base class)
-- `GitRemoteCommitsInfo` (no explicit base class)
-- `GitRepoReleaseInfo` (no explicit base class)
-- Top-level functions:
-- `strip_auth_from_url(url: str) -> str`: Remove any authentication info from URL.
-- `extract_author_repo(url: str) -> tuple[str, str]`
-- `_format_git_timestamp(timestamp: int) -> str`
-- `_split_describe_version(describe: str) -> tuple[str, int]`
-- `_format_release_version(branch: str, short_tag: str, commits_since_tag: int, commit_hash: str) -> str`
-- `get_remote_releases(author: str, repo: str) -> GitRemoteReleasesResult`
-- `get_remote_commits_since_local(repo_path: str) -> GitRemoteCommitsInfo`
-- `get_repo_release_info(repo_path: str) -> GitRepoReleaseInfo`
-- `get_git_info()`
-- `get_version()`
-- `is_official_agent_zero_repo() -> bool`: Return True when origin points to agent0ai/agent-zero.
-- `clone_repo(url: str, dest: str, token: str | None=...)`: Clone a git repository. Uses http.extraHeader for token auth (never stored in URL/config).
-- `update_repo(repo_path: str) -> Repo`
-- `get_repo_status(repo_path: str) -> dict`: Get Git repository status, ignoring A0 project metadata files.
-- Notable constants/configuration names: `A0_IGNORE_PATTERNS`.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem writes, filesystem deletion, network calls, subprocess/runtime control, plugin state, settings/state persistence, secret handling.
-- Imported dependency areas include: `base64`, `dataclasses`, `datetime`, `git`, `giturlparse`, `helpers`, `helpers.localization`, `os`, `re`, `subprocess`, `urllib.parse`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `urlparse`, `urlunparse`, `parse`, `strip`, `repo.endswith`, `datetime.fromtimestamp.strftime`, `describe.strip`, `re.fullmatch`, `files.get_base_dir`, `get_repo_release_info`, `os.environ.copy`, `subprocess.run`, `Repo`, `repo.active_branch.tracking_branch`, `strip_auth_from_url`, `ValueError`, `match.group`, `branch.upper`, `author.strip`, `repo.strip`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_docker_release_plan.py`
- - `tests/test_git_version_label.py`
- - `tests/test_model_config_api_keys.py`
- - `tests/test_model_config_project_presets.py`
- - `tests/test_model_search.py`
- - `tests/test_oauth_github_copilot.py`
- - `tests/test_oauth_providers.py`
- - `tests/test_onboarding_static.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/guids.py.dox.md b/helpers/guids.py.dox.md
deleted file mode 100644
index 9fea993bf..000000000
--- a/helpers/guids.py.dox.md
+++ /dev/null
@@ -1,40 +0,0 @@
-# guids.py DOX
-
-## Purpose
-
-- Own the `guids.py` helper module.
-- This module generates framework IDs.
-- Keep this file-level DOX profile synchronized with `guids.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `guids.py` owns the runtime implementation.
-- `guids.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Top-level functions:
-- `generate_id(length: int=...) -> str`
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Imported dependency areas include: `random`, `string`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `join`, `random.choices`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/history.py b/helpers/history.py
index 1ff802d87..4fdefd179 100644
--- a/helpers/history.py
+++ b/helpers/history.py
@@ -40,12 +40,9 @@ MessageContent = Union[
]
-class OutputMessage(TypedDict, total=False):
+class OutputMessage(TypedDict):
ai: bool
content: MessageContent
- metadata: dict[str, Any]
- id: str
- sequence: int
class Record:
@@ -85,20 +82,10 @@ class Record:
class Message(Record):
- def __init__(
- self,
- ai: bool,
- content: MessageContent,
- tokens: int = 0,
- id: str = "",
- metadata: dict[str, Any] | None = None,
- sequence: int = 0,
- ):
+ def __init__(self, ai: bool, content: MessageContent, tokens: int = 0, id: str = ""):
self.id = id or str(uuid.uuid4())
self.ai = ai
self.content = content
- self.metadata = metadata or {}
- self.sequence = sequence
self.summary: str = ""
self.tokens: int = tokens or self.calculate_tokens()
@@ -119,15 +106,7 @@ class Message(Record):
return False
def output(self):
- return [
- OutputMessage(
- ai=self.ai,
- content=self.summary or self.content,
- metadata=self.metadata,
- id=self.id,
- sequence=self.sequence,
- )
- ]
+ return [OutputMessage(ai=self.ai, content=self.summary or self.content)]
def output_langchain(self):
return output_langchain(self.output())
@@ -141,8 +120,6 @@ class Message(Record):
"id": self.id,
"ai": self.ai,
"content": self.content,
- "metadata": self.metadata,
- "sequence": self.sequence,
"summary": self.summary,
"tokens": self.tokens,
}
@@ -150,13 +127,7 @@ class Message(Record):
@staticmethod
def from_dict(data: dict, history: "History"):
content = data.get("content", "Content lost")
- msg = Message(
- ai=data["ai"],
- content=content,
- id=data.get("id", ""),
- metadata=data.get("metadata", {}) if isinstance(data.get("metadata"), dict) else {},
- sequence=int(data.get("sequence", 0) or 0),
- )
+ msg = Message(ai=data["ai"], content=content, id=data.get("id", ""))
msg.summary = data.get("summary", "")
msg.tokens = data.get("tokens", 0)
return msg
@@ -175,22 +146,9 @@ class Topic(Record):
return sum(msg.get_tokens() for msg in self.messages)
def add_message(
- self,
- ai: bool,
- content: MessageContent,
- tokens: int = 0,
- id: str = "",
- metadata: dict[str, Any] | None = None,
- sequence: int = 0,
+ self, ai: bool, content: MessageContent, tokens: int = 0, id: str = ""
) -> Message:
- msg = Message(
- ai=ai,
- content=content,
- tokens=tokens,
- id=id,
- metadata=metadata,
- sequence=sequence,
- )
+ msg = Message(ai=ai, content=content, tokens=tokens, id=id)
self.messages.append(msg)
return msg
@@ -377,22 +335,10 @@ class History(Record):
return self.current.get_tokens()
def add_message(
- self,
- ai: bool,
- content: MessageContent,
- tokens: int = 0,
- id: str = "",
- metadata: dict[str, Any] | None = None,
+ self, ai: bool, content: MessageContent, tokens: int = 0, id: str = ""
) -> Message:
self.counter += 1
- return self.current.add_message(
- ai,
- content=content,
- tokens=tokens,
- id=id,
- metadata=metadata,
- sequence=self.counter,
- )
+ return self.current.add_message(ai, content=content, tokens=tokens, id=id)
def new_topic(self):
if self.current.messages:
@@ -407,35 +353,6 @@ class History(Record):
result += self.current.output()
return result
- def messages_since(self, sequence: int) -> list[Message]:
- return [
- message
- for message in self.all_messages()
- if int(message.sequence or 0) > int(sequence or 0)
- ]
-
- def all_messages(self) -> list[Message]:
- messages: list[Message] = []
- for bulk in self.bulks:
- messages.extend(_messages_from_record(bulk))
- for topic in self.topics:
- messages.extend(topic.messages)
- messages.extend(self.current.messages)
- return messages
-
- def latest_llm_result_for_model(self, provider_model_key: str):
- from helpers.llm_result import result_from_metadata
-
- for message in reversed(self.all_messages()):
- if not message.ai:
- continue
- result = result_from_metadata(message.metadata)
- if not result:
- continue
- if result.provider_model_key == provider_model_key and result.response_id:
- return result
- return None
-
def trim_embeds(self, max_embeds: int) -> int:
if max_embeds == -1:
return 0
@@ -723,28 +640,6 @@ def output_text(messages: list[OutputMessage], ai_label="ai", human_label="human
return "\n".join(_stringify_output(o, ai_label, human_label) for o in messages)
-def clear_responses_provider_state(agent) -> None:
- key = getattr(agent, "DATA_NAME_RESPONSES_STATE", "responses_state")
- get_data = getattr(agent, "get_data", None)
- set_data = getattr(agent, "set_data", None)
- if not callable(get_data) or not callable(set_data):
- return
-
- state = get_data(key)
- if not isinstance(state, dict):
- return
-
- state = dict(state)
- removed = False
- for field in ("response_id", "previous_response_id"):
- if field in state:
- state.pop(field, None)
- removed = True
-
- if removed:
- set_data(key, state)
-
-
def _merge_outputs(a: MessageContent, b: MessageContent) -> MessageContent:
if isinstance(a, str) and isinstance(b, str):
return a + "\n" + b
@@ -784,19 +679,6 @@ def _is_embedded_data(obj: object) -> bool:
return isinstance(obj, Mapping) and obj.get("type") == "image_url"
-def _messages_from_record(record: Record) -> list[Message]:
- if isinstance(record, Message):
- return [record]
- if isinstance(record, Topic):
- return list(record.messages)
- if isinstance(record, Bulk):
- messages: list[Message] = []
- for nested in record.records:
- messages.extend(_messages_from_record(nested))
- return messages
- return []
-
-
def _json_dumps(obj):
return json.dumps(obj, ensure_ascii=False)
diff --git a/helpers/history.py.dox.md b/helpers/history.py.dox.md
deleted file mode 100644
index 53a7149e0..000000000
--- a/helpers/history.py.dox.md
+++ /dev/null
@@ -1,111 +0,0 @@
-# history.py DOX
-
-## Purpose
-
-- Own the `history.py` helper module.
-- This module owns chat history message records and model-output conversion.
-- Keep this file-level DOX profile synchronized with `history.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `history.py` owns the runtime implementation.
-- `history.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `RawMessage` (`TypedDict`)
-- `OutputMessage` (`TypedDict`)
-- `Record` (no explicit base class)
- - `get_tokens(self) -> int`
- - `async compress(self) -> bool`
- - `output(self) -> list[OutputMessage]`
- - `async summarize(self) -> str`
- - `to_dict(self) -> dict`
- - `from_dict(data: dict, history: 'History')`
- - `output_langchain(self)`
- - `output_text(self, human_label=..., ai_label=...)`
-- `Message` (`Record`)
- - `get_tokens(self) -> int`
- - `calculate_tokens(self)`
- - `set_summary(self, summary: str)`
- - `async compress(self)`
- - `output(self)`
- - `output_langchain(self)`
- - `output_text(self, human_label=..., ai_label=...)`
- - `to_dict(self)`
-- `Topic` (`Record`)
- - `get_tokens(self)`
- - `add_message(self, ai: bool, content: MessageContent, tokens: int=..., id: str=...) -> Message`
- - `output(self) -> list[OutputMessage]`
- - `async summarize(self)`
- - `compress_large_messages(self, message_ratio: float=...) -> bool`
- - `async compress(self) -> bool`
- - `async compress_attention(self, ratio: float=...) -> bool`
- - `async summarize_messages(self, messages: list[Message])`
-- `Bulk` (`Record`)
- - `get_tokens(self)`
- - `output(self, human_label: str=..., ai_label: str=...) -> list[OutputMessage]`
- - `async compress(self)`
- - `async summarize(self)`
- - `to_dict(self)`
- - `from_dict(data: dict, history: 'History')`
-- `History` (`Record`)
- - `get_tokens(self) -> int`
- - `is_over_limit(self)`
- - `get_bulks_tokens(self) -> int`
- - `get_topics_tokens(self) -> int`
- - `get_current_topic_tokens(self) -> int`
- - `add_message(self, ai: bool, content: MessageContent, tokens: int=..., id: str=...) -> Message`
- - `new_topic(self)`
- - `output(self) -> list[OutputMessage]`
-- Top-level functions:
-- `deserialize_history(json_data: str, agent) -> History`
-- `_stringify_output(output: OutputMessage, ai_label=..., human_label=...)`
-- `_stringify_content(content: MessageContent) -> str`
-- `_output_content_langchain(content: MessageContent)`
-- `group_outputs_abab(outputs: list[OutputMessage]) -> list[OutputMessage]`
-- `group_messages_abab(messages: list[BaseMessage]) -> list[BaseMessage]`
-- `output_langchain(messages: list[OutputMessage])`
-- `output_text(messages: list[OutputMessage], ai_label=..., human_label=...)`
-- `clear_responses_provider_state(agent) -> None`
-- `_merge_outputs(a: MessageContent, b: MessageContent) -> MessageContent`
-- `_merge_properties(a: Dict[str, MessageContent], b: Dict[str, MessageContent]) -> Dict[str, MessageContent]`
-- `_is_raw_message(obj: object) -> bool`
-- `_is_embedded_data(obj: object) -> bool`
-- `_json_dumps(obj)`
-- `_json_loads(obj)`
-- Notable constants/configuration names: `BULK_MERGE_COUNT`, `TOPICS_MERGE_COUNT`, `CURRENT_TOPIC_RATIO`, `HISTORY_TOPIC_RATIO`, `HISTORY_BULK_RATIO`, `CURRENT_TOPIC_ATTENTION_COMPRESSION`, `HISTORY_TOPIC_ATTENTION_COMPRESSION`, `LARGE_MESSAGE_TO_CURRENT_TOPIC_RATIO`, `LARGE_MESSAGE_TO_HISTORY_TOPIC_RATIO`, `RAW_MESSAGE_OUTPUT_TEXT_TRIM`, `COMPRESSION_TARGET_RATIO`.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- `clear_responses_provider_state(agent)` removes the active provider continuation IDs after local history rewrites while preserving stored response ID lists for later cleanup.
-- Observed side-effect areas: filesystem writes, filesystem deletion, model calls, plugin state, settings/state persistence, secret handling.
-- Imported dependency areas include: `abc`, `asyncio`, `collections`, `collections.abc`, `enum`, `helpers`, `json`, `langchain_core.messages`, `math`, `plugins._model_config.helpers.model_config`, `typing`, `uuid`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `History`, `_is_raw_message`, `_json_dumps`, `group_messages_abab`, `join`, `make_list`, `cast`, `a.copy`, `json.dumps`, `json.loads`, `globals.from_dict`, `output_langchain`, `output_text`, `self.output_text`, `tokens.approximate_tokens`, `self.calculate_tokens`, `Message`, `get_chat_model_config`, `large_msgs.sort`, `self.compress_large_messages`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_browser_agent_regressions.py`
- - `tests/test_chat_compaction.py`
- - `tests/test_error_retry_plugin.py`
- - `tests/test_history_compression_wait.py`
- - `tests/test_mcp_handler_multimodal.py`
- - `tests/test_memory_quality.py`
- - `tests/test_model_config_project_presets.py`
- - `tests/test_office_document_store.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/images.py.dox.md b/helpers/images.py.dox.md
deleted file mode 100644
index 1d2cd0320..000000000
--- a/helpers/images.py.dox.md
+++ /dev/null
@@ -1,48 +0,0 @@
-# images.py DOX
-
-## Purpose
-
-- Own the `images.py` helper module.
-- This module resolves, compresses, and serializes image references for model inputs.
-- Keep this file-level DOX profile synchronized with `images.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `images.py` owns the runtime implementation.
-- `images.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Top-level functions:
-- `prepare_content(content: Any) -> Any`
-- `is_local_ref(url: str) -> bool`
-- `to_data_url(url: str) -> str`
-- `resolve_ref(url: str) -> Path`
-- `compress_image(image_data: bytes, max_pixels: int=..., quality: int=...) -> bytes`: Compress an image by scaling it down and converting to JPEG with quality settings.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem reads, network calls, settings/state persistence.
-- Imported dependency areas include: `PIL`, `base64`, `io`, `math`, `mimetypes`, `pathlib`, `typing`, `urllib.parse`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `url.lower`, `lowered.startswith`, `resolve_ref`, `base64.b64encode.decode`, `Path.expanduser`, `raw_path.startswith`, `FileNotFoundError`, `io.BytesIO`, `img.save`, `output.getvalue`, `prepare_content`, `url.startswith`, `mimetypes.guess_type`, `ValueError`, `url.lower.startswith`, `unquote`, `seen.add`, `math.sqrt`, `img.resize`, `img.convert`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_browser_agent_regressions.py`
- - `tests/test_image_get_security.py`
- - `tests/test_vision_load_image_refs.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/integration_commands.py b/helpers/integration_commands.py
index b4c4cc361..d78f3ff6e 100644
--- a/helpers/integration_commands.py
+++ b/helpers/integration_commands.py
@@ -1,7 +1,6 @@
from __future__ import annotations
import re
-from dataclasses import dataclass
from typing import TYPE_CHECKING
from helpers import message_queue as mq
@@ -15,93 +14,7 @@ if TYPE_CHECKING:
_CLEAR_VALUES = {"", "default", "none", "clear", "off"}
-
-
-@dataclass(frozen=True)
-class IntegrationCommandDef:
- name: str
- description: str
- category: str
- aliases: tuple[str, ...] = ()
- args_hint: str = ""
- menu: bool = True
- integrations: tuple[str, ...] = ()
-
-
-COMMAND_REGISTRY: tuple[IntegrationCommandDef, ...] = (
- IntegrationCommandDef("commands", "Show all integration commands.", "Info", aliases=("help",)),
- IntegrationCommandDef(
- "status",
- "Show this chat's project, model, agent, and queue state.",
- "Info",
- ),
- IntegrationCommandDef("new", "Start a fresh chat context.", "Session"),
- IntegrationCommandDef(
- "sessions",
- "Show or switch recent chat sessions.",
- "Session",
- aliases=("session",),
- integrations=("telegram",),
- ),
- IntegrationCommandDef("clear", "Reset the current chat context.", "Session", aliases=("reset",)),
- IntegrationCommandDef(
- "queue",
- "Show or manage queued messages.",
- "Session",
- args_hint="[send|clear]",
- ),
- IntegrationCommandDef("send", "Send queued messages now.", "Session", aliases=("push",)),
- IntegrationCommandDef(
- "steer",
- "Intervene in the currently running task.",
- "Session",
- args_hint="",
- ),
- IntegrationCommandDef("pause", "Pause the active run.", "Session"),
- IntegrationCommandDef("resume", "Resume a paused run.", "Session"),
- IntegrationCommandDef("nudge", "Nudge the active run.", "Session"),
- IntegrationCommandDef(
- "stream",
- "Enable or disable Telegram response streaming.",
- "Configuration",
- args_hint="[on|off]",
- integrations=("telegram",),
- ),
- IntegrationCommandDef(
- "tools",
- "Show or hide Telegram tool progress.",
- "Configuration",
- args_hint="[on|off]",
- integrations=("telegram",),
- ),
- IntegrationCommandDef(
- "project",
- "Show or switch the active project.",
- "Configuration",
- args_hint="[name|none]",
- ),
- IntegrationCommandDef(
- "model",
- "Show or switch the chat model preset.",
- "Configuration",
- aliases=("config", "preset"),
- args_hint="[preset|default]",
- ),
- IntegrationCommandDef(
- "agent",
- "Show or switch the agent profile.",
- "Configuration",
- aliases=("profile",),
- args_hint="[profile]",
- ),
-)
-
-
-_COMMAND_LOOKUP = {
- f"/{name}": command
- for command in COMMAND_REGISTRY
- for name in (command.name, *command.aliases)
-}
+_SUPPORTED_COMMANDS = {"/send", "/queue", "/project", "/config", "/preset"}
def extract_command_line(text: str) -> str:
@@ -113,132 +26,36 @@ def extract_command_line(text: str) -> str:
return ""
-def parse_command(text: str, *, integration: str | None = None) -> tuple[str, str] | None:
+def parse_command(text: str) -> tuple[str, str] | None:
line = extract_command_line(text)
if not line.startswith("/"):
return None
command, _, args = line.partition(" ")
- command = _normalize_command_token(command)
- resolved = resolve_command(command, integration=integration)
- if not resolved:
+ command = command.strip().lower()
+ if command not in _SUPPORTED_COMMANDS:
return None
- return f"/{resolved.name}", args.strip()
+ return command, args.strip()
-def resolve_command(command: str, *, integration: str | None = None) -> IntegrationCommandDef | None:
- normalized = _normalize_command_token(command)
- if not normalized.startswith("/"):
- normalized = f"/{normalized}"
- command_def = _COMMAND_LOOKUP.get(normalized)
- if not command_def or not _is_command_available(command_def, integration):
- return None
- return command_def
-
-
-def telegram_menu_commands() -> list[tuple[str, str]]:
- return [
- (command.name, _telegram_description(command))
- for command in COMMAND_REGISTRY
- if command.menu and _is_command_available(command, "telegram")
- ]
-
-
-def command_names(include_aliases: bool = True, *, integration: str | None = None) -> list[str]:
- names: list[str] = []
- for command in COMMAND_REGISTRY:
- if not _is_command_available(command, integration):
- continue
- names.append(command.name)
- if include_aliases:
- names.extend(command.aliases)
- return names
-
-
-def help_text(*, full: bool = False, integration: str | None = None) -> str:
- commands = tuple(
- command
- for command in COMMAND_REGISTRY
- if _is_command_available(command, integration) and (full or command.menu)
- )
- lines = ["Available commands:"]
- for command in commands:
- args = f" {command.args_hint}" if command.args_hint else ""
- alias_text = ""
- if command.aliases:
- alias_text = f" (alias: {', '.join('/' + alias for alias in command.aliases)})"
- lines.append(f"/{command.name}{args} - {command.description}{alias_text}")
- return "\n".join(lines)
-
-
-def unknown_command_text(command: str, *, integration: str | None = None) -> str:
- token = _normalize_command_token(command).split(" ", 1)[0]
- return f"Unknown command: {token}\n\n{help_text(full=True, integration=integration)}"
-
-
-def try_handle_command(
- context: "AgentContext",
- text: str,
- *,
- integration: str | None = None,
-) -> str | None:
- parsed = parse_command(text, integration=integration)
+def try_handle_command(context: "AgentContext", text: str) -> str | None:
+ parsed = parse_command(text)
if not parsed:
return None
command, args = parsed
- if command == "/commands":
- return help_text(full=True, integration=integration)
- if command == "/status":
- return _handle_status(context)
- if command == "/sessions":
- return _handle_sessions(context)
- if command in {"/new", "/clear"}:
- return _handle_clear(context, new_chat=(command == "/new"))
if command == "/send":
return _handle_queue(context, "send")
if command == "/queue":
return _handle_queue(context, args)
- if command == "/steer":
- return _handle_steer(context, args)
- if command == "/pause":
- return _handle_pause(context)
- if command == "/resume":
- return _handle_resume(context)
- if command == "/nudge":
- return _handle_nudge(context)
- if command == "/stream":
- return _handle_toggle(context, args, "telegram_stream_enabled", "Response streaming")
- if command == "/tools":
- return _handle_toggle(context, args, "telegram_tools_enabled", "Tool progress")
if command == "/project":
return _handle_project(context, args)
- if command == "/model":
- return _handle_model(context, args)
- if command == "/agent":
- return _handle_agent(context, args)
+ if command in {"/config", "/preset"}:
+ return _handle_config(context, args)
return None
-def _normalize_command_token(command: str) -> str:
- normalized = command.strip().lower()
- if not normalized:
- return ""
- token, *rest = normalized.split(" ", 1)
- if "@" in token:
- token = token.split("@", 1)[0]
- return f"{token} {rest[0]}".strip() if rest else token
-
-
-def _is_command_available(command: IntegrationCommandDef, integration: str | None) -> bool:
- if not command.integrations:
- return True
- if not integration:
- return False
- return integration.lower() in command.integrations
-
-
def _handle_queue(context: "AgentContext", args: str) -> str:
queue = mq.get_queue(context)
count = len(queue)
@@ -251,13 +68,8 @@ def _handle_queue(context: "AgentContext", args: str) -> str:
"Use /send or /queue send to send everything as one batch."
)
- if action in {"clear", "reset"}:
- mq.remove(context)
- mark_dirty_for_context(context.id, reason="integration_commands.queue_clear")
- return "Queue cleared."
-
if action not in {"send", "all"}:
- return "Unknown queue action. Use /queue send to flush or /queue clear to clear."
+ return "Unknown queue action. Use /queue send to flush the queue."
if count == 0:
return "Queue is empty."
@@ -268,91 +80,6 @@ def _handle_queue(context: "AgentContext", args: str) -> str:
return f"Sent {sent_count} queued {noun} as one batch."
-def _handle_status(context: "AgentContext") -> str:
- project_name = context.get_data("project") or "none"
- override = context.get_data("chat_model_override")
- agent_profile = getattr(context.agent0.config, "profile", "default")
- running = "running" if context.is_running() else "idle"
- if getattr(context, "paused", False):
- running = "paused"
- queue_count = len(mq.get_queue(context))
- return (
- f"Status: {running}\n"
- f"Project: {project_name}\n"
- f"Model: {_describe_override(override)}\n"
- f"Agent: {agent_profile}\n"
- f"Queued messages: {queue_count}"
- )
-
-
-def _handle_sessions(context: "AgentContext") -> str:
- from agent import AgentContext
-
- contexts = sorted(
- AgentContext.all(),
- key=lambda item: str(item.output().get("last_message") or ""),
- reverse=True,
- )
- lines = ["Recent sessions:"]
- for item in contexts[:4]:
- marker = " (current)" if item.id == context.id else ""
- running = " - running" if item.is_running() else ""
- lines.append(f"- {item.name or item.id}{marker}{running}")
- if len(contexts) > 4:
- lines.append(f"And {len(contexts) - 4} more. Use Telegram buttons to page through them.")
- return "\n".join(lines)
-
-
-def _handle_clear(context: "AgentContext", *, new_chat: bool) -> str:
- context.reset()
- mq.remove(context)
- save_tmp_chat(context)
- reason = "integration_commands.new" if new_chat else "integration_commands.clear"
- mark_dirty_for_context(context.id, reason=reason)
- return "Started a fresh chat." if new_chat else "Chat cleared."
-
-
-def _handle_steer(context: "AgentContext", args: str) -> str:
- message = args.strip()
- if not message:
- return "Usage: /steer "
- from agent import UserMessage
-
- context.communicate(UserMessage(message=message))
- if context.is_running():
- return "Steering message sent to the active run."
- return "Message sent."
-
-
-def _handle_pause(context: "AgentContext") -> str:
- if not context.is_running():
- return "No active run is currently running."
- context.paused = True
- return "Agent paused."
-
-
-def _handle_resume(context: "AgentContext") -> str:
- context.paused = False
- return "Agent resumed."
-
-
-def _handle_nudge(context: "AgentContext") -> str:
- context.nudge()
- return "Agent nudged."
-
-
-def _handle_toggle(context: "AgentContext", args: str, key: str, label: str) -> str:
- value = _parse_toggle(args)
- current = _get_toggle(context, key)
- if value is None:
- state = "on" if current else "off"
- return f"{label}: {state}. Use /{key.split('_')[1]} on or /{key.split('_')[1]} off."
- context.set_data(key, value)
- save_tmp_chat(context)
- mark_dirty_for_context(context.id, reason=f"integration_commands.{key}")
- return f"{label} {'enabled' if value else 'disabled'}."
-
-
def _handle_project(context: "AgentContext", args: str) -> str:
items = projects.get_active_projects_list() or []
current_name = context.get_data("project") or ""
@@ -388,7 +115,7 @@ def _handle_project(context: "AgentContext", args: str) -> str:
return f"Switched project to {match.get('title') or match['name']}."
-def _handle_model(context: "AgentContext", args: str) -> str:
+def _handle_config(context: "AgentContext", args: str) -> str:
allowed = model_config.is_chat_override_allowed(context.agent0)
presets = [preset for preset in model_config.get_presets() if preset.get("name")]
current_override = context.get_data("chat_model_override")
@@ -396,12 +123,12 @@ def _handle_model(context: "AgentContext", args: str) -> str:
if not args:
current_label = _describe_override(current_override)
available = ", ".join(preset["name"] for preset in presets) or "none"
- suffix = "Use /model to switch, or /model default to clear it."
+ suffix = "Use /config to switch, or /config default to clear it."
if not allowed:
suffix = "Per-chat config switching is disabled in Model Configuration."
return (
- f"Current model: {current_label}\n"
- f"Available presets: {available}\n"
+ f"Current config: {current_label}\n"
+ f"Available configs: {available}\n"
f"{suffix}"
)
@@ -432,45 +159,7 @@ def _handle_model(context: "AgentContext", args: str) -> str:
context.set_data("chat_model_override", {"preset_name": preset_name})
save_tmp_chat(context)
mark_dirty_for_context(context.id, reason="integration_commands.config_set")
- return f"Switched model preset to {preset_name}."
-
-
-def _handle_agent(context: "AgentContext", args: str) -> str:
- from helpers import subagents
- from initialize import initialize_agent
-
- items = subagents.get_all_agents_list()
- current = getattr(context.agent0.config, "profile", "default")
- if not args:
- available = ", ".join(_format_agent_entry(item) for item in items) or "none"
- return (
- f"Current agent: {current}\n"
- f"Available agents: {available}\n"
- "Use /agent to switch after the current run finishes."
- )
-
- if context.is_running():
- return "Agent profile can be changed after the current run finishes."
-
- desired = _strip_quotes(args)
- match, ambiguous = _match_named_item(items, desired, keys=("key", "label"))
- if ambiguous:
- names = ", ".join(_format_agent_entry(item) for item in ambiguous)
- return f"Agent profile is ambiguous. Matches: {names}"
- if not match:
- available = ", ".join(_format_agent_entry(item) for item in items) or "none"
- return f"Agent profile '{desired}' was not found. Available agents: {available}"
-
- profile = str(match["key"])
- if profile == current:
- return f"Already using agent {match.get('label') or profile}."
-
- config = initialize_agent(override_settings={"agent_profile": profile})
- context.config = config
- context.agent0.config = config
- save_tmp_chat(context)
- mark_dirty_for_context(context.id, reason="integration_commands.agent_set")
- return f"Switched agent to {match.get('label') or profile}."
+ return f"Switched config to {preset_name}."
def _format_project_entry(item: dict) -> str:
@@ -481,19 +170,6 @@ def _format_project_entry(item: dict) -> str:
return name or title
-def _format_agent_entry(item: dict) -> str:
- key = str(item.get("key", "") or "").strip()
- label = str(item.get("label", "") or "").strip()
- if label and label.lower() != key.lower():
- return f"{label} ({key})"
- return key or label
-
-
-def _telegram_description(command: IntegrationCommandDef) -> str:
- description = command.description.strip()
- return description[:255] if len(description) > 255 else description
-
-
def _describe_project(items: list[dict], current_name: str) -> str:
if not current_name:
return "none"
@@ -525,20 +201,6 @@ def _normalize_lookup(value: str) -> str:
return lowered.strip()
-def _get_toggle(context: "AgentContext", key: str) -> bool:
- value = context.get_data(key)
- return True if value is None else bool(value)
-
-
-def _parse_toggle(args: str) -> bool | None:
- value = _normalize_lookup(args)
- if value in {"on", "enable", "enabled", "yes", "true", "1"}:
- return True
- if value in {"off", "disable", "disabled", "no", "false", "0"}:
- return False
- return None
-
-
def _match_named_item(
items: list[dict],
desired: str,
diff --git a/helpers/integration_commands.py.dox.md b/helpers/integration_commands.py.dox.md
deleted file mode 100644
index 1d00fb083..000000000
--- a/helpers/integration_commands.py.dox.md
+++ /dev/null
@@ -1,55 +0,0 @@
-# integration_commands.py DOX
-
-## Purpose
-
-- Own the `integration_commands.py` helper module.
-- This module parses external integration slash commands such as project/config/send controls.
-- Keep this file-level DOX profile synchronized with `integration_commands.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `integration_commands.py` owns the runtime implementation.
-- `integration_commands.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Top-level functions:
-- `extract_command_line(text: str) -> str`
-- `parse_command(text: str) -> tuple[str, str] | None`
-- `try_handle_command(context: 'AgentContext', text: str) -> str | None`
-- `_handle_queue(context: 'AgentContext', args: str) -> str`
-- `_handle_project(context: 'AgentContext', args: str) -> str`
-- `_handle_config(context: 'AgentContext', args: str) -> str`
-- `_format_project_entry(item: dict) -> str`
-- `_describe_project(items: list[dict], current_name: str) -> str`
-- `_describe_override(override: dict | None) -> str`
-- `_strip_quotes(value: str) -> str`
-- `_normalize_lookup(value: str) -> str`
-- `_match_named_item(items: list[dict], desired: str, keys: tuple[str, ...]) -> tuple[dict | None, list[dict]]`
-- Notable constants/configuration names: `_CLEAR_VALUES`, `_SUPPORTED_COMMANDS`.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem writes, model calls, plugin state, settings/state persistence.
-- Imported dependency areas include: `__future__`, `helpers`, `helpers.persist_chat`, `helpers.state_monitor_integration`, `plugins._model_config.helpers`, `re`, `typing`.
-- `/agent` switches the top-level chat profile and preserves existing subordinate agent profiles.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `splitlines`, `extract_command_line`, `line.partition`, `command.strip.lower`, `parse_command`, `mq.get_queue`, `args.strip.lower`, `mq.send_all_aggregated`, `mark_dirty_for_context`, `_strip_quotes`, `_match_named_item`, `projects.activate_project`, `initialize_agent`, `model_config.is_chat_override_allowed`, `context.get_data`, `context.set_data`, `save_tmp_chat`, `str.strip`, `value.strip`, `value.lower.strip`, `re.sub`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_subagent_profiles.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/job_loop.py.dox.md b/helpers/job_loop.py.dox.md
deleted file mode 100644
index b0414749d..000000000
--- a/helpers/job_loop.py.dox.md
+++ /dev/null
@@ -1,46 +0,0 @@
-# job_loop.py DOX
-
-## Purpose
-
-- Own the `job_loop.py` helper module.
-- This module runs periodic scheduler and maintenance loops.
-- Keep this file-level DOX profile synchronized with `job_loop.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `job_loop.py` owns the runtime implementation.
-- `job_loop.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Top-level functions:
-- `async run_loop()`
-- `async scheduler_tick()`
-- `pause_loop()`
-- `resume_loop()`
-- Notable constants/configuration names: `SLEEP_TIME`.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: scheduler state.
-- Imported dependency areas include: `asyncio`, `datetime`, `helpers`, `helpers.print_style`, `helpers.task_scheduler`, `time`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `time.time`, `runtime.is_development`, `scheduler.tick`, `call_extensions_async`, `resume_loop`, `asyncio.sleep`, `runtime.call_development_function`, `PrintStyle.error`, `scheduler_tick`, `errors.format_error`, `PrintStyle`, `errors.error_text`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_api_chat_lifetime.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/kvp.py.dox.md b/helpers/kvp.py.dox.md
deleted file mode 100644
index d7e6e0de0..000000000
--- a/helpers/kvp.py.dox.md
+++ /dev/null
@@ -1,52 +0,0 @@
-# kvp.py DOX
-
-## Purpose
-
-- Own the `kvp.py` helper module.
-- This module provides runtime and persistent key-value storage helpers.
-- Keep this file-level DOX profile synchronized with `kvp.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `kvp.py` owns the runtime implementation.
-- `kvp.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Top-level functions:
-- `_persistent_dir() -> str`
-- `_validate_key(key: str) -> None`
-- `_key_to_path(key: str) -> str`
-- `get_runtime(key: str, default: Any=...) -> Any`
-- `set_runtime(key: str, value: Any) -> None`
-- `remove_runtime(key: str) -> None`
-- `find_runtime(pattern: str) -> list[str]`
-- `get_persistent(key: str, default: Any=...) -> Any`
-- `set_persistent(key: str, value: Any) -> None`
-- `remove_persistent(key: str) -> None`
-- `find_persistent(pattern: str) -> list[str]`
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem reads, filesystem writes, filesystem deletion, settings/state persistence.
-- Imported dependency areas include: `fnmatch`, `glob`, `helpers.files`, `json`, `os`, `tempfile`, `threading`, `typing`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `threading.RLock`, `get_abs_path`, `_validate_key`, `os.path.join`, `_key_to_path`, `os.path.dirname`, `_persistent_dir`, `ValueError`, `_runtime_store.pop`, `os.makedirs`, `tempfile.mkstemp`, `glob.glob`, `keys.sort`, `os.replace`, `os.unlink`, `os.path.isdir`, `json.load`, `os.fdopen`, `json.dump`, `f.flush`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_browser_agent_regressions.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/litellm_transport.py b/helpers/litellm_transport.py
deleted file mode 100644
index a3dd40bf8..000000000
--- a/helpers/litellm_transport.py
+++ /dev/null
@@ -1,2007 +0,0 @@
-from __future__ import annotations
-
-from dataclasses import dataclass, field
-from enum import Enum
-import hashlib
-import inspect
-import json
-from typing import Any, AsyncIterator, Iterator, Optional
-
-from litellm import (
- acompletion,
- adelete_responses,
- aresponses,
- completion,
- delete_responses,
- responses,
-)
-
-from helpers import images
-from helpers.llm_result import LLMResult
-
-
-ChatChunk = dict[str, str]
-
-
-class TransportMode(Enum):
- RESPONSES = "responses"
- CHAT_COMPLETIONS = "chat_completions"
-
-
-class TransportRecovery(Enum):
- RAISE = "raise"
- RETRY_RESPONSES = "retry_responses"
- RETRY_LOCAL_RESPONSES = "retry_local_responses"
- FALLBACK_TO_CHAT = "fallback_to_chat"
-
-
-CHAT_COMPLETIONS_ALIASES = {
- "chat",
- "chat_completion",
- "chat_completions",
- "completion",
- "completions",
-}
-RESPONSES_ALIASES = {"", "auto", "default", "response", "responses", "responses_api"}
-RESPONSES_REASONING_EFFORTS = {"minimal", "low", "medium", "high"}
-RESPONSES_REASONING_FALLBACK_EFFORT = "high"
-NO_REASONING_EFFORT_ALIASES = {"", "0", "false", "no", "none", "off", "disabled"}
-RESPONSES_UNSUPPORTED_CACHE: set[str] = set()
-RESPONSES_STATE_UNSUPPORTED_CACHE: set[str] = set()
-RESPONSES_BUILTIN_UNSUPPORTED_CACHE: dict[str, set[str]] = {}
-OPENAI_RESPONSES_EXTRA_BODY_PARAMS = {
- "context_management",
- "prompt_cache_retention",
-}
-CACHE_CONTROL_PROMPT_PROVIDERS = {
- "anthropic",
- "bedrock",
- "databricks",
- "dashscope",
- "gemini",
- "gemini_api_oauth",
- "minimax",
- "openrouter",
- "vertex_ai",
- "vertexai",
- "z_ai",
- "zai",
-}
-OPENAI_PROMPT_CACHE_PROVIDERS = {"openai", "azure"}
-RESPONSES_STATE_PROVIDER = "provider"
-RESPONSES_STATE_LOCAL = "local"
-RESPONSES_STATE_OFF = "off"
-RESPONSES_STATES = {
- RESPONSES_STATE_PROVIDER,
- RESPONSES_STATE_LOCAL,
- RESPONSES_STATE_OFF,
-}
-
-
-@dataclass
-class TransportPolicy:
- mode: TransportMode
- allow_fallback: bool = True
- retried_reasoning: bool = False
- fallback_error: Exception | None = None
- state_fallback_error: Exception | None = None
- cache_key: str = ""
- state: str = RESPONSES_STATE_PROVIDER
-
- @classmethod
- def from_request(
- cls,
- model: str,
- kwargs: dict[str, Any],
- messages: list[dict[str, Any]] | None = None,
- ) -> "TransportPolicy":
- mode = cls._pop_mode(kwargs)
- allow_fallback = _coerce_bool(
- kwargs.pop("a0_responses_fallback", True), default=True
- )
- cache_key = _responses_cache_key(model, kwargs)
- state = _normalize_responses_state(kwargs.get("responses_state"))
-
- if mode is TransportMode.CHAT_COMPLETIONS:
- _drop_responses_only_kwargs(kwargs)
- return cls(
- mode=mode,
- allow_fallback=allow_fallback,
- cache_key=cache_key,
- state=RESPONSES_STATE_OFF,
- )
-
- if (
- state == RESPONSES_STATE_PROVIDER
- and cache_key in RESPONSES_STATE_UNSUPPORTED_CACHE
- ):
- kwargs["responses_state"] = RESPONSES_STATE_LOCAL
- state = RESPONSES_STATE_LOCAL
-
- _filter_unsupported_builtin_tools(kwargs, cache_key)
-
- if _should_preserve_cache_control_on_chat(model, kwargs, messages or []):
- return cls(
- mode=TransportMode.CHAT_COMPLETIONS,
- allow_fallback=allow_fallback,
- cache_key=cache_key,
- state=RESPONSES_STATE_OFF,
- )
-
- if cache_key in RESPONSES_UNSUPPORTED_CACHE:
- return cls(
- mode=TransportMode.CHAT_COMPLETIONS,
- allow_fallback=allow_fallback,
- fallback_error=RuntimeError("Responses API previously failed"),
- cache_key=cache_key,
- state=RESPONSES_STATE_OFF,
- )
-
- return cls(
- mode=TransportMode.RESPONSES,
- allow_fallback=allow_fallback,
- cache_key=cache_key,
- state=state,
- )
-
- @staticmethod
- def _pop_mode(kwargs: dict[str, Any]) -> TransportMode:
- value = str(kwargs.pop("a0_api_mode", "responses") or "").lower().strip()
- if value in CHAT_COMPLETIONS_ALIASES:
- return TransportMode.CHAT_COMPLETIONS
- if value in RESPONSES_ALIASES:
- return TransportMode.RESPONSES
- return TransportMode.RESPONSES
-
- @property
- def using_responses(self) -> bool:
- return self.mode is TransportMode.RESPONSES
-
- def recover(self, exc: Exception, *, got_any_chunk: bool) -> TransportRecovery:
- if not self.using_responses or got_any_chunk:
- return TransportRecovery.RAISE
- if not self.retried_reasoning and _is_responses_reasoning_effort_error(exc):
- self.retried_reasoning = True
- return TransportRecovery.RETRY_RESPONSES
- if (
- self.state == RESPONSES_STATE_PROVIDER
- and _is_responses_state_unsupported_error(exc)
- ):
- self.state = RESPONSES_STATE_LOCAL
- self.state_fallback_error = exc
- if self.cache_key:
- RESPONSES_STATE_UNSUPPORTED_CACHE.add(self.cache_key)
- return TransportRecovery.RETRY_LOCAL_RESPONSES
- if self.allow_fallback and _is_responses_not_supported_error(exc):
- self.mode = TransportMode.CHAT_COMPLETIONS
- self.fallback_error = exc
- self.state = RESPONSES_STATE_OFF
- if self.cache_key:
- RESPONSES_UNSUPPORTED_CACHE.add(self.cache_key)
- return TransportRecovery.FALLBACK_TO_CHAT
- return TransportRecovery.RAISE
-
-
-@dataclass
-class LiteLLMTransport:
- model: str
- messages: list[dict[str, Any]]
- kwargs: dict[str, Any]
- stop: Optional[list[str]] = None
- policy: TransportPolicy = field(init=False)
- last_result: LLMResult | None = field(init=False, default=None)
- last_request_state: str = field(init=False, default=RESPONSES_STATE_PROVIDER)
- explicit_prompt_caching: bool = field(init=False, default=False)
-
- def __post_init__(self) -> None:
- self.kwargs = _without_stream_kwarg(dict(self.kwargs))
- self.explicit_prompt_caching = _coerce_bool(
- self.kwargs.pop("a0_explicit_prompt_caching", False), default=False
- )
- if self.explicit_prompt_caching:
- self.messages = apply_chat_prompt_cache_markers(
- self.messages,
- model=self.model,
- kwargs=self.kwargs,
- )
- self.policy = TransportPolicy.from_request(
- self.model,
- self.kwargs,
- messages=self.messages,
- )
-
- def complete(self) -> ChatChunk:
- while True:
- try:
- if self.policy.mode is TransportMode.CHAT_COMPLETIONS:
- parsed = ChatCompletionsTransport.parse(
- completion(**self._chat_request(stream=False))
- )
- self.last_result = self._llm_result_from_chat(parsed)
- return parsed
- request = self._responses_request(stream=False)
- raw_response = responses(**request)
- parsed = ResponsesTransport.parse_response(raw_response)
- self.last_result = self._llm_result_from_response(
- raw_response, request
- )
- return parsed
- except Exception as exc:
- if self._recover(exc, got_any_chunk=False):
- continue
- raise
-
- async def acomplete(self) -> ChatChunk:
- while True:
- try:
- if self.policy.mode is TransportMode.CHAT_COMPLETIONS:
- parsed = ChatCompletionsTransport.parse(
- await acompletion(**self._chat_request(stream=False))
- )
- self.last_result = self._llm_result_from_chat(parsed)
- return parsed
- request = self._responses_request(stream=False)
- raw_response = await aresponses(**request)
- parsed = ResponsesTransport.parse_response(raw_response)
- self.last_result = self._llm_result_from_response(
- raw_response, request
- )
- return parsed
- except Exception as exc:
- if self._recover(exc, got_any_chunk=False):
- continue
- raise
-
- def stream(self) -> Iterator[ChatChunk]:
- while True:
- iterator = None
- exhausted = False
- got_any_chunk = False
- try:
- if self.policy.mode is TransportMode.CHAT_COMPLETIONS:
- iterator = completion(**self._chat_request(stream=True))
- parser = ChatCompletionsStreamParser()
- for chunk in iterator:
- parsed = parser.parse(chunk)
- if _has_chunk_delta(parsed):
- got_any_chunk = True
- yield parsed
- parsed = parser.flush()
- if _has_chunk_delta(parsed):
- got_any_chunk = True
- yield parsed
- self.last_result = self._stream_result_from_chat_parser(parser)
- else:
- request = self._responses_request(stream=True)
- iterator = responses(**request)
- parser = ResponsesEventParser()
- for event in iterator:
- parsed = parser.parse(event)
- if _has_chunk_delta(parsed):
- got_any_chunk = True
- yield parsed
- self.last_result = self._stream_result_from_parser(
- parser, request
- )
- exhausted = True
- return
- except Exception as exc:
- if self._recover(exc, got_any_chunk=got_any_chunk):
- continue
- raise
- finally:
- if iterator is not None and not exhausted:
- _close_sync_stream(iterator)
-
- async def astream(self) -> AsyncIterator[ChatChunk]:
- while True:
- iterator = None
- exhausted = False
- got_any_chunk = False
- try:
- if self.policy.mode is TransportMode.CHAT_COMPLETIONS:
- iterator = await acompletion(**self._chat_request(stream=True))
- parser = ChatCompletionsStreamParser()
- async for chunk in iterator: # type: ignore[union-attr]
- parsed = parser.parse(chunk)
- if _has_chunk_delta(parsed):
- got_any_chunk = True
- yield parsed
- parsed = parser.flush()
- if _has_chunk_delta(parsed):
- got_any_chunk = True
- yield parsed
- self.last_result = self._stream_result_from_chat_parser(parser)
- else:
- request = self._responses_request(stream=True)
- iterator = await aresponses(**request)
- parser = ResponsesEventParser()
- async for event in iterator: # type: ignore[union-attr]
- parsed = parser.parse(event)
- if _has_chunk_delta(parsed):
- got_any_chunk = True
- yield parsed
- self.last_result = self._stream_result_from_parser(
- parser, request
- )
- exhausted = True
- return
- except Exception as exc:
- if self._recover(exc, got_any_chunk=got_any_chunk):
- continue
- raise
- finally:
- if iterator is not None and not exhausted:
- await _close_async_stream(iterator)
-
- def _recover(self, exc: Exception, *, got_any_chunk: bool) -> bool:
- if (
- self.policy.using_responses
- and not got_any_chunk
- and self.kwargs.get("responses_builtin_tools")
- and _is_responses_builtin_tool_error(exc)
- ):
- downgraded = _builtin_tool_types(self.kwargs.get("responses_builtin_tools"))
- if downgraded:
- RESPONSES_BUILTIN_UNSUPPORTED_CACHE.setdefault(
- self.policy.cache_key, set()
- ).update(downgraded)
- self.kwargs["_a0_responses_builtin_downgrades"] = sorted(downgraded)
- self.kwargs["responses_builtin_tools"] = []
- return True
-
- recovery = self.policy.recover(exc, got_any_chunk=got_any_chunk)
- if recovery is TransportRecovery.RETRY_RESPONSES:
- self.kwargs["reasoning"] = {
- "effort": RESPONSES_REASONING_FALLBACK_EFFORT
- }
- return True
- if recovery is TransportRecovery.RETRY_LOCAL_RESPONSES:
- self.kwargs["responses_state"] = RESPONSES_STATE_LOCAL
- self.kwargs.pop("previous_response_id", None)
- return True
- return recovery is TransportRecovery.FALLBACK_TO_CHAT
-
- def _chat_request(self, *, stream: bool) -> dict[str, Any]:
- chat_kwargs = ChatCompletionsTransport.prepare_kwargs(
- self.kwargs,
- fallback_error=self.policy.fallback_error,
- model=self.model,
- messages=self.messages,
- explicit_prompt_caching=self.explicit_prompt_caching,
- )
- request = {
- "model": self.model,
- "messages": ChatCompletionsTransport.prepare_messages(
- self.messages,
- model=self.model,
- kwargs=chat_kwargs,
- ),
- "stream": stream,
- **chat_kwargs,
- }
- if self.stop is not None:
- request["stop"] = self.stop
- return request
-
- def _responses_request(self, *, stream: bool) -> dict[str, Any]:
- response_kwargs = ResponsesTransport.from_chat(
- self.messages,
- self.kwargs,
- stop=self.stop,
- model=self.model,
- )
- self.last_request_state = _normalize_responses_state(
- self.kwargs.get("responses_state")
- )
- return {
- "model": self.model,
- "stream": stream,
- **response_kwargs,
- }
-
- def _llm_result_from_chat(self, parsed: ChatChunk) -> LLMResult:
- return LLMResult.from_chat(
- response=parsed["response_delta"],
- reasoning=parsed["reasoning_delta"],
- input_items=ResponsesTransport.input_from_messages(self.messages),
- output_items=parsed.get("_output_items"),
- provider_model_key=self.model,
- capability=self._capability_metadata(),
- )
-
- def _llm_result_from_response(
- self, response: Any, request: dict[str, Any]
- ) -> LLMResult:
- return LLMResult.from_response(
- response,
- input_items=_as_list(request.get("input")),
- previous_response_id=str(request.get("previous_response_id") or ""),
- provider_model_key=self.model,
- mode=TransportMode.RESPONSES.value,
- state=self.last_request_state,
- capability=self._capability_metadata(),
- )
-
- def _stream_result_from_parser(
- self, parser: "ResponsesEventParser", request: dict[str, Any]
- ) -> LLMResult | None:
- if parser.completed_response is None:
- return None
- return self._llm_result_from_response(parser.completed_response, request)
-
- def _stream_result_from_chat_parser(
- self, parser: "ChatCompletionsStreamParser"
- ) -> LLMResult | None:
- output_items = parser.output_items()
- if not output_items:
- return None
- return LLMResult.from_chat(
- response=parser.function_calls_text(),
- input_items=ResponsesTransport.input_from_messages(self.messages),
- output_items=output_items,
- provider_model_key=self.model,
- capability=self._capability_metadata(),
- )
-
- def _capability_metadata(self) -> dict[str, Any]:
- return {
- "mode": self.policy.mode.value,
- "state": self.policy.state,
- "cache_key": self.policy.cache_key,
- "fallback_error": _exception_text(self.policy.fallback_error)
- if self.policy.fallback_error
- else "",
- "state_fallback_error": _exception_text(self.policy.state_fallback_error)
- if self.policy.state_fallback_error
- else "",
- "builtin_tool_downgrades": list(
- self.kwargs.get("_a0_responses_builtin_downgrades") or []
- ),
- }
-
-
-class ChatCompletionsTransport:
- @staticmethod
- def prepare_messages(
- messages: list[dict[str, Any]],
- *,
- model: str = "",
- kwargs: dict[str, Any] | None = None,
- ) -> list[dict[str, Any]]:
- if _is_openai_prompt_cache_provider(model, kwargs or {}):
- stripped = _without_cache_control(messages)
- return stripped if isinstance(stripped, list) else messages
- return messages
-
- @staticmethod
- def prepare_kwargs(
- kwargs: dict[str, Any],
- fallback_error: Exception | None = None,
- *,
- model: str = "",
- messages: list[dict[str, Any]] | None = None,
- explicit_prompt_caching: bool = False,
- ) -> dict[str, Any]:
- chat_kwargs = dict(kwargs)
- _drop_internal_transport_kwargs(chat_kwargs)
- if not _has_tools(chat_kwargs.get("tools")):
- chat_kwargs.pop("tools", None)
- chat_kwargs.pop("tool_choice", None)
- chat_kwargs.pop("parallel_tool_calls", None)
- if _is_openai_prompt_cache_provider(model, chat_kwargs):
- _prepare_openai_prompt_cache_params(
- chat_kwargs,
- messages or [],
- model=model,
- )
- elif _supports_cache_control_markers(model, chat_kwargs) and (
- explicit_prompt_caching or _has_cache_control(messages or [])
- ):
- _apply_tool_cache_control(chat_kwargs)
- if fallback_error is not None:
- chat_kwargs.setdefault("drop_params", True)
- return {key: value for key, value in chat_kwargs.items() if value is not None}
-
- @staticmethod
- def parse(chunk: Any) -> ChatChunk:
- choice = _first_choice(chunk)
- delta = _get_value(choice, "delta") or {}
- message = _get_value(choice, "message") or _get_value(
- _get_value(choice, "model_extra") or {}, "message"
- ) or {}
- response_delta = _get_value(delta, "content") or _get_value(
- message, "content"
- ) or ""
- reasoning_delta = _get_value(delta, "reasoning_content") or _get_value(
- message, "reasoning_content"
- ) or ""
- parsed = {"reasoning_delta": reasoning_delta, "response_delta": response_delta}
- if not response_delta:
- tool_calls = _as_list(_get_value(message, "tool_calls"))
- response_delta = ChatCompletionsTransport.tool_calls_text(tool_calls)
- if response_delta:
- parsed["response_delta"] = response_delta
- parsed["_output_items"] = ChatCompletionsTransport.output_items(
- tool_calls
- )
- return parsed
-
- @classmethod
- def tool_calls_text(cls, tool_calls: Any) -> str:
- calls = [cls.tool_call_object(call) for call in _as_list(tool_calls)]
- calls = [call for call in calls if call]
- if not calls:
- return ""
- if len(calls) == 1:
- return json.dumps(calls[0])
- return json.dumps(
- {"tool_name": "parallel_tool_calls", "tool_args": {"calls": calls}}
- )
-
- @classmethod
- def output_items(cls, tool_calls: Any) -> list[dict[str, Any]]:
- items = []
- for index, tool_call in enumerate(_as_list(tool_calls)):
- item = cls.function_call_item(tool_call, fallback_index=index)
- if item:
- items.append(item)
- return items
-
- @classmethod
- def function_call_item(
- cls, tool_call: Any, *, fallback_index: int = 0
- ) -> dict[str, Any]:
- function = _get_value(tool_call, "function") or {}
- name = _get_value(function, "name") or _get_value(tool_call, "name")
- if not name:
- return {}
- raw_arguments = _get_value(function, "arguments")
- if raw_arguments is None:
- raw_arguments = _get_value(tool_call, "arguments") or "{}"
- call_id = str(_get_value(tool_call, "id") or f"call_{fallback_index}")
- return {
- "type": "function_call",
- "id": call_id,
- "call_id": call_id,
- "name": str(name),
- "arguments": raw_arguments
- if isinstance(raw_arguments, str)
- else json.dumps(raw_arguments),
- }
-
- @classmethod
- def tool_call_object(cls, tool_call: Any) -> dict[str, Any]:
- item = cls.function_call_item(tool_call)
- if not item:
- return {}
- return ResponsesTransport.function_call_object(item)
-
-
-class ChatCompletionsStreamParser:
- def __init__(self) -> None:
- self.tool_calls: dict[str, dict[str, Any]] = {}
- self.order: list[str] = []
- self.emitted = False
-
- def parse(self, chunk: Any) -> ChatChunk:
- parsed = ChatCompletionsTransport.parse(chunk)
- choice = _first_choice(chunk)
- delta = _get_value(choice, "delta") or {}
- self._append_tool_calls(_get_value(delta, "tool_calls"))
- self._append_legacy_function_call(_get_value(delta, "function_call"))
-
- if _get_value(choice, "finish_reason") in {"tool_calls", "function_call"}:
- text = self._emit()
- if text and not parsed["response_delta"]:
- parsed["response_delta"] = text
- return parsed
-
- def flush(self) -> ChatChunk:
- return {"reasoning_delta": "", "response_delta": self._emit()}
-
- def function_calls_text(self) -> str:
- return ChatCompletionsTransport.tool_calls_text(self._ordered_tool_calls())
-
- def output_items(self) -> list[dict[str, Any]]:
- return ChatCompletionsTransport.output_items(self._ordered_tool_calls())
-
- def _append_tool_calls(self, tool_calls: Any) -> None:
- for fallback_index, tool_call in enumerate(_as_list(tool_calls)):
- key = self._tool_call_key(tool_call, fallback_index)
- current = self._current_tool_call(key)
- if _get_value(tool_call, "id"):
- current["id"] = _get_value(tool_call, "id")
- if _get_value(tool_call, "type"):
- current["type"] = _get_value(tool_call, "type")
- self._append_function_delta(current, _get_value(tool_call, "function"))
-
- def _append_legacy_function_call(self, function_call: Any) -> None:
- if not function_call:
- return
- current = self._current_tool_call("0")
- current["type"] = "function"
- self._append_function_delta(current, function_call)
-
- def _append_function_delta(self, tool_call: dict[str, Any], delta: Any) -> None:
- if not delta:
- return
- function = tool_call.setdefault("function", {})
- if _get_value(delta, "name"):
- function["name"] = _get_value(delta, "name")
- if _get_value(delta, "arguments") is not None:
- function["arguments"] = str(function.get("arguments") or "") + str(
- _get_value(delta, "arguments") or ""
- )
-
- def _current_tool_call(self, key: str) -> dict[str, Any]:
- if key not in self.tool_calls:
- self.tool_calls[key] = {"type": "function", "function": {}}
- self.order.append(key)
- return self.tool_calls[key]
-
- def _ordered_tool_calls(self) -> list[dict[str, Any]]:
- return [self.tool_calls[key] for key in self.order]
-
- def _emit(self) -> str:
- if self.emitted:
- return ""
- text = self.function_calls_text()
- if text:
- self.emitted = True
- return text
-
- @staticmethod
- def _tool_call_key(tool_call: Any, fallback_index: int) -> str:
- index = _get_value(tool_call, "index")
- if index is not None:
- return str(index)
- if _get_value(tool_call, "id"):
- return str(_get_value(tool_call, "id"))
- return str(fallback_index)
-
-
-class ResponsesTransport:
- @classmethod
- def from_chat(
- cls,
- messages: list[dict[str, Any]],
- kwargs: dict[str, Any],
- stop: Optional[list[str]] = None,
- model: str = "",
- ) -> dict[str, Any]:
- request = cls.prepare_kwargs(kwargs, stop=stop, model=model, messages=messages)
- state = _normalize_responses_state(kwargs.get("responses_state"))
- input_items = cls._select_input_items(kwargs, messages, state)
- request["input"] = input_items or ""
- cls.apply_state(request, kwargs, state=state)
- return request
-
- @classmethod
- def from_input(
- cls,
- input_items: list[dict[str, Any]],
- kwargs: dict[str, Any],
- stop: Optional[list[str]] = None,
- model: str = "",
- messages: list[dict[str, Any]] | None = None,
- ) -> dict[str, Any]:
- request = cls.prepare_kwargs(kwargs, stop=stop, model=model, messages=messages)
- state = _normalize_responses_state(kwargs.get("responses_state"))
- request["input"] = list(input_items or []) or ""
- cls.apply_state(request, kwargs, state=state)
- return request
-
- @classmethod
- def prepare_kwargs(
- cls,
- kwargs: dict[str, Any],
- stop: Optional[list[str]] = None,
- model: str = "",
- messages: list[dict[str, Any]] | None = None,
- ) -> dict[str, Any]:
- request = dict(kwargs)
- response_function_tools = request.pop("a0_responses_function_tools", None)
- response_builtin_tools = request.pop("responses_builtin_tools", None)
- _drop_responses_only_kwargs(request)
- _drop_legacy_transport_kwargs(request)
- request.pop("stop", None)
-
- max_completion_tokens = request.pop("max_completion_tokens", None)
- max_tokens = request.pop("max_tokens", None)
- if "max_output_tokens" not in request:
- request["max_output_tokens"] = max_completion_tokens or max_tokens
-
- reasoning_effort = request.pop("reasoning_effort", None)
- if "reasoning" in request:
- request["reasoning"] = cls.normalize_reasoning(request["reasoning"])
- elif reasoning_effort is not None:
- request["reasoning"] = cls.normalize_reasoning(reasoning_effort)
-
- response_format = request.pop("response_format", None)
- if response_format is not None:
- text_param, text_format = cls.text_from_response_format(response_format)
- if text_param is not None and "text" not in request:
- request["text"] = text_param
- if text_format is not None and "text_format" not in request:
- request["text_format"] = text_format
-
- functions = request.pop("functions", None)
- if functions and "tools" not in request:
- request["tools"] = [
- {"type": "function", **function}
- for function in functions
- if isinstance(function, dict)
- ]
-
- tools = cls.tools_from_chat(request.pop("tools", None))
- tools = cls.merge_response_tools(
- tools,
- response_function_tools=response_function_tools,
- response_builtin_tools=response_builtin_tools,
- )
- if _has_tools(tools):
- request["tools"] = tools
- else:
- request.pop("tools", None)
-
- function_call = request.pop("function_call", None)
- if function_call is not None and "tool_choice" not in request:
- request["tool_choice"] = cls.tool_choice_from_function_call(function_call)
- elif "tool_choice" in request:
- request["tool_choice"] = cls.tool_choice_from_chat(request["tool_choice"])
-
- if not _has_tools(request.get("tools")):
- request.pop("tool_choice", None)
- request.pop("parallel_tool_calls", None)
-
- cls.prepare_prompt_caching(request, messages or [], model=model)
-
- _ = stop
- return {key: value for key, value in request.items() if value is not None}
-
- @classmethod
- def _select_input_items(
- cls,
- kwargs: dict[str, Any],
- messages: list[dict[str, Any]],
- state: str,
- ) -> list[dict[str, Any]]:
- provider_items = kwargs.get("responses_input_items")
- local_items = kwargs.get("responses_local_input_items")
- previous_response_id = kwargs.get("previous_response_id")
-
- if (
- state == RESPONSES_STATE_PROVIDER
- and previous_response_id
- and isinstance(provider_items, list)
- ):
- return [dict(item) for item in provider_items if isinstance(item, dict)]
-
- if state == RESPONSES_STATE_LOCAL and isinstance(local_items, list):
- return [dict(item) for item in local_items if isinstance(item, dict)]
-
- return cls.input_from_messages(messages)
-
- @staticmethod
- def apply_state(
- request: dict[str, Any],
- kwargs: dict[str, Any],
- *,
- state: str,
- ) -> None:
- if state == RESPONSES_STATE_PROVIDER:
- request.setdefault("store", True)
- previous_response_id = str(kwargs.get("previous_response_id") or "")
- if previous_response_id:
- request["previous_response_id"] = previous_response_id
- elif state == RESPONSES_STATE_LOCAL:
- request.setdefault("store", False)
- else:
- request.setdefault("store", False)
-
- @classmethod
- def merge_response_tools(
- cls,
- tools: Any,
- *,
- response_function_tools: Any = None,
- response_builtin_tools: Any = None,
- ) -> list[Any]:
- merged: list[Any] = []
- for source in (tools, response_function_tools, response_builtin_tools):
- source_tools = (
- source if isinstance(source, list) else [source] if source else []
- )
- for tool in source_tools:
- normalized = cls.normalize_response_tool(tool)
- if normalized:
- merged.append(normalized)
- return merged
-
- @staticmethod
- def normalize_response_tool(tool: Any) -> dict[str, Any] | None:
- if isinstance(tool, str):
- tool = {"type": tool}
- if not isinstance(tool, dict):
- return None
- normalized = dict(tool)
- if normalized.get("type") == "function":
- normalized["parameters"] = _normalize_function_parameters(
- normalized.get("parameters")
- )
- return normalized
-
- @staticmethod
- def prepare_prompt_caching(
- request: dict[str, Any],
- messages: list[dict[str, Any]],
- model: str = "",
- ) -> None:
- if not _is_openai_prompt_cache_provider(model, request):
- return
-
- _prepare_openai_prompt_cache_params(request, messages, model=model)
-
- for key in OPENAI_RESPONSES_EXTRA_BODY_PARAMS:
- if key not in request:
- continue
- extra_body = request.get("extra_body")
- if not isinstance(extra_body, dict):
- extra_body = {}
- extra_body.setdefault(key, request.pop(key))
- request["extra_body"] = extra_body
-
- @classmethod
- def input_from_messages(
- cls, messages: list[dict[str, Any]]
- ) -> list[dict[str, Any]]:
- response_input: list[dict[str, Any]] = []
-
- for message in messages:
- role = str(message.get("role") or "user")
- content = message.get("content", "")
-
- if role == "tool":
- response_input.append(
- {
- "type": "function_call_output",
- "call_id": str(message.get("tool_call_id") or ""),
- "output": _content_to_text(content),
- }
- )
- continue
-
- tool_calls = message.get("tool_calls")
- if role == "assistant" and isinstance(tool_calls, list) and tool_calls:
- if _has_real_content(content):
- response_input.append(
- {
- "role": "assistant",
- "content": cls.content_from_chat(content, role=role),
- }
- )
- response_input.extend(cls.tool_calls_from_chat(tool_calls))
- continue
-
- response_input.append(
- {
- "role": role
- if role in {"user", "assistant", "system", "developer"}
- else "user",
- "content": cls.content_from_chat(content, role=role),
- }
- )
-
- return response_input
-
- @classmethod
- def content_from_chat(cls, content: Any, role: str = "user") -> Any:
- content = images.prepare_content(content)
- if not isinstance(content, list):
- return content
- return [
- converted
- for item in content
- if (converted := cls.content_part_from_chat(item, role=role)) is not None
- ]
-
- @staticmethod
- def content_part_from_chat(item: Any, role: str = "user") -> Any:
- if not isinstance(item, dict):
- return item
-
- item_type = item.get("type")
- if item_type in {"input_text", "output_text", "input_image", "input_file"}:
- return dict(item)
- if item_type == "text":
- return {
- "type": "output_text" if role == "assistant" else "input_text",
- "text": item.get("text", ""),
- }
- if item_type == "image_url":
- image_url = item.get("image_url")
- if isinstance(image_url, dict):
- url = image_url.get("url", "")
- detail = image_url.get("detail")
- else:
- url = image_url or ""
- detail = item.get("detail")
- result = {"type": "input_image", "image_url": url}
- if detail:
- result["detail"] = detail
- return result
-
- return dict(item)
-
- @staticmethod
- def tool_calls_from_chat(tool_calls: list[Any]) -> list[dict[str, Any]]:
- response_input: list[dict[str, Any]] = []
- for tool_call in tool_calls:
- if not isinstance(tool_call, dict):
- continue
- function = tool_call.get("function") or {}
- if not isinstance(function, dict):
- function = {}
- response_input.append(
- {
- "type": "function_call",
- "call_id": str(tool_call.get("id") or ""),
- "id": str(tool_call.get("id") or ""),
- "name": str(function.get("name") or tool_call.get("name") or ""),
- "arguments": str(function.get("arguments") or ""),
- "status": "completed",
- }
- )
- return response_input
-
- @staticmethod
- def tools_from_chat(tools: Any) -> Any:
- if not isinstance(tools, list):
- return tools
- response_tools: list[Any] = []
- for tool in tools:
- if not isinstance(tool, dict):
- response_tools.append(tool)
- continue
- if tool.get("type") == "function" and isinstance(
- tool.get("function"), dict
- ):
- function = tool["function"]
- response_tool = {
- "type": "function",
- "name": function.get("name", ""),
- "description": function.get("description", ""),
- "parameters": _normalize_function_parameters(
- function.get("parameters")
- ),
- }
- if "strict" in function:
- response_tool["strict"] = function["strict"]
- response_tools.append(response_tool)
- else:
- response_tools.append(dict(tool))
- return response_tools
-
- @staticmethod
- def tool_choice_from_function_call(function_call: Any) -> Any:
- if isinstance(function_call, str):
- return function_call
- if isinstance(function_call, dict) and function_call.get("name"):
- return {"type": "function", "name": function_call["name"]}
- return function_call
-
- @staticmethod
- def tool_choice_from_chat(tool_choice: Any) -> Any:
- if (
- isinstance(tool_choice, dict)
- and tool_choice.get("type") == "function"
- and isinstance(tool_choice.get("function"), dict)
- ):
- return {"type": "function", "name": tool_choice["function"].get("name", "")}
- return tool_choice
-
- @staticmethod
- def text_from_response_format(response_format: Any) -> tuple[Any, Any]:
- if isinstance(response_format, type):
- return None, response_format
- if not isinstance(response_format, dict):
- return response_format, None
-
- format_type = response_format.get("type")
- if format_type == "json_schema":
- schema = response_format.get("json_schema") or {}
- return (
- {
- "format": {
- "type": "json_schema",
- "name": schema.get("name", "response_schema"),
- "schema": schema.get("schema", {}),
- "strict": schema.get("strict", False),
- }
- },
- None,
- )
- if format_type:
- return {"format": {"type": format_type}}, None
- return response_format, None
-
- @staticmethod
- def normalize_reasoning(reasoning: Any) -> Any:
- if isinstance(reasoning, dict):
- normalized = dict(reasoning)
- if "effort" in normalized:
- effort = _normalize_reasoning_effort(normalized.get("effort"))
- if effort is None:
- normalized.pop("effort", None)
- else:
- normalized["effort"] = effort
- return normalized or None
- if reasoning is None:
- return None
- effort = _normalize_reasoning_effort(reasoning)
- return {"effort": effort} if effort is not None else None
-
- @classmethod
- def parse_response(cls, response: Any) -> ChatChunk:
- response_delta = cls.output_text(response)
- reasoning_delta = cls.reasoning_text(response)
- if not response_delta:
- response_delta = cls.function_calls_text(response)
- return {"reasoning_delta": reasoning_delta, "response_delta": response_delta}
-
- @classmethod
- def parse_event(cls, event: Any) -> ChatChunk:
- return ResponsesEventParser().parse(event)
-
- @classmethod
- def output_text(cls, response: Any) -> str:
- output_text = _get_value(response, "output_text")
- if isinstance(output_text, str):
- return output_text
-
- pieces: list[str] = []
- for item in _as_list(_get_value(response, "output")):
- if _get_value(item, "type") != "message":
- continue
- for block in _as_list(_get_value(item, "content")):
- block_type = _get_value(block, "type")
- if block_type in {"output_text", "text"}:
- text = _get_value(block, "text")
- if isinstance(text, str):
- pieces.append(text)
- elif block_type == "refusal":
- refusal = _get_value(block, "refusal")
- if isinstance(refusal, str):
- pieces.append(refusal)
- return "".join(pieces)
-
- @staticmethod
- def reasoning_text(response: Any) -> str:
- pieces: list[str] = []
- for item in _as_list(_get_value(response, "output")):
- if _get_value(item, "type") != "reasoning":
- continue
- for block in _as_list(_get_value(item, "summary")):
- text = _get_value(block, "text") or _get_value(block, "reasoning")
- if isinstance(text, str):
- pieces.append(text)
- return "".join(pieces)
-
- @classmethod
- def function_calls_text(cls, response: Any) -> str:
- calls = [
- cls.function_call_object(item)
- for item in _as_list(_get_value(response, "output"))
- ]
- calls = [call for call in calls if call]
- if not calls:
- return ""
- if len(calls) == 1:
- return json.dumps(calls[0])
- return json.dumps(
- {"tool_name": "parallel_tool_calls", "tool_args": {"calls": calls}}
- )
-
- @classmethod
- def function_call_text(cls, item: Any) -> str:
- call = cls.function_call_object(item)
- if not call:
- return ""
- return json.dumps(call)
-
- @staticmethod
- def function_call_object(item: Any) -> dict[str, Any]:
- if _get_value(item, "type") != "function_call":
- return {}
- name = _get_value(item, "name")
- if not name:
- return {}
- raw_arguments = _get_value(item, "arguments") or "{}"
- if isinstance(raw_arguments, str):
- try:
- args = json.loads(raw_arguments or "{}")
- except Exception:
- args = {"arguments": raw_arguments}
- elif isinstance(raw_arguments, dict):
- args = raw_arguments
- else:
- args = {"arguments": raw_arguments}
- if not isinstance(args, dict):
- args = {"arguments": args}
- return {
- "tool_name": str(name),
- "tool_args": args,
- }
-
-
-class ResponsesEventParser:
- """Stateful parser for Responses streaming events."""
-
- def __init__(self) -> None:
- self.function_calls: dict[str, dict[str, Any]] = {}
- self.output_index_keys: dict[str, str] = {}
- self.emitted_function_calls: set[str] = set()
- self.seen_response_delta = False
- self.seen_reasoning_delta = False
- self.completed_response: Any = None
-
- def parse(self, event: Any) -> ChatChunk:
- event_type = _get_value(event, "type") or ""
- response_delta = ""
- reasoning_delta = ""
-
- if event_type in {
- "response.output_text.delta",
- "response.refusal.delta",
- "response.text.delta",
- }:
- response_delta = str(_get_value(event, "delta") or "")
- elif event_type in {
- "response.reasoning_summary_text.delta",
- "response.reasoning_text.delta",
- }:
- reasoning_delta = str(_get_value(event, "delta") or "")
- elif event_type == "response.output_item.added":
- self._remember_function_call(_get_value(event, "item"), event)
- elif event_type == "response.function_call_arguments.delta":
- self._append_function_call_arguments(event)
- elif event_type == "response.function_call_arguments.done":
- response_delta = self._complete_function_call(event)
- elif event_type == "response.output_item.done":
- response_delta = self._complete_output_item(_get_value(event, "item"), event)
- elif event_type == "response.completed":
- response_delta, reasoning_delta = self._complete_response(event)
- elif event_type == "response.failed":
- raise RuntimeError(self._response_error_message(event))
- elif event_type == "error":
- error = _get_value(event, "error")
- message = _get_value(error, "message") or error
- raise RuntimeError(str(message))
-
- if response_delta:
- self.seen_response_delta = True
- if reasoning_delta:
- self.seen_reasoning_delta = True
-
- return {"reasoning_delta": reasoning_delta, "response_delta": response_delta}
-
- def _remember_function_call(self, item: Any, event: Any) -> str:
- if _get_value(item, "type") != "function_call":
- return ""
- key = self._event_key(event, item)
- if not key:
- return ""
- current = self.function_calls.get(key, {})
- merged = {**current, **_object_to_dict(item)}
- self.function_calls[key] = merged
- output_index = _get_value(event, "output_index")
- if output_index is not None:
- self.output_index_keys[str(output_index)] = key
- return key
-
- def _append_function_call_arguments(self, event: Any) -> None:
- key = self._event_key(event)
- if not key:
- return
- current = self.function_calls.setdefault(key, {"type": "function_call"})
- current["arguments"] = str(current.get("arguments") or "") + str(
- _get_value(event, "delta") or ""
- )
-
- def _complete_function_call(self, event: Any) -> str:
- key = self._event_key(event)
- if not key:
- return ""
- current = self.function_calls.setdefault(key, {"type": "function_call"})
- if _get_value(event, "arguments") is not None:
- current["arguments"] = _get_value(event, "arguments")
- if _get_value(event, "name"):
- current["name"] = _get_value(event, "name")
- return self._emit_function_call(key, current)
-
- def _complete_output_item(self, item: Any, event: Any) -> str:
- key = self._remember_function_call(item, event)
- if not key:
- return ""
- return self._emit_function_call(key, self.function_calls[key])
-
- def _complete_response(self, event: Any) -> tuple[str, str]:
- self.completed_response = _get_value(event, "response")
- if self.seen_response_delta or self.emitted_function_calls:
- return "", ""
- parsed = ResponsesTransport.parse_response(self.completed_response)
- if self.seen_reasoning_delta:
- parsed["reasoning_delta"] = ""
- return parsed["response_delta"], parsed["reasoning_delta"]
-
- def _emit_function_call(self, key: str, item: Any) -> str:
- if key in self.emitted_function_calls:
- return ""
- text = ResponsesTransport.function_call_text(item)
- if text:
- self.emitted_function_calls.add(key)
- return text
-
- def _event_key(self, event: Any, item: Any = None) -> str:
- key = _get_value(event, "item_id") or _get_value(item, "id")
- if key:
- return str(key)
- output_index = _get_value(event, "output_index")
- if output_index is not None:
- output_key = self.output_index_keys.get(str(output_index))
- if output_key:
- return output_key
- return f"output:{output_index}"
- return ""
-
- def _response_error_message(self, event: Any) -> str:
- response = _get_value(event, "response") or {}
- error = _get_value(response, "error") or _get_value(event, "error")
- message = _get_value(error, "message") or error
- return str(message or "Responses API request failed")
-
-
-def clear_transport_capability_cache() -> None:
- RESPONSES_UNSUPPORTED_CACHE.clear()
- RESPONSES_STATE_UNSUPPORTED_CACHE.clear()
- RESPONSES_BUILTIN_UNSUPPORTED_CACHE.clear()
-
-
-def delete_stored_response_ids(
- response_ids: list[str], **kwargs: Any
-) -> list[tuple[str, str]]:
- errors: list[tuple[str, str]] = []
- for response_id in response_ids:
- try:
- delete_responses(response_id=response_id, **kwargs)
- except Exception as exc:
- errors.append((response_id, _exception_text(exc)))
- return errors
-
-
-async def adelete_stored_response_ids(
- response_ids: list[str], **kwargs: Any
-) -> list[tuple[str, str]]:
- errors: list[tuple[str, str]] = []
- for response_id in response_ids:
- try:
- await adelete_responses(response_id=response_id, **kwargs)
- except Exception as exc:
- errors.append((response_id, _exception_text(exc)))
- return errors
-
-
-def _coerce_bool(value: Any, default: bool = False) -> bool:
- if value is None:
- return default
- if isinstance(value, bool):
- return value
- if isinstance(value, str):
- normalized = value.strip().lower()
- if normalized in {"1", "true", "yes", "on"}:
- return True
- if normalized in {"0", "false", "no", "off", "none"}:
- return False
- return bool(value)
-
-
-def _responses_cache_key(model: str, kwargs: dict[str, Any]) -> str:
- api_base = (
- kwargs.get("api_base")
- or kwargs.get("base_url")
- or kwargs.get("api_base_url")
- or ""
- )
- custom_provider = kwargs.get("custom_llm_provider") or ""
- return "|".join(str(part) for part in (model, custom_provider, api_base))
-
-
-def _drop_legacy_transport_kwargs(kwargs: dict[str, Any]) -> None:
- kwargs.pop("a0_api_mode", None)
- kwargs.pop("a0_responses_fallback", None)
-
-
-def _drop_responses_only_kwargs(kwargs: dict[str, Any]) -> None:
- kwargs.pop("responses_state", None)
- kwargs.pop("responses_delete_on_chat_delete", None)
- kwargs.pop("responses_input_items", None)
- kwargs.pop("responses_local_input_items", None)
- kwargs.pop("previous_response_id", None)
- kwargs.pop("_a0_responses_builtin_downgrades", None)
-
-
-def _drop_internal_transport_kwargs(kwargs: dict[str, Any]) -> None:
- _drop_legacy_transport_kwargs(kwargs)
- kwargs.pop("a0_explicit_prompt_caching", None)
- kwargs.pop("a0_responses_function_tools", None)
- kwargs.pop("responses_builtin_tools", None)
- _drop_responses_only_kwargs(kwargs)
-
-
-def _normalize_responses_state(value: Any) -> str:
- normalized = str(value or RESPONSES_STATE_PROVIDER).strip().lower()
- return normalized if normalized in RESPONSES_STATES else RESPONSES_STATE_PROVIDER
-
-
-def _filter_unsupported_builtin_tools(kwargs: dict[str, Any], cache_key: str) -> None:
- unsupported = RESPONSES_BUILTIN_UNSUPPORTED_CACHE.get(cache_key)
- if not unsupported:
- return
- tools = kwargs.get("responses_builtin_tools")
- if not isinstance(tools, list):
- return
- filtered = []
- downgraded = []
- for tool in tools:
- tool_type = _response_tool_type(tool)
- if tool_type in unsupported:
- downgraded.append(tool_type)
- continue
- filtered.append(tool)
- if downgraded:
- kwargs["responses_builtin_tools"] = filtered
- kwargs["_a0_responses_builtin_downgrades"] = sorted(set(downgraded))
-
-
-def _builtin_tool_types(tools: Any) -> set[str]:
- return {
- tool_type
- for tool in _as_list(tools)
- if (tool_type := _response_tool_type(tool))
- }
-
-
-def _response_tool_type(tool: Any) -> str:
- if isinstance(tool, str):
- return tool
- if isinstance(tool, dict):
- return str(tool.get("type") or "")
- return ""
-
-
-def _normalize_function_parameters(parameters: Any) -> dict[str, Any]:
- if not isinstance(parameters, dict):
- return _permissive_function_parameters()
-
- normalized = dict(parameters)
- normalized.setdefault("type", "object")
- if normalized.get("type") == "object" and not isinstance(
- normalized.get("properties"), dict
- ):
- normalized["properties"] = {}
- return normalized or _permissive_function_parameters()
-
-
-def _permissive_function_parameters() -> dict[str, Any]:
- return {"type": "object", "properties": {}, "additionalProperties": True}
-
-
-def apply_chat_prompt_cache_markers(
- messages: list[dict[str, Any]],
- *,
- model: str = "",
- kwargs: dict[str, Any] | None = None,
-) -> list[dict[str, Any]]:
- if not _supports_cache_control_markers(model, kwargs or {}):
- return [dict(message) for message in messages]
-
- prepared = [_strip_message_cache_control(message) for message in messages]
- for index in _prompt_cache_message_indexes(prepared):
- prepared[index] = _message_with_cache_control(prepared[index])
- return prepared
-
-
-def _prompt_cache_message_indexes(messages: list[dict[str, Any]]) -> list[int]:
- indexes: list[int] = []
-
- leading_context: list[int] = []
- for index, message in enumerate(messages):
- role = str(message.get("role") or "")
- if role in {"system", "developer"}:
- leading_context.append(index)
- continue
- break
- if leading_context:
- indexes.append(leading_context[-1])
-
- user_indexes = [
- index
- for index, message in enumerate(messages)
- if str(message.get("role") or "") == "user"
- ]
- indexes.extend(user_indexes[-2:])
-
- deduplicated: list[int] = []
- for index in indexes:
- if index not in deduplicated:
- deduplicated.append(index)
- return deduplicated[:3]
-
-
-def _strip_message_cache_control(message: dict[str, Any]) -> dict[str, Any]:
- result = dict(message)
- result.pop("cache_control", None)
- return result
-
-
-def _message_with_cache_control(message: dict[str, Any]) -> dict[str, Any]:
- result = dict(message)
- result["content"] = _content_with_cache_control(result.get("content", ""))
- return result
-
-
-def _content_with_cache_control(content: Any) -> Any:
- marker = _cache_control_marker()
- if isinstance(content, list):
- blocks = [_copy_content_block(block) for block in content]
- if not blocks:
- return [{"type": "text", "text": "", "cache_control": marker}]
- for index in range(len(blocks) - 1, -1, -1):
- block = blocks[index]
- if isinstance(block, dict):
- block["cache_control"] = marker
- return blocks
- if isinstance(block, str):
- blocks[index] = {
- "type": "text",
- "text": block,
- "cache_control": marker,
- }
- return blocks
- return blocks
-
- if isinstance(content, dict):
- block = dict(content)
- block["cache_control"] = marker
- return block
-
- return [
- {
- "type": "text",
- "text": _content_to_text(content),
- "cache_control": marker,
- }
- ]
-
-
-def _copy_content_block(block: Any) -> Any:
- if isinstance(block, dict):
- return dict(block)
- if isinstance(block, list):
- return [_copy_content_block(item) for item in block]
- return block
-
-
-def _apply_tool_cache_control(kwargs: dict[str, Any]) -> None:
- tools = kwargs.get("tools")
- if not isinstance(tools, list) or not tools or _has_cache_control(tools):
- return
-
- prepared = [dict(tool) if isinstance(tool, dict) else tool for tool in tools]
- for index in range(len(prepared) - 1, -1, -1):
- tool = prepared[index]
- if not isinstance(tool, dict):
- continue
- if tool.get("type") == "function" and isinstance(tool.get("function"), dict):
- function = dict(tool["function"])
- function["cache_control"] = _cache_control_marker()
- tool = dict(tool)
- tool["function"] = function
- prepared[index] = tool
- else:
- tool = dict(tool)
- tool["cache_control"] = _cache_control_marker()
- prepared[index] = tool
- kwargs["tools"] = prepared
- return
-
-
-def _cache_control_marker() -> dict[str, str]:
- return {"type": "ephemeral"}
-
-
-def _should_preserve_cache_control_on_chat(
- model: str,
- kwargs: dict[str, Any],
- messages: list[dict[str, Any]],
-) -> bool:
- if not (_has_cache_control(messages) or _has_cache_control(kwargs.get("tools"))):
- return False
- return not _is_native_responses_provider(model, kwargs)
-
-
-def _is_native_responses_provider(model: str, kwargs: dict[str, Any]) -> bool:
- api_base = _api_base(kwargs)
- if "openrouter.ai" in api_base or "anthropic.com" in api_base:
- return False
-
- provider = _normalized_provider(model, kwargs)
- return provider in {"openai", "azure", "azure_ai", "xai"}
-
-
-def _supports_cache_control_markers(
- model: str,
- kwargs: dict[str, Any],
-) -> bool:
- api_base = _api_base(kwargs)
- if "openrouter.ai" in api_base or "anthropic.com" in api_base:
- return True
- provider = _normalized_provider(model, kwargs)
- return provider in CACHE_CONTROL_PROMPT_PROVIDERS
-
-
-def _is_openai_prompt_cache_provider(model: str, kwargs: dict[str, Any]) -> bool:
- api_base = _api_base(kwargs)
- provider = _normalized_provider(model, kwargs)
-
- if api_base:
- if "api.openai.com" in api_base:
- return provider in {"", "openai"}
- if "openai.azure.com" in api_base:
- return provider in {"", "azure", "openai"}
- return False
-
- if not provider and str(model):
- provider = "openai"
- return provider in OPENAI_PROMPT_CACHE_PROVIDERS
-
-
-def _api_base(kwargs: dict[str, Any]) -> str:
- return str(
- kwargs.get("api_base")
- or kwargs.get("base_url")
- or kwargs.get("api_base_url")
- or ""
- ).lower()
-
-
-def _normalized_provider(model: str, kwargs: dict[str, Any]) -> str:
- provider = str(kwargs.get("custom_llm_provider") or "").strip().lower()
- if not provider and "/" in str(model):
- provider = str(model).split("/", 1)[0].strip().lower()
- return provider.replace("-", "_")
-
-
-def _prepare_openai_prompt_cache_params(
- request: dict[str, Any],
- messages: list[dict[str, Any]],
- *,
- model: str = "",
-) -> None:
- if "prompt_cache_key" in request:
- return
-
- sanitized_messages = _without_cache_control(messages)
- sanitized_request = _without_cache_control(request)
- prompt_cache_key = _default_prompt_cache_key(
- model,
- sanitized_messages if isinstance(sanitized_messages, list) else messages,
- sanitized_request if isinstance(sanitized_request, dict) else request,
- )
- if prompt_cache_key:
- request["prompt_cache_key"] = prompt_cache_key
-
-
-def _default_prompt_cache_key(
- model: str,
- messages: list[dict[str, Any]],
- request: dict[str, Any],
-) -> str:
- material = _prompt_cache_key_material(messages, request)
- if not material:
- return ""
- digest = hashlib.sha256(
- json.dumps(
- {
- "model": model,
- "material": material,
- },
- sort_keys=True,
- default=str,
- separators=(",", ":"),
- ).encode("utf-8")
- ).hexdigest()[:32]
- return f"a0-{digest}"
-
-
-def _prompt_cache_key_material(
- messages: list[dict[str, Any]],
- request: dict[str, Any],
-) -> dict[str, Any]:
- material: dict[str, Any] = {}
-
- leading_messages: list[dict[str, Any]] = []
- for message in messages:
- role = str(message.get("role") or "")
- if role not in {"system", "developer"}:
- break
- leading_messages.append(
- {
- "role": role,
- "content": message.get("content"),
- }
- )
- if leading_messages:
- material["messages"] = leading_messages
-
- if request.get("instructions"):
- material["instructions"] = request["instructions"]
- if request.get("prompt"):
- material["prompt"] = request["prompt"]
- if request.get("tools"):
- material["tools"] = request["tools"]
-
- return material
-
-
-def _has_cache_control(value: Any) -> bool:
- if isinstance(value, dict):
- if value.get("cache_control") is not None:
- return True
- return any(_has_cache_control(item) for item in value.values())
- if isinstance(value, list):
- return any(_has_cache_control(item) for item in value)
- return False
-
-
-def _without_cache_control(value: Any) -> Any:
- if isinstance(value, dict):
- return {
- key: _without_cache_control(item)
- for key, item in value.items()
- if key != "cache_control"
- }
- if isinstance(value, list):
- return [_without_cache_control(item) for item in value]
- return value
-
-
-def _object_to_dict(obj: Any) -> dict[str, Any]:
- if isinstance(obj, dict):
- return dict(obj)
- if hasattr(obj, "model_dump"):
- dumped = obj.model_dump()
- return dict(dumped) if isinstance(dumped, dict) else {}
- if hasattr(obj, "dict"):
- dumped = obj.dict()
- return dict(dumped) if isinstance(dumped, dict) else {}
- return {}
-
-
-def _normalize_reasoning_effort(effort: Any) -> str | None:
- if isinstance(effort, str):
- normalized = effort.strip().lower()
- else:
- normalized = str(effort).strip().lower() if effort is not None else ""
- if normalized in RESPONSES_REASONING_EFFORTS:
- return normalized
- if normalized in NO_REASONING_EFFORT_ALIASES:
- return None
- return RESPONSES_REASONING_FALLBACK_EFFORT
-
-
-def _is_responses_reasoning_effort_error(exc: Exception) -> bool:
- text = _exception_text(exc).lower()
- return (
- "response.reasoning.effort" in text
- and "minimal" in text
- and "high" in text
- and "none" in text
- )
-
-
-def _is_responses_not_supported_error(exc: Exception) -> bool:
- text = _exception_text(exc).lower()
- if any(marker in text for marker in ("429", "too many requests", "rate limit")):
- return False
- if _is_sse_json_decode_error(exc):
- return True
- if _is_bad_request_error(exc) and _looks_like_responses_request_rejected(text):
- return True
- if _is_server_error(exc) and _looks_like_responses_endpoint(text):
- return True
- if _is_not_found_error(exc) and _looks_like_responses_endpoint_not_found(text):
- return True
- if "/v1/responses" in text and any(
- marker in text for marker in ("404", "not found")
- ):
- return True
- return any(
- marker in text
- for marker in (
- "responses api",
- "does not support responses",
- "not support responses",
- "unsupportedparamserror",
- "does not support parameters",
- "no 'tools' defined while 'tool_choice' is specified",
- "tools` must not be an empty array",
- "tools must not be an empty array",
- "not available through this proxy",
- "litellm[proxy]",
- "no module named 'fastapi'",
- )
- )
-
-
-def _is_not_found_error(exc: Exception) -> bool:
- if _exception_status_code(exc) == 404:
- return True
- return "notfounderror" in _exception_type_chain(exc).lower()
-
-
-def _is_bad_request_error(exc: Exception) -> bool:
- if _exception_status_code(exc) == 400:
- return True
- type_chain = _exception_type_chain(exc).lower()
- if "badrequesterror" in type_chain:
- return True
- text = _exception_text(exc).lower()
- return "400" in text and "bad request" in text
-
-
-def _is_server_error(exc: Exception) -> bool:
- status_code = _exception_status_code(exc)
- if isinstance(status_code, int) and 500 <= status_code < 600:
- return True
- type_chain = _exception_type_chain(exc).lower()
- if "internalservererror" in type_chain:
- return True
- text = _exception_text(exc).lower()
- return any(
- marker in text
- for marker in (
- "500 internal server error",
- "server error '500",
- "internalservererror",
- )
- )
-
-
-def _is_sse_json_decode_error(exc: Exception) -> bool:
- current: BaseException | None = exc
- while current is not None:
- if isinstance(current, json.JSONDecodeError) and _looks_like_sse_payload(
- current.doc
- ):
- return True
- current = current.__cause__ or (
- current.__context__ if current.__context__ is not current.__cause__ else None
- )
- return _looks_like_sse_payload(_exception_text(exc))
-
-
-def _looks_like_sse_payload(text: Any) -> bool:
- if not isinstance(text, str):
- return False
- lowered = text.lstrip().lower()
- return lowered.startswith("event:") and "\ndata:" in lowered
-
-
-def _looks_like_responses_request_rejected(text: str) -> bool:
- if "/v1/responses" in text or "responses api" in text:
- return True
- return any(
- marker in text
- for marker in (
- "input_image",
- "response.input",
- "expected object, received string",
- "expected string, received array",
- "zod",
- "failed to deserialize input",
- "failed to deserialize response",
- "failed to deserialize responses",
- )
- )
-
-
-def _looks_like_responses_endpoint_not_found(text: str) -> bool:
- if "/v1/responses" in text:
- return True
- if "not found" not in text:
- return False
- if "openaiexception" in text:
- return True
- return "detail" in text and "not found" in text
-
-
-def _looks_like_responses_endpoint(text: str) -> bool:
- return "/responses" in text or "path /api/v1/responses" in text
-
-
-def _is_responses_state_unsupported_error(exc: Exception) -> bool:
- text = _exception_text(exc).lower()
- if any(marker in text for marker in ("429", "too many requests", "rate limit")):
- return False
- if "404" in text and "/v1/responses/" in text:
- return True
- return any(
- marker in text
- for marker in (
- "previous_response_id",
- "store",
- "stored response",
- "response not found",
- "no response found",
- "does not support response storage",
- "doesn't support response storage",
- "response storage is not supported",
- "state is not supported",
- )
- )
-
-
-def _is_responses_builtin_tool_error(exc: Exception) -> bool:
- text = _exception_text(exc).lower()
- if any(marker in text for marker in ("429", "too many requests", "rate limit")):
- return False
- return any(
- marker in text
- for marker in (
- "unsupported tool",
- "unsupported tools",
- "invalid tool",
- "tool type",
- "tools[",
- "web_search",
- "file_search",
- "code_interpreter",
- "image_generation",
- "computer_use_preview",
- "mcp",
- )
- )
-
-
-def _exception_text(exc: Exception | None) -> str:
- if exc is None:
- return ""
- parts = [exc.__class__.__name__, str(exc)]
- for attr in ("status_code", "code", "message", "body"):
- value = getattr(exc, attr, None)
- if value not in (None, ""):
- parts.append(f"{attr}={value}")
- response = getattr(exc, "response", None)
- if response is not None:
- response_text = getattr(response, "text", None)
- if response_text:
- parts.append(str(response_text))
- response_url = getattr(response, "url", None)
- if response_url:
- parts.append(str(response_url))
- cause = getattr(exc, "__cause__", None)
- context = getattr(exc, "__context__", None)
- if cause is not None:
- parts.append(str(cause))
- if context is not None and context is not cause:
- parts.append(str(context))
- return "\n".join(parts)
-
-
-def _exception_status_code(exc: Exception | None) -> int | None:
- if exc is None:
- return None
- for attr in ("status_code", "code"):
- value = getattr(exc, attr, None)
- if isinstance(value, int):
- return value
- if isinstance(value, str) and value.isdigit():
- return int(value)
- response = getattr(exc, "response", None)
- value = getattr(response, "status_code", None)
- return value if isinstance(value, int) else None
-
-
-def _exception_type_chain(exc: Exception | None) -> str:
- names: list[str] = []
- current = exc
- while current is not None:
- names.append(current.__class__.__name__)
- cause = getattr(current, "__cause__", None)
- context = getattr(current, "__context__", None)
- current = cause or (context if context is not cause else None)
- return "\n".join(names)
-
-
-def _close_sync_stream(stream: Any) -> None:
- for method_name in ("close", "aclose"):
- close = getattr(stream, method_name, None)
- if close is None:
- continue
- result = close()
- if inspect.isawaitable(result):
- result.close()
- return
-
-
-async def _close_async_stream(stream: Any) -> None:
- for method_name in ("aclose", "close"):
- close = getattr(stream, method_name, None)
- if close is None:
- continue
- result = close()
- if inspect.isawaitable(result):
- await result
- return
-
-
-def _without_stream_kwarg(kwargs: dict[str, Any]) -> dict[str, Any]:
- kwargs.pop("stream", None)
- return kwargs
-
-
-def _first_choice(chunk: Any) -> Any:
- choices = _get_value(chunk, "choices") or []
- return choices[0] if choices else {}
-
-
-def _get_value(obj: Any, key: str) -> Any:
- if isinstance(obj, dict):
- return obj.get(key)
- value = getattr(obj, key, None)
- if value is not None:
- return value
- return _object_to_dict(obj).get(key)
-
-
-def _as_list(value: Any) -> list[Any]:
- return value if isinstance(value, list) else []
-
-
-def _has_tools(tools: Any) -> bool:
- if isinstance(tools, list):
- return bool(tools)
- return bool(tools)
-
-
-def _has_chunk_delta(chunk: ChatChunk) -> bool:
- return bool(chunk.get("response_delta") or chunk.get("reasoning_delta"))
-
-
-def _has_real_content(content: Any) -> bool:
- if content == "empty":
- return False
- if isinstance(content, str):
- return bool(content.strip())
- if isinstance(content, list):
- return len(content) > 0
- return content is not None
-
-
-def _content_to_text(content: Any) -> str:
- content = images.prepare_content(content)
- if isinstance(content, str):
- return content
- if isinstance(content, list):
- pieces: list[str] = []
- for item in content:
- if isinstance(item, str):
- pieces.append(item)
- elif isinstance(item, dict):
- text = item.get("text")
- if isinstance(text, str):
- pieces.append(text)
- return "\n".join(pieces)
- return "" if content is None else str(content)
diff --git a/helpers/litellm_transport.py.dox.md b/helpers/litellm_transport.py.dox.md
deleted file mode 100644
index add4be9ab..000000000
--- a/helpers/litellm_transport.py.dox.md
+++ /dev/null
@@ -1,50 +0,0 @@
-# litellm_transport.py DOX
-
-## Purpose
-
-- Own Agent Zero's LiteLLM transport adapter for Chat Completions and Responses API calls.
-- Normalize Agent Zero model-call kwargs into provider-safe LiteLLM requests.
-- Preserve canonical response metadata for history, provider-state continuation, and fallback decisions.
-
-## Ownership
-
-- `litellm_transport.py` owns the runtime implementation.
-- `litellm_transport.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `TransportMode`
-- `TransportRecovery`
-- `TransportPolicy`
-- `LiteLLMTransport`
-- `ChatCompletionsTransport`
-- `ResponsesTransport`
-- `ResponsesEventParser`
-- Top-level functions include transport cache reset, request normalization, parsing, prompt-cache preparation, and response/error classifiers.
-
-## Runtime Contracts
-
-- Keep provider selection and provider-specific defaults outside this helper; callers pass a resolved LiteLLM model name and kwargs.
-- Strip Agent Zero internal kwargs before sending requests to LiteLLM.
-- Do not send orphan tool controls when no tools are present; strict OpenAI-compatible servers can reject empty `tools` arrays.
-- Normalize function tool parameter schemas with an explicit object `properties` field before Responses requests so OpenAI-compatible chat backends reached through LiteLLM can validate them.
-- Prefer Responses API when configured, but fallback to Chat Completions when the provider does not support Responses.
-- Fall back to Chat Completions when a Responses request is rejected before any output by an endpoint-specific or shape-specific Bad Request indicating the provider cannot parse Responses payloads.
-- Fall back to Chat Completions when a Responses endpoint fails before output with an endpoint-specific server error, proxy path-unavailable error, or LiteLLM proxy-extra import error.
-- Fall back to Chat Completions when LiteLLM's Responses mock streaming path tries to JSON-decode a real SSE stream before any output.
-- Preserve Chat Completions tool calls from both non-streaming responses and streaming deltas as canonical `LLMResult` function-call items.
-- Preserve provider-state metadata when Responses API calls succeed, and fall back to local replay when provider state is unsupported.
-- Keep prompt-cache markers only for providers that accept them.
-
-## Work Guidance
-
-- Add provider-agnostic request cleanup here when multiple OpenAI-compatible providers can benefit.
-- Treat fallback behavior as a shared transport contract, not a provider registry.
-- Keep tool conversion symmetric between Chat Completions and Responses requests.
-
-## Verification
-
-- Run `pytest tests/test_stream_tool_early_stop.py tests/test_responses_architecture.py -q` after changing transport normalization or fallback behavior.
-- Run local-provider smoke checks when changing OpenAI-compatible request cleanup.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/llm_result.py b/helpers/llm_result.py
deleted file mode 100644
index 874edec9b..000000000
--- a/helpers/llm_result.py
+++ /dev/null
@@ -1,315 +0,0 @@
-from __future__ import annotations
-
-from dataclasses import dataclass, field
-import json
-from typing import Any
-
-
-RESPONSE_METADATA_KEY = "responses"
-LOCAL_FUNCTION_TOOL_TYPES = {"function_call"}
-TEXT_OUTPUT_TYPES = {"message"}
-REASONING_OUTPUT_TYPES = {"reasoning"}
-
-
-@dataclass
-class ResponseItem:
- type: str
- data: dict[str, Any] = field(default_factory=dict)
-
- @classmethod
- def from_any(cls, item: Any) -> "ResponseItem":
- data = object_to_dict(item)
- return cls(type=str(data.get("type") or ""), data=data)
-
- def to_dict(self) -> dict[str, Any]:
- return dict(self.data)
-
-
-@dataclass
-class ResponseFunctionCall:
- name: str
- arguments: dict[str, Any]
- call_id: str
- item_id: str = ""
- raw: dict[str, Any] = field(default_factory=dict)
-
- @classmethod
- def from_item(cls, item: ResponseItem) -> "ResponseFunctionCall | None":
- if item.type != "function_call":
- return None
- name = str(item.data.get("name") or "")
- if not name:
- return None
- return cls(
- name=name,
- arguments=parse_arguments(item.data.get("arguments")),
- call_id=str(item.data.get("call_id") or item.data.get("id") or ""),
- item_id=str(item.data.get("id") or ""),
- raw=dict(item.data),
- )
-
-
-@dataclass
-class LLMResult:
- response: str = ""
- reasoning: str = ""
- response_id: str = ""
- previous_response_id: str = ""
- input_items: list[dict[str, Any]] = field(default_factory=list)
- output_items: list[ResponseItem] = field(default_factory=list)
- provider_model_key: str = ""
- mode: str = "responses"
- state: str = "provider"
- usage: dict[str, Any] = field(default_factory=dict)
- raw: dict[str, Any] = field(default_factory=dict)
- capability: dict[str, Any] = field(default_factory=dict)
-
- @classmethod
- def from_dict(cls, data: dict[str, Any] | None) -> "LLMResult":
- data = data or {}
- return cls(
- response=str(data.get("response") or ""),
- reasoning=str(data.get("reasoning") or ""),
- response_id=str(data.get("response_id") or ""),
- previous_response_id=str(data.get("previous_response_id") or ""),
- input_items=list(data.get("input_items") or []),
- output_items=[
- ResponseItem.from_any(item) for item in data.get("output_items") or []
- ],
- provider_model_key=str(data.get("provider_model_key") or ""),
- mode=str(data.get("mode") or "responses"),
- state=str(data.get("state") or "provider"),
- usage=object_to_dict(data.get("usage") or {}),
- raw=object_to_dict(data.get("raw") or {}),
- capability=object_to_dict(data.get("capability") or {}),
- )
-
- @classmethod
- def from_response(
- cls,
- response: Any,
- *,
- input_items: list[dict[str, Any]] | None = None,
- previous_response_id: str = "",
- provider_model_key: str = "",
- mode: str = "responses",
- state: str = "provider",
- capability: dict[str, Any] | None = None,
- ) -> "LLMResult":
- raw = object_to_dict(response)
- output_items = [ResponseItem.from_any(item) for item in as_list(raw.get("output"))]
- result = cls(
- response_id=str(raw.get("id") or ""),
- previous_response_id=str(
- raw.get("previous_response_id") or previous_response_id or ""
- ),
- input_items=list(input_items or []),
- output_items=output_items,
- provider_model_key=provider_model_key,
- mode=mode,
- state=state,
- usage=object_to_dict(raw.get("usage") or {}),
- raw=raw,
- capability=dict(capability or {}),
- )
- result.response = output_text(raw, output_items)
- result.reasoning = reasoning_text(output_items)
- if not result.response and result.function_calls:
- result.response = result.function_calls_text()
- return result
-
- @classmethod
- def from_chat(
- cls,
- *,
- response: str,
- reasoning: str = "",
- input_items: list[dict[str, Any]] | None = None,
- output_items: list[dict[str, Any]] | None = None,
- provider_model_key: str = "",
- capability: dict[str, Any] | None = None,
- ) -> "LLMResult":
- items = [ResponseItem.from_any(item) for item in output_items or []]
- if response and not items:
- items.append(
- ResponseItem(
- type="message",
- data={
- "type": "message",
- "role": "assistant",
- "content": [{"type": "output_text", "text": response}],
- },
- )
- )
- if reasoning:
- items.insert(
- 0,
- ResponseItem(
- type="reasoning",
- data={
- "type": "reasoning",
- "summary": [{"type": "summary_text", "text": reasoning}],
- },
- ),
- )
- result = cls(
- response=response,
- reasoning=reasoning,
- input_items=list(input_items or []),
- output_items=items,
- provider_model_key=provider_model_key,
- mode="chat_completions",
- state="off",
- capability=dict(capability or {}),
- )
- if not result.response and result.function_calls:
- result.response = result.function_calls_text()
- return result
-
- @property
- def function_calls(self) -> list[ResponseFunctionCall]:
- calls: list[ResponseFunctionCall] = []
- for item in self.output_items:
- call = ResponseFunctionCall.from_item(item)
- if call:
- calls.append(call)
- return calls
-
- @property
- def builtin_items(self) -> list[ResponseItem]:
- return [
- item
- for item in self.output_items
- if item.type
- and item.type not in TEXT_OUTPUT_TYPES
- and item.type not in REASONING_OUTPUT_TYPES
- and item.type not in LOCAL_FUNCTION_TOOL_TYPES
- ]
-
- def function_calls_text(self) -> str:
- calls = [
- {"tool_name": call.name, "tool_args": call.arguments}
- for call in self.function_calls
- ]
- if not calls:
- return ""
- if len(calls) == 1:
- return json.dumps(calls[0])
- return json.dumps(
- {"tool_name": "parallel_tool_calls", "tool_args": {"calls": calls}}
- )
-
- def to_dict(self) -> dict[str, Any]:
- return {
- "response": self.response,
- "reasoning": self.reasoning,
- "response_id": self.response_id,
- "previous_response_id": self.previous_response_id,
- "input_items": self.input_items,
- "output_items": [item.to_dict() for item in self.output_items],
- "provider_model_key": self.provider_model_key,
- "mode": self.mode,
- "state": self.state,
- "usage": self.usage,
- "raw": self.raw,
- "capability": self.capability,
- }
-
- def metadata(self) -> dict[str, Any]:
- return {RESPONSE_METADATA_KEY: self.to_dict()}
-
-
-def function_call_output_item(
- call_id: str,
- output: str,
- *,
- acknowledged_safety_checks: list[dict[str, Any]] | None = None,
-) -> dict[str, Any]:
- item: dict[str, Any] = {
- "type": "function_call_output",
- "call_id": str(call_id or ""),
- "output": output,
- }
- if acknowledged_safety_checks:
- item["acknowledged_safety_checks"] = acknowledged_safety_checks
- return item
-
-
-def metadata_from_llm_result(result: LLMResult | None) -> dict[str, Any]:
- return result.metadata() if result else {}
-
-
-def result_from_metadata(metadata: dict[str, Any] | None) -> LLMResult | None:
- if not isinstance(metadata, dict):
- return None
- data = metadata.get(RESPONSE_METADATA_KEY)
- if not isinstance(data, dict):
- return None
- return LLMResult.from_dict(data)
-
-
-def object_to_dict(obj: Any) -> dict[str, Any]:
- if isinstance(obj, dict):
- return dict(obj)
- if hasattr(obj, "model_dump"):
- dumped = obj.model_dump()
- return dict(dumped) if isinstance(dumped, dict) else {}
- if hasattr(obj, "dict"):
- dumped = obj.dict()
- return dict(dumped) if isinstance(dumped, dict) else {}
- return {}
-
-
-def as_list(value: Any) -> list[Any]:
- return value if isinstance(value, list) else []
-
-
-def output_text(raw: dict[str, Any], output_items: list[ResponseItem]) -> str:
- direct = raw.get("output_text")
- if isinstance(direct, str):
- return direct
- pieces: list[str] = []
- for item in output_items:
- if item.type != "message":
- continue
- for block in as_list(item.data.get("content")):
- if not isinstance(block, dict):
- continue
- block_type = block.get("type")
- if block_type in {"output_text", "text", "input_text"}:
- text = block.get("text")
- if isinstance(text, str):
- pieces.append(text)
- elif block_type == "refusal":
- refusal = block.get("refusal")
- if isinstance(refusal, str):
- pieces.append(refusal)
- return "".join(pieces)
-
-
-def reasoning_text(output_items: list[ResponseItem]) -> str:
- pieces: list[str] = []
- for item in output_items:
- if item.type != "reasoning":
- continue
- for block in as_list(item.data.get("summary")):
- if isinstance(block, dict):
- text = block.get("text") or block.get("reasoning")
- if isinstance(text, str):
- pieces.append(text)
- elif isinstance(block, str):
- pieces.append(block)
- return "".join(pieces)
-
-
-def parse_arguments(raw_arguments: Any) -> dict[str, Any]:
- if isinstance(raw_arguments, dict):
- return raw_arguments
- if isinstance(raw_arguments, str):
- try:
- parsed = json.loads(raw_arguments or "{}")
- except Exception:
- parsed = {"arguments": raw_arguments}
- else:
- parsed = {"arguments": raw_arguments}
- return parsed if isinstance(parsed, dict) else {"arguments": parsed}
diff --git a/helpers/llm_result.py.dox.md b/helpers/llm_result.py.dox.md
deleted file mode 100644
index 78b472885..000000000
--- a/helpers/llm_result.py.dox.md
+++ /dev/null
@@ -1,39 +0,0 @@
-# llm_result.py DOX
-
-## Purpose
-
-- Own canonical LLM result metadata shared by model transports, history, and tool-result processing.
-- Preserve Responses API output items, provider response IDs, reasoning text, usage, and capability metadata in a serializable form.
-
-## Ownership
-
-- `llm_result.py` owns the runtime implementation.
-- `llm_result.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `ResponseItem`
-- `ResponseFunctionCall`
-- `LLMResult`
-- Top-level functions include metadata conversion, function-call output item construction, object normalization, output-text extraction, reasoning extraction, and function-call argument parsing.
-
-## Runtime Contracts
-
-- `LLMResult.metadata()` stores data under `RESPONSE_METADATA_KEY` so history can round-trip provider state.
-- `from_response(...)` must preserve provider `response_id`, `previous_response_id`, raw output items, usage, and capability metadata.
-- `from_chat(...)` must produce an equivalent chat-completions result with `mode="chat_completions"` and `state="off"`, preserving optional function-call output items when the chat transport supplies them.
-- Function-call output items must preserve `call_id` and optional acknowledged safety checks.
-- Argument parsing must tolerate JSON strings, dictionaries, and malformed values without throwing.
-
-## Work Guidance
-
-- Keep metadata backward-compatible with existing serialized chat history.
-- Treat unknown response item types as preserved built-in items unless they are local function calls, message text, or reasoning.
-- Avoid provider-specific assumptions in result parsing.
-
-## Verification
-
-- Run `pytest tests/test_responses_architecture.py -q` after changing result metadata behavior.
-- Run focused history/tool-processing tests when changing function-call serialization.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/localization.py.dox.md b/helpers/localization.py.dox.md
deleted file mode 100644
index b33e410b8..000000000
--- a/helpers/localization.py.dox.md
+++ /dev/null
@@ -1,50 +0,0 @@
-# localization.py DOX
-
-## Purpose
-
-- Own the `localization.py` helper module.
-- This module loads and resolves localized UI/application text.
-- Keep this file-level DOX profile synchronized with `localization.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `localization.py` owns the runtime implementation.
-- `localization.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `Localization` (no explicit base class)
- - `get(cls, *args, **kwargs)`
- - `get_timezone(self) -> str`
- - `get_tzinfo(self)`
- - `get_offset_minutes(self) -> int`
- - `apply_process_timezone(self) -> None`
- - `now(self) -> datetime`
- - `now_iso(self, sep: str=..., timespec: str=...) -> str`
- - `localize_naive_datetime(self, dt: datetime) -> datetime`
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem writes, settings/state persistence.
-- Imported dependency areas include: `datetime`, `helpers.dotenv`, `helpers.print_style`, `os`, `pytz`, `time`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `get_dotenv_value`, `pytz.timezone`, `datetime.now`, `now_in_tz.utcoffset`, `self.now.isoformat`, `self.get_tzinfo`, `cls`, `self.set_timezone`, `self._compute_offset_minutes`, `self.apply_process_timezone`, `tzinfo.localize`, `PrintStyle.debug`, `save_dotenv_value`, `localtime_str.strip.replace`, `local_datetime_obj.astimezone`, `utc_dt.astimezone`, `local_datetime_obj.isoformat`, `dt.astimezone`, `local_dt.isoformat`, `time.tzset`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_timezone_regressions.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/log.py.dox.md b/helpers/log.py.dox.md
deleted file mode 100644
index ce74dec69..000000000
--- a/helpers/log.py.dox.md
+++ /dev/null
@@ -1,68 +0,0 @@
-# log.py DOX
-
-## Purpose
-
-- Own the `log.py` helper module.
-- This module owns chat log items, outputs, truncation, and dirty-state integration.
-- Keep this file-level DOX profile synchronized with `log.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `log.py` owns the runtime implementation.
-- `log.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `LogItem` (no explicit base class)
- - `update(self, type: Type | None=..., heading: str | None=..., content: str | None=..., kvps: dict | None=..., update_progress: ProgressUpdate | None=..., **kwargs)`
- - `stream(self, heading: str | None=..., content: str | None=..., **kwargs)`
- - `output(self)`
-- `LogOutput` (no explicit base class)
-- `Log` (no explicit base class)
- - `log(self, type: Type, heading: str | None=..., content: str | None=..., kvps: dict | None=..., update_progress: ProgressUpdate | None=..., id: Optional[str]=..., **kwargs) -> LogItem`
- - `set_progress(self, progress: str, no: int=..., active: bool=...)`
- - `set_initial_progress(self)`
- - `output(self, start=..., end=...)`
- - `reset(self)`
-- Top-level functions:
-- `_lazy_mark_dirty_all(reason: str | None=...) -> None`
-- `_lazy_mark_dirty_for_context(context_id: str, reason: str | None=...) -> None`
-- `_truncate_heading(text: str | None) -> str`
-- `_truncate_progress(text: str | None) -> str`
-- `_truncate_key(text: str) -> str`
-- `_truncate_value(val: T) -> T`
-- `_truncate_content(text: str | None, type: Type) -> str`
-- Notable constants/configuration names: `_MARK_DIRTY_ALL`, `_MARK_DIRTY_FOR_CONTEXT`, `T`, `HEADING_MAX_LEN`, `CONTENT_MAX_LEN`, `RESPONSE_CONTENT_MAX_LEN`, `KEY_MAX_LEN`, `VALUE_MAX_LEN`, `PROGRESS_MAX_LEN`.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem writes, filesystem deletion, settings/state persistence, secret handling, scheduler state.
-- Imported dependency areas include: `collections`, `copy`, `dataclasses`, `helpers.secrets`, `helpers.strings`, `json`, `threading`, `time`, `typing`, `uuid`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `TypeVar`, `dataclass`, `_MARK_DIRTY_ALL`, `_MARK_DIRTY_FOR_CONTEXT`, `truncate_text_by_ratio`, `cast`, `threading.RLock`, `self.set_initial_progress`, `self._update_item`, `self._notify_state_monitor`, `_lazy_mark_dirty_all`, `_lazy_mark_dirty_for_context`, `self._mask_recursive`, `_truncate_progress`, `self.set_progress`, `LogOutput`, `_truncate_value`, `json.dumps`, `time.time`, `self.log._update_item`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_browser_agent_regressions.py`
- - `tests/test_chat_compaction.py`
- - `tests/test_error_retry_plugin.py`
- - `tests/test_fasta2a_client.py`
- - `tests/test_file_tree_visualize.py`
- - `tests/test_history_compression_wait.py`
- - `tests/test_http_auth_csrf.py`
- - `tests/test_mcp_handler_multimodal.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/login.py.dox.md b/helpers/login.py.dox.md
deleted file mode 100644
index 344895198..000000000
--- a/helpers/login.py.dox.md
+++ /dev/null
@@ -1,49 +0,0 @@
-# login.py DOX
-
-## Purpose
-
-- Own the `login.py` helper module.
-- This module checks login requirements and credential hashes.
-- Keep this file-level DOX profile synchronized with `login.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `login.py` owns the runtime implementation.
-- `login.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Top-level functions:
-- `get_credentials_hash()`
-- `is_login_required()`
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: secret handling.
-- Imported dependency areas include: `hashlib`, `helpers`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `dotenv.get_dotenv_value`, `hashlib.sha256.hexdigest`, `hashlib.sha256`, `encode`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_http_auth_csrf.py`
- - `tests/test_oauth_gemini_api.py`
- - `tests/test_oauth_github_copilot.py`
- - `tests/test_oauth_providers.py`
- - `tests/test_oauth_xai_grok.py`
- - `tests/test_tunnel_remote_link.py`
- - `tests/test_ws_security.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/mcp_handler.py b/helpers/mcp_handler.py
index e30c3916c..ea23d61f4 100644
--- a/helpers/mcp_handler.py
+++ b/helpers/mcp_handler.py
@@ -21,7 +21,6 @@ from contextlib import AsyncExitStack
from shutil import which
from datetime import timedelta
import json
-import shlex
import uuid
from helpers import errors
from helpers import settings
@@ -44,14 +43,10 @@ from pydantic import BaseModel, Field, Discriminator, Tag, PrivateAttr
from helpers import dirty_json, media_artifacts
from helpers.print_style import PrintStyle
from helpers.tool import Tool, Response
-from helpers.defer import DeferredTask
MCP_MEDIA_TOKENS_ESTIMATE = 1500
MAX_MCP_RESOURCE_TEXT_CHARS = 12_000
-MCP_SESSION_CLEANUP_TIMEOUT_SECONDS = 5.0
-MCP_OPERATION_TIMEOUT_GRACE_SECONDS = MCP_SESSION_CLEANUP_TIMEOUT_SECONDS + 2.0
-DEFAULT_MCP_SERVERS_CONFIG = '{\n "mcpServers": {}\n}'
def _mcp_get(item: Any, key: str, default: Any = None) -> Any:
@@ -97,60 +92,6 @@ def _is_streaming_http_type(server_type: str) -> bool:
return server_type.lower() in ["http-stream", "streaming-http", "streamable-http", "http-streaming"]
-def _split_qualified_tool_name(tool_name: str) -> tuple[str, str]:
- """Split Agent Zero's server.tool MCP name while preserving dots in MCP tool names."""
- if "." not in tool_name:
- raise ValueError(f"Tool {tool_name} not found")
- server_name_part, tool_name_part = tool_name.split(".", 1)
- if not server_name_part or not tool_name_part:
- raise ValueError(f"Tool {tool_name} not found")
- return server_name_part, tool_name_part
-
-
-def _normalize_disabled_tools(value: Any) -> list[str]:
- if not isinstance(value, list):
- return []
- return [str(item).strip() for item in value if str(item).strip()]
-
-
-def _split_stdio_command(command: Any) -> tuple[str, list[str]]:
- text = str(command or "").strip()
- if not text:
- return "", []
- try:
- parts = shlex.split(text)
- except ValueError:
- return text, []
- if not parts:
- return "", []
- return parts[0], parts[1:]
-
-
-def _split_stdio_arg_fragment(arg: str) -> list[str]:
- try:
- parts = shlex.split(arg)
- except ValueError:
- return [arg]
- if len(parts) <= 1:
- return parts or []
- if parts[0].startswith("-") and "=" not in parts[0]:
- return parts
- if any(part.startswith("-") for part in parts[1:]):
- return parts
- return [arg]
-
-
-def _normalize_stdio_args(value: Any) -> list[str]:
- if not isinstance(value, list):
- return []
- args: list[str] = []
- for item in value:
- text = str(item).strip()
- if text:
- args.extend(_split_stdio_arg_fragment(text))
- return args
-
-
def initialize_mcp(mcp_servers_config: str):
if not MCPConfig.get_instance().is_initialized():
try:
@@ -205,45 +146,23 @@ class MCPTool(Tool):
encoded: str,
mime_type: str,
label: str,
- index: int,
- preferred_name: str = "",
- ) -> tuple[str, dict[str, Any] | None, str]:
+ ) -> tuple[str, dict[str, Any] | None]:
try:
- safe_mime = media_artifacts.normalize_mime(
- mime_type,
- default="image/png",
- required_prefix="image/",
- )
- artifact = media_artifacts.save_base64_artifact(
+ image = media_artifacts.image_data_url_from_base64(
encoded,
- mime_type=safe_mime,
- directory_parts=self._artifact_directory_parts(),
- preferred_name=preferred_name,
- default_filename=self._default_artifact_filename(
- label=label,
- index=index,
- mime_type=safe_mime,
- ),
+ mime_type=mime_type,
)
except media_artifacts.EmptyBase64Data:
- return f"MCP returned an empty {label} attachment.", None, ""
+ return f"MCP returned an empty {label} attachment.", None
except media_artifacts.InvalidBase64Data:
- return (
- f"MCP returned a {label} attachment that could not be decoded.",
- None,
- "",
- )
+ return f"MCP returned a {label} attachment that could not be decoded.", None
return (
- (
- f"Saved MCP {label} attachment "
- f"({artifact.mime}, {artifact.size} bytes) to {artifact.path}."
- ),
+ f"MCP returned {label} attachment ({image.mime}, {image.size} bytes).",
{
"type": "image_url",
- "image_url": {"url": artifact.path},
+ "image_url": {"url": image.url},
},
- artifact.path,
)
def _materialize_binary_content(
@@ -327,7 +246,6 @@ class MCPTool(Tool):
text_parts: list[str] = []
notes: list[str] = []
raw_images: list[dict[str, Any]] = []
- image_paths: list[str] = []
content_items = list(getattr(response, "content", []) or [])
for index, item in enumerate(content_items, start=1):
@@ -340,17 +258,14 @@ class MCPTool(Tool):
continue
if item_type == "image":
- note, raw_content, path = self._format_image_content(
+ note, raw_content = self._format_image_content(
encoded=str(_mcp_get(item, "data", "") or ""),
mime_type=str(_mcp_get(item, "mimeType", "") or "image/png"),
label="image",
- index=index,
)
notes.append(note)
if raw_content:
raw_images.append(raw_content)
- if path:
- image_paths.append(path)
continue
if item_type == "audio":
@@ -377,12 +292,10 @@ class MCPTool(Tool):
_mcp_get(resource, "mimeType", "") or "application/octet-stream"
).strip().lower()
if mime_type.startswith("image/"):
- note, raw_content, path = self._format_image_content(
+ note, raw_content = self._format_image_content(
encoded=blob,
mime_type=mime_type,
label="resource image",
- index=index,
- preferred_name=uri,
)
else:
note = self._materialize_binary_content(
@@ -393,12 +306,9 @@ class MCPTool(Tool):
preferred_name=uri,
)
raw_content = None
- path = ""
notes.append(note)
if raw_content:
raw_images.append(raw_content)
- if path:
- image_paths.append(path)
continue
if uri:
@@ -426,8 +336,6 @@ class MCPTool(Tool):
"raw_content": raw_images,
"preview": f"",
"_tokens": MCP_MEDIA_TOKENS_ESTIMATE * len(raw_images),
- "attachments": image_paths,
- "media_paths": image_paths,
}
return message, additional
@@ -436,7 +344,7 @@ class MCPTool(Tool):
error = ""
additional: dict[str, Any] | None = None
try:
- response: CallToolResult = await MCPConfig.get_for_agent(self.agent).call_tool(
+ response: CallToolResult = await MCPConfig.get_instance().call_tool(
self.name, kwargs
)
message, additional = self._format_tool_result(response)
@@ -531,8 +439,6 @@ class MCPServerRemote(BaseModel):
tool_timeout: int = Field(default=0)
verify: bool = Field(default=True, description="Verify SSL certificates")
disabled: bool = Field(default=False)
- disabled_tools: list[str] = Field(default_factory=list)
- scope: str = Field(default="global")
__lock: ClassVar[threading.Lock] = PrivateAttr(default=threading.Lock())
__client: Optional["MCPClientRemote"] = PrivateAttr(default=None)
@@ -551,26 +457,12 @@ class MCPServerRemote(BaseModel):
return self.__client.get_log() # type: ignore
def get_tools(self) -> List[dict[str, Any]]:
- """Get enabled tools from the server"""
+ """Get all tools from the server"""
with self.__lock:
- tools = self.__client.get_tools() # type: ignore
- disabled = set(self.disabled_tools)
- return [tool for tool in tools if tool.get("name") not in disabled]
-
- def get_all_tools(self) -> List[dict[str, Any]]:
- """Get all tools from the server and mark disabled tools for UI detail views."""
- with self.__lock:
- tools = self.__client.get_tools() # type: ignore
- disabled = set(self.disabled_tools)
- return [
- {**tool, "disabled": tool.get("name") in disabled}
- for tool in tools
- ]
+ return self.__client.tools # type: ignore
def has_tool(self, tool_name: str) -> bool:
"""Check if a tool is available"""
- if tool_name in self.disabled_tools:
- return False
with self.__lock:
return self.__client.has_tool(tool_name) # type: ignore
@@ -578,12 +470,9 @@ class MCPServerRemote(BaseModel):
self, tool_name: str, input_data: Dict[str, Any]
) -> CallToolResult:
"""Call a tool with the given input data"""
- client = self.__client
- if client is None:
- raise RuntimeError("MCP remote client is not initialized")
- if tool_name in self.disabled_tools:
- raise ValueError(f"Tool {tool_name} is disabled for server {self.name}.")
- return await client.call_tool(tool_name, input_data)
+ with self.__lock:
+ # We already run in an event loop, dont believe Pylance
+ return await self.__client.call_tool(tool_name, input_data) # type: ignore
def update(self, config: dict[str, Any]) -> "MCPServerRemote":
with self.__lock:
@@ -598,16 +487,12 @@ class MCPServerRemote(BaseModel):
"init_timeout",
"tool_timeout",
"disabled",
- "disabled_tools",
"verify",
- "scope",
]:
if key == "name":
value = normalize_name(value)
if key == "serverUrl":
key = "url" # remap serverUrl to url
- if key == "disabled_tools":
- value = _normalize_disabled_tools(value)
setattr(self, key, value)
return self
@@ -632,8 +517,6 @@ class MCPServerLocal(BaseModel):
tool_timeout: int = Field(default=0)
verify: bool = Field(default=True, description="Verify SSL certificates")
disabled: bool = Field(default=False)
- disabled_tools: list[str] = Field(default_factory=list)
- scope: str = Field(default="global")
__lock: ClassVar[threading.Lock] = PrivateAttr(default=threading.Lock())
__client: Optional["MCPClientLocal"] = PrivateAttr(default=None)
@@ -652,26 +535,12 @@ class MCPServerLocal(BaseModel):
return self.__client.get_log() # type: ignore
def get_tools(self) -> List[dict[str, Any]]:
- """Get enabled tools from the server"""
+ """Get all tools from the server"""
with self.__lock:
- tools = self.__client.get_tools() # type: ignore
- disabled = set(self.disabled_tools)
- return [tool for tool in tools if tool.get("name") not in disabled]
-
- def get_all_tools(self) -> List[dict[str, Any]]:
- """Get all tools from the server and mark disabled tools for UI detail views."""
- with self.__lock:
- tools = self.__client.get_tools() # type: ignore
- disabled = set(self.disabled_tools)
- return [
- {**tool, "disabled": tool.get("name") in disabled}
- for tool in tools
- ]
+ return self.__client.tools # type: ignore
def has_tool(self, tool_name: str) -> bool:
"""Check if a tool is available"""
- if tool_name in self.disabled_tools:
- return False
with self.__lock:
return self.__client.has_tool(tool_name) # type: ignore
@@ -679,24 +548,12 @@ class MCPServerLocal(BaseModel):
self, tool_name: str, input_data: Dict[str, Any]
) -> CallToolResult:
"""Call a tool with the given input data"""
- client = self.__client
- if client is None:
- raise RuntimeError("MCP local client is not initialized")
- if tool_name in self.disabled_tools:
- raise ValueError(f"Tool {tool_name} is disabled for server {self.name}.")
- return await client.call_tool(tool_name, input_data)
+ with self.__lock:
+ # We already run in an event loop, dont believe Pylance
+ return await self.__client.call_tool(tool_name, input_data) # type: ignore
def update(self, config: dict[str, Any]) -> "MCPServerLocal":
with self.__lock:
- command = self.command
- command_args: list[str] = []
- args = list(self.args)
- if "command" in config:
- command, command_args = _split_stdio_command(config.get("command"))
- args = [*command_args, *args]
- if "args" in config:
- args = [*command_args, *_normalize_stdio_args(config.get("args"))]
-
for key, value in config.items():
if key in [
"name",
@@ -710,18 +567,10 @@ class MCPServerLocal(BaseModel):
"init_timeout",
"tool_timeout",
"disabled",
- "disabled_tools",
- "scope",
]:
- if key in ["command", "args"]:
- continue
if key == "name":
value = normalize_name(value)
- if key == "disabled_tools":
- value = _normalize_disabled_tools(value)
setattr(self, key, value)
- self.command = command
- self.args = args
return self
async def initialize(self) -> "MCPServerLocal":
@@ -741,136 +590,16 @@ MCPServer = Annotated[
class MCPConfig(BaseModel):
servers: list[MCPServer] = Field(default_factory=list)
disconnected_servers: list[dict[str, Any]] = Field(default_factory=list)
- config_scope: str = Field(default="global")
__lock: ClassVar[threading.Lock] = PrivateAttr(default=threading.Lock())
__instance: ClassVar[Any] = PrivateAttr(default=None)
__initialized: ClassVar[bool] = PrivateAttr(default=False)
- __project_instances: ClassVar[dict[str, tuple[str, "MCPConfig"]]] = {}
@classmethod
def get_instance(cls) -> "MCPConfig":
- with cls.__lock:
- if cls.__instance is None:
- cls.__instance = cls(servers_list=[], config_scope="global")
- return cls.__instance
-
- @classmethod
- def clear_project_instances(cls):
- with cls.__lock:
- cls.__project_instances = {}
-
- @classmethod
- def parse_config_string(cls, config_str: str) -> List[Dict[str, Any]]:
- servers_data: List[Dict[str, Any]] = []
-
- if not (config_str and config_str.strip()):
- return servers_data
-
- try:
- parsed_value = dirty_json.try_parse(config_str)
- normalized = cls.normalize_config(parsed_value)
-
- if isinstance(normalized, list):
- for item in normalized:
- if isinstance(item, dict):
- servers_data.append(dict(item))
- else:
- PrintStyle(
- background_color="yellow",
- font_color="black",
- padding=True,
- ).print(
- f"Warning: MCP config item was not a dictionary and was ignored: {item}"
- )
- else:
- PrintStyle(
- background_color="red", font_color="white", padding=True
- ).print(
- f"Error: Parsed MCP config top-level structure is not a list. Config string was: '{config_str}'"
- )
- except Exception as e_json:
- PrintStyle.error(
- f"Error parsing MCP config string: {e_json}. Config string was: '{config_str}'"
- )
-
- return servers_data
-
- @classmethod
- def merge_config_strings(
- cls, global_config: str, project_config: str
- ) -> tuple[list[dict[str, Any]], str]:
- merged: dict[str, dict[str, Any]] = {}
- unnamed: list[dict[str, Any]] = []
-
- def add_servers(config_str: str, scope: str):
- for server in cls.parse_config_string(config_str):
- server_copy = dict(server)
- server_copy["scope"] = scope
- name = str(server_copy.get("name", "") or "").strip()
- if not name:
- unnamed.append(server_copy)
- continue
- normalized_name = normalize_name(name)
- server_copy["name"] = normalized_name
- merged[normalized_name] = server_copy
-
- add_servers(global_config or DEFAULT_MCP_SERVERS_CONFIG, "global")
- add_servers(project_config or DEFAULT_MCP_SERVERS_CONFIG, "project")
-
- servers = [*unnamed, *merged.values()]
- cache_key = dirty_json.stringify(
- {
- "mcpServers": {
- s.get("name", f"unnamed_{i}"): s
- for i, s in enumerate(servers)
- }
- }
- )
- return servers, cache_key
-
- @classmethod
- def get_project_instance(cls, project_name: str | None, *, force: bool = False) -> "MCPConfig":
- project_key = str(project_name or "").strip()
- if not project_key:
- return cls.get_instance()
-
- from helpers import projects
- project_key = projects.validate_project_name(project_key)
-
- global_config = settings.get_settings().get(
- "mcp_servers", DEFAULT_MCP_SERVERS_CONFIG
- )
- project_config = projects.load_project_mcp_servers(project_key)
- servers_data, cache_key = cls.merge_config_strings(global_config, project_config)
-
- with cls.__lock:
- cached = cls.__project_instances.get(project_key)
- if cached and cached[0] == cache_key and not force:
- return cached[1]
-
- instance = cls(servers_list=servers_data, config_scope=f"project:{project_key}")
- with cls.__lock:
- cls.__project_instances[project_key] = (cache_key, instance)
- return instance
-
- @classmethod
- def refresh_project(cls, project_name: str) -> "MCPConfig":
- project_key = str(project_name or "").strip()
- with cls.__lock:
- cls.__project_instances.pop(project_key, None)
- return cls.get_project_instance(project_key, force=True)
-
- @classmethod
- def get_for_agent(cls, agent: Any) -> "MCPConfig":
- try:
- from helpers import projects
-
- project_name = projects.get_context_project_name(agent.context)
- if project_name:
- return cls.get_project_instance(project_name)
- except Exception:
- pass
- return cls.get_instance()
+ # with cls.__lock:
+ if cls.__instance is None:
+ cls.__instance = cls(servers_list=[])
+ return cls.__instance
@classmethod
def wait_for_lock(cls):
@@ -879,20 +608,97 @@ class MCPConfig(BaseModel):
@classmethod
def update(cls, config_str: str) -> Any:
- servers_data = cls.parse_config_string(config_str)
- new_instance = cls(servers_list=servers_data, config_scope="global")
with cls.__lock:
- # Build and initialize outside the class lock so a slow or wedged MCP
- # server cannot freeze status reads, prompts, or later tool calls.
- instance = cls.__instance
- if instance is None:
- instance = new_instance
- cls.__instance = instance
- else:
- instance.servers = new_instance.servers
- instance.disconnected_servers = new_instance.disconnected_servers
- instance.config_scope = new_instance.config_scope
- cls.__project_instances = {}
+ servers_data: List[Dict[str, Any]] = [] # Default to empty list
+
+ if (
+ config_str and config_str.strip()
+ ): # Only parse if non-empty and not just whitespace
+ try:
+ # Try with standard json.loads first, as it should handle escaped strings correctly
+ parsed_value = dirty_json.try_parse(config_str)
+ normalized = cls.normalize_config(parsed_value)
+
+ if isinstance(normalized, list):
+ valid_servers = []
+ for item in normalized:
+ if isinstance(item, dict):
+ valid_servers.append(item)
+ else:
+ PrintStyle(
+ background_color="yellow",
+ font_color="black",
+ padding=True,
+ ).print(
+ f"Warning: MCP config item (from json.loads) was not a dictionary and was ignored: {item}"
+ )
+ servers_data = valid_servers
+ else:
+ PrintStyle(
+ background_color="red", font_color="white", padding=True
+ ).print(
+ f"Error: Parsed MCP config (from json.loads) top-level structure is not a list. Config string was: '{config_str}'"
+ )
+ # servers_data remains empty
+ except (
+ Exception
+ ) as e_json: # Catch json.JSONDecodeError specifically if possible, or general Exception
+ PrintStyle.error(
+ f"Error parsing MCP config string: {e_json}. Config string was: '{config_str}'"
+ )
+
+ # # Fallback to DirtyJson or log error if standard json.loads fails
+ # PrintStyle(background_color="orange", font_color="black", padding=True).print(
+ # f"Standard json.loads failed for MCP config: {e_json}. Attempting DirtyJson as fallback."
+ # )
+ # try:
+ # parsed_value = DirtyJson.parse_string(config_str)
+ # if isinstance(parsed_value, list):
+ # valid_servers = []
+ # for item in parsed_value:
+ # if isinstance(item, dict):
+ # valid_servers.append(item)
+ # else:
+ # PrintStyle(background_color="yellow", font_color="black", padding=True).print(
+ # f"Warning: MCP config item (from DirtyJson) was not a dictionary and was ignored: {item}"
+ # )
+ # servers_data = valid_servers
+ # else:
+ # PrintStyle(background_color="red", font_color="white", padding=True).print(
+ # f"Error: Parsed MCP config (from DirtyJson) top-level structure is not a list. Config string was: '{config_str}'"
+ # )
+ # # servers_data remains empty
+ # except Exception as e_dirty:
+ # PrintStyle(background_color="red", font_color="white", padding=True).print(
+ # f"Error parsing MCP config string with DirtyJson as well: {e_dirty}. Config string was: '{config_str}'"
+ # )
+ # # servers_data remains empty, allowing graceful degradation
+
+ # Initialize/update the singleton instance with the (potentially empty) list of server data
+ instance = cls.get_instance()
+ # Directly update the servers attribute of the existing instance or re-initialize carefully
+ # For simplicity and to ensure __init__ logic runs if needed for setup:
+ new_instance_data = {
+ "servers": servers_data
+ } # Prepare data for re-initialization or update
+
+ # Option 1: Re-initialize the existing instance (if __init__ is idempotent for other fields)
+ instance.__init__(servers_list=servers_data)
+
+ # Option 2: Or, if __init__ has side effects we don't want to repeat,
+ # and 'servers' is the primary thing 'update' changes:
+ # instance.servers = [] # Clear existing servers first
+ # for server_item_data in servers_data:
+ # try:
+ # if server_item_data.get("url", None):
+ # instance.servers.append(MCPServerRemote(server_item_data))
+ # else:
+ # instance.servers.append(MCPServerLocal(server_item_data))
+ # except Exception as e_init:
+ # PrintStyle(background_color="grey", font_color="red", padding=True).print(
+ # f"MCPConfig.update: Failed to create MCPServer from item '{server_item_data.get('name', 'Unknown')}': {e_init}"
+ # )
+
cls.__initialized = True
return instance
@@ -902,24 +708,23 @@ class MCPConfig(BaseModel):
if isinstance(servers, list):
for server in servers:
if isinstance(server, dict):
- normalized.append(dict(server))
+ normalized.append(server)
elif isinstance(servers, dict):
if "mcpServers" in servers:
if isinstance(servers["mcpServers"], dict):
for key, value in servers["mcpServers"].items():
if isinstance(value, dict):
- server = dict(value)
- server["name"] = key
- normalized.append(server)
+ value["name"] = key
+ normalized.append(value)
elif isinstance(servers["mcpServers"], list):
for server in servers["mcpServers"]:
if isinstance(server, dict):
- normalized.append(dict(server))
+ normalized.append(server)
else:
- normalized.append(dict(servers)) # single server?
+ normalized.append(servers) # single server?
return normalized
- def __init__(self, servers_list: List[Dict[str, Any]], config_scope: str = "global"):
+ def __init__(self, servers_list: List[Dict[str, Any]]):
from collections.abc import Mapping, Iterable
# # DEBUG: Print the received servers_list
@@ -936,7 +741,6 @@ class MCPConfig(BaseModel):
# Clear any servers potentially initialized by super().__init__() before we populate based on servers_list
self.servers = []
- self.config_scope = config_scope
# initialize failed servers list
self.disconnected_servers = []
@@ -971,11 +775,6 @@ class MCPConfig(BaseModel):
)
continue
- server_item = dict(server_item)
- server_item["disabled_tools"] = _normalize_disabled_tools(
- server_item.get("disabled_tools")
- )
-
if server_item.get("disabled", False):
# get server name if available
server_name = server_item.get("name", "unnamed_server")
@@ -1068,10 +867,10 @@ class MCPConfig(BaseModel):
name = server.name
# get tool count
tool_count = len(server.get_tools())
+ # check if server is connected
+ connected = True # tool_count > 0
# get error message if any
error = server.get_error()
- # A server object can exist while its initialization failed.
- connected = not bool(error)
# get log bool
has_log = server.get_log() != ""
@@ -1079,9 +878,6 @@ class MCPConfig(BaseModel):
result.append(
{
"name": name,
- "scope": getattr(server, "scope", self.config_scope),
- "type": getattr(server, "type", ""),
- "description": getattr(server, "description", ""),
"connected": connected,
"error": error,
"tool_count": tool_count,
@@ -1094,9 +890,6 @@ class MCPConfig(BaseModel):
result.append(
{
"name": disconnected["name"],
- "scope": disconnected.get("config", {}).get("scope", self.config_scope),
- "type": disconnected.get("config", {}).get("type", ""),
- "description": disconnected.get("config", {}).get("description", ""),
"connected": False,
"error": disconnected["error"],
"tool_count": 0,
@@ -1111,15 +904,12 @@ class MCPConfig(BaseModel):
for server in self.servers:
if server.name == server_name:
try:
- get_all_tools = getattr(server, "get_all_tools", None)
- tools = get_all_tools() if callable(get_all_tools) else server.get_tools()
+ tools = server.get_tools()
except Exception:
tools = []
return {
"name": server.name,
"description": server.description,
- "scope": getattr(server, "scope", self.config_scope),
- "type": getattr(server, "type", ""),
"tools": tools,
}
return {}
@@ -1196,10 +986,9 @@ class MCPConfig(BaseModel):
def has_tool(self, tool_name: str) -> bool:
"""Check if a tool is available"""
- try:
- server_name_part, tool_name_part = _split_qualified_tool_name(tool_name)
- except ValueError:
+ if "." not in tool_name:
return False
+ server_name_part, tool_name_part = tool_name.split(".")
with self.__lock:
for server in self.servers:
if server.name == server_name_part:
@@ -1207,9 +996,6 @@ class MCPConfig(BaseModel):
return False
def get_tool(self, agent: Any, tool_name: str) -> MCPTool | None:
- effective_config = MCPConfig.get_for_agent(agent)
- if effective_config is not self:
- return effective_config.get_tool(agent, tool_name)
if not self.has_tool(tool_name):
return None
return MCPTool(agent=agent, name=tool_name, method=None, args={}, message="", loop_data=None)
@@ -1218,16 +1004,14 @@ class MCPConfig(BaseModel):
self, tool_name: str, input_data: Dict[str, Any]
) -> CallToolResult:
"""Call a tool with the given input data"""
- server_name_part, tool_name_part = _split_qualified_tool_name(tool_name)
- matched_server = None
+ if "." not in tool_name:
+ raise ValueError(f"Tool {tool_name} not found")
+ server_name_part, tool_name_part = tool_name.split(".")
with self.__lock:
for server in self.servers:
if server.name == server_name_part and server.has_tool(tool_name_part):
- matched_server = server
- break
- if matched_server is None:
+ return await server.call_tool(tool_name_part, input_data)
raise ValueError(f"Tool {tool_name} not found")
- return await matched_server.call_tool(tool_name_part, input_data)
T = TypeVar("T")
@@ -1247,49 +1031,6 @@ class MCPClientBase(ABC):
self.log: List[str] = []
self.log_file: Optional[TextIO] = None
- def _operation_timeout_seconds(self, read_timeout_seconds: float) -> float:
- try:
- seconds = float(read_timeout_seconds)
- except (TypeError, ValueError):
- seconds = 60.0
- if seconds <= 0:
- seconds = 60.0
- return seconds + MCP_OPERATION_TIMEOUT_GRACE_SECONDS
-
- def _operation_thread_name(self, operation_name: str) -> str:
- server_name = normalize_name(str(getattr(self.server, "name", "") or "server"))
- return f"MCPClient-{server_name[:32] or 'server'}-{operation_name}-{uuid.uuid4().hex[:8]}"
-
- async def _run_isolated_operation(
- self,
- operation_name: str,
- operation: Callable[[], Awaitable[T]],
- timeout_seconds: float,
- ) -> T:
- worker = DeferredTask(thread_name=self._operation_thread_name(operation_name))
- timed_out = False
- try:
- return await asyncio.wait_for(
- worker.execute_inside(operation),
- timeout=timeout_seconds,
- )
- except asyncio.TimeoutError as exc:
- timed_out = True
- message = (
- f"MCPClientBase ({self.server.name} - {operation_name}): "
- f"operation did not finish within {timeout_seconds:.1f}s; "
- "abandoning the isolated worker so Agent Zero can continue."
- )
- PrintStyle.warning(message)
- with self.__lock:
- self.error = message
- raise TimeoutError(message) from exc
- finally:
- if timed_out:
- worker.kill(terminate_thread=False)
- else:
- worker.kill(terminate_thread=True)
-
# Protected method
@abstractmethod
async def _create_stdio_transport(
@@ -1312,56 +1053,54 @@ class MCPClientBase(ABC):
"""
operation_name = coro_func.__name__ # For logging
# PrintStyle(font_color="cyan").print(f"MCPClientBase ({self.server.name}): Creating new session for operation '{operation_name}'...")
+ # Store the original exception outside the async block
original_exception = None
- result: T | None = None
- has_result = False
- temp_stack = AsyncExitStack()
try:
- stdio, write = await self._create_stdio_transport(temp_stack)
- # PrintStyle(font_color="cyan").print(f"MCPClientBase ({self.server.name} - {operation_name}): Transport created. Initializing session...")
- session = await temp_stack.enter_async_context(
- ClientSession(
- stdio, # type: ignore
- write, # type: ignore
- read_timeout_seconds=timedelta(
- seconds=read_timeout_seconds
- ),
- )
- )
- await session.initialize()
+ async with AsyncExitStack() as temp_stack:
+ try:
- result = await coro_func(session)
- has_result = True
+ stdio, write = await self._create_stdio_transport(temp_stack)
+ # PrintStyle(font_color="cyan").print(f"MCPClientBase ({self.server.name} - {operation_name}): Transport created. Initializing session...")
+ session = await temp_stack.enter_async_context(
+ ClientSession(
+ stdio, # type: ignore
+ write, # type: ignore
+ read_timeout_seconds=timedelta(
+ seconds=read_timeout_seconds
+ ),
+ )
+ )
+ await session.initialize()
+
+ result = await coro_func(session)
+
+ return result
+ except Exception as e:
+ # Store the original exception and raise a dummy exception
+ excs = getattr(e, "exceptions", None) # Python 3.11+ ExceptionGroup
+ if excs:
+ original_exception = excs[0]
+ else:
+ original_exception = e
+ # Create a dummy exception to break out of the async block
+ raise RuntimeError("Dummy exception to break out of async block")
except Exception as e:
- excs = getattr(e, "exceptions", None) # Python 3.11+ ExceptionGroup
- if excs:
- original_exception = excs[0]
- else:
- original_exception = e
- try:
- await asyncio.wait_for(
- temp_stack.aclose(),
- timeout=MCP_SESSION_CLEANUP_TIMEOUT_SECONDS,
- )
- except asyncio.TimeoutError:
- PrintStyle.warning(
- f"MCPClientBase ({self.server.name} - {operation_name}): "
- f"session cleanup exceeded {MCP_SESSION_CLEANUP_TIMEOUT_SECONDS:.1f}s."
- )
- except Exception as cleanup_exception:
- PrintStyle.warning(
- f"MCPClientBase ({self.server.name} - {operation_name}): "
- f"session cleanup failed: {type(cleanup_exception).__name__}: {cleanup_exception}"
- )
- if original_exception is not None:
+ # Check if this is our dummy exception
+ if original_exception is not None:
+ e = original_exception
+ # We have the original exception stored
PrintStyle(
background_color="#AA4455", font_color="white", padding=False
).print(
- f"MCPClientBase ({self.server.name} - {operation_name}): Error during operation: {type(original_exception).__name__}: {original_exception}"
+ f"MCPClientBase ({self.server.name} - {operation_name}): Error during operation: {type(e).__name__}: {e}"
)
- raise original_exception
- if has_result:
- return cast(T, result)
+ raise e # Re-raise the original exception
+ # finally:
+ # PrintStyle(font_color="cyan").print(
+ # f"MCPClientBase ({self.server.name} - {operation_name}): Session and transport will be closed by AsyncExitStack."
+ # )
+ # This line should ideally be unreachable if the try/except/finally logic within the 'async with' is exhaustive.
+ # Adding it to satisfy linters that might not fully trace the raise/return paths through async context managers.
raise RuntimeError(
f"MCPClientBase ({self.server.name} - {operation_name}): _execute_with_session exited 'async with' block unexpectedly."
)
@@ -1380,25 +1119,16 @@ class MCPClientBase(ABC):
}
for tool in response.tools
]
- self.error = ""
PrintStyle(font_color="green").print(
f"MCPClientBase ({self.server.name}): Tools updated. Found {len(self.tools)} tools."
)
try:
- current_settings = settings.get_settings()
- init_timeout = (
- self.server.init_timeout
- or current_settings.get("mcp_client_init_timeout", 10)
- or 10
- )
- await self._run_isolated_operation(
- "update_tools",
- lambda: self._execute_with_session(
- list_tools_op,
- read_timeout_seconds=init_timeout,
- ),
- timeout_seconds=self._operation_timeout_seconds(init_timeout),
+ set = settings.get_settings()
+ await self._execute_with_session(
+ list_tools_op,
+ read_timeout_seconds=self.server.init_timeout
+ or set["mcp_client_init_timeout"],
)
except Exception as e:
# e = eg.exceptions[0]
@@ -1425,7 +1155,7 @@ class MCPClientBase(ABC):
def get_tools(self) -> List[dict[str, Any]]:
"""Get all tools from the server (uses cached tools)"""
with self.__lock:
- return [dict(tool) for tool in self.tools]
+ return self.tools
async def call_tool(
self, tool_name: str, input_data: Dict[str, Any]
@@ -1447,35 +1177,19 @@ class MCPClientBase(ABC):
f"MCPClientBase ({self.server.name}): Tool '{tool_name}' found after updating tools."
)
- current_settings = settings.get_settings()
- tool_timeout = (
- self.server.tool_timeout
- or current_settings.get("mcp_client_tool_timeout", 120)
- or 120
- )
-
async def call_tool_op(current_session: ClientSession):
+ set = settings.get_settings()
# PrintStyle(font_color="cyan").print(f"MCPClientBase ({self.server.name}): Executing 'call_tool' for '{tool_name}' via MCP session...")
response: CallToolResult = await current_session.call_tool(
tool_name,
input_data,
- read_timeout_seconds=timedelta(seconds=tool_timeout),
+ read_timeout_seconds=timedelta(seconds=set["mcp_client_tool_timeout"]),
)
# PrintStyle(font_color="green").print(f"MCPClientBase ({self.server.name}): Tool '{tool_name}' call successful via session.")
return response
try:
- response = await self._run_isolated_operation(
- "call_tool",
- lambda: self._execute_with_session(
- call_tool_op,
- read_timeout_seconds=tool_timeout,
- ),
- timeout_seconds=self._operation_timeout_seconds(tool_timeout),
- )
- with self.__lock:
- self.error = ""
- return response
+ return await self._execute_with_session(call_tool_op)
except Exception as e:
# Error logged by _execute_with_session. Re-raise a specific error for the caller.
PrintStyle(
@@ -1590,19 +1304,11 @@ class MCPClientRemote(MCPClientBase):
]:
"""Connect to an MCP server, init client and save stdio/write streams"""
server: MCPServerRemote = cast(MCPServerRemote, self.server)
- current_settings = settings.get_settings()
+ set = settings.get_settings()
# Resolve timeout: check server config first, then settings, defaulting to 5s/10s
- init_timeout = (
- server.init_timeout
- or current_settings.get("mcp_client_init_timeout", 10)
- or 10
- )
- tool_timeout = (
- server.tool_timeout
- or current_settings.get("mcp_client_tool_timeout", 120)
- or 120
- )
+ init_timeout = server.init_timeout or set["mcp_client_init_timeout"] or 5
+ tool_timeout = server.tool_timeout or set["mcp_client_tool_timeout"] or 10
client_factory = CustomHTTPClientFactory(verify=server.verify)
# Check if this is a streaming HTTP type
diff --git a/helpers/mcp_handler.py.dox.md b/helpers/mcp_handler.py.dox.md
deleted file mode 100644
index 1cab10612..000000000
--- a/helpers/mcp_handler.py.dox.md
+++ /dev/null
@@ -1,115 +0,0 @@
-# mcp_handler.py DOX
-
-## Purpose
-
-- Own the `mcp_handler.py` helper module.
-- This module loads global and project-scoped MCP server configuration and exposes MCP tools to agents.
-- Keep this file-level DOX profile synchronized with `mcp_handler.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `mcp_handler.py` owns the runtime implementation.
-- `mcp_handler.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `MCPTool` (`Tool`)
- - `get_log_object(self) -> LogItem`
- - `async execute(self, **kwargs)`
- - `async before_execution(self, **kwargs)`
- - `async after_execution(self, response: Response, **kwargs)`
-- `MCPServerRemote` (`BaseModel`)
- - `get_error(self) -> str`
- - `get_log(self) -> str`
- - `get_tools(self) -> List[dict[str, Any]]`
- - `get_all_tools(self) -> List[dict[str, Any]]`
- - `has_tool(self, tool_name: str) -> bool`
- - `async call_tool(self, tool_name: str, input_data: Dict[str, Any]) -> CallToolResult`
- - `update(self, config: dict[str, Any]) -> 'MCPServerRemote'`
- - `async initialize(self) -> 'MCPServerRemote'`
-- `MCPServerLocal` (`BaseModel`)
- - `get_error(self) -> str`
- - `get_log(self) -> str`
- - `get_tools(self) -> List[dict[str, Any]]`
- - `get_all_tools(self) -> List[dict[str, Any]]`
- - `has_tool(self, tool_name: str) -> bool`
- - `async call_tool(self, tool_name: str, input_data: Dict[str, Any]) -> CallToolResult`
- - `update(self, config: dict[str, Any]) -> 'MCPServerLocal'`
- - `async initialize(self) -> 'MCPServerLocal'`
-- `MCPConfig` (`BaseModel`)
- - `get_instance(cls) -> 'MCPConfig'`
- - `clear_project_instances(cls)`
- - `parse_config_string(cls, config_str: str) -> List[Dict[str, Any]]`
- - `merge_config_strings(cls, global_config: str, project_config: str) -> tuple[List[Dict[str, Any]], str]`
- - `get_project_instance(cls, project_name: str | None, *, force: bool = False) -> 'MCPConfig'`
- - `refresh_project(cls, project_name: str) -> 'MCPConfig'`
- - `get_for_agent(cls, agent: Any) -> 'MCPConfig'`
- - `wait_for_lock(cls)`
- - `update(cls, config_str: str) -> Any`
- - `normalize_config(cls, servers: Any)`
- - `get_server_log(self, server_name: str) -> str`
- - `get_servers_status(self) -> list[dict[str, Any]]`
- - `get_server_detail(self, server_name: str) -> dict[str, Any]`
- - `is_initialized(self) -> bool`
-- `MCPClientBase` (`ABC`)
- - `async update_tools(self) -> 'MCPClientBase'`
- - `has_tool(self, tool_name: str) -> bool`
- - `get_tools(self) -> List[dict[str, Any]]`
- - `async call_tool(self, tool_name: str, input_data: Dict[str, Any]) -> CallToolResult`
- - `get_log(self)`
-- `MCPClientLocal` (`MCPClientBase`)
-- `CustomHTTPClientFactory` (`ABC`)
-- `MCPClientRemote` (`MCPClientBase`)
- - `get_session_id(self) -> Optional[str]`
-- Top-level functions:
-- `_mcp_get(item: Any, key: str, default: Any=...) -> Any`
-- `normalize_name(name: str) -> str`
-- `_determine_server_type(config_dict: dict) -> str`: Determine the server type based on configuration, with backward compatibility.
-- `_is_streaming_http_type(server_type: str) -> bool`: Check if the server type is a streaming HTTP variant.
-- `_split_qualified_tool_name(tool_name: str) -> tuple[str, str]`: Split `server.tool` names while preserving dots inside MCP tool names.
-- `_normalize_disabled_tools(value: Any) -> list[str]`: Normalize the optional per-server disabled tool list.
-- `_split_stdio_command(command: Any) -> tuple[str, list[str]]`: Split shell-style local MCP command lines into an executable plus leading arguments.
-- `_split_stdio_arg_fragment(arg: str) -> list[str]`: Split collapsed option/value argument fragments while preserving obvious single values with spaces.
-- `_normalize_stdio_args(value: Any) -> list[str]`: Normalize local MCP argument lists after manager/raw JSON parsing.
-- `initialize_mcp(mcp_servers_config: str)`
-- Notable constants/configuration names: `DEFAULT_MCP_SERVERS_CONFIG`, `MCP_MEDIA_TOKENS_ESTIMATE`, `MAX_MCP_RESOURCE_TEXT_CHARS`, `MCP_SESSION_CLEANUP_TIMEOUT_SECONDS`, `MCP_OPERATION_TIMEOUT_GRACE_SECONDS`, `T`.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- `MCPTool` is a `Tool`.
-- `MCPTool` defines `execute(...)`.
-- Global MCP configuration remains backed by settings; project MCP configuration is loaded through `helpers.projects` and merged with global config when an active agent context has `context.project`.
-- Project-scoped MCP servers overlay global servers by normalized name. The resulting `MCPConfig` cache key is derived from both config strings so project instances refresh when either scope changes.
-- Server status and detail responses include `scope`, and MCP tools resolve through `MCPConfig.get_for_agent(agent)` before execution.
-- MCP tool names are qualified as `server_name.tool_name`; server names are normalized without dots, and the tool portion may contain dots.
-- Servers may define `disabled_tools` as a list of MCP tool names. Disabled tools are omitted from agent-facing prompts, status counts, `has_tool`, and calls, while detail views can still retrieve them through `get_all_tools()` with a `disabled` flag so users can re-enable them.
-- Server-specific `init_timeout` and `tool_timeout` override global MCP client timeout settings for list-tools and call-tool operations.
-- Local stdio server configs accept either strict MCP JSON (`command: "uvx", args: [...]`) or manager-style command lines (`command: "uvx package"`) and normalize them before spawning the process.
-- MCP image and image-resource content is materialized to scoped artifact files and returned both as model-visible image attachments and as path metadata (`attachments`/`media_paths`) for downstream delivery.
-- MCP config locks must not be held across awaited server initialization or tool-call operations. Slow or wedged MCP servers must not block status reads, prompt construction, unrelated MCP servers, or later tool calls through the shared config lock.
-- MCP client session work runs inside disposable isolated `DeferredTask` workers with an outer timeout. Normal `AsyncExitStack` cleanup is also bounded; if cleanup or transport shutdown does not finish, the operation reports failure or warning while Agent Zero keeps control of the agent loop.
-- Server status marks initialized server objects with cached initialization errors as disconnected, even if the config object exists.
-- Observed side-effect areas: filesystem writes, network calls, WebSocket state, settings/state persistence, secret handling.
-- Imported dependency areas include: `abc`, `anyio.streams.memory`, `asyncio`, `contextlib`, `datetime`, `helpers`, `helpers.defer`, `helpers.log`, `helpers.print_style`, `helpers.tool`, `httpx`, `json`, `mcp`, `mcp.client.sse`, `mcp.client.stdio`, `mcp.client.streamable_http`, `mcp.shared.message`, `shlex`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `TypeVar`, `name.strip.lower`, `re.sub`, `Field`, `PrivateAttr`, `threading.Lock`, `DeferredTask`, `_split_qualified_tool_name`, `config_dict.lower`, `server_type.lower`, `MCPConfig.get_instance.is_initialized`, `MCPConfig.get_for_agent`, `projects.validate_project_name`, `projects.load_project_mcp_servers`, `settings.get_settings`, `self.agent.context.log.log`, `str.strip`, `media_artifacts.guess_extension`, `callable`, `self._content_item_dump`, `join`, `Response`, `self.get_log_object`, `self._raw_tool_response`, `additional.pop`, `self._coerce_media_token_estimate`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-- Keep MCP timeout and cleanup changes covered by deterministic tests that do not require real MCP servers or network credentials.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_mcp_handler_multimodal.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/mcp_server.py.dox.md b/helpers/mcp_server.py.dox.md
deleted file mode 100644
index 4151d3d89..000000000
--- a/helpers/mcp_server.py.dox.md
+++ /dev/null
@@ -1,54 +0,0 @@
-# mcp_server.py DOX
-
-## Purpose
-
-- Own the `mcp_server.py` helper module.
-- This module serves Agent Zero chats through a dynamic MCP proxy.
-- Keep this file-level DOX profile synchronized with `mcp_server.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `mcp_server.py` owns the runtime implementation.
-- `mcp_server.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `ToolResponse` (`BaseModel`)
-- `ToolError` (`BaseModel`)
-- `DynamicMcpProxy` (no explicit base class)
- - `get_instance()`
- - `reconfigure(self, token: str)`
-- Top-level functions:
-- `async send_message(message: Annotated[str, Field(description='The message to send to the remote Agent Zero Instance', title='message')], attachments: Annotated[list[str], Field(description='Optional: A list of attachments (file paths or web urls) to send to the remote Agent Zero Instance with the message. Default: Empty list', title='attachments')] | None=..., chat_id: Annotated[str, Field(description='Optional: ID of the chat. Used to continue a chat. This value is returned in response to sending previous message. Default: Empty string', title='chat_id')] | None=..., persistent_chat: Annotated[bool, Field(description='Optional: Whether to use a persistent chat. If true, the chat will be saved and can be continued later. Default: False.', title='persistent_chat')] | None=...) -> Annotated[Union[ToolResponse, ToolError], Field(description='The response from the remote Agent Zero Instance', title='response')]`
-- `async finish_chat(chat_id: Annotated[str, Field(description='ID of the chat to be finished. This value is returned in response to sending previous message.', title='chat_id')]) -> Annotated[Union[ToolResponse, ToolError], Field(description='The response from the remote Agent Zero Instance', title='response')]`
-- `async _run_chat(context: AgentContext, message: str, attachments: list[str] | None=...)`
-- `async mcp_middleware(request: Request, call_next)`: Middleware to check if MCP server is enabled.
-- Notable constants/configuration names: `_PRINTER`, `SEND_MESSAGE_DESCRIPTION`, `FINISH_CHAT_DESCRIPTION`.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem reads, filesystem writes, filesystem deletion, network calls, settings/state persistence, secret handling, scheduler state.
-- Imported dependency areas include: `agent`, `asyncio`, `contextvars`, `fastmcp`, `fastmcp.server.http`, `helpers`, `helpers.persist_chat`, `helpers.print_style`, `initialize`, `openai`, `os`, `pydantic`, `starlette.exceptions`, `starlette.middleware`, `starlette.middleware.base`, `starlette.requests`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `PrintStyle`, `contextvars.ContextVar`, `FastMCP`, `mcp_server.tool`, `Field`, `settings.get_settings`, `initialize_agent`, `AgentContext`, `ToolError`, `ToolResponse`, `context.reset`, `AgentContext.remove`, `remove_chat`, `context.communicate`, `threading.RLock`, `self.reconfigure`, `StreamableHTTPSessionManager`, `mcp_server._get_additional_http_routes`, `create_base_app`, `PrintStyle.error`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_default_prompt_budget.py`
- - `tests/test_fasta2a_client.py`
- - `tests/test_ws_security.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/media_artifacts.py.dox.md b/helpers/media_artifacts.py.dox.md
deleted file mode 100644
index 9f4d0c39f..000000000
--- a/helpers/media_artifacts.py.dox.md
+++ /dev/null
@@ -1,60 +0,0 @@
-# media_artifacts.py DOX
-
-## Purpose
-
-- Own the `media_artifacts.py` helper module.
-- This module validates, decodes, names, and normalizes media artifact payloads.
-- Keep this file-level DOX profile synchronized with `media_artifacts.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `media_artifacts.py` owns the runtime implementation.
-- `media_artifacts.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `MediaArtifactError` (`ValueError`)
-- `EmptyBase64Data` (`MediaArtifactError`)
-- `InvalidBase64Data` (`MediaArtifactError`)
-- `ArtifactTooLarge` (`MediaArtifactError`)
-- `Base64Payload` (no explicit base class)
-- `ImageDataUrl` (no explicit base class)
-- `SavedArtifact` (no explicit base class)
-- Top-level functions:
-- `compact_base64(data: str) -> str`
-- `estimated_base64_decoded_size(data: str) -> int`
-- `decode_base64_payload(data: str, max_bytes: int | None=...) -> Base64Payload`
-- `normalize_mime(mime_type: str, default: str=..., required_prefix: str=...) -> str`
-- `guess_extension(mime_type: str, fallback: str=...) -> str`
-- `filename_from_uri(uri: str) -> str`
-- `safe_filename(value: str, default: str=..., default_extension: str=...) -> str`
-- `image_data_url_from_base64(data: str, mime_type: str=..., max_bytes: int | None=...) -> ImageDataUrl`
-- `save_base64_artifact(data: str, mime_type: str=..., directory_parts: tuple[str, ...], preferred_name: str=..., default_filename: str=..., max_bytes: int | None=...) -> SavedArtifact`
-- Notable constants/configuration names: `DEFAULT_MAX_ARTIFACT_SIZE_BYTES`.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem reads, filesystem writes, network calls.
-- Imported dependency areas include: `__future__`, `base64`, `binascii`, `dataclasses`, `helpers`, `mimetypes`, `pathlib`, `urllib.parse`, `uuid`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `dataclass`, `join`, `compact_base64`, `Base64Payload`, `str.strip.lower`, `str.strip`, `urlparse`, `decode_base64_payload`, `normalize_mime`, `ImageDataUrl`, `guess_extension`, `safe_filename`, `Path`, `artifact_dir.mkdir`, `path.write_bytes`, `SavedArtifact`, `super.__init__`, `EmptyBase64Data`, `estimated_base64_decoded_size`, `base64.b64decode`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_mcp_handler_multimodal.py`
- - `tests/test_media_artifacts.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/message_queue.py.dox.md b/helpers/message_queue.py.dox.md
deleted file mode 100644
index df300ed67..000000000
--- a/helpers/message_queue.py.dox.md
+++ /dev/null
@@ -1,53 +0,0 @@
-# message_queue.py DOX
-
-## Purpose
-
-- Own the `message_queue.py` helper module.
-- This module stores and drains queued user messages for a context.
-- Keep this file-level DOX profile synchronized with `message_queue.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `message_queue.py` owns the runtime implementation.
-- `message_queue.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Top-level functions:
-- `get_queue(context: 'AgentContext') -> list`: Get current queue from context.data.
-- `_get_next_seq(context: 'AgentContext') -> int`: Get next sequence number.
-- `_sync_output(context: 'AgentContext')`: Sync queue to output_data for frontend polling.
-- `add(context: 'AgentContext', text: str, attachments: list[str] | None=..., item_id: str | None=...) -> dict`: Add message to queue. Attachments should be filenames, will be converted to full paths.
-- `remove(context: 'AgentContext', item_id: str | None=...) -> int`: Remove item(s). If item_id is None, clears all. Returns remaining count.
-- `pop_first(context: 'AgentContext') -> dict | None`: Remove and return first item.
-- `pop_item(context: 'AgentContext', item_id: str) -> dict | None`: Remove and return specific item.
-- `has_queue(context: 'AgentContext') -> bool`: Check if queue has items.
-- `log_user_message(context: 'AgentContext', message: str, attachment_paths: list[str], message_id: str | None=..., source: str=...)`: Log user message to console and UI. Used by message API and queue processing.
-- `send_message(context: 'AgentContext', item: dict, source: str=...)`: Send a single queued message (log + communicate).
-- `send_next(context: 'AgentContext') -> bool`: Send next queued message. Returns True if sent, False if queue empty.
-- `send_all_aggregated(context: 'AgentContext') -> int`: Aggregate and send all queued messages as one. Returns count of items sent.
-- Notable constants/configuration names: `QUEUE_KEY`, `QUEUE_SEQ_KEY`, `UPLOAD_FOLDER`.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem reads, filesystem deletion, settings/state persistence.
-- Imported dependency areas include: `helpers`, `helpers.print_style`, `os`, `typing`, `uuid`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `context.set_data`, `get_queue`, `context.set_output_data`, `_sync_output`, `queue.pop`, `context.log.log`, `log_user_message`, `context.communicate`, `pop_first`, `has_queue`, `join`, `context.get_data`, `att.startswith`, `_get_next_seq`, `uuid.uuid4`, `UserMessage`, `send_message`, `guids.generate_id`, `os.path.basename`, `PrintStyle`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/messages.py.dox.md b/helpers/messages.py.dox.md
deleted file mode 100644
index e61772114..000000000
--- a/helpers/messages.py.dox.md
+++ /dev/null
@@ -1,50 +0,0 @@
-# messages.py DOX
-
-## Purpose
-
-- Own the `messages.py` helper module.
-- This module truncates message payloads for display and storage safety.
-- Keep this file-level DOX profile synchronized with `messages.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `messages.py` owns the runtime implementation.
-- `messages.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Top-level functions:
-- `truncate_text(agent, output, threshold=...)`
-- `truncate_dict_by_ratio(agent, data: dict | list | str, threshold_chars: int, truncate_to: int)`
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem reads, filesystem writes, settings/state persistence.
-- Imported dependency areas include: `json`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `agent.read_prompt`, `process_item`, `json.dumps`, `truncate_text`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/email_parser_test.py`
- - `tests/test_browser_agent_regressions.py`
- - `tests/test_chat_compaction.py`
- - `tests/test_document_query_fallback.py`
- - `tests/test_download_toast_regressions.py`
- - `tests/test_fasta2a_client.py`
- - `tests/test_mcp_handler_multimodal.py`
- - `tests/test_oauth_codex.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/microsoft_tunnel.py.dox.md b/helpers/microsoft_tunnel.py.dox.md
deleted file mode 100644
index 32ee7027c..000000000
--- a/helpers/microsoft_tunnel.py.dox.md
+++ /dev/null
@@ -1,50 +0,0 @@
-# microsoft_tunnel.py DOX
-
-## Purpose
-
-- Own the `microsoft_tunnel.py` helper module.
-- This module implements Microsoft Dev Tunnel provider behavior.
-- Keep this file-level DOX profile synchronized with `microsoft_tunnel.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `microsoft_tunnel.py` owns the runtime implementation.
-- `microsoft_tunnel.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `AgentZeroMicrosoftTunnel` (`MicrosoftTunnel`)
- - `notify(self, event, message, data=...)`
- - `agent_zero_notifications(self)`
-- `MicrosoftDevTunnel` (`FlaredanticTunnelHelper`)
- - `build_tunnel(self)`
- - `start(self)`
-- Top-level functions:
-- `default_microsoft_tunnel_id()`
-- Notable constants/configuration names: `MICROSOFT_TUNNEL_ID_ENV_KEYS`, `MICROSOFT_TUNNEL_TIMEOUT`.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem reads, network calls, settings/state persistence, tunnel state.
-- Imported dependency areas include: `flaredantic`, `getpass`, `hashlib`, `helpers`, `helpers.tunnel_common`, `os`, `socket`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `join`, `strip`, `hashlib.sha256.hexdigest`, `self.notify`, `callable`, `self._notify_progress`, `self._run_cmd`, `MicrosoftConfig`, `AgentZeroMicrosoftTunnel`, `getpass.getuser`, `socket.gethostname`, `files.get_abs_path`, `super.notify`, `parent`, `super.start`, `hashlib.sha256`, `MicrosoftTunnelError`, `default_microsoft_tunnel_id`, `RuntimeError`, `seed.encode`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_tunnel_remote_link.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/migration.py.dox.md b/helpers/migration.py.dox.md
deleted file mode 100644
index c548951ab..000000000
--- a/helpers/migration.py.dox.md
+++ /dev/null
@@ -1,52 +0,0 @@
-# migration.py DOX
-
-## Purpose
-
-- Own the `migration.py` helper module.
-- This module migrates legacy user data and runtime layout at startup.
-- Keep this file-level DOX profile synchronized with `migration.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `migration.py` owns the runtime implementation.
-- `migration.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Top-level functions:
-- `startup_migration() -> None`
-- `migrate_user_data() -> None`: Migrate user data from /tmp and other locations to /usr.
-- `convert_agents_json_yaml() -> None`
-- `_move_dir(src: str, dst: str, overwrite: bool=...) -> None`: Move a directory from src to dst if src exists and dst does not.
-- `_move_file(src: str, dst: str, overwrite: bool=...) -> None`: Move a file from src to dst if src exists and dst does not.
-- `_migrate_memory(base_path: str=...) -> None`: Migrate memory subdirectories.
-- `_merge_dir_contents(src_parent: str, dst_parent: str) -> None`: Moves all items from src_parent to dst_parent.
-- `_cleanup_obsolete() -> None`: Remove directories that are no longer needed.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem reads, filesystem writes, filesystem deletion, settings/state persistence, secret handling, scheduler state.
-- Imported dependency areas include: `helpers`, `helpers.print_style`, `json`, `os`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `migrate_user_data`, `convert_agents_json_yaml`, `extension.call_extensions_sync`, `_move_dir`, `_move_file`, `_migrate_memory`, `_merge_dir_contents`, `_cleanup_obsolete`, `subagents.get_agents_roots`, `files.get_subdirectories`, `files.list_files`, `files.deabsolute_path`, `files.exists`, `files.move_dir`, `files.move_file`, `files.get_abs_path`, `os.path.isdir`, `os.path.join`, `files.delete_dir`, `os.path.isfile`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_browser_agent_regressions.py`
- - `tests/test_office_canvas_setup.py`
- - `tests/test_office_document_store.py`
- - `tests/test_speech_plugin_split.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/modules.py.dox.md b/helpers/modules.py.dox.md
deleted file mode 100644
index 9f42143ed..000000000
--- a/helpers/modules.py.dox.md
+++ /dev/null
@@ -1,53 +0,0 @@
-# modules.py DOX
-
-## Purpose
-
-- Own the `modules.py` helper module.
-- This module imports modules and classes dynamically with namespace cache control.
-- Keep this file-level DOX profile synchronized with `modules.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `modules.py` owns the runtime implementation.
-- `modules.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Top-level functions:
-- `import_module(file_path: str) -> ModuleType`
-- `load_classes_from_folder(folder: str, name_pattern: str, base_class: Type[T], one_per_file: bool=...) -> list[Type[T]]`
-- `load_classes_from_file(file: str, base_class: type[T], one_per_file: bool=...) -> list[type[T]]`
-- `purge_namespace(namespace: str)`
-- Notable constants/configuration names: `T`.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem reads, filesystem deletion.
-- Imported dependency areas include: `fnmatch`, `helpers.files`, `importlib`, `importlib.util`, `inspect`, `os`, `re`, `sys`, `types`, `typing`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `TypeVar`, `get_abs_path`, `os.path.basename.replace`, `importlib.util.spec_from_file_location`, `importlib.util.module_from_spec`, `spec.loader.exec_module`, `import_module`, `inspect.getmembers`, `to_delete.sort`, `importlib.invalidate_caches`, `ImportError`, `os.path.join`, `os.path.basename`, `issubclass`, `os.listdir`, `name.startswith`, `n.count`, `fnmatch`, `file_name.endswith`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_a0_connector_prompt_gating.py`
- - `tests/test_browser_agent_regressions.py`
- - `tests/test_docker_release_plan.py`
- - `tests/test_error_retry_plugin.py`
- - `tests/test_file_tree_visualize.py`
- - `tests/test_git_version_label.py`
- - `tests/test_mcp_handler_multimodal.py`
- - `tests/test_memory_quality.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/network.py.dox.md b/helpers/network.py.dox.md
deleted file mode 100644
index 792836311..000000000
--- a/helpers/network.py.dox.md
+++ /dev/null
@@ -1,55 +0,0 @@
-# network.py DOX
-
-## Purpose
-
-- Own the `network.py` helper module.
-- This module validates public URLs and fetches remote resources safely.
-- Keep this file-level DOX profile synchronized with `network.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `network.py` owns the runtime implementation.
-- `network.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `HttpFetchResult` (no explicit base class)
-- `UnsafeUrlError` (`ValueError`)
-- Top-level functions:
-- `_build_request_headers() -> dict[str, str]`
-- `_normalize_content_type(content_type: str | None) -> str | None`
-- `resolve_host_ips(hostname: str) -> tuple[ipaddress._BaseAddress, ...]`
-- `validate_public_http_url(url: str) -> tuple[ipaddress._BaseAddress, ...]`
-- `fetch_public_http_resource(url: str, max_bytes: int, max_redirects: int=..., timeout: tuple[float, float]=...) -> HttpFetchResult`
-- `is_loopback_address(address: str) -> bool`: Check whether *address* resolves to a loopback interface.
-- Notable constants/configuration names: `SAFE_HTTP_SCHEMES`, `DEFAULT_FETCH_TIMEOUT`, `DEFAULT_HTTP_USER_AGENT`.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: network calls, secret handling.
-- Imported dependency areas include: `__future__`, `dataclasses`, `ipaddress`, `os`, `requests`, `socket`, `struct`, `urllib.parse`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `frozenset`, `dataclass`, `strip`, `urlparse`, `parsed.hostname.rstrip.lower`, `resolve_host_ips`, `requests.Session`, `ValueError`, `content_type.split.strip.lower`, `socket.getaddrinfo`, `ipaddress.ip_address`, `seen.add`, `UnsafeUrlError`, `hostname.endswith`, `validate_public_http_url`, `socket.inet_pton`, `_checkers`, `parsed.hostname.rstrip`, `os.getenv`, `content_type.split.strip`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_oauth_gemini_api.py`
- - `tests/test_oauth_github_copilot.py`
- - `tests/test_oauth_xai_grok.py`
- - `tests/test_plugin_scan_prompt.py`
- - `tests/test_tunnel_remote_link.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/notification.py.dox.md b/helpers/notification.py.dox.md
deleted file mode 100644
index d8f198f4a..000000000
--- a/helpers/notification.py.dox.md
+++ /dev/null
@@ -1,62 +0,0 @@
-# notification.py DOX
-
-## Purpose
-
-- Own the `notification.py` helper module.
-- This module owns notification data models and notification manager state.
-- Keep this file-level DOX profile synchronized with `notification.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `notification.py` owns the runtime implementation.
-- `notification.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `NotificationType` (`Enum`)
-- `NotificationPriority` (`Enum`)
-- `NotificationItem` (no explicit base class)
- - `mark_read(self)`
- - `output(self)`
-- `NotificationManager` (no explicit base class)
- - `send_notification(type: NotificationType, priority: NotificationPriority, message: str, title: str=..., detail: str=..., display_time: int=..., group: str=..., id: str=...) -> NotificationItem`
- - `add_notification(self, type: NotificationType, priority: NotificationPriority, message: str, title: str=..., detail: str=..., display_time: int=..., group: str=..., id: str=...) -> NotificationItem`
- - `get_recent_notifications(self, seconds: int=...) -> list[NotificationItem]`
- - `output(self, start: int | None=..., end: int | None=...) -> list[dict]`
- - `output_all(self) -> list[dict]`
- - `mark_read_by_ids(self, notification_ids: list[str]) -> int`
- - `update_item(self, no: int, **kwargs) -> None`
- - `mark_all_read(self)`
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem deletion, settings/state persistence.
-- Imported dependency areas include: `dataclasses`, `datetime`, `enum`, `helpers.localization`, `threading`, `uuid`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `self.manager.update_item`, `threading.RLock`, `AgentContext.get_notification_manager.add_notification`, `mark_dirty_all`, `self._update_item`, `NotificationType`, `Localization.get.serialize_datetime`, `uuid.uuid4`, `Localization.get.now`, `timedelta`, `n.output`, `AgentContext.get_notification_manager`, `next`, `NotificationPriority`, `NotificationItem`, `self._enforce_limit`, `seen.add`, `notifications.output`, `nid.strip`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_download_toast_regressions.py`
- - `tests/test_multi_tab_isolation.py`
- - `tests/test_self_update_tag_filter.py`
- - `tests/test_snapshot_parity.py`
- - `tests/test_snapshot_schema_v1.py`
- - `tests/test_state_monitor.py`
- - `tests/test_state_sync_handler.py`
- - `tests/test_state_sync_welcome_screen.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/parallel_tools.py b/helpers/parallel_tools.py
deleted file mode 100644
index 0b9c9f72a..000000000
--- a/helpers/parallel_tools.py
+++ /dev/null
@@ -1,754 +0,0 @@
-from __future__ import annotations
-
-import asyncio
-import json
-import time
-import uuid
-from dataclasses import dataclass, field, replace
-from typing import Any, Literal, TYPE_CHECKING
-
-from helpers import extract_tools
-from helpers.defer import DeferredTask, THREAD_BACKGROUND
-from helpers.extension import call_extensions_async
-from helpers.print_style import PrintStyle
-
-if TYPE_CHECKING:
- from agent import Agent, AgentConfig, AgentContext
- from helpers.log import LogItem
-
-
-PARALLEL_JOBS_KEY = "_parallel_jobs"
-PARALLEL_WORKER_PARENT_CONTEXT_KEY = "_parallel_parent_context_id"
-PARALLEL_WORKER_JOB_KEY = "_parallel_job_id"
-PARALLEL_WORKER_KIND_KEY = "_parallel_worker_kind"
-
-CHILD_PARENT_CONTEXT_ID_KEY = "parent_context_id"
-CHILD_PARENT_CONTEXT_KIND_KEY = "parent_context_kind"
-CHILD_PARENT_CONTEXT_LABEL_KEY = "parent_context_label"
-CHILD_PARALLEL_JOB_ID_KEY = "parallel_job_id"
-CHILD_PARALLEL_TOOL_NAME_KEY = "parallel_tool_name"
-
-DEFAULT_MAX_CALLS = 8
-DEFAULT_TIMEOUT_SECONDS = 300
-POLL_INTERVAL_SECONDS = 0.5
-DISALLOWED_PARALLEL_TOOLS = {"document_query"}
-
-TERMINAL_STATES = {"success", "error", "cancelled", "timeout"}
-JobState = Literal["pending", "running", "success", "error", "cancelled", "timeout"]
-JobKind = Literal["tool", "subordinate"]
-
-
-@dataclass
-class NormalizedToolCall:
- index: int
- tool_name: str
- tool_args: dict[str, Any]
-
-
-@dataclass
-class ParallelJob:
- id: str
- parent_context_id: str
- index: int
- tool_name: str
- tool_args: dict[str, Any]
- kind: JobKind
- state: JobState = "pending"
- created_at: float = field(default_factory=time.time)
- started_at: float | None = None
- completed_at: float | None = None
- result: str | None = None
- error: str | None = None
- worker_context_id: str | None = None
- log_id: str = field(default_factory=lambda: str(uuid.uuid4()))
- log_item: "LogItem | None" = field(default=None, repr=False)
- deferred_task: DeferredTask | None = field(default=None, repr=False)
-
- def elapsed(self) -> float:
- end = self.completed_at or time.time()
- start = self.started_at or self.created_at
- return max(0.0, end - start)
-
-
-def extract_tool_calls(args: dict[str, Any]) -> Any:
- for key in ("tool_calls", "calls", "items"):
- if key in args:
- return args.get(key)
- return None
-
-
-def normalize_parallel_tool_calls(raw_calls: Any) -> list[NormalizedToolCall]:
- if isinstance(raw_calls, str):
- try:
- raw_calls = json.loads(raw_calls)
- except json.JSONDecodeError as exc:
- raise ValueError(
- "`tool_calls` must be an array of normal tool-call objects."
- ) from exc
- if not isinstance(raw_calls, list):
- raise ValueError("`tool_calls` must be an array of normal tool-call objects.")
- if not raw_calls:
- raise ValueError("`tool_calls` must contain at least one tool call.")
- if len(raw_calls) > DEFAULT_MAX_CALLS:
- raise ValueError(f"`tool_calls` supports at most {DEFAULT_MAX_CALLS} items.")
-
- calls: list[NormalizedToolCall] = []
- for index, raw_call in enumerate(raw_calls):
- try:
- tool_name, tool_args = extract_tools.normalize_tool_request(raw_call)
- except ValueError as exc:
- raise ValueError(f"tool_calls[{index}] is not a valid tool call: {exc}") from exc
-
- if tool_name == "parallel":
- raise ValueError("`parallel` cannot be nested inside another `parallel` call.")
- if tool_name in DISALLOWED_PARALLEL_TOOLS:
- raise ValueError(
- f"`{tool_name}` cannot be used inside `parallel`; call it sequentially."
- )
-
- calls.append(
- NormalizedToolCall(
- index=index,
- tool_name=tool_name,
- tool_args=dict(tool_args),
- )
- )
- return calls
-
-
-def normalize_job_ids(raw_job_ids: Any) -> list[str]:
- if raw_job_ids is None:
- return []
- if isinstance(raw_job_ids, str):
- return [raw_job_ids]
- if isinstance(raw_job_ids, list):
- return [str(item) for item in raw_job_ids if str(item).strip()]
- raise ValueError("`job_ids` must be a string or an array of strings.")
-
-
-def coerce_bool(value: Any, default: bool) -> bool:
- if value is None:
- return default
- if isinstance(value, bool):
- return value
- if isinstance(value, str):
- normalized = value.strip().lower()
- if normalized in {"1", "true", "yes", "on"}:
- return True
- if normalized in {"0", "false", "no", "off"}:
- return False
- return bool(value)
-
-
-def coerce_timeout(value: Any) -> int:
- if value in (None, ""):
- return DEFAULT_TIMEOUT_SECONDS
- try:
- timeout = int(value)
- except (TypeError, ValueError) as exc:
- raise ValueError(f"`timeout` must be an integer number of seconds, got {value!r}.") from exc
- if timeout <= 0:
- raise ValueError("`timeout` must be greater than 0.")
- return timeout
-
-
-def _parallel_worker_kind(agent: "Agent | None") -> JobKind | None:
- context = getattr(agent, "context", None)
- if not context:
- return None
- kind = context.get_data(PARALLEL_WORKER_KIND_KEY)
- if kind in {"tool", "subordinate"}:
- return kind
- if context.get_data(PARALLEL_WORKER_JOB_KEY):
- return "tool"
- return None
-
-
-def is_parallel_worker(agent: "Agent | None") -> bool:
- return _parallel_worker_kind(agent) == "tool"
-
-
-def _jobs_for_context(context: "AgentContext") -> dict[str, ParallelJob]:
- jobs = context.get_data(PARALLEL_JOBS_KEY)
- if not isinstance(jobs, dict):
- jobs = {}
- context.set_data(PARALLEL_JOBS_KEY, jobs)
- return jobs
-
-
-def _get_job(parent_context_id: str, job_id: str) -> ParallelJob | None:
- from agent import AgentContext
-
- context = AgentContext.get(parent_context_id)
- if not context:
- return None
- job = _jobs_for_context(context).get(job_id)
- return job if isinstance(job, ParallelJob) else None
-
-
-def _new_job_id(tool_name: str) -> str:
- prefix = "".join(ch for ch in tool_name if ch.isalnum())[:12] or "job"
- return f"{prefix}-{uuid.uuid4().hex[:8]}"
-
-
-async def start_parallel_jobs(
- agent: "Agent",
- calls: list[NormalizedToolCall],
-) -> list[ParallelJob]:
- jobs: list[ParallelJob] = []
- context = agent.context
- job_store = _jobs_for_context(context)
-
- for call in calls:
- kind: JobKind = "subordinate" if call.tool_name == "call_subordinate" else "tool"
- job = ParallelJob(
- id=_new_job_id(call.tool_name),
- parent_context_id=context.id,
- index=call.index,
- tool_name=call.tool_name,
- tool_args=call.tool_args,
- kind=kind,
- )
- job_store[job.id] = job
- jobs.append(job)
- _log_parallel_child_started(agent, job)
-
- try:
- job.state = "running"
- job.started_at = time.time()
- task = DeferredTask(thread_name=THREAD_BACKGROUND)
- job.deferred_task = task
- task.start_task(_run_parallel_job, context.id, job.id)
- except Exception as exc:
- _finish_job(job, "error", error=str(exc))
-
- return jobs
-
-
-async def await_parallel_jobs(
- agent: "Agent",
- job_ids: list[str],
- timeout: int = DEFAULT_TIMEOUT_SECONDS,
- *,
- collect: bool = True,
- wait: bool = True,
-) -> list[dict[str, Any]]:
- if not job_ids:
- raise ValueError("No `job_ids` were provided to await.")
-
- deadline = time.time() + timeout
- known_job_ids = set(job_ids)
- wait_timed_out_job_ids: set[str] = set()
- while True:
- await refresh_parallel_jobs(agent)
- jobs = [_jobs_for_context(agent.context).get(job_id) for job_id in job_ids]
- missing = [job_id for job_id, job in zip(job_ids, jobs) if job is None]
- if missing:
- raise ValueError(f"Unknown parallel job id(s): {', '.join(missing)}")
-
- active = [job for job in jobs if job and job.state not in TERMINAL_STATES]
- if not wait or not active:
- break
-
- if time.time() >= deadline:
- wait_timed_out_job_ids = {job.id for job in active}
- break
-
- await asyncio.sleep(POLL_INTERVAL_SECONDS)
-
- snapshots = []
- for job_id in job_ids:
- job = _jobs_for_context(agent.context).get(job_id)
- if job:
- snapshot = _job_snapshot(job, include_result=True)
- if job.id in wait_timed_out_job_ids and job.state not in TERMINAL_STATES:
- snapshot["wait_timed_out"] = True
- snapshots.append(snapshot)
-
- if collect:
- for job_id in known_job_ids:
- job = _jobs_for_context(agent.context).get(job_id)
- if job and job.state in TERMINAL_STATES:
- await cleanup_parallel_job(agent, job)
- _jobs_for_context(agent.context).pop(job_id, None)
-
- return snapshots
-
-
-async def cancel_parallel_jobs(agent: "Agent", job_ids: list[str]) -> list[dict[str, Any]]:
- if not job_ids:
- raise ValueError("No `job_ids` were provided to cancel.")
-
- await refresh_parallel_jobs(agent)
- snapshots = []
- for job_id in job_ids:
- job = _jobs_for_context(agent.context).get(job_id)
- if not job:
- raise ValueError(f"Unknown parallel job id: {job_id}")
- await _cancel_job(job)
- snapshots.append(_job_snapshot(job, include_result=True))
- await cleanup_parallel_job(agent, job)
- _jobs_for_context(agent.context).pop(job_id, None)
- return snapshots
-
-
-async def refresh_parallel_jobs(agent: "Agent") -> list[ParallelJob]:
- jobs = list(_jobs_for_context(agent.context).values())
- for job in jobs:
- if job.state in TERMINAL_STATES:
- continue
- task = job.deferred_task
- if not task:
- continue
- if task.is_ready():
- try:
- await task.result()
- except asyncio.CancelledError:
- _finish_job(job, "cancelled", error="Parallel job was cancelled.")
- except Exception as exc:
- _finish_job(job, "error", error=str(exc))
- elif task.is_alive():
- job.state = "running"
- if job.started_at is None:
- job.started_at = time.time()
- return jobs
-
-
-async def cleanup_parallel_job(agent: "Agent", job: ParallelJob) -> None:
- if job.deferred_task and job.deferred_task.is_alive():
- job.deferred_task.kill()
- if job.kind == "tool":
- await _remove_context(job.worker_context_id)
-
-
-async def build_parallel_jobs_extras(agent: "Agent") -> str:
- await refresh_parallel_jobs(agent)
- jobs = [
- job
- for job in _jobs_for_context(agent.context).values()
- if isinstance(job, ParallelJob) and job.state not in {"cancelled", "timeout"}
- ]
- if not jobs:
- return ""
-
- active = [job for job in jobs if job.state not in TERMINAL_STATES]
- ready = [job for job in jobs if job.state in TERMINAL_STATES]
- if not active and not ready:
- return ""
-
- lines = ["parallel jobs:"]
- if active:
- lines.append("running:")
- for job in active:
- lines.append(
- f"- {job.id}: {job.tool_name} [{job.state}], running for {job.elapsed():.1f}s"
- )
- if ready:
- lines.append("ready to collect with `parallel` and `job_ids`:")
- for job in ready:
- lines.append(
- f"- {job.id}: {job.tool_name} [{job.state}], duration {job.elapsed():.1f}s"
- )
- lines.append("call the `parallel` tool with `job_ids` to await/collect results or `action: \"cancel\"` to cancel.")
- return "\n".join(lines)
-
-
-def format_started_jobs(jobs: list[ParallelJob]) -> str:
- payload = {
- "status": "started",
- "jobs": [_job_snapshot(job, include_result=False) for job in jobs],
- "instruction": "Use the parallel tool with job_ids to await or cancel these background jobs.",
- }
- return json.dumps(payload, indent=2, ensure_ascii=False)
-
-
-def format_parallel_results(results: list[dict[str, Any]]) -> str:
- states = [result.get("state") for result in results]
- has_active_jobs = any(state not in TERMINAL_STATES for state in states)
- wait_timed_out = any(result.get("wait_timed_out") for result in results)
- if has_active_jobs:
- status = "waiting" if wait_timed_out else "running"
- elif states and all(state == "success" for state in states):
- status = "success"
- elif states and all(state == "cancelled" for state in states):
- status = "cancelled"
- elif any(state == "success" for state in states):
- status = "partial"
- else:
- status = "error"
-
- payload = {
- "status": status,
- "jobs": results,
- }
- if wait_timed_out:
- payload["wait_timeout"] = True
- if has_active_jobs:
- payload["instruction"] = (
- "Some jobs are still running. Call `parallel` with `action: \"await\"` "
- "and the listed `job_ids` to wait again, or `action: \"cancel\"` to stop them."
- )
- return json.dumps(payload, indent=2, ensure_ascii=False)
-
-
-async def _run_parallel_job(parent_context_id: str, job_id: str) -> None:
- job = _get_job(parent_context_id, job_id)
- if not job:
- return
- try:
- if job.kind == "subordinate":
- result = await _run_subordinate_context_job(parent_context_id, job)
- else:
- result = await _run_direct_tool_job(parent_context_id, job)
- _finish_job(job, "success", result=result)
- except asyncio.CancelledError:
- _finish_job(job, "cancelled", error="Parallel job was cancelled.")
- raise
- except Exception as exc:
- _finish_job(job, "error", error=str(exc))
- PrintStyle.error(f"Parallel job {job.id} failed: {exc}")
-
-
-async def _run_subordinate_context_job(parent_context_id: str, job: ParallelJob) -> str:
- from agent import AgentContext, AgentContextType, UserMessage
- from helpers import message_queue, persist_chat
-
- parent_context = AgentContext.get(parent_context_id)
- if not parent_context:
- raise ValueError("Parent context not found.")
-
- args = job.tool_args
- message = str(args.get("message") or "").strip()
- if not message:
- raise ValueError("call_subordinate requires `tool_args.message`.")
-
- profile = str(args.get("profile") or args.get("agent_profile") or "").strip()
- attachments = args.get("attachments") if isinstance(args.get("attachments"), list) else []
- attachments = [str(item) for item in attachments]
-
- child_name = _subordinate_context_name(job)
- worker_context = AgentContext(
- config=_clone_config(parent_context.config, profile=profile),
- name=child_name,
- type=AgentContextType.USER,
- )
- job.worker_context_id = worker_context.id
- if job.deferred_task:
- worker_context.task = job.deferred_task
-
- worker_context.set_data(PARALLEL_WORKER_PARENT_CONTEXT_KEY, parent_context.id)
- worker_context.set_data(PARALLEL_WORKER_JOB_KEY, job.id)
- worker_context.set_data(PARALLEL_WORKER_KIND_KEY, job.kind)
- worker_context.set_output_data(CHILD_PARENT_CONTEXT_ID_KEY, parent_context.id)
- worker_context.set_output_data(CHILD_PARENT_CONTEXT_KIND_KEY, "parallel")
- worker_context.set_output_data(CHILD_PARENT_CONTEXT_LABEL_KEY, child_name)
- worker_context.set_output_data(CHILD_PARALLEL_JOB_ID_KEY, job.id)
- worker_context.set_output_data(CHILD_PARALLEL_TOOL_NAME_KEY, job.tool_name)
- _copy_project(parent_context, worker_context)
-
- system_prompt = _subordinate_worker_system_prompt(profile)
- message_queue.log_user_message(worker_context, message, attachments, source=" (parallel)")
- worker_context.agent0.hist_add_user_message(
- UserMessage(
- message=message,
- attachments=attachments,
- system_message=[system_prompt],
- )
- )
- persist_chat.save_tmp_chat(worker_context)
-
- try:
- result = await worker_context.agent0.monologue()
- worker_context.agent0.history.new_topic()
- return result
- finally:
- persist_chat.save_tmp_chat(worker_context)
-
-
-async def _run_direct_tool_job(parent_context_id: str, job: ParallelJob) -> str:
- from agent import AgentContext, AgentContextType, LoopData
-
- parent_context = AgentContext.get(parent_context_id)
- if not parent_context:
- raise ValueError("Parent context not found.")
-
- worker_context: AgentContext | None = None
- try:
- worker_context = AgentContext(
- config=_clone_config(parent_context.config),
- name=f"parallel:{job.tool_name}",
- type=AgentContextType.BACKGROUND,
- )
- worker_context.set_data(PARALLEL_WORKER_PARENT_CONTEXT_KEY, parent_context_id)
- worker_context.set_data(PARALLEL_WORKER_JOB_KEY, job.id)
- worker_context.set_data(PARALLEL_WORKER_KIND_KEY, job.kind)
- job.worker_context_id = worker_context.id
- _copy_project(parent_context, worker_context)
-
- worker_agent = worker_context.agent0
- worker_agent.loop_data = LoopData()
- return await execute_tool_call(
- worker_agent,
- job.tool_name,
- job.tool_args,
- log_item=job.log_item,
- )
- finally:
- if worker_context:
- await _remove_context(worker_context.id)
-
-
-async def execute_tool_call(
- agent: "Agent",
- tool_name: str,
- tool_args: dict[str, Any],
- *,
- log_item: "LogItem | None" = None,
-) -> str:
- if tool_name == "parallel":
- raise ValueError("`parallel` cannot be nested inside a parallel worker.")
-
- tool = _resolve_parallel_tool(agent, tool_name, tool_args, strict=True)
- if not tool:
- raise ValueError(f"Tool '{tool_name}' not found or could not be initialized.")
-
- original_get_log_object = None
- if log_item is not None:
- original_get_log_object = tool.get_log_object
- tool.get_log_object = lambda: log_item
-
- agent.loop_data.current_tool = tool
- try:
- await agent.handle_intervention()
- await tool.before_execution(**tool_args)
- await agent.handle_intervention()
- await call_extensions_async(
- "tool_execute_before",
- agent,
- tool_args=tool_args or {},
- tool_name=tool_name,
- )
- response = await tool.execute(**tool_args)
- await agent.handle_intervention()
- await call_extensions_async(
- "tool_execute_after",
- agent,
- response=response,
- tool_name=tool_name,
- )
- await tool.after_execution(response)
- await agent.handle_intervention()
- return response.message
- finally:
- if original_get_log_object is not None:
- tool.get_log_object = original_get_log_object
- agent.loop_data.current_tool = None
-
-
-def _resolve_parallel_tool(
- agent: "Agent",
- tool_name: str,
- tool_args: dict[str, Any],
- *,
- strict: bool = False,
-):
- message = json.dumps({"tool_name": tool_name, "tool_args": tool_args})
-
- tool = None
- try:
- import helpers.mcp_handler as mcp_helper
-
- tool = mcp_helper.MCPConfig.get_instance().get_tool(agent, tool_name)
- except ImportError:
- tool = None
- except Exception as exc:
- if strict:
- raise
- PrintStyle.warning(f"Failed to initialize MCP tool '{tool_name}' for parallel job: {exc}")
-
- if not tool:
- get_tool = getattr(agent, "get_tool", None)
- if not callable(get_tool):
- if strict:
- raise ValueError(f"Tool '{tool_name}' not found or could not be initialized.")
- return None
- try:
- tool = get_tool(
- name=tool_name,
- method=None,
- args=tool_args,
- message=message,
- loop_data=getattr(agent, "loop_data", None),
- )
- except Exception as exc:
- if strict:
- raise
- PrintStyle.warning(f"Failed to initialize tool '{tool_name}' for parallel job: {exc}")
- tool = None
-
- if not tool:
- return None
-
- try:
- tool.args = dict(tool_args)
- except Exception:
- pass
-
- return tool
-
-
-async def _cancel_job(
- job: ParallelJob,
- *,
- state: JobState = "cancelled",
- message: str = "Parallel job was cancelled.",
-) -> None:
- if job.deferred_task and job.deferred_task.is_alive():
- job.deferred_task.kill()
- _finish_job(job, state, error=message)
-
-
-def _finish_job(
- job: ParallelJob,
- state: JobState,
- *,
- result: str | None = None,
- error: str | None = None,
-) -> None:
- job.state = state
- job.completed_at = time.time()
- if job.started_at is None:
- job.started_at = job.created_at
- if result is not None:
- job.result = result
- if error is not None:
- job.error = error
- _update_parallel_child_log(job)
-
-
-async def _remove_context(context_id: str | None) -> None:
- if not context_id:
- return
- from agent import AgentContext
- from helpers import persist_chat
-
- context = AgentContext.get(context_id)
- if context:
- try:
- context.reset()
- except Exception:
- pass
- AgentContext.remove(context_id)
- persist_chat.remove_chat(context_id)
-
-
-def _log_parallel_child_started(agent: "Agent", job: ParallelJob) -> None:
- if job.kind == "subordinate":
- job.log_item = agent.context.log.log(
- type="subagent",
- heading=f"icon://communication {agent.agent_name}: Calling Subordinate Agent",
- content="",
- kvps=job.tool_args,
- id=job.log_id,
- )
- return
-
- tool = _resolve_parallel_tool(agent, job.tool_name, job.tool_args)
- if tool is not None:
- try:
- job.log_item = tool.get_log_object()
- if job.log_item is not None:
- return
- except Exception as exc:
- PrintStyle.warning(
- f"Failed to derive parallel child log for {job.tool_name}: {exc}"
- )
-
- heading = f"icon://construction {agent.agent_name}: Using tool '{job.tool_name}'"
- job.log_item = agent.context.log.log(
- type="tool",
- heading=heading,
- content="",
- kvps=job.tool_args,
- id=job.log_id,
- _tool_name=job.tool_name,
- )
-
-
-def _update_parallel_child_log(job: ParallelJob) -> None:
- if not job.log_item:
- return
- if job.state == "success":
- content = job.result if job.result else "(completed without textual output)"
- else:
- content = f"Error: {job.error or job.state}"
- try:
- job.log_item.update(content=content)
- except Exception:
- pass
-
-
-def _job_snapshot(job: ParallelJob, *, include_result: bool) -> dict[str, Any]:
- data: dict[str, Any] = {
- "job_id": job.id,
- "tool_name": job.tool_name,
- "state": job.state,
- "duration_seconds": round(job.elapsed(), 3),
- }
- if job.worker_context_id:
- data["context_id"] = job.worker_context_id
- if include_result:
- if job.result is not None:
- data["result"] = job.result
- if job.error is not None:
- data["error"] = job.error
- return data
-
-
-def _clone_config(config: "AgentConfig", *, profile: str = "") -> "AgentConfig":
- try:
- cloned = replace(
- config,
- knowledge_subdirs=list(config.knowledge_subdirs),
- additional=dict(config.additional),
- )
- if profile:
- cloned.profile = profile
- return cloned
- except Exception:
- return config
-
-
-def _copy_project(parent_context: "AgentContext", worker_context: "AgentContext") -> None:
- try:
- from helpers import projects
-
- project_name = projects.get_context_project_name(parent_context)
- if project_name:
- projects.activate_project(worker_context.id, project_name, mark_dirty=False)
- except Exception:
- pass
-
-
-def _subordinate_worker_system_prompt(profile: str) -> str:
- lines = [
- "You are running as an isolated parallel worker for a parent Agent Zero chat.",
- "Return a concise final textual summary for the parent. Artifacts and files are supplementary, not a substitute for the textual result.",
- ]
- if profile:
- lines.append(f"Act with the `{profile}` profile's expertise and priorities.")
- return "\n".join(lines)
-
-
-def _subordinate_context_name(job: ParallelJob) -> str:
- name = str(job.tool_args.get("name") or "").strip()
- if name:
- return name
- message = str(job.tool_args.get("message") or "").strip()
- label = _short_label(message)
- return label or f"Parallel subordinate {job.index + 1}"
-
-
-def _short_label(text: str, limit: int = 80) -> str:
- compact = " ".join(text.split())
- return compact[:limit].rstrip()
diff --git a/helpers/parallel_tools.py.dox.md b/helpers/parallel_tools.py.dox.md
deleted file mode 100644
index d2456f2f2..000000000
--- a/helpers/parallel_tools.py.dox.md
+++ /dev/null
@@ -1,59 +0,0 @@
-# parallel_tools.py DOX
-
-## Purpose
-
-- Own the shared runtime for parallel tool-call jobs.
-- Normalize wrapped tool-call payloads, start background jobs, await or cancel jobs, and render prompt extras for active parallel work.
-- Keep this file-level DOX profile synchronized with `parallel_tools.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `parallel_tools.py` owns the runtime implementation.
-- `parallel_tools.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Public concepts:
-- `NormalizedToolCall`
-- `ParallelJob`
-- `start_parallel_jobs(...)`
-- `await_parallel_jobs(...)`
-- `cancel_parallel_jobs(...)`
-- `build_parallel_jobs_extras(...)`
-- `format_parallel_results(...)`
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Wrapped tool-call items must use the same shape as normal tool calls: a tool name plus arguments.
-- Normalization accepts full agent-reply-shaped objects when `tool_name` and `tool_args` are present; non-contract planning fields such as `thoughts` or `headline` are ignored.
-- `tool_calls` should be an array, but normalization also accepts a valid JSON string encoding of that array to recover provider/model stringification.
-- Normalization rejects `document_query` inside `parallel` because document parsing and Q&A fan out into heavier worker/model paths that must run sequentially.
-- `call_subordinate` jobs run in isolated child chat contexts tagged with parent-chat metadata; they must not be added to the scheduler task list and may use normal child-chat tools, including `parallel`.
-- Direct tool jobs run in isolated background contexts and are blocked from recursively invoking `parallel`.
-- Direct tool background context cleanup removes both the in-memory context and any transient chat folder left on disk.
-- Parent-visible child log items are created for each wrapped call so the WebUI can inspect concurrent children separately while the wrapper result remains model-history-only.
-- Child tool logs mirror normal tool-call visible args; job ids remain available through wrapper results and prompt extras rather than visible process-step args.
-- Wrapped tool child logs use each tool's native `get_log_object()` output when available, preserving special log rendering (for example: `code_execution_tool` uses `code_exe`, `wait` uses `progress`, MCP tools use `mcp`, and regular tools use `tool`).
-- Direct parallel worker execution reuses the parent-visible child log item so tool `before_execution()` cannot create a second generic worker log or lose the native badge type.
-- Job IDs are stable handles for later await, collect, or cancel operations.
-- Prompt extras must stay bounded and expose only job IDs, tool names, status, and compact result/error summaries.
-
-## Key Concepts
-
-- The parent context stores in-flight jobs under a private data key; collected terminal jobs are removed from that registry.
-- `wait=True` starts jobs and awaits them before returning until all requested jobs finish or the wait timeout is reached; the timeout stops waiting but does not cancel running jobs.
-- `collect` returns already-finished job results without waiting; `await` waits for requested job IDs.
-- Canceled jobs should be marked terminal and should stop their background `DeferredTask` when cancellation is possible.
-
-## Work Guidance
-
-- Keep normalization compatible with provider tool-call envelopes and direct JSON objects.
-- Avoid importing heavy runtime modules at import time unless startup behavior is verified.
-- Coordinate argument, output, or status changes with `tools/parallel.py`, prompt instructions, and tests.
-
-## Verification
-
-- Run targeted tests for normalization, recursion guard, prompt extras, and tool result formatting.
-- Run a live Agent Zero chat when changing parallel execution, child chat metadata, or subordinate task behavior.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/performance.py.dox.md b/helpers/performance.py.dox.md
deleted file mode 100644
index 36181986d..000000000
--- a/helpers/performance.py.dox.md
+++ /dev/null
@@ -1,42 +0,0 @@
-# performance.py DOX
-
-## Purpose
-
-- Own the `performance.py` helper module.
-- This module provides lightweight performance tracing helpers.
-- Keep this file-level DOX profile synchronized with `performance.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `performance.py` owns the runtime implementation.
-- `performance.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Top-level functions:
-- `trace_performance(show_all=..., color=..., unicode=...)`: Decorator that profiles a function and prints a call tree when it finishes.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Imported dependency areas include: `functools`, `inspect`, `pyinstrument`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `inspect.iscoroutinefunction`, `functools.wraps`, `Profiler`, `profiler.start`, `profiler.stop`, `func`, `profiler.output_text`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_extensions_stress.py`
- - `tests/test_ws_manager.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/perplexity_search.py.dox.md b/helpers/perplexity_search.py.dox.md
deleted file mode 100644
index 1144ec0e9..000000000
--- a/helpers/perplexity_search.py.dox.md
+++ /dev/null
@@ -1,41 +0,0 @@
-# perplexity_search.py DOX
-
-## Purpose
-
-- Own the `perplexity_search.py` helper module.
-- This module queries Perplexity search using configured model/provider credentials.
-- Keep this file-level DOX profile synchronized with `perplexity_search.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `perplexity_search.py` owns the runtime implementation.
-- `perplexity_search.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Top-level functions:
-- `perplexity_search(query: str, model_name=..., api_key=..., base_url=...)`
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: secret handling.
-- Imported dependency areas include: `models`, `openai`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `OpenAI`, `client.chat.completions.create`, `models.get_api_key`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/persist_chat.py b/helpers/persist_chat.py
index 47479bd35..086a68fa7 100644
--- a/helpers/persist_chat.py
+++ b/helpers/persist_chat.py
@@ -1,15 +1,11 @@
-import json
-import os
-import tempfile
-import uuid
from collections import OrderedDict
from datetime import datetime
from typing import Any
-
+import uuid
from agent import Agent, AgentConfig, AgentContext, AgentContextType
from helpers import files, history
-from helpers.litellm_transport import delete_stored_response_ids
from helpers.localization import Localization
+import json
from initialize import initialize_agent
from helpers.log import Log, LogItem
@@ -17,7 +13,6 @@ from helpers.log import Log, LogItem
CHATS_FOLDER = "usr/chats"
LOG_SIZE = 1000
CHAT_FILE_NAME = "chat.json"
-SAVED_CHAT_CONTEXT_DATA_KEY = "_persist_chat_saved"
def _fallback_datetime_iso() -> str:
@@ -54,10 +49,10 @@ def save_tmp_chat(context: AgentContext):
return
path = _get_chat_file_path(context.id)
+ files.make_dirs(path)
data = _serialize_context(context)
js = _safe_json_serialize(data, ensure_ascii=False)
- _write_atomic(path, js)
- mark_chat_saved(context)
+ files.write_file(path, js)
def save_tmp_chats():
@@ -75,9 +70,7 @@ def load_tmp_chats():
folders = files.list_files(CHATS_FOLDER, "*")
json_files = []
for folder_name in folders:
- chat_file = _get_chat_file_path(folder_name)
- if files.exists(chat_file):
- json_files.append(chat_file)
+ json_files.append(_get_chat_file_path(folder_name))
ctxids = []
for file in json_files:
@@ -85,7 +78,6 @@ def load_tmp_chats():
js = files.read_file(file)
data = json.loads(js)
ctx = _deserialize_context(data)
- mark_chat_saved(ctx)
ctxids.append(ctx.id)
except Exception as e:
print(f"Error loading chat {file}: {e}")
@@ -96,41 +88,6 @@ def _get_chat_file_path(ctxid: str):
return files.get_abs_path(CHATS_FOLDER, ctxid, CHAT_FILE_NAME)
-def _write_atomic(path: str, content: str) -> None:
- directory = os.path.dirname(path)
- os.makedirs(directory, exist_ok=True)
- fd, tmp_path = tempfile.mkstemp(
- prefix=f".{os.path.basename(path)}.", suffix=".tmp", dir=directory
- )
- try:
- with os.fdopen(fd, "w", encoding="utf-8") as handle:
- handle.write(content.encode("utf-8", "replace").decode("utf-8"))
- handle.flush()
- os.fsync(handle.fileno())
- os.replace(tmp_path, path)
- directory_fd = os.open(directory, os.O_RDONLY)
- try:
- os.fsync(directory_fd)
- finally:
- os.close(directory_fd)
- finally:
- if os.path.exists(tmp_path):
- os.unlink(tmp_path)
-
-
-def mark_chat_saved(context: AgentContext) -> None:
- context.data[SAVED_CHAT_CONTEXT_DATA_KEY] = True
-
-
-def saved_chat_ids() -> set[str]:
- return {
- files.basename(files.dirname(path))
- for path in files.find_existing_paths_by_pattern(
- files.get_abs_path(CHATS_FOLDER, "*", CHAT_FILE_NAME)
- )
- }
-
-
def _convert_v080_chats():
json_files = files.list_files(CHATS_FOLDER, "*.json")
for file in json_files:
@@ -161,7 +118,6 @@ def export_json_chat(context: AgentContext):
def remove_chat(ctxid):
"""Remove a chat or task context"""
- _delete_provider_responses_for_chat(ctxid)
path = get_chat_folder_path(ctxid)
files.delete_dir(path)
@@ -174,8 +130,8 @@ def remove_msg_files(ctxid):
def _serialize_context(context: AgentContext):
profile = str(
- getattr(context.agent0.config, "profile", None)
- or getattr(context.config, "profile", None)
+ getattr(context.config, "profile", None)
+ or getattr(context.agent0.config, "profile", None)
or ""
)
@@ -222,7 +178,6 @@ def _serialize_agent(agent: Agent):
return {
"number": agent.number,
- "agent_profile": str(getattr(agent.config, "profile", "") or ""),
"data": data,
"history": history,
}
@@ -275,23 +230,11 @@ def _deserialize_context(data):
streaming_agent = streaming_agent.data.get(Agent.DATA_NAME_SUBORDINATE, None)
context.agent0 = agent0
- context.config = agent0.config
context.streaming_agent = streaming_agent
return context
-def _deserialize_agent_config(
- agent_data: dict[str, Any], fallback_config: AgentConfig
-) -> AgentConfig:
- fallback_profile = str(getattr(fallback_config, "profile", "") or "")
- profile = str(agent_data.get("agent_profile") or fallback_profile).strip()
- if profile == fallback_profile:
- return fallback_config
- override_settings = {"agent_profile": profile} if profile else None
- return initialize_agent(override_settings=override_settings)
-
-
def _deserialize_agents(
agents: list[dict[str, Any]], config: AgentConfig, context: AgentContext
) -> Agent:
@@ -301,7 +244,7 @@ def _deserialize_agents(
for ag in agents:
current = Agent(
number=ag["number"],
- config=_deserialize_agent_config(ag, config),
+ config=config,
context=context,
)
current.data = ag.get("data", {})
@@ -381,70 +324,3 @@ def _safe_json_serialize(obj, **kwargs):
return False
return json.dumps(obj, default=serializer, **kwargs)
-
-
-def _delete_provider_responses_for_chat(ctxid: str) -> None:
- try:
- data = json.loads(files.read_file(_get_chat_file_path(ctxid)))
- except Exception:
- return
- if _responses_delete_disabled(data):
- return
- response_ids = _collect_response_ids(data)
- if not response_ids:
- return
- delete_stored_response_ids(response_ids)
-
-
-def _responses_delete_disabled(data: dict[str, Any]) -> bool:
- if data.get("responses_delete_on_chat_delete") is False:
- return True
- context_data = data.get("data")
- if isinstance(context_data, dict) and context_data.get("responses_delete_on_chat_delete") is False:
- return True
- for agent_data in data.get("agents", []) or []:
- if not isinstance(agent_data, dict):
- continue
- state = agent_data.get("data")
- if isinstance(state, dict) and state.get("responses_delete_on_chat_delete") is False:
- return True
- return False
-
-
-def _collect_response_ids(data: Any) -> list[str]:
- found: list[str] = []
- seen: set[str] = set()
-
- def add(value: Any) -> None:
- response_id = str(value or "").strip()
- if response_id and response_id not in seen:
- seen.add(response_id)
- found.append(response_id)
-
- def walk(obj: Any) -> None:
- if isinstance(obj, dict):
- state = obj.get(Agent.DATA_NAME_RESPONSES_STATE)
- if isinstance(state, dict):
- add(state.get("response_id"))
- for response_id in state.get("response_ids") or []:
- add(response_id)
-
- metadata = obj.get("metadata")
- if isinstance(metadata, dict):
- responses = metadata.get("responses")
- if isinstance(responses, dict):
- add(responses.get("response_id"))
-
- for value in obj.values():
- walk(value)
- elif isinstance(obj, list):
- for value in obj:
- walk(value)
- elif isinstance(obj, str) and '"response_id"' in obj:
- try:
- walk(json.loads(obj))
- except Exception:
- return
-
- walk(data)
- return found
diff --git a/helpers/persist_chat.py.dox.md b/helpers/persist_chat.py.dox.md
deleted file mode 100644
index 09205e7d0..000000000
--- a/helpers/persist_chat.py.dox.md
+++ /dev/null
@@ -1,74 +0,0 @@
-# persist_chat.py DOX
-
-## Purpose
-
-- Own the `persist_chat.py` helper module.
-- This module persists and reloads chat contexts and message artifacts.
-- Keep this file-level DOX profile synchronized with `persist_chat.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `persist_chat.py` owns the runtime implementation.
-- `persist_chat.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Top-level functions:
-- `_fallback_datetime_iso() -> str`
-- `_parse_persisted_datetime(value: str | None) -> datetime`
-- `get_chat_folder_path(ctxid: str)`: Get the folder path for any context (chat or task).
-- `get_chat_msg_files_folder(ctxid: str)`
-- `save_tmp_chat(context: AgentContext)`: Save context to the chats folder
-- `save_tmp_chats()`: Save all contexts to the chats folder
-- `load_tmp_chats()`: Load all contexts from the chats folder
-- `_get_chat_file_path(ctxid: str)`
-- `_convert_v080_chats()`
-- `load_json_chats(jsons: list[str])`: Load contexts from JSON strings
-- `export_json_chat(context: AgentContext)`: Export context as JSON string
-- `remove_chat(ctxid)`: Remove a chat or task context
-- `remove_msg_files(ctxid)`: Remove all message files for a chat or task context
-- `mark_chat_saved(context: AgentContext) -> None`
-- `saved_chat_ids() -> set[str]`
-- `_serialize_context(context: AgentContext)`
-- `_serialize_agent(agent: Agent)`
-- `_serialize_log(log: Log)`
-- `_deserialize_context(data)`
-- `_deserialize_agent_config(agent_data: dict[str, Any], fallback_config: AgentConfig) -> AgentConfig`
-- `_deserialize_agents(agents: list[dict[str, Any]], config: AgentConfig, context: AgentContext) -> Agent`
-- `_deserialize_log(data: dict[str, Any]) -> 'Log'`
-- `_safe_json_serialize(obj, **kwargs)`
-- Notable constants/configuration names: `CHATS_FOLDER`, `LOG_SIZE`, `CHAT_FILE_NAME`, `SAVED_CHAT_CONTEXT_DATA_KEY`.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem reads, filesystem writes, filesystem deletion, settings/state persistence, scheduler state.
-- Imported dependency areas include: `agent`, `collections`, `datetime`, `helpers`, `helpers.localization`, `helpers.log`, `initialize`, `json`, `typing`, `uuid`.
-- Serialized chats store `agent_profile` both at the context level for the main chat and on each serialized agent so subordinate profiles survive server restart.
-- Deserialization must rebuild each agent with its serialized profile when present, falling back to the context profile for older chat files.
-- Chat loading skips directories that do not contain `chat.json`; malformed existing chat files still report load errors.
-- Chat saves write and fsync a same-directory temporary file, atomically replace `chat.json`, and fsync the directory so an interrupted save cannot truncate the previous chat.
-- Contexts are marked with private `SAVED_CHAT_CONTEXT_DATA_KEY` only after a successful save or load from disk so snapshot code can detect deleted chat files without hiding fresh unsaved chats.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `datetime.fromtimestamp.isoformat`, `datetime.fromisoformat`, `files.get_abs_path`, `_get_chat_file_path`, `files.make_dirs`, `_serialize_context`, `_safe_json_serialize`, `files.write_file`, `_convert_v080_chats`, `files.list_files`, `get_chat_folder_path`, `files.delete_dir`, `get_chat_msg_files_folder`, `agent.history.serialize`, `initialize_agent`, `_deserialize_log`, `AgentContext`, `_deserialize_agent_config`, `_deserialize_agents`, `Log`, `log.set_initial_progress`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_api_chat_lifetime.py`
- - `tests/test_browser_agent_regressions.py`
- - `tests/test_persist_chat_log_ids.py`
- - `tests/test_subagent_profiles.py`
- - `tests/test_tool_action_contracts.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/plugins.py.dox.md b/helpers/plugins.py.dox.md
deleted file mode 100644
index 4447d8850..000000000
--- a/helpers/plugins.py.dox.md
+++ /dev/null
@@ -1,81 +0,0 @@
-# plugins.py DOX
-
-## Purpose
-
-- Own the `plugins.py` helper module.
-- This module discovers plugins, resolves plugin config/toggles, runs hooks, and tracks plugin updates.
-- Keep this file-level DOX profile synchronized with `plugins.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `plugins.py` owns the runtime implementation.
-- `plugins.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `PluginAssetFile` (`TypedDict`)
-- `PluginMetadata` (`BaseModel`)
-- `PluginListItem` (`BaseModel`)
-- `PluginUpdateInfo` (`BaseModel`)
-- Top-level functions:
-- `register_watchdogs()`
-- `after_plugin_change(plugin_names: list[str] | None=..., python_change: bool=...)`
-- `refresh_plugin_modules(plugin_names: list[str] | None=...)`
-- `clear_plugin_cache(plugin_names: list[str] | None=...)`
-- `get_plugin_roots(plugin_name: str=...) -> List[str]`: Plugin root directories, ordered by priority (user first).
-- `get_plugins_list()`
-- `get_enhanced_plugins_list(custom: bool=..., builtin: bool=..., plugin_names: list[str] | None=...) -> List[PluginListItem]`: Discover plugins by directory convention. First root wins on ID conflict.
-- `get_custom_plugins_updates(plugin_names: list[str] | None=...) -> List[PluginUpdateInfo]`
-- `get_plugin_meta(plugin_name: str)`
-- `find_plugin_dir(plugin_name: str)`
-- `uninstall_plugin(plugin_name)`
-- `delete_plugin(plugin_name: str)`
-- `get_plugin_paths(*subpaths) -> List[str]`
-- `get_enabled_plugin_paths(agent: Agent | None, *subpaths) -> List[str]`
-- `get_enabled_plugins(agent: Agent | None)`
-- `determined_toggle_from_paths(default: bool, paths: Iterator[str])`
-- `get_toggle_state(plugin_name: str) -> ToggleState`
-- `toggle_plugin(plugin_name: str, enabled: bool, project_name: str=..., agent_profile: str=..., clear_overrides: bool=...)`
-- `get_plugin_config(plugin_name: str, agent: Agent | None=..., project_name: str | None=..., agent_profile: str | None=...)`
-- `get_default_plugin_config(plugin_name: str)`
-- `save_plugin_config(plugin_name: str, project_name: str, agent_profile: str, settings: dict)`
-- `find_plugin_asset(plugin_name: str, *subpaths, project_name=..., agent_profile=...)`
-- `find_plugin_assets(*subpaths, plugin_name: str=..., project_name: str=..., agent_profile: str=..., only_first: bool=...) -> list[PluginAssetFile]`
-- `determine_plugin_asset_path(plugin_name: str, project_name: str, agent_profile: str, *subpaths)`
-- `send_frontend_reload_notification(plugin_names: list[str] | None=...)`: If the plugin changed has webui extensions, notify frontend to reload the page
-- `call_plugin_hook(plugin_name: str, hook_name: str, default: Any=..., *args, **kwargs)`
-- `_apply_defaults_from_env(plugin_name: str, config: dict[str, Any])`
-- Notable constants/configuration names: `_META_TARGET_RE`, `META_FILE_NAME`, `CONFIG_FILE_NAME`, `CONFIG_DEFAULT_FILE_NAME`, `DISABLED_FILE_NAME`, `ENABLED_FILE_NAME`, `TOGGLE_FILE_PATTERN`, `HOOKS_SCRIPT`, `HOOKS_CACHE_AREA`, `PLUGINS_LIST_CACHE_AREA`, `ENABLED_PLUGINS_LIST_CACHE_AREA`, `ENABLED_PLUGINS_PATHS_CACHE_AREA`.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem reads, filesystem writes, filesystem deletion, WebSocket state, plugin state, settings/state persistence, secret handling.
-- Imported dependency areas include: `__future__`, `asyncio`, `glob`, `helpers`, `helpers.defer`, `helpers.watchdog`, `json`, `pathlib`, `pydantic`, `re`, `regex`, `time`, `typing`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `re.compile`, `Field`, `watchdog.add_watchdog`, `clear_plugin_cache`, `send_frontend_reload_notification`, `DeferredTask.start_task`, `get_plugin_roots`, `result.sort`, `cache.add`, `get_enhanced_plugins_list`, `find_plugin_dir`, `files.get_abs_path`, `files.exists`, `call_plugin_hook`, `delete_plugin`, `files.delete_dir`, `after_plugin_change`, `get_enabled_plugins`, `get_plugins_list`, `get_plugin_meta`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_a0_connector_computer_use_metadata.py`
- - `tests/test_a0_connector_prompt_gating.py`
- - `tests/test_browser_agent_regressions.py`
- - `tests/test_chat_compaction.py`
- - `tests/test_default_prompt_budget.py`
- - `tests/test_document_query_plugin.py`
- - `tests/test_error_retry_plugin.py`
- - `tests/test_host_browser_connector.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/print_catch.py.dox.md b/helpers/print_catch.py.dox.md
deleted file mode 100644
index 16c630d70..000000000
--- a/helpers/print_catch.py.dox.md
+++ /dev/null
@@ -1,40 +0,0 @@
-# print_catch.py DOX
-
-## Purpose
-
-- Own the `print_catch.py` helper module.
-- This module captures printed output from async callables.
-- Keep this file-level DOX profile synchronized with `print_catch.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `print_catch.py` owns the runtime implementation.
-- `print_catch.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Top-level functions:
-- `capture_prints_async(func: Callable[..., Awaitable[Any]], *args, **kwargs) -> Tuple[Awaitable[Any], Callable[[], str]]`
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Imported dependency areas include: `asyncio`, `io`, `sys`, `typing`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `io.StringIO`, `captured_output.getvalue`, `wrapped_func`, `func`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/print_style.py.dox.md b/helpers/print_style.py.dox.md
deleted file mode 100644
index 193a9bb26..000000000
--- a/helpers/print_style.py.dox.md
+++ /dev/null
@@ -1,54 +0,0 @@
-# print_style.py DOX
-
-## Purpose
-
-- Own the `print_style.py` helper module.
-- This module formats console/debug output consistently.
-- Keep this file-level DOX profile synchronized with `print_style.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `print_style.py` owns the runtime implementation.
-- `print_style.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `PrintStyle` (no explicit base class)
- - `get(self, *args, sep=..., **kwargs)`
- - `print(self, *args, sep=..., end=..., flush=...)`
- - `stream(self, *args, sep=..., flush=...)`
- - `is_last_line_empty(self)`
- - `standard(*args, sep=..., end=..., flush=...)`
- - `hint(*args, sep=..., end=..., flush=...)`
- - `info(*args, sep=..., end=..., flush=...)`
- - `success(*args, sep=..., end=..., flush=...)`
-- Top-level functions:
-- `_get_runtime()`
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem reads, WebSocket state, secret handling.
-- Imported dependency areas include: `atexit`, `collections.abc`, `datetime`, `html`, `os`, `strings`, `sys`, `webcolors`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `atexit.register`, `self._get_rgb_color_code`, `join`, `html.escape.replace`, `sep.join`, `self._format_args`, `sanitize_string`, `self._add_padding_if_needed`, `end.endswith`, `self._log_html`, `sys.stdin.readlines`, `PrintStyle._prefixed_args`, `files.get_abs_path`, `os.makedirs`, `datetime.now.strftime`, `os.path.join`, `f.write`, `self.secrets_mgr.mask_values`, `self._get_styled_text`, `self._get_html_styled_text`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_print_style.py`
- - `tests/test_tool_action_contracts.py`
- - `tests/test_ws_manager.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/process.py.dox.md b/helpers/process.py.dox.md
deleted file mode 100644
index 217a7b3aa..000000000
--- a/helpers/process.py.dox.md
+++ /dev/null
@@ -1,54 +0,0 @@
-# process.py DOX
-
-## Purpose
-
-- Own the `process.py` helper module.
-- This module manages process reload, restart, exit, and server references.
-- Keep this file-level DOX profile synchronized with `process.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `process.py` owns the runtime implementation.
-- `process.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Top-level functions:
-- `set_server(server)`
-- `get_server(server)`
-- `stop_server()`
-- `reload()`
-- `restart_process()`
-- `exit_process()`
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: subprocess/runtime control.
-- Imported dependency areas include: `helpers`, `helpers.print_style`, `os`, `sys`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `stop_server`, `runtime.is_dockerized`, `PrintStyle.standard`, `os.execv`, `sys.exit`, `_server.shutdown`, `exit_process`, `restart_process`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_api_chat_lifetime.py`
- - `tests/test_browser_agent_regressions.py`
- - `tests/test_docker_release_plan.py`
- - `tests/test_document_query_plugin.py`
- - `tests/test_download_toast_regressions.py`
- - `tests/test_git_version_label.py`
- - `tests/test_image_get_security.py`
- - `tests/test_model_config_project_presets.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/projects.py b/helpers/projects.py
index 41bbf9d44..e103e5f87 100644
--- a/helpers/projects.py
+++ b/helpers/projects.py
@@ -1,7 +1,7 @@
import os
-from typing import NotRequired, TypedDict, TYPE_CHECKING, cast
+from typing import Literal, NotRequired, TypedDict, TYPE_CHECKING, cast
-from helpers import files, dirty_json, persist_chat, file_tree, extension
+from helpers import files, dirty_json, persist_chat, file_tree
from helpers.print_style import PrintStyle
@@ -12,17 +12,8 @@ PROJECTS_PARENT_DIR = "usr/projects"
PROJECT_META_DIR = ".a0proj"
PROJECT_INSTRUCTIONS_DIR = "instructions"
PROJECT_KNOWLEDGE_DIR = "knowledge"
-PROJECT_SKILLS_DIR = "skills"
PROJECT_HEADER_FILE = "project.json"
-PROJECT_MCP_SERVERS_FILE = "mcp_servers.json"
-PROJECT_AGENTS_MD_FILES = (
- "AGENTS.override.md",
- "AGENTS.Override.md",
- "AGENTS.md",
- "Agents.md",
- "agents.md",
-)
-DEFAULT_MCP_SERVERS_CONFIG = '{\n "mcpServers": {}\n}'
+PROJECT_AGENTS_MD_FILES = ("AGENTS.md", "Agents.md", "agents.md")
CONTEXT_DATA_KEY_PROJECT = "project"
@@ -43,7 +34,6 @@ class BasicProjectData(TypedDict):
description: str
instructions: str
include_agents_md: NotRequired[bool]
- mcp_servers: NotRequired[str]
color: str
git_url: str
file_structure: FileStructureInjectionSettings
@@ -63,17 +53,10 @@ class EditProjectData(BasicProjectData):
knowledge_files_count: int
variables: str
secrets: str
- mcp_servers: str
subagents: dict[str, SubAgentSettings]
git_status: GitStatusData
-ProjectExtendedData = dict[str, object]
-_PROJECT_CORE_EDIT_KEYS = frozenset(BasicProjectData.__annotations__) | frozenset(
- EditProjectData.__annotations__
-)
-_PROJECT_TRANSIENT_INPUT_KEYS = frozenset({"git_token"})
-
def get_projects_parent_folder():
return files.get_abs_path(PROJECTS_PARENT_DIR)
@@ -87,17 +70,6 @@ def get_project_meta(name: str, *sub_dirs: str):
return files.get_abs_path(get_project_folder(name), PROJECT_META_DIR, *sub_dirs)
-def validate_project_name(name: str | None) -> str:
- candidate = str(name or "").strip()
- if (
- not candidate
- or candidate in {".", ".."}
- or os.path.basename(candidate) != candidate
- ):
- raise ValueError("Invalid project name")
- return candidate
-
-
def delete_project(name: str):
abs_path = files.get_abs_path(PROJECTS_PARENT_DIR, name)
files.delete_dir(abs_path)
@@ -106,16 +78,14 @@ def delete_project(name: str):
def create_project(name: str, data: BasicProjectData):
- extended_data = _project_extended_data_for_save(data)
- mcp_servers = data.get("mcp_servers") if isinstance(data, dict) else None
+ llm_data = data.get("llm") if isinstance(data, dict) else None
abs_path = files.create_dir_safe(
files.get_abs_path(PROJECTS_PARENT_DIR, name), rename_format="{name}_{number}"
)
create_project_meta_folders(name)
data = _normalizeBasicData(data)
save_project_header(name, data)
- save_project_mcp_servers(name, mcp_servers or DEFAULT_MCP_SERVERS_CONFIG)
- save_project_extended_data(name, extended_data)
+ save_project_llm_settings(name, llm_data)
return name
@@ -123,8 +93,7 @@ def clone_git_project(name: str, git_url: str, git_token: str, data: BasicProjec
"""Clone a git repository as a new A0 project. Token is used only for cloning via http header."""
from helpers import git
- extended_data = _project_extended_data_for_save(data)
- mcp_servers = data.get("mcp_servers") if isinstance(data, dict) else None
+ llm_data = data.get("llm") if isinstance(data, dict) else None
abs_path = files.create_dir_safe(
files.get_abs_path(PROJECTS_PARENT_DIR, name), rename_format="{name}_{number}"
@@ -152,9 +121,7 @@ def clone_git_project(name: str, git_url: str, git_token: str, data: BasicProjec
data["git_url"] = clean_url
save_project_header(actual_name, data)
- if mcp_servers:
- save_project_mcp_servers(actual_name, mcp_servers)
- save_project_extended_data(actual_name, extended_data)
+ save_project_llm_settings(actual_name, llm_data)
return actual_name
except Exception as e:
@@ -216,7 +183,6 @@ def _normalizeEditData(data: EditProjectData) -> EditProjectData:
data.get("include_agents_md", True)
),
"variables": data.get("variables", ""),
- "mcp_servers": data.get("mcp_servers", DEFAULT_MCP_SERVERS_CONFIG),
"color": data.get("color", ""),
"git_url": data.get("git_url", ""),
"git_status": data.get("git_status", {"is_git_repo": False}),
@@ -254,7 +220,7 @@ def _basic_data_to_edit_data(data: BasicProjectData) -> EditProjectData:
def update_project(name: str, data: EditProjectData):
- extended_data = _project_extended_data_for_save(data)
+ llm_data = data.get("llm") if isinstance(data, dict) else None
# merge with current state
current = load_edit_project_data(name)
@@ -268,9 +234,8 @@ def update_project(name: str, data: EditProjectData):
# save secrets
save_project_variables(name, current["variables"])
save_project_secrets(name, current["secrets"])
- save_project_mcp_servers(name, current["mcp_servers"])
save_project_subagents(name, current["subagents"])
- save_project_extended_data(name, extended_data)
+ save_project_llm_settings(name, llm_data)
reactivate_project_in_chats(name)
return name
@@ -286,10 +251,8 @@ def load_edit_project_data(name: str) -> EditProjectData:
from helpers import git
data = load_basic_project_data(name)
- create_project_meta_folders(name)
additional_instructions = get_additional_instructions_files(name)
variables = load_project_variables(name)
- mcp_servers = load_project_mcp_servers(name)
secrets = load_project_secrets_masked(name)
subagents = load_project_subagents(name)
knowledge_files_count = get_knowledge_files_count(name)
@@ -303,14 +266,13 @@ def load_edit_project_data(name: str) -> EditProjectData:
"instruction_files_count": len(additional_instructions),
"knowledge_files_count": knowledge_files_count,
"variables": variables,
- "mcp_servers": mcp_servers,
"secrets": secrets,
"subagents": subagents,
"git_status": git_status,
},
)
data = _normalizeEditData(data)
- _merge_project_extended_data(data, load_project_extended_data(name))
+ data["llm"] = load_project_llm_data(name) # type: ignore[typeddict-unknown-key]
return data
@@ -324,56 +286,53 @@ def save_project_header(name: str, data: BasicProjectData):
files.write_file(abs_path, header)
-@extension.extensible
-def load_project_extended_data(name: str) -> ProjectExtendedData:
- return {}
+def load_project_llm_data(name: str) -> dict:
+ from plugins._model_config.helpers import model_config
-
-@extension.extensible
-def save_project_extended_data(name: str, project_data: ProjectExtendedData):
- return None
-
-
-def _project_extended_data_for_save(data: object) -> ProjectExtendedData:
- if not isinstance(data, dict):
- return {}
+ config = model_config.normalize_config_for_save(
+ model_config.get_config(project_name=name) or {}
+ )
return {
- str(key): value
- for key, value in data.items()
- if str(key) not in _PROJECT_CORE_EDIT_KEYS
- and str(key) not in _PROJECT_TRANSIENT_INPUT_KEYS
+ "config": config,
+ "selected_preset": {
+ "scope": "current",
+ "project_name": name,
+ "name": "Current config",
+ },
+ "presets": model_config.get_combined_presets(name),
+ "global_presets": model_config.get_presets(),
+ "project_presets": model_config.get_project_presets(name),
}
-def _merge_project_extended_data(
- data: EditProjectData,
- extended_data: object,
-) -> None:
- if not isinstance(extended_data, dict):
+def save_project_llm_settings(name: str, llm_data: object):
+ if not isinstance(llm_data, dict):
return
- conflicts = sorted(str(key) for key in extended_data if key in _PROJECT_CORE_EDIT_KEYS)
- if conflicts:
- raise ValueError(
- "Project extension data cannot overwrite core project fields: "
- + ", ".join(conflicts)
- )
+ from helpers import plugins
+ from plugins._model_config.helpers import model_config
- data.update(extended_data) # type: ignore[typeddict-item]
+ project_presets = llm_data.get("project_presets")
+ if isinstance(project_presets, list):
+ model_config.save_presets(project_presets, project_name=name)
+ config_to_save = None
+ selected_preset = llm_data.get("selected_preset")
+ if isinstance(selected_preset, dict) and selected_preset.get("scope") in {
+ "global",
+ "project",
+ }:
+ preset = model_config.resolve_preset_selection(selected_preset, project_name=name)
+ if preset:
+ base_config = model_config.get_config(project_name=name) or {}
+ config_to_save = model_config.build_config_from_preset(preset, base_config)
-def load_project_mcp_servers(name: str) -> str:
- project_name = validate_project_name(name)
- try:
- return files.read_file(get_project_meta(project_name, PROJECT_MCP_SERVERS_FILE))
- except Exception:
- return DEFAULT_MCP_SERVERS_CONFIG
+ config = llm_data.get("config")
+ if config_to_save is None and isinstance(config, dict):
+ config_to_save = model_config.normalize_config_for_save(config)
-
-def save_project_mcp_servers(name: str, mcp_servers: str):
- project_name = validate_project_name(name)
- content = mcp_servers if isinstance(mcp_servers, str) else DEFAULT_MCP_SERVERS_CONFIG
- files.write_file(get_project_meta(project_name, PROJECT_MCP_SERVERS_FILE), content)
+ if config_to_save is not None:
+ plugins.save_plugin_config("_model_config", name, "", config_to_save)
def get_active_projects_list():
@@ -472,10 +431,9 @@ def deactivate_project_in_chats(name: str):
def build_system_prompt_vars(name: str):
project_data = load_basic_project_data(name)
main_instructions = project_data.get("instructions", "") or ""
- include_agents_md = project_data.get("include_agents_md", True)
instruction_files = get_project_instruction_files(
name,
- include_agents_md=include_agents_md,
+ include_agents_md=project_data.get("include_agents_md", True),
)
instruction_parts = [
main_instructions,
@@ -488,75 +446,11 @@ def build_system_prompt_vars(name: str):
"project_name": project_data.get("title", ""),
"project_description": project_data.get("description", ""),
"project_instructions": complete_instructions or "",
- "include_agents_md": include_agents_md,
"project_path": files.normalize_a0_path(get_project_folder(name)),
"project_git_url": project_data.get("git_url", ""),
}
-def get_agents_md_chain(root: str, target: str) -> list[tuple[str, str]]:
- root_real = os.path.realpath(files.fix_dev_path(root))
- target_real = os.path.realpath(files.fix_dev_path(target))
- if os.path.isfile(target_real):
- target_real = os.path.dirname(target_real)
-
- if files.is_in_dir(target_real, root_real):
- dirs = []
- cursor = target_real
- while True:
- dirs.append(cursor)
- if cursor == root_real:
- break
- parent = os.path.dirname(cursor)
- if parent == cursor:
- break
- cursor = parent
- dirs.reverse()
- else:
- dirs = [root_real]
-
- chain = []
- for dir_path in dirs:
- for filename in PROJECT_AGENTS_MD_FILES:
- matches = files.read_text_files_in_dir(dir_path, pattern=filename)
- if filename not in matches:
- continue
- chain.append((files.get_abs_path(dir_path, filename), matches[filename]))
- break
- return chain
-
-
-def build_agents_md_protocol(name: str, target: str | None = None) -> str:
- project_folder = get_project_folder(name)
- project_agents_md = get_project_agents_md_instruction_file(name)
- project_agents_md_path = (
- os.path.realpath(files.fix_dev_path(project_agents_md[0]))
- if project_agents_md
- else ""
- )
- entries = [
- (path, content)
- for path, content in get_agents_md_chain(
- files.get_abs_path(""),
- target or project_folder,
- )
- if os.path.realpath(path) != project_agents_md_path
- ]
- if not entries:
- return ""
-
- instructions = []
- for path, content in entries:
- instructions.append(
- f"### path: {files.normalize_a0_path(path)}\n\n{content.strip()}"
- )
- return files.read_prompt_file(
- "agent.protocol.projects.agents_md.md",
- _directories=["prompts"],
- agents_md_instructions="\n\n".join(instructions),
- ).strip()
-
-
def get_additional_instructions_files(name: str):
instructions_folder = files.get_abs_path(
get_project_folder(name), PROJECT_META_DIR, PROJECT_INSTRUCTIONS_DIR
@@ -593,8 +487,11 @@ def get_project_instruction_files(
def get_project_agents_md_instruction_file(name: str) -> tuple[str, str] | None:
project_folder = get_project_folder(name)
- for path, content in get_agents_md_chain(project_folder, project_folder):
- return (files.normalize_a0_path(path), content)
+ for filename in PROJECT_AGENTS_MD_FILES:
+ matches = files.read_text_files_in_dir(project_folder, pattern=filename)
+ if filename in matches:
+ path = files.get_abs_path(project_folder, filename)
+ return (files.normalize_a0_path(path), matches[filename])
return None
@@ -704,9 +601,6 @@ def create_project_meta_folders(name: str):
# create knowledge folders (plugins create their own subdirs lazily)
files.create_dir(get_project_meta(name, PROJECT_KNOWLEDGE_DIR))
- # create project skills folder for Project Settings > Skills > Open Folder
- files.create_dir(get_project_meta(name, PROJECT_SKILLS_DIR))
-
def get_knowledge_files_count(name: str):
knowledge_folder = files.get_abs_path(
diff --git a/helpers/projects.py.dox.md b/helpers/projects.py.dox.md
deleted file mode 100644
index e1e1b592e..000000000
--- a/helpers/projects.py.dox.md
+++ /dev/null
@@ -1,101 +0,0 @@
-# projects.py DOX
-
-## Purpose
-
-- Own the `projects.py` helper module.
-- This module owns project metadata, workspace creation, Git status, and project-scoped settings including per-project MCP server config.
-- Keep this file-level DOX profile synchronized with `projects.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `projects.py` owns the runtime implementation.
-- `projects.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `FileStructureInjectionSettings` (`TypedDict`)
-- `SubAgentSettings` (`TypedDict`)
-- `BasicProjectData` (`TypedDict`)
-- `GitStatusData` (`TypedDict`)
-- `EditProjectData` (`BasicProjectData`)
-- Top-level functions:
-- `get_projects_parent_folder()`
-- `get_project_folder(name: str)`
-- `get_project_meta(name: str, *sub_dirs)`
-- `validate_project_name(name: str | None) -> str`
-- `delete_project(name: str)`
-- `create_project(name: str, data: BasicProjectData)`
-- `clone_git_project(name: str, git_url: str, git_token: str, data: BasicProjectData)`: Clone a git repository as a new A0 project. Token is used only for cloning via http header.
-- `load_project_header(name: str)`
-- `_default_file_structure_settings()`
-- `_normalizeBasicData(data: BasicProjectData) -> BasicProjectData`
-- `_normalizeEditData(data: EditProjectData) -> EditProjectData`
-- `_edit_data_to_basic_data(data: EditProjectData)`
-- `_basic_data_to_edit_data(data: BasicProjectData) -> EditProjectData`
-- `update_project(name: str, data: EditProjectData)`
-- `load_basic_project_data(name: str) -> BasicProjectData`
-- `load_edit_project_data(name: str) -> EditProjectData`
-- `save_project_header(name: str, data: BasicProjectData)`
-- `load_project_extended_data(name: str) -> ProjectExtendedData`
-- `save_project_extended_data(name: str, project_data: ProjectExtendedData)`
-- `_project_extended_data_for_save(data: object) -> ProjectExtendedData`
-- `_merge_project_extended_data(data: EditProjectData, extended_data: object) -> None`
-- `load_project_mcp_servers(name: str) -> str`
-- `save_project_mcp_servers(name: str, mcp_servers: str)`
-- `get_active_projects_list()`
-- `_get_projects_list(parent_dir)`
-- `activate_project(context_id: str, name: str, mark_dirty: bool=...)`
-- `deactivate_project(context_id: str, mark_dirty: bool=...)`
-- `reactivate_project_in_chats(name: str)`
-- `deactivate_project_in_chats(name: str)`
-- `build_system_prompt_vars(name: str)`
-- `get_agents_md_chain(root: str, target: str) -> list[tuple[str, str]]`
-- `build_agents_md_protocol(name: str, target: str | None=...) -> str`
-- `get_additional_instructions_files(name: str)`
-- `get_project_instruction_files(name: str, include_agents_md: bool=...) -> list[tuple[str, str]]`
-- `get_project_agents_md_instruction_file(name: str) -> tuple[str, str] | None`
-- `_format_project_instruction_files(instruction_files: list[tuple[str, str]]) -> str`
-- `_normalize_include_agents_md(value: object) -> bool`
-- Notable constants/configuration names: `PROJECTS_PARENT_DIR`, `PROJECT_META_DIR`, `PROJECT_INSTRUCTIONS_DIR`, `PROJECT_KNOWLEDGE_DIR`, `PROJECT_SKILLS_DIR`, `PROJECT_HEADER_FILE`, `PROJECT_MCP_SERVERS_FILE`, `PROJECT_AGENTS_MD_FILES`, `DEFAULT_MCP_SERVERS_CONFIG`, `CONTEXT_DATA_KEY_PROJECT`.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Per-project MCP server configuration is persisted as `.a0proj/mcp_servers.json`, exposed through `load_edit_project_data(...)`, and saved during project create/clone/update flows.
-- Additional project-edit payload sections are delegated through extensible `load_project_extended_data(...)` and `save_project_extended_data(...)`; the project helper must stay storage-agnostic and plugin-specific config rules belong to the owning plugin.
-- Project extension data may add named top-level sections such as `llm`, but it must not overwrite core project fields owned by `EditProjectData`.
-- Project extension save payloads exclude core project fields and transient inputs such as `git_token`; plugins needing core metadata should load it by project name.
-- Project metadata setup creates and repairs `.a0proj/instructions`, `.a0proj/knowledge`, and `.a0proj/skills` so settings surfaces can open those folders consistently.
-- AGENTS.md discovery is a linear root-to-target chain walk with `AGENTS.override.md` precedence; sibling directories are not scanned.
-- Active-project AGENTS.md protocol guidance excludes the exact project root AGENTS.md because `build_system_prompt_vars(...)` already loads it into project instructions; prose for that protocol block lives in `prompts/agent.protocol.projects.agents_md.md`.
-- Project MCP config uses the same JSON string shape as global MCP settings: an object with `mcpServers`.
-- Project MCP load/save paths validate project names as simple folder basenames before touching `.a0proj/mcp_servers.json`.
-- Observed side-effect areas: filesystem reads, filesystem writes, filesystem deletion, plugin state, settings/state persistence, secret handling.
-- Imported dependency areas include: `helpers`, `helpers.print_style`, `os`, `typing`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `files.get_abs_path`, `files.delete_dir`, `deactivate_project_in_chats`, `files.create_dir_safe`, `create_project_meta_folders`, `_normalizeBasicData`, `save_project_header`, `save_project_mcp_servers`, `load_project_mcp_servers`, `save_project_extended_data`, `load_project_extended_data`, `_project_extended_data_for_save`, `_merge_project_extended_data`, `_PROJECT_CORE_EDIT_KEYS`, `_PROJECT_TRANSIENT_INPUT_KEYS`, `extension.extensible`, `files.basename`, `dirty_json.parse`, `FileStructureInjectionSettings`, `cast`, `_normalizeEditData`, `load_edit_project_data`, `_edit_data_to_basic_data`, `save_project_variables`, `save_project_secrets`, `save_project_subagents`, `reactivate_project_in_chats`, `load_basic_project_data`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_model_config_project_presets.py`
- - `tests/test_office_document_store.py`
- - `tests/test_plugin_activation_ui.py`
- - `tests/test_projects.py`
- - `tests/test_skills_runtime.py`
- - `tests/test_task_scheduler_timezone.py`
- - `tests/test_time_travel.py`
- - `tests/test_tool_action_contracts.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/providers.py.dox.md b/helpers/providers.py.dox.md
deleted file mode 100644
index 083ccecfc..000000000
--- a/helpers/providers.py.dox.md
+++ /dev/null
@@ -1,61 +0,0 @@
-# providers.py DOX
-
-## Purpose
-
-- Own the `providers.py` helper module.
-- This module loads model provider configuration and provider metadata.
-- Keep this file-level DOX profile synchronized with `providers.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `providers.py` owns the runtime implementation.
-- `providers.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `FieldOption` (`TypedDict`)
-- `ProviderManager` (no explicit base class)
- - `get_instance(cls)`
- - `reload(cls)`
- - `get_providers(self, provider_type: ModelType) -> List[FieldOption]`
- - `get_raw_providers(self, provider_type: ModelType) -> List[Dict[str, str]]`
- - `get_provider_config(self, provider_type: ModelType, provider_id: str) -> Optional[Dict[str, str]]`
-- Top-level functions:
-- `get_providers(provider_type: ModelType) -> List[FieldOption]`: Convenience function to get providers of a specific type.
-- `get_raw_providers(provider_type: ModelType) -> List[Dict[str, str]]`: Return full metadata for providers of a given type.
-- `get_provider_config(provider_type: ModelType, provider_id: str) -> Optional[Dict[str, str]]`: Return metadata for a single provider (None if not found).
-- `reload_providers()`: Re-merge base + plugin provider configs. Call after plugin changes.
-- Notable constants/configuration names: `PROVIDER_MANAGER_CACHE_AREA`, `PROVIDER_MANAGER_CACHE_KEY`.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem reads, filesystem deletion, plugin state, settings/state persistence.
-- Imported dependency areas include: `helpers`, `typing`, `yaml`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `ProviderManager.get_instance.get_providers`, `ProviderManager.get_instance.get_raw_providers`, `ProviderManager.get_instance.get_provider_config`, `ProviderManager.reload`, `cache.remove`, `cls.get_instance`, `inst._load_providers`, `files.get_abs_path`, `self._normalise_yaml`, `get_enabled_plugin_paths`, `provider_id.lower`, `self.get_raw_providers`, `cls`, `cache.add`, `self._load_providers`, `self._load_yaml`, `items.sort`, `ProviderManager.get_instance`, `lower`, `yaml.safe_load`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_fastmcp_openapi_security.py`
- - `tests/test_model_config_api_keys.py`
- - `tests/test_oauth_codex.py`
- - `tests/test_oauth_gemini_api.py`
- - `tests/test_oauth_github_copilot.py`
- - `tests/test_oauth_providers.py`
- - `tests/test_oauth_xai_grok.py`
- - `tests/test_onboarding_static.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/rate_limiter.py.dox.md b/helpers/rate_limiter.py.dox.md
deleted file mode 100644
index 22572ec2b..000000000
--- a/helpers/rate_limiter.py.dox.md
+++ /dev/null
@@ -1,45 +0,0 @@
-# rate_limiter.py DOX
-
-## Purpose
-
-- Own the `rate_limiter.py` helper module.
-- This module provides simple rate-limiting primitives.
-- Keep this file-level DOX profile synchronized with `rate_limiter.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `rate_limiter.py` owns the runtime implementation.
-- `rate_limiter.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `RateLimiter` (no explicit base class)
- - `add(self, **kwargs)`
- - `async cleanup(self)`
- - `async get_total(self, key: str) -> int`
- - `async wait(self, callback: Callable[[str, str, int, int], Awaitable[bool]] | None=...)`
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Imported dependency areas include: `asyncio`, `time`, `typing`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `asyncio.Lock`, `time.time`, `self.cleanup`, `asyncio.sleep`, `self.get_total`, `callback`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_stream_tool_early_stop.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/responses_tools.py b/helpers/responses_tools.py
deleted file mode 100644
index 453e7d731..000000000
--- a/helpers/responses_tools.py
+++ /dev/null
@@ -1,275 +0,0 @@
-from __future__ import annotations
-
-import hashlib
-import json
-import os
-import re
-from typing import Any
-
-from helpers import files, subagents
-
-
-FUNCTION_NAME_PATTERN = re.compile(r"^[A-Za-z0-9_-]{1,64}$")
-TOOL_NAME_EXAMPLE_PATTERN = re.compile(
- r"""["']tool_name["']\s*:\s*["']([A-Za-z0-9_-]{1,64})["']"""
-)
-TOOL_HEADING_PATTERN = re.compile(r"^\s{0,3}#{1,6}\s+(.+?)\s*$", re.MULTILINE)
-TOOL_PROMPT_PREFIX = "agent.system.tool."
-TOOL_PROMPT_SUFFIX = ".md"
-MAX_TOOL_DESCRIPTION_CHARS = 1024
-
-
-def build_responses_function_tools(agent: Any) -> tuple[list[dict[str, Any]], dict[str, str]]:
- """Build permissive Responses function tools from A0 tool prompts and MCP schemas."""
-
- tools: list[dict[str, Any]] = []
- name_map: dict[str, str] = {}
-
- for tool_name, prompt in _local_tool_prompts(agent):
- native_name = _native_tool_name(tool_name)
- name_map[native_name] = tool_name
- tools.append(
- {
- "type": "function",
- "name": native_name,
- "description": _description_from_prompt(prompt, fallback=tool_name),
- "parameters": _schema_from_prompt(prompt),
- }
- )
-
- for tool_name, tool in _mcp_tools(agent):
- native_name = _native_tool_name(tool_name)
- name_map[native_name] = tool_name
- tools.append(
- {
- "type": "function",
- "name": native_name,
- "description": _truncate(str(tool.get("description") or tool_name)),
- "parameters": _schema_from_any(tool.get("input_schema")),
- }
- )
-
- return _dedupe_tools(tools), name_map
-
-
-def original_tool_name(native_name: str, name_map: dict[str, str] | None) -> str:
- if not name_map:
- return native_name
- return name_map.get(native_name, native_name)
-
-
-def _local_tool_prompts(agent: Any) -> list[tuple[str, str]]:
- prompt_dirs = subagents.get_paths(agent, "prompts")
- tool_files = files.get_unique_filenames_in_dirs(
- prompt_dirs, f"{TOOL_PROMPT_PREFIX}*{TOOL_PROMPT_SUFFIX}"
- )
- result: list[tuple[str, str]] = []
- for tool_file in tool_files:
- basename = os.path.basename(tool_file)
- fallback_name = _tool_name_from_prompt_basename(basename)
- if not fallback_name:
- continue
- try:
- prompt = agent.read_prompt(basename)
- except Exception:
- try:
- prompt = files.read_file(tool_file)
- except Exception:
- prompt = ""
- tool_name = _tool_name_from_prompt(prompt, fallback=fallback_name)
- if not _include_local_tool_prompt(agent, tool_name):
- continue
- result.append((tool_name, prompt))
- return result
-
-
-def _include_local_tool_prompt(agent: Any, tool_name: str) -> bool:
- try:
- from plugins._a0_connector.helpers.remote_tool_prompts import (
- should_include_remote_tool_prompt,
- )
- except Exception:
- return True
-
- return should_include_remote_tool_prompt(agent, tool_name)
-
-
-def _mcp_tools(agent: Any) -> list[tuple[str, dict[str, Any]]]:
- try:
- import helpers.mcp_handler as mcp_helper
-
- raw_tools = mcp_helper.MCPConfig.get_instance().get_tools()
- except Exception:
- return []
-
- result: list[tuple[str, dict[str, Any]]] = []
- for entry in raw_tools or []:
- if not isinstance(entry, dict):
- continue
- for tool_name, tool in entry.items():
- if isinstance(tool, dict):
- result.append((str(tool_name), tool))
- return result
-
-
-def _tool_name_from_prompt_basename(basename: str) -> str:
- if not basename.startswith(TOOL_PROMPT_PREFIX) or not basename.endswith(TOOL_PROMPT_SUFFIX):
- return ""
- name = basename[len(TOOL_PROMPT_PREFIX) : -len(TOOL_PROMPT_SUFFIX)]
- if not name or name in {"tools", "tools_vision"}:
- return ""
- return name
-
-
-def _tool_name_from_prompt(prompt: str, *, fallback: str) -> str:
- for match in TOOL_NAME_EXAMPLE_PATTERN.finditer(prompt or ""):
- name = match.group(1).strip()
- if FUNCTION_NAME_PATTERN.fullmatch(name):
- return name
-
- for match in TOOL_HEADING_PATTERN.finditer(prompt or ""):
- name = _tool_name_from_heading(match.group(1))
- if name:
- return name
-
- return fallback
-
-
-def _tool_name_from_heading(heading: str) -> str:
- token = (heading or "").strip().split(None, 1)[0] if heading else ""
- name = token.strip("`'\" :")
- if FUNCTION_NAME_PATTERN.fullmatch(name):
- return name
- return ""
-
-
-def _native_tool_name(tool_name: str) -> str:
- if FUNCTION_NAME_PATTERN.fullmatch(tool_name):
- return tool_name
- slug = re.sub(r"[^A-Za-z0-9_-]+", "_", tool_name).strip("_")
- digest = hashlib.sha1(tool_name.encode("utf-8")).hexdigest()[:8]
- native = f"{slug[:52]}_{digest}" if slug else f"a0_tool_{digest}"
- return native[:64]
-
-
-def _description_from_prompt(prompt: str, *, fallback: str) -> str:
- lines: list[str] = []
- in_fence = False
- for raw_line in (prompt or "").splitlines():
- line = raw_line.strip()
- if line.startswith("```"):
- in_fence = not in_fence
- continue
- if in_fence or not line:
- continue
- if line.startswith("#"):
- line = line.lstrip("#").strip()
- if line.lower() == fallback.lower():
- continue
- lines.append(line)
- if sum(len(part) for part in lines) >= MAX_TOOL_DESCRIPTION_CHARS:
- break
- description = " ".join(lines).strip() or fallback
- return _truncate(description)
-
-
-def _schema_from_prompt(prompt: str) -> dict[str, Any]:
- schema = _schema_from_embedded_json(prompt)
- if schema:
- return schema
- return _schema_from_args_line(prompt)
-
-
-def _schema_from_embedded_json(prompt: str) -> dict[str, Any]:
- marker = "Input schema for tool_args:"
- index = (prompt or "").find(marker)
- if index == -1:
- return {}
- tail = prompt[index + len(marker) :].strip()
- match = re.search(r"\{(?:[^{}]|(?R))*\}", tail, flags=re.DOTALL) if hasattr(re, "VERSION1") else None
- candidate = match.group(0) if match else _balanced_json_object(tail)
- if not candidate:
- return {}
- try:
- return _schema_from_any(json.loads(candidate))
- except Exception:
- return {}
-
-
-def _schema_from_args_line(prompt: str) -> dict[str, Any]:
- properties: dict[str, Any] = {}
- for line in (prompt or "").splitlines():
- normalized = line.strip()
- if "args:" not in normalized.lower() and "argument:" not in normalized.lower():
- continue
- for name in re.findall(r"`([A-Za-z_][A-Za-z0-9_-]*)`", normalized):
- properties.setdefault(name, {"type": "string"})
- if properties:
- return {
- "type": "object",
- "properties": properties,
- "additionalProperties": True,
- }
- return _permissive_schema()
-
-
-def _schema_from_any(schema: Any) -> dict[str, Any]:
- if isinstance(schema, dict):
- normalized = dict(schema)
- normalized.setdefault("type", "object")
- if normalized.get("type") == "object" and not isinstance(
- normalized.get("properties"), dict
- ):
- normalized["properties"] = {}
- normalized.setdefault("additionalProperties", True)
- return normalized
- return _permissive_schema()
-
-
-def _permissive_schema() -> dict[str, Any]:
- return {"type": "object", "properties": {}, "additionalProperties": True}
-
-
-def _balanced_json_object(text: str) -> str:
- start = text.find("{")
- if start == -1:
- return ""
- depth = 0
- in_string = False
- escape = False
- for index, char in enumerate(text[start:], start=start):
- if in_string:
- if escape:
- escape = False
- elif char == "\\":
- escape = True
- elif char == '"':
- in_string = False
- continue
- if char == '"':
- in_string = True
- elif char == "{":
- depth += 1
- elif char == "}":
- depth -= 1
- if depth == 0:
- return text[start : index + 1]
- return ""
-
-
-def _dedupe_tools(tools: list[dict[str, Any]]) -> list[dict[str, Any]]:
- seen: set[str] = set()
- result: list[dict[str, Any]] = []
- for tool in tools:
- name = str(tool.get("name") or "")
- if not name or name in seen:
- continue
- seen.add(name)
- result.append(tool)
- return result
-
-
-def _truncate(text: str) -> str:
- if len(text) <= MAX_TOOL_DESCRIPTION_CHARS:
- return text
- return text[: MAX_TOOL_DESCRIPTION_CHARS - 3].rstrip() + "..."
diff --git a/helpers/responses_tools.py.dox.md b/helpers/responses_tools.py.dox.md
deleted file mode 100644
index e7ec0e760..000000000
--- a/helpers/responses_tools.py.dox.md
+++ /dev/null
@@ -1,30 +0,0 @@
-# responses_tools.py DOX
-
-## Purpose
-
-- Own conversion of Agent Zero tool prompt files and MCP tool metadata into OpenAI Responses API function tool definitions.
-- Keep native Responses function availability synchronized with the text tool prompt surface.
-
-## Ownership
-
-- `responses_tools.py` owns runtime implementation.
-- `responses_tools.py.dox.md` owns durable notes about responsibilities, prompt-derived contracts, and verification for this helper.
-
-## Local Contracts
-
-- Build local function tools from enabled `agent.system.tool.*.md` prompt files.
-- Local prompt-derived function names prefer explicit `"tool_name"` examples, then the first prompt heading, and only fall back to the prompt filename when the prompt declares no callable name.
-- Function parameter schemas are object schemas with an explicit `properties` object so OpenAI-compatible servers that validate chat-style tool payloads accept permissive tools.
-- Preserve original Agent Zero tool names through the native Responses name map.
-- Keep MCP tool schemas merged after local prompt-derived tools.
-- Connector remote tools are advertised only when `_a0_connector` runtime metadata says the matching connected CLI capability is currently available.
-
-## Work Guidance
-
-- Keep prompt-derived descriptions bounded by `MAX_TOOL_DESCRIPTION_CHARS`.
-- Treat plugin-specific tool gates as optional imports so core helper loading does not require a plugin that is absent or disabled.
-
-## Verification
-
-- Run targeted Responses/tool prompt tests after changing function-tool construction.
-- Run connector prompt gating tests when changing remote tool availability.
diff --git a/helpers/rfc.py.dox.md b/helpers/rfc.py.dox.md
deleted file mode 100644
index dbe29b918..000000000
--- a/helpers/rfc.py.dox.md
+++ /dev/null
@@ -1,48 +0,0 @@
-# rfc.py DOX
-
-## Purpose
-
-- Own the `rfc.py` helper module.
-- This module implements remote function call dispatch and serialization.
-- Keep this file-level DOX profile synchronized with `rfc.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `rfc.py` owns the runtime implementation.
-- `rfc.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `RFCInput` (`TypedDict`)
-- `RFCCall` (`TypedDict`)
-- Top-level functions:
-- `async call_rfc(url: str, password: str, module: str, function_name: str, args: list, kwargs: dict)`
-- `async handle_rfc(rfc_call: RFCCall, password: str)`
-- `async _call_function(module: str, function_name: str, *args, **kwargs)`
-- `_get_function(module: str, function_name: str)`
-- `async _send_json_data(url: str, data)`
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem writes, network calls, settings/state persistence, secret handling.
-- Imported dependency areas include: `aiohttp`, `helpers`, `importlib`, `inspect`, `json`, `typing`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `RFCInput`, `RFCCall`, `json.loads`, `_get_function`, `inspect.iscoroutinefunction`, `importlib.import_module`, `_send_json_data`, `crypto.verify_data`, `Exception`, `_call_function`, `func`, `aiohttp.ClientSession`, `json.dumps`, `crypto.hash_data`, `session.post`, `response.json`, `response.text`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/rfc_exchange.py.dox.md b/helpers/rfc_exchange.py.dox.md
deleted file mode 100644
index c960dabd9..000000000
--- a/helpers/rfc_exchange.py.dox.md
+++ /dev/null
@@ -1,42 +0,0 @@
-# rfc_exchange.py DOX
-
-## Purpose
-
-- Own the `rfc_exchange.py` helper module.
-- This module handles privileged RFC exchange data such as root password provisioning.
-- Keep this file-level DOX profile synchronized with `rfc_exchange.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `rfc_exchange.py` owns the runtime implementation.
-- `rfc_exchange.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Top-level functions:
-- `async get_root_password()`
-- `_provide_root_password(public_key_pem: str)`
-- `_get_root_password()`
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Imported dependency areas include: `helpers`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `runtime.is_dockerized`, `_get_root_password`, `crypto.encrypt_data`, `crypto._generate_private_key`, `crypto._generate_public_key`, `crypto.decrypt_data`, `dotenv.get_dotenv_value`, `runtime.call_development_function`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/rfc_files.py.dox.md b/helpers/rfc_files.py.dox.md
deleted file mode 100644
index d05d75828..000000000
--- a/helpers/rfc_files.py.dox.md
+++ /dev/null
@@ -1,70 +0,0 @@
-# rfc_files.py DOX
-
-## Purpose
-
-- Own the `rfc_files.py` helper module.
-- This module exposes RFC-safe filesystem operations.
-- Keep this file-level DOX profile synchronized with `rfc_files.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `rfc_files.py` owns the runtime implementation.
-- `rfc_files.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Top-level functions:
-- `get_abs_path(*relative_paths)`: Convert relative paths to absolute paths based on the base directory.
-- `read_file_bin(relative_path: str, backup_dirs=...) -> bytes`: Read binary file content.
-- `read_file_base64(relative_path: str, backup_dirs=...) -> str`: Read file content and return as base64 string.
-- `write_file_binary(relative_path: str, content: bytes) -> bool`: Write binary content to a file.
-- `write_file_base64(relative_path: str, content: str) -> bool`: Write base64 content to a file.
-- `delete_file(relative_path: str) -> bool`: Delete a file.
-- `delete_directory(relative_path: str) -> bool`: Delete a directory recursively.
-- `list_directory(relative_path: str, include_hidden: bool=...) -> list`: List directory contents.
-- `make_directories(relative_path: str) -> bool`: Create directories recursively.
-- `path_exists(relative_path: str) -> bool`: Check if a path exists.
-- `file_exists(relative_path: str) -> bool`: Check if a file exists.
-- `folder_exists(relative_path: str) -> bool`: Check if a folder exists.
-- `get_subdirectories(relative_path: str, include: str | list[str]=..., exclude: str | list[str] | None=...) -> list[str]`: Get subdirectories in a directory.
-- `zip_directory(relative_path: str) -> str`: Create a zip archive of a directory.
-- `move_file(source_path: str, destination_path: str) -> bool`: Move a file from source to destination.
-- `read_directory_as_zip(relative_path: str) -> bytes`: Read entire directory contents as a zip file.
-- `find_file_in_dirs(file_path: str, backup_dirs: list[str]) -> str`: Find a file in the main directory or backup directories.
-- `_read_file_binary_impl(file_path: str) -> str`: Implementation function to read a file in binary mode.
-- `_write_file_binary_impl(file_path: str, b64_content: str) -> bool`: Implementation function to write binary content to a file.
-- `_delete_file_impl(file_path: str) -> bool`: Implementation function to delete a file.
-- `_delete_folder_impl(folder_path: str) -> bool`: Implementation function to delete a folder recursively.
-- `_list_folder_impl(folder_path: str, include_hidden: bool=...) -> list`: Implementation function to list folder contents.
-- `_make_dirs_impl(folder_path: str) -> bool`: Implementation function to create directories.
-- `_path_exists_impl(file_path: str) -> bool`: Implementation function to check if path exists.
-- `_file_exists_impl(file_path: str) -> bool`: Implementation function to check if file exists.
-- `_folder_exists_impl(folder_path: str) -> bool`: Implementation function to check if folder exists.
-- `_get_subdirectories_impl(folder_path: str, include: str | list[str], exclude: str | list[str] | None) -> list[str]`: Implementation function to get subdirectories.
-- `_zip_dir_impl(folder_path: str) -> str`: Implementation function to create a zip archive of a directory.
-- `_move_file_impl(source_path: str, destination_path: str) -> bool`: Implementation function to move a file.
-- `_read_directory_impl(dir_path: str) -> str`: Implementation function to zip a directory and return base64 encoded zip.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem reads, filesystem writes, filesystem deletion.
-- Imported dependency areas include: `base64`, `fnmatch`, `helpers`, `os`, `shutil`, `tempfile`, `zipfile`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `os.path.abspath`, `os.path.join`, `find_file_in_dirs`, `runtime.call_development_function_sync`, `base64.b64decode`, `get_abs_path`, `base64.b64encode.decode`, `FileNotFoundError`, `os.path.exists`, `os.path.basename`, `os.path.isfile`, `Exception`, `os.makedirs`, `os.remove`, `os.path.isdir`, `shutil.rmtree`, `os.listdir`, `items.sort`, `tempfile.NamedTemporaryFile`, `zipfile.ZipFile`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/runtime.py.dox.md b/helpers/runtime.py.dox.md
deleted file mode 100644
index e9e5ae922..000000000
--- a/helpers/runtime.py.dox.md
+++ /dev/null
@@ -1,69 +0,0 @@
-# runtime.py DOX
-
-## Purpose
-
-- Own the `runtime.py` helper module.
-- This module owns runtime identity, environment flags, and startup arguments.
-- Keep this file-level DOX profile synchronized with `runtime.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `runtime.py` owns the runtime implementation.
-- `runtime.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Top-level functions:
-- `initialize()`
-- `get_arg(name: str)`
-- `has_arg(name: str)`
-- `is_dockerized() -> bool`
-- `is_development() -> bool`
-- `get_local_url()`
-- `get_runtime_id() -> str`
-- `get_persistent_id() -> str`
-- `async call_development_function(func: Callable[..., Awaitable[T]], *args, **kwargs) -> T`
-- `async call_development_function(func: Callable[..., T], *args, **kwargs) -> T`
-- `async call_development_function(func: Union[Callable[..., T], Callable[..., Awaitable[T]]], *args, **kwargs) -> T`
-- `async handle_rfc(rfc_call: rfc.RFCCall)`
-- `_get_rfc_password() -> str`
-- `_get_rfc_url() -> str`
-- `call_development_function_sync(func: Union[Callable[..., T], Callable[..., Awaitable[T]]], *args, **kwargs) -> T`
-- `get_web_ui_port()`
-- `get_tunnel_api_port()`
-- `get_platform()`
-- `is_windows()`
-- `get_terminal_executable()`
-- Notable constants/configuration names: `T`, `R`.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem reads, filesystem writes, subprocess/runtime control, settings/state persistence, secret handling, tunnel state.
-- Imported dependency areas include: `argparse`, `asyncio`, `helpers`, `inspect`, `nest_asyncio`, `pathlib`, `queue`, `secrets`, `sys`, `threading`, `typing`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `nest_asyncio.apply`, `TypeVar`, `argparse.ArgumentParser`, `parser.add_argument`, `parser.parse_known_args`, `vars`, `is_dockerized`, `dotenv.get_dotenv_value`, `is_development`, `settings.get_settings`, `url.endswith`, `queue.Queue`, `threading.Thread`, `thread.start`, `thread.join`, `thread.is_alive`, `result_queue.get_nowait`, `cast`, `is_windows`, `get_arg`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_a0_connector_computer_use_metadata.py`
- - `tests/test_a0_connector_prompt_gating.py`
- - `tests/test_browser_agent_regressions.py`
- - `tests/test_default_prompt_budget.py`
- - `tests/test_document_query_plugin.py`
- - `tests/test_host_browser_connector.py`
- - `tests/test_http_auth_csrf.py`
- - `tests/test_image_get_security.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/searxng.py.dox.md b/helpers/searxng.py.dox.md
deleted file mode 100644
index 889e62df1..000000000
--- a/helpers/searxng.py.dox.md
+++ /dev/null
@@ -1,43 +0,0 @@
-# searxng.py DOX
-
-## Purpose
-
-- Own the `searxng.py` helper module.
-- This module queries SearXNG search.
-- Keep this file-level DOX profile synchronized with `searxng.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `searxng.py` owns the runtime implementation.
-- `searxng.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Top-level functions:
-- `async search(query: str)`
-- `async _search(query: str)`
-- Notable constants/configuration names: `URL`.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: network calls, settings/state persistence.
-- Imported dependency areas include: `aiohttp`, `helpers`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `runtime.call_development_function`, `aiohttp.ClientSession`, `session.post`, `response.json`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/secrets.py.dox.md b/helpers/secrets.py.dox.md
deleted file mode 100644
index 2824362aa..000000000
--- a/helpers/secrets.py.dox.md
+++ /dev/null
@@ -1,62 +0,0 @@
-# secrets.py DOX
-
-## Purpose
-
-- Own the `secrets.py` helper module.
-- This module loads, aliases, masks, and streams secret values safely.
-- Keep this file-level DOX profile synchronized with `secrets.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `secrets.py` owns the runtime implementation.
-- `secrets.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `EnvLine` (no explicit base class)
-- `StreamingSecretsFilter` (no explicit base class)
- - `process_chunk(self, chunk: str) -> str`
- - `finalize(self) -> str`
-- `SecretsManager` (no explicit base class)
- - `get_instance(cls, *secrets_files) -> 'SecretsManager'`
- - `read_secrets_raw(self) -> str`
- - `load_secrets(self) -> Dict[str, str]`
- - `save_secrets(self, secrets_content: str)`
- - `save_secrets_with_merge(self, submitted_content: str)`
- - `get_keys(self) -> List[str]`
- - `get_secrets_for_prompt(self) -> str`
- - `create_streaming_filter(self) -> 'StreamingSecretsFilter'`
-- Top-level functions:
-- `alias_for_key(key: str, placeholder: str=...) -> str`
-- `get_secrets_manager(context: 'AgentContext|None'=...) -> SecretsManager`
-- `get_project_secrets_manager(project_name: str, merge_with_global: bool=...) -> SecretsManager`
-- `get_default_secrets_manager() -> SecretsManager`
-- Notable constants/configuration names: `ALIAS_PATTERN`, `DEFAULT_SECRETS_FILE`.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem reads, filesystem writes, filesystem deletion, WebSocket state, settings/state persistence, secret handling.
-- Imported dependency areas include: `dataclasses`, `dotenv.parser`, `helpers`, `helpers.errors`, `helpers.extension`, `io`, `os`, `re`, `threading`, `time`, `typing`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `key.upper`, `placeholder.format`, `SecretsManager.get_instance`, `self._replace_full_values`, `self._longest_suffix_prefix`, `threading.RLock`, `join`, `files.write_file`, `self._invalidate_all_caches`, `self.load_secrets`, `self.read_secrets_raw`, `self.parse_env_lines`, `self._serialize_env_lines`, `StreamingSecretsFilter`, `re.sub`, `self.parse_env_content`, `parse_stream`, `AgentContext.current`, `projects.get_context_project_name`, `files.get_abs_path`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_plugin_scan_prompt.py`
- - `tests/test_print_style.py`
- - `tests/test_time_travel.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/security.py.dox.md b/helpers/security.py.dox.md
deleted file mode 100644
index 882286b52..000000000
--- a/helpers/security.py.dox.md
+++ /dev/null
@@ -1,45 +0,0 @@
-# security.py DOX
-
-## Purpose
-
-- Own the `security.py` helper module.
-- This module contains small security helpers such as filename sanitization.
-- Keep this file-level DOX profile synchronized with `security.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `security.py` owns the runtime implementation.
-- `security.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Top-level functions:
-- `safe_filename(filename: str) -> Optional[str]`
-- Notable constants/configuration names: `FORBIDDEN_CHARS_RE`, `WINDOWS_RESERVED`, `FILENAME_MAX_LENGTH`.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem reads, filesystem deletion.
-- Imported dependency areas include: `pathlib`, `re`, `typing`, `unicodedata`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `re.compile`, `frozenset`, `unicodedata.normalize`, `FORBIDDEN_CHARS_RE.sub`, `filename.lstrip.rstrip`, `Path`, `join`, `stem.upper`, `filename.lstrip`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_fastmcp_openapi_security.py`
- - `tests/test_image_get_security.py`
- - `tests/test_ws_security.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/self_update.py.dox.md b/helpers/self_update.py.dox.md
deleted file mode 100644
index b1e552539..000000000
--- a/helpers/self_update.py.dox.md
+++ /dev/null
@@ -1,77 +0,0 @@
-# self_update.py DOX
-
-## Purpose
-
-- Own the `self_update.py` helper module.
-- This module manages self-update status, scheduling, tag selection, and durable scripts.
-- Keep this file-level DOX profile synchronized with `self_update.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `self_update.py` owns the runtime implementation.
-- `self_update.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `PendingUpdateConfig` (`TypedDict`)
-- `UpdateStatus` (`TypedDict`)
-- `SelectorTagOption` (`TypedDict`)
-- Top-level functions:
-- `_now_iso() -> str`
-- `get_update_file_path() -> Path`
-- `get_status_file_path() -> Path`
-- `get_log_file_path() -> Path`
-- `get_durable_exe_dir() -> Path`
-- `get_durable_self_update_manager_path() -> Path`
-- `_load_yaml(path: Path) -> dict[str, Any] | None`
-- `_write_yaml(path: Path, payload: dict[str, Any]) -> None`
-- `load_pending_update() -> PendingUpdateConfig | None`
-- `load_last_status() -> UpdateStatus | None`
-- `get_log_text() -> str`
-- `get_default_backup_dir(repo_dir: str | Path | None=...) -> Path`
-- `get_repo_dir(repo_dir: str | Path | None=...) -> Path`
-- `get_repo_self_update_manager_path(repo_dir: str | Path | None=...) -> Path`
-- `_get_official_remote_url() -> str`
-- `_run_git_raw(*args) -> str`
-- `_run_git(repo_dir: str | Path, *args) -> str`
-- `_normalize_describe_to_version(describe: str) -> str`
-- `_split_describe_version(describe: str) -> tuple[str, int]`
-- `_is_latest_selector_tag(tag: str) -> bool`
-- `_get_tag_release_time_in_repo(repo_dir: str | Path, tag: str) -> str`
-- `get_repo_version_info(repo_dir: str | Path | None=...) -> dict[str, str]`
-- `_sanitize_filename(name: str, default_name: str) -> str`
-- `_slugify_version(text: str) -> str`
-- `build_default_backup_name(current_version: str, target_tag: str | None=...) -> str`
-- `_resolve_backup_path(backup_path: str, repo_dir: str | Path | None=...) -> Path`
-- `_is_excluded_self_update_branch(branch: str) -> bool`
-- `_sort_branch_names(branches: list[str]) -> list[str]`
-- `_get_remote_branch_names() -> list[str]`
-- `_get_local_origin_branch_names(repo_dir: str | Path | None=...) -> list[str]`
-- Notable constants/configuration names: `OFFICIAL_REPO_AUTHOR`, `OFFICIAL_REPO_NAME`, `BRANCH_OPTIONS`, `SUPPORTED_BRANCHES`, `BACKUP_CONFLICT_POLICIES`, `MIN_SELECTOR_VERSION`, `REMOTE_BRANCH_TAG_CACHE_TTL_SECONDS`, `REMOTE_BRANCH_LIST_CACHE_TTL_SECONDS`, `UPDATE_FILE_PATH`, `STATUS_FILE_PATH`, `LOG_FILE_PATH`, `DURABLE_EXE_DIR`.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem reads, filesystem writes, subprocess/runtime control, settings/state persistence.
-- Imported dependency areas include: `__future__`, `datetime`, `helpers`, `helpers.localization`, `os`, `pathlib`, `re`, `subprocess`, `tempfile`, `time`, `typing`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `Path`, `Localization.get.now_iso`, `yaml.loads`, `path.parent.mkdir`, `path.write_text`, `_load_yaml`, `get_log_file_path`, `path.read_text`, `subprocess.run`, `completed.stdout.strip`, `re.fullmatch`, `describe.strip`, `tag.strip`, `get_repo_dir`, `_run_git`, `_normalize_describe_to_version`, `strip`, `re.sub.strip`, `Localization.get.now.strftime`, `path.resolve`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_office_document_store.py`
- - `tests/test_self_update_tag_filter.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/serveo_tunnel.py.dox.md b/helpers/serveo_tunnel.py.dox.md
deleted file mode 100644
index c209237f1..000000000
--- a/helpers/serveo_tunnel.py.dox.md
+++ /dev/null
@@ -1,43 +0,0 @@
-# serveo_tunnel.py DOX
-
-## Purpose
-
-- Own the `serveo_tunnel.py` helper module.
-- This module implements Serveo tunnel provider behavior.
-- Keep this file-level DOX profile synchronized with `serveo_tunnel.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `serveo_tunnel.py` owns the runtime implementation.
-- `serveo_tunnel.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `ServeoTunnelHelper` (`FlaredanticTunnelHelper`)
- - `build_tunnel(self)`
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: settings/state persistence, tunnel state.
-- Imported dependency areas include: `flaredantic`, `helpers.tunnel_common`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `ServeoConfig`, `ServeoTunnel`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_tunnel_remote_link.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/server_startup.py.dox.md b/helpers/server_startup.py.dox.md
deleted file mode 100644
index a8417341c..000000000
--- a/helpers/server_startup.py.dox.md
+++ /dev/null
@@ -1,62 +0,0 @@
-# server_startup.py DOX
-
-## Purpose
-
-- Own the `server_startup.py` helper module.
-- This module runs Uvicorn startup with health checks and retry tracking.
-- Keep this file-level DOX profile synchronized with `server_startup.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `server_startup.py` owns the runtime implementation.
-- `server_startup.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `StartupConfig` (no explicit base class)
- - `from_env(cls) -> 'StartupConfig'`
-- `StartupStageRecord` (no explicit base class)
-- `StartupMonitor` (no explicit base class)
- - `mark(self, stage: str, detail: str | None=...) -> None`
- - `stage(self, stage: str, detail: str | None=...) -> Iterator[None]`
- - `lifespan(self)`
- - `attach_server(self, server: uvicorn.Server) -> None`
- - `start_watchdog(self) -> None`
- - `mark_ready(self, source: str=...) -> None`
- - `is_ready(self) -> bool`
- - `close(self) -> None`
-- `_UvicornServerWrapper` (no explicit base class)
- - `shutdown(self) -> None`
-- Top-level functions:
-- `_env_int(name: str, default: int, minimum: int=...) -> int`
-- `_env_float(name: str, default: float, minimum: float=...) -> float`
-- `get_health_probe_host(bind_host: str) -> str`
-- `run_uvicorn_with_retries(host: str, port: int, build_asgi_app: Callable[[StartupMonitor], object], flush_callback: Callable[[str], None], access_log: bool=..., log_level: str=..., ws: str=..., startup_config: StartupConfig | None=...) -> None`
-- `_run_server_attempt(host: str, health_host: str, port: int, startup_monitor: StartupMonitor, build_asgi_app: Callable[[StartupMonitor], object], flush_callback: Callable[[str], None], access_log: bool, log_level: str, ws: str) -> bool`
-- `wait_for_health(host: str, port: int, startup_monitor: StartupMonitor) -> None`
-- `_serve_uvicorn(server: uvicorn.Server) -> None`
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem writes, network calls, subprocess/runtime control, settings/state persistence.
-- Imported dependency areas include: `asyncio`, `collections`, `contextlib`, `dataclasses`, `faulthandler`, `helpers`, `helpers.print_style`, `os`, `sys`, `threading`, `time`, `typing`, `urllib.request`, `uvicorn`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `dataclass`, `get_health_probe_host`, `PrintStyle.debug`, `RuntimeError`, `startup_monitor.start_watchdog`, `server.config.get_loop_factory`, `cls`, `time.monotonic`, `deque`, `threading.Event`, `threading.RLock`, `self.mark`, `threading.Thread`, `self._watchdog_thread.start`, `self._ready.is_set`, `self.snapshot`, `PrintStyle.error`, `join`, `StartupConfig.from_env`, `StartupMonitor`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/settings.py b/helpers/settings.py
index fcd66eef6..e9b707f5e 100644
--- a/helpers/settings.py
+++ b/helpers/settings.py
@@ -56,10 +56,8 @@ class Settings(TypedDict):
agent_profile: str
agent_knowledge_subdir: str
- max_consecutive_unusable_responses: int
timezone: str
time_format: str
- ui_control_visibility: dict[str, dict[str, bool]]
workdir_path: str
workdir_show: bool
@@ -68,7 +66,6 @@ class Settings(TypedDict):
workdir_max_folders: int
workdir_max_lines: int
workdir_gitignore: str
- file_browser_remember_last_directory: bool
api_keys: dict[str, str]
@@ -165,12 +162,6 @@ API_KEY_PLACEHOLDER = "************"
TIMEZONE_AUTO = "auto"
TIME_FORMAT_12H = "12h"
TIME_FORMAT_24H = "24h"
-UI_CONTROL_VISIBILITY_DEFAULTS = {
- "projectSelector": {"mobile": True, "desktop": True},
- "time": {"mobile": False, "desktop": True},
- "connectionStatus": {"mobile": True, "desktop": True},
- "rightCanvasRail": {"mobile": True, "desktop": True},
-}
SETTINGS_FILE = files.get_abs_path("usr/settings.json")
_settings: Settings | None = None
@@ -217,22 +208,6 @@ def _normalize_time_format(value: Any, default: str = TIME_FORMAT_12H) -> str:
return default if default in {TIME_FORMAT_12H, TIME_FORMAT_24H} else TIME_FORMAT_12H
-def _normalize_ui_control_visibility(value: Any) -> dict[str, dict[str, bool]]:
- submitted = value if isinstance(value, dict) else {}
- normalized = {}
- for control, devices in UI_CONTROL_VISIBILITY_DEFAULTS.items():
- submitted_devices = submitted.get(control, {})
- if not isinstance(submitted_devices, dict):
- submitted_devices = {}
- normalized[control] = {
- device: submitted_devices.get(device)
- if isinstance(submitted_devices.get(device), bool)
- else default
- for device, default in devices.items()
- }
- return normalized
-
-
def _resolve_runtime_timezone(setting_value: str, browser_timezone: str | None = None) -> str:
if setting_value == TIMEZONE_AUTO:
candidate = str(browser_timezone or "").strip()
@@ -425,12 +400,8 @@ def normalize_settings(settings: Settings) -> Settings:
# mcp server token is set automatically
copy["mcp_server_token"] = create_auth_token()
- copy["max_consecutive_unusable_responses"] = max(
- 1, copy["max_consecutive_unusable_responses"]
- )
copy["timezone"] = _normalize_timezone_setting(copy.get("timezone"), default["timezone"])
copy["time_format"] = _normalize_time_format(copy.get("time_format"), default["time_format"])
- copy["ui_control_visibility"] = _normalize_ui_control_visibility(copy.get("ui_control_visibility"))
return copy
@@ -526,14 +497,8 @@ def get_default_settings() -> Settings:
root_password="",
agent_profile=get_default_value("agent_profile", "agent0"),
agent_knowledge_subdir=get_default_value("agent_knowledge_subdir", "custom"),
- max_consecutive_unusable_responses=get_default_value(
- "max_consecutive_unusable_responses", 2
- ),
timezone=_normalize_timezone_setting(get_default_value("timezone", TIMEZONE_AUTO)),
time_format=_normalize_time_format(get_default_value("time_format", TIME_FORMAT_12H)),
- ui_control_visibility=_normalize_ui_control_visibility(
- get_default_value("ui_control_visibility", UI_CONTROL_VISIBILITY_DEFAULTS)
- ),
workdir_path=get_default_value("workdir_path", files.get_abs_path_dockerized("usr/workdir")),
workdir_show=get_default_value("workdir_show", True),
workdir_max_depth=get_default_value("workdir_max_depth", 5),
@@ -541,10 +506,6 @@ def get_default_settings() -> Settings:
workdir_max_folders=get_default_value("workdir_max_folders", 20),
workdir_max_lines=get_default_value("workdir_max_lines", 250),
workdir_gitignore=get_default_value("workdir_gitignore", gitignore),
- file_browser_remember_last_directory=get_default_value(
- "file_browser_remember_last_directory",
- True,
- ),
rfc_auto_docker=get_default_value("rfc_auto_docker", True),
rfc_url=get_default_value("rfc_url", "localhost"),
rfc_password="",
@@ -606,27 +567,18 @@ def _apply_settings(previous: Settings | None, browser_timezone: str | None = No
if _settings:
_apply_timezone_setting(previous, browser_timezone)
- from agent import Agent, AgentContext
+ from agent import AgentContext
from initialize import initialize_agent
for ctx in AgentContext.all():
- profile = str(
- getattr(ctx.config, "profile", "") or _settings["agent_profile"]
- )
- ctx.config = initialize_agent(override_settings={"agent_profile": profile})
+ profile = str(getattr(ctx.config, "profile", "") or _settings["agent_profile"])
+ config = initialize_agent(override_settings={"agent_profile": profile})
+ ctx.config = config # reinitialize context config with new settings
+ # apply config to agents
agent = ctx.agent0
while agent:
- agent_profile = str(
- getattr(getattr(agent, "config", None), "profile", "") or profile
- )
- agent.config = (
- ctx.config
- if agent is ctx.agent0 and agent_profile == profile
- else initialize_agent(
- override_settings={"agent_profile": agent_profile}
- )
- )
- agent = agent.get_data(Agent.DATA_NAME_SUBORDINATE)
+ agent.config = ctx.config
+ agent = agent.get_data(agent.DATA_NAME_SUBORDINATE)
# update mcp settings if necessary
if not previous or _settings["mcp_servers"] != previous["mcp_servers"]:
@@ -682,7 +634,7 @@ def _apply_settings(previous: Settings | None, browser_timezone: str | None = No
)
task2 = defer.DeferredTask().start_task(
- update_mcp_settings, _settings["mcp_servers"]
+ update_mcp_settings, config.mcp_servers
) # TODO overkill, replace with background task
# update token in mcp server
diff --git a/helpers/settings.py.dox.md b/helpers/settings.py.dox.md
deleted file mode 100644
index d4bdb974c..000000000
--- a/helpers/settings.py.dox.md
+++ /dev/null
@@ -1,94 +0,0 @@
-# settings.py DOX
-
-## Purpose
-
-- Own the `settings.py` helper module.
-- This module defines settings models, defaults, validation, and serialization.
-- Keep this file-level DOX profile synchronized with `settings.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `settings.py` owns the runtime implementation.
-- `settings.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `Settings` (`TypedDict`)
-- `PartialSettings` (`Settings`)
-- `FieldOption` (`TypedDict`)
-- `SettingsField` (`TypedDict`)
-- `SettingsSection` (`TypedDict`)
-- `ModelProvider` (`ProvidersFO`)
-- `SettingsOutputAdditional` (`TypedDict`)
-- `SettingsOutput` (`TypedDict`)
-- Top-level functions:
-- `get_default_value(name: str, value: T) -> T`: Load setting value from .env with A0_SET_ prefix, falling back to default.
-- `_ensure_option_present(options: list[OptionT] | None, current_value: str | None) -> list[OptionT]`: Ensure the currently selected value exists in a dropdown options list.
-- `_is_valid_timezone(value: str) -> bool`
-- `_normalize_timezone_setting(value: Any, default: str=...) -> str`
-- `_normalize_time_format(value: Any, default: str=...) -> str`
-- `_normalize_ui_control_visibility(value: Any) -> dict[str, dict[str, bool]]`
-- `_resolve_runtime_timezone(setting_value: str, browser_timezone: str | None=...) -> str`
-- `_timezone_options() -> list[FieldOption]`
-- `convert_out(settings: Settings) -> SettingsOutput`
-- `_get_api_key_field(settings: Settings, provider: str, title: str) -> SettingsField`
-- `convert_in(settings: Settings) -> Settings`
-- `get_settings() -> Settings`
-- `reload_settings() -> Settings`
-- `set_runtime_settings_snapshot(settings: Settings) -> None`
-- `set_settings(settings: Settings, apply: bool=..., browser_timezone: str | None=...)`
-- `set_settings_delta(delta: dict, apply: bool=...)`
-- `merge_settings(original: Settings, delta: dict) -> Settings`
-- `normalize_settings(settings: Settings) -> Settings`
-- `_adjust_to_version(settings: Settings, default: Settings)`
-- `_load_sensitive_settings(settings: Settings)`
-- `_read_settings_file() -> Settings | None`
-- `_write_settings_file(settings: Settings)`
-- `_remove_sensitive_settings(settings: Settings)`
-- `_write_sensitive_settings(settings: Settings)`
-- `get_default_settings() -> Settings`
-- `_apply_timezone_setting(previous: Settings | None, browser_timezone: str | None=...) -> None`
-- `_apply_settings(previous: Settings | None, browser_timezone: str | None=...)`
-- `_env_to_dict(data: str)`
-- `_dict_to_env(data_dict)`
-- `set_root_password(password: str)`
-- `get_runtime_config(set: Settings)`
-- Notable constants/configuration names: `T`, `PASSWORD_PLACEHOLDER`, `API_KEY_PLACEHOLDER`, `TIMEZONE_AUTO`, `TIME_FORMAT_12H`, `TIME_FORMAT_24H`, `UI_CONTROL_VISIBILITY_DEFAULTS`, `SETTINGS_FILE`.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem reads, filesystem writes, filesystem deletion, network calls, subprocess/runtime control, model calls, WebSocket state, plugin state, settings/state persistence, secret handling, scheduler state.
-- Imported dependency areas include: `base64`, `hashlib`, `helpers`, `helpers.notification`, `helpers.print_style`, `helpers.providers`, `helpers.secrets`, `json`, `models`, `os`, `pytz`, `re`, `subprocess`, `typing`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `TypeVar`, `files.get_abs_path`, `dotenv.get_dotenv_value`, `opts.insert`, `str.strip`, `_is_valid_timezone`, `str.strip.lower`, `_normalize_timezone_setting`, `SettingsOutput`, `get_default_settings`, `_ensure_option_present`, `_resolve_runtime_timezone`, `get_default_secrets_manager`, `get_settings`, `normalize_settings`, `_load_sensitive_settings`, `settings.copy`, `_write_settings_file`, `reload_settings`, `set_settings`, `initialize_agent`.
-- Applying settings refreshes active context configs while preserving each subordinate agent's own profile.
-- Applying settings starts a deferred `MCPConfig.update(...)` with the current `mcp_servers` string when global MCP server settings change.
-- `max_consecutive_unusable_responses` defaults to `2` and controls the cost circuit breaker for malformed or repeated main-model outputs.
-- `ui_control_visibility` stores validated mobile and desktop visibility flags for the project selector, clock, connection status, and right canvas rail; missing or malformed values fall back per device.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_browser_agent_regressions.py`
- - `tests/test_document_query_plugin.py`
- - `tests/test_download_toast_regressions.py`
- - `tests/test_fasta2a_client.py`
- - `tests/test_mcp_handler_multimodal.py`
- - `tests/test_model_config_api_keys.py`
- - `tests/test_model_config_project_presets.py`
- - `tests/test_oauth_static.py`
- - `tests/test_subagent_profiles.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/skills.py b/helpers/skills.py
index 1eeaa8504..8a042fe58 100644
--- a/helpers/skills.py
+++ b/helpers/skills.py
@@ -21,11 +21,9 @@ except Exception: # pragma: no cover
MAX_ACTIVE_SKILLS = 20
ACTIVE_SKILLS_PLUGIN_NAME = "_skills"
AGENT_DATA_NAME_LOADED_SKILLS = "loaded_skills"
-CONTEXT_DATA_NAME_LOADED_SKILLS = AGENT_DATA_NAME_LOADED_SKILLS
CONTEXT_DATA_NAME_CHAT_ACTIVE_SKILLS = "skills_chat_active"
CONTEXT_DATA_NAME_CHAT_DISABLED_SKILLS = "skills_chat_disabled"
CONTEXT_DATA_NAME_CHAT_VISIBLE_SKILLS = "skills_chat_visible"
-_WARNED_SKILL_PARSE_PATHS: set[Path] = set()
class ActiveSkillEntry(TypedDict, total=False):
@@ -255,47 +253,6 @@ def parse_frontmatter(frontmatter_text: str) -> Tuple[Dict[str, Any], List[str]]
return parsed, errors
-def _emit_skill_scan_warning(message: str) -> None:
- try:
- from helpers.print_style import PrintStyle
-
- PrintStyle.warning(message)
- except Exception:
- print(f"Warning: {message}")
-
-
-def _frontmatter_error_line(lines: List[str], error: str) -> int | None:
- if not lines:
- return 1
-
- if error.startswith("Frontmatter must start"):
- for index, line in enumerate(lines, start=1):
- if line.strip():
- return index
- return 1
- if error.startswith("Missing YAML frontmatter"):
- return 1
- if error.startswith("Unterminated YAML frontmatter"):
- return max(len(lines), 1)
- return None
-
-
-def _warn_skill_skipped(skill_md_path: Path, markdown: str, errors: List[str]) -> None:
- if not errors:
- return
- if skill_md_path in _WARNED_SKILL_PARSE_PATHS:
- return
- _WARNED_SKILL_PARSE_PATHS.add(skill_md_path)
-
- error = str(errors[0] or "invalid frontmatter").strip()
- line = _frontmatter_error_line((markdown or "").splitlines(), error)
- skill_label = skill_md_path.parent.name or str(skill_md_path)
- location = f" at line {line}" if line is not None else ""
- _emit_skill_scan_warning(
- f"skill {skill_label} skipped: invalid frontmatter{location}: {error}"
- )
-
-
def skill_from_markdown(
skill_md_path: Path,
*,
@@ -309,7 +266,6 @@ def skill_from_markdown(
fm, body, fm_errors = split_frontmatter(text)
if fm_errors:
- _warn_skill_skipped(skill_md_path, text, fm_errors)
return None
skill_dir = Path(files.normalize_a0_path(str(skill_md_path.parent)))
@@ -495,20 +451,6 @@ def load_skill_for_agent(
return "\n".join(lines)
-def skill_instruction_name(message: Any) -> str:
- match message:
- case {
- "content": {
- "skill_instructions": {
- "content_included": included,
- "name": name,
- }
- }
- } if included:
- return str(name or "").strip()
- return ""
-
-
def _get_skill_files(skill_dir: Path) -> str:
"""Get file tree for skill directory."""
if not skill_dir.exists():
@@ -543,15 +485,11 @@ def search_skills(
if not q:
return []
- raw_terms = re.findall(r"[a-z0-9][a-z0-9_-]*", q)
+ raw_terms = [t for t in re.split(r"\s+", q) if t]
terms = [
t for t in raw_terms
- if len(t) >= 4 or any(ch.isdigit() for ch in t)
- ]
- long_terms = [
- t for t in raw_terms
- if len(t) >= 6 or any(ch.isdigit() for ch in t)
- ]
+ if len(t) >= 3 or any(ch.isdigit() for ch in t)
+ ] or raw_terms
candidates = list_skills(agent, include_hidden=include_hidden)
scored: List[Tuple[int, Skill]] = []
@@ -572,13 +510,14 @@ def search_skills(
score += 4
if any(q in tag for tag in tags):
score += 3
- if any(q in trigger or trigger in q for trigger in triggers):
+ if any(q in trigger for trigger in triggers):
score += 8
for term in terms:
if term in name:
score += 3
- for term in long_terms:
+ if term in desc:
+ score += 2
if any(term in tag for tag in tags):
score += 1
if any(term in trigger for trigger in triggers):
@@ -891,77 +830,19 @@ def get_active_skills(agent: Agent | None) -> list[ActiveSkillEntry]:
return _build_active_skills(agent, limit=get_max_active_skills(agent=agent))
-def _normalize_loaded_skill_names(raw: Any) -> list[str]:
- if not isinstance(raw, list):
- return []
-
- names: list[str] = []
- for value in raw:
- name = str(value or "").strip()
- if name and name not in names:
- names.append(name)
- return names
-
-
-def get_loaded_skill_names(agent: Agent | None) -> list[str]:
- if not agent:
- return []
-
- context = getattr(agent, "context", None)
- if context and hasattr(context, "get_data"):
- names = _normalize_loaded_skill_names(
- context.get_data(CONTEXT_DATA_NAME_LOADED_SKILLS)
- )
- if names:
- data = getattr(agent, "data", None)
- if isinstance(data, dict):
- data.pop(AGENT_DATA_NAME_LOADED_SKILLS, None)
- return names
-
- legacy_names = _normalize_loaded_skill_names(
- getattr(agent, "data", {}).get(AGENT_DATA_NAME_LOADED_SKILLS)
- )
- if legacy_names:
- set_loaded_skill_names(agent, legacy_names)
- return legacy_names
-
-
-def set_loaded_skill_names(agent: Agent | None, skill_names: Any) -> list[str]:
- names = _normalize_loaded_skill_names(skill_names)[-MAX_ACTIVE_SKILLS:]
- if not agent:
- return names
-
- context = getattr(agent, "context", None)
- if context and hasattr(context, "set_data"):
- context.set_data(CONTEXT_DATA_NAME_LOADED_SKILLS, names or None)
- data = getattr(agent, "data", None)
- if isinstance(data, dict):
- data.pop(AGENT_DATA_NAME_LOADED_SKILLS, None)
- return names
-
- data = getattr(agent, "data", None)
- if isinstance(data, dict):
- data[AGENT_DATA_NAME_LOADED_SKILLS] = names
- return names
-
-
-def add_loaded_skill_name(
- agent: Agent | None,
- skill_name: str,
- *,
- limit: int | None = None,
-) -> list[str]:
- name = str(skill_name or "").strip()
- if not name:
- return get_loaded_skill_names(agent)
-
- names = [loaded for loaded in get_loaded_skill_names(agent) if loaded != name]
- names.append(name)
- return set_loaded_skill_names(agent, names[-(limit or MAX_ACTIVE_SKILLS):])
-
-
def get_loaded_skill_entries(agent: Agent | None) -> list[ActiveSkillEntry]:
- return [{"name": skill_name} for skill_name in get_loaded_skill_names(agent)]
+ if not agent:
+ return []
+
+ loaded = getattr(agent, "data", {}).get(AGENT_DATA_NAME_LOADED_SKILLS)
+ if not isinstance(loaded, list):
+ return []
+
+ return [
+ {"name": str(skill_name).strip()}
+ for skill_name in loaded
+ if str(skill_name).strip()
+ ]
def unload_agent_skill(agent: Agent | None, entry: Any) -> bool:
@@ -969,9 +850,17 @@ def unload_agent_skill(agent: Agent | None, entry: Any) -> bool:
if not agent or not normalized:
return False
+ data = getattr(agent, "data", None)
+ if not isinstance(data, dict):
+ return False
+
+ loaded = data.get(AGENT_DATA_NAME_LOADED_SKILLS)
+ if not isinstance(loaded, list):
+ return False
+
next_loaded: list[str] = []
removed = False
- for skill_name in get_loaded_skill_names(agent):
+ for skill_name in loaded:
loaded_entry = _normalize_active_skill_entry(str(skill_name))
if loaded_entry and _entries_match(loaded_entry, normalized):
removed = True
@@ -979,7 +868,7 @@ def unload_agent_skill(agent: Agent | None, entry: Any) -> bool:
next_loaded.append(skill_name)
if removed:
- set_loaded_skill_names(agent, next_loaded)
+ data[AGENT_DATA_NAME_LOADED_SKILLS] = next_loaded
return removed
@@ -1135,6 +1024,7 @@ def hide_chat_skill(agent: Agent, entry: Any) -> list[ActiveSkillEntry]:
CONTEXT_DATA_NAME_CHAT_VISIBLE_SKILLS,
visible_entries,
)
+ unload_agent_skill(agent, normalized)
return get_hidden_skills(agent)
@@ -1185,7 +1075,8 @@ def clear_chat_skill_overrides(agent: Agent) -> list[ActiveSkillEntry]:
def build_active_skills_prompt(agent: Agent | None) -> str:
- return ""
+ items = _resolve_active_skill_entries(agent, get_active_skills(agent))
+ return "\n\n".join(item["content"] for item in items if item.get("content")).strip()
def _format_skill_prompt(skill: Skill) -> str:
diff --git a/helpers/skills.py.dox.md b/helpers/skills.py.dox.md
deleted file mode 100644
index 902caf3fc..000000000
--- a/helpers/skills.py.dox.md
+++ /dev/null
@@ -1,92 +0,0 @@
-# skills.py DOX
-
-## Purpose
-
-- Own the `skills.py` helper module.
-- This module discovers, parses, filters, and resolves Agent Zero skills.
-- Keep this file-level DOX profile synchronized with `skills.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `skills.py` owns the runtime implementation.
-- `skills.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `ActiveSkillEntry` (`TypedDict`)
-- `CatalogSkill` (`TypedDict`)
-- `Skill` (no explicit base class)
-- Top-level functions:
-- `get_skills_base_dir() -> Path`
-- `get_skill_roots(agent: Agent | None=...) -> List[str]`
-- `_is_hidden_path(path: Path) -> bool`
-- `discover_skill_md_files(root: Path) -> List[Path]`: Recursively discover SKILL.md files under a root directory.
-- `_coerce_list(value: Any) -> List[str]`
-- `_normalize_name(name: str) -> str`
-- `_read_text(path: Path) -> str`
-- `split_frontmatter(markdown: str) -> Tuple[Dict[str, Any], str, List[str]]`: Splits a SKILL.md into (frontmatter_dict, body_text, errors).
-- `_parse_frontmatter_fallback(frontmatter_text: str) -> Dict[str, Any]`
-- `parse_frontmatter(frontmatter_text: str) -> Tuple[Dict[str, Any], List[str]]`: Parse YAML frontmatter with PyYAML when available,
-- `skill_from_markdown(skill_md_path: Path, include_content: bool=..., validate: bool=...) -> Optional[Skill]`
-- `list_skills(agent: Agent | None=..., include_content: bool=..., include_hidden: bool=...) -> List[Skill]`: List skills, optionally filtered by agent scope.
-- `delete_skill(skill_path: str) -> None`: Delete a skill directory.
-- `find_skill(skill_name: str, agent: Agent | None=..., include_content: bool=..., include_hidden: bool=...) -> Optional[Skill]`
-- `load_skill_for_agent(skill_name: str, agent: Agent | None=...) -> str`: Load skill and format it as a complete string for agent context.
-- `skill_instruction_name(message: Any) -> str`
-- `_get_skill_files(skill_dir: Path) -> str`: Get file tree for skill directory.
-- `search_skills(query: str, limit: int=..., agent: Agent | None=..., include_hidden: bool=...) -> List[Skill]`
-- `validate_skill(skill: Skill) -> List[str]`
-- `validate_skill_md(skill_md_path: Path) -> List[str]`
-- `_normalize_max_active_skills(value: Any) -> int`
-- `get_max_active_skills(agent: Agent | None=..., project_name: str | None=...) -> int`
-- `normalize_skills_config(config: dict[str, Any] | None) -> dict[str, Any]`
-- `normalize_active_skills(raw: Any, limit: int | None=...) -> list[ActiveSkillEntry]`
-- `normalize_hidden_skills(raw: Any) -> list[ActiveSkillEntry]`
-- `normalize_skill_entries(raw: Any, limit: int | None=...) -> list[ActiveSkillEntry]`
-- `list_skill_catalog(project_name: str=..., agent: Agent | None=...) -> list[CatalogSkill]`
-- `get_scope_active_skills(agent: Agent | None) -> list[ActiveSkillEntry]`
-- `get_scope_hidden_skills(agent: Agent | None) -> list[ActiveSkillEntry]`
-- `get_chat_active_skills(context: Any | None) -> list[ActiveSkillEntry]`
-- `get_chat_disabled_skills(context: Any | None) -> list[ActiveSkillEntry]`
-- `get_loaded_skill_names(agent: Agent | None) -> list[str]`
-- `set_loaded_skill_names(agent: Agent | None, skill_names: Any) -> list[str]`
-- `add_loaded_skill_name(agent: Agent | None, skill_name: str, limit: int | None=...) -> list[str]`
-- Notable constants/configuration names: `MAX_ACTIVE_SKILLS`, `ACTIVE_SKILLS_PLUGIN_NAME`, `AGENT_DATA_NAME_LOADED_SKILLS`, `CONTEXT_DATA_NAME_LOADED_SKILLS`, `CONTEXT_DATA_NAME_CHAT_ACTIVE_SKILLS`, `CONTEXT_DATA_NAME_CHAT_DISABLED_SKILLS`, `CONTEXT_DATA_NAME_CHAT_VISIBLE_SKILLS`, `_NAME_RE`.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Loaded skill names are chat-wide context data under `CONTEXT_DATA_NAME_LOADED_SKILLS`; legacy agent-local `loaded_skills` lists are migrated into context data and cleared when read.
-- Loaded skill bodies live in chat history; hiding a skill changes catalog visibility but does not remove the loaded-skill ledger.
-- `build_active_skills_prompt()` returns empty because selected skills are loaded through history, not prompt protocol.
-- `search_skills()` normalizes query words, scores normal terms against skill names, and scores only long terms against tags/triggers; descriptions match only full query phrases so generic prose does not produce irrelevant suggestions.
-- Invalid `SKILL.md` frontmatter emits a once-per-path scan warning with the skipped skill path/name and a line number when the parser can identify one directly.
-- Observed side-effect areas: filesystem reads, filesystem deletion, plugin state, settings/state persistence, context data, secret handling.
-- Imported dependency areas include: `__future__`, `dataclasses`, `helpers`, `os`, `pathlib`, `re`, `typing`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `dataclass`, `re.compile`, `field`, `Path`, `root.rglob`, `results.sort`, `re.sub`, `path.read_text`, `text.splitlines`, `join.strip`, `parse_frontmatter`, `frontmatter_text.splitlines`, `_parse_frontmatter_fallback`, `split_frontmatter`, `str.strip`, `_coerce_list`, `Skill`, `get_skill_roots`, `_filter_hidden_skills`, `files.get_abs_path`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_a0_connector_prompt_gating.py`
- - `tests/test_browser_agent_regressions.py`
- - `tests/test_document_query_plugin.py`
- - `tests/test_fasta2a_client.py`
- - `tests/test_office_canvas_setup.py`
- - `tests/test_office_document_store.py`
- - `tests/test_skills_runtime.py`
- - `tests/test_time_travel.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/skills_cli.py.dox.md b/helpers/skills_cli.py.dox.md
deleted file mode 100644
index 02aa7922d..000000000
--- a/helpers/skills_cli.py.dox.md
+++ /dev/null
@@ -1,51 +0,0 @@
-# skills_cli.py DOX
-
-## Purpose
-
-- Own the `skills_cli.py` helper module.
-- This module provides command-line skill listing, search, validation, and creation helpers.
-- Keep this file-level DOX profile synchronized with `skills_cli.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `skills_cli.py` owns the runtime implementation.
-- `skills_cli.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `Skill` (no explicit base class)
-- Top-level functions:
-- `get_skills_dirs() -> List[Path]`: Get all skill directories
-- `parse_skill_file(skill_path: Path) -> Optional[Skill]`: Parse a SKILL.md file and return a Skill object
-- `list_skills() -> List[Skill]`: List all available skills
-- `find_skill(name: str) -> Optional[Skill]`: Find a skill by name
-- `search_skills(query: str) -> List[Skill]`: Search skills by name, description, or tags
-- `validate_skill(skill: Skill) -> List[str]`: Validate a skill and return list of issues
-- `create_skill(name: str, description: str=..., author: str=...) -> Path`: Create a new skill from template
-- `print_skill_table(skills: List[Skill])`: Print skills in a formatted table
-- `main()`
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem reads, filesystem writes, settings/state persistence, secret handling.
-- Imported dependency areas include: `argparse`, `dataclasses`, `datetime`, `helpers`, `os`, `pathlib`, `re`, `sys`, `typing`, `yaml`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `sys.path.insert`, `field`, `Path`, `get_skills_dirs`, `list_skills`, `query.lower`, `exists`, `custom_dir.mkdir`, `skill_dir.exists`, `skill_dir.mkdir`, `mkdir`, `skill_file.write_text`, `readme.write_text`, `argparse.ArgumentParser`, `parser.add_subparsers`, `subparsers.add_parser`, `list_parser.add_argument`, `create_parser.add_argument`, `show_parser.add_argument`, `validate_parser.add_argument`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/skills_import.py b/helpers/skills_import.py
index a043dcf3f..c73ffa55d 100644
--- a/helpers/skills_import.py
+++ b/helpers/skills_import.py
@@ -2,7 +2,6 @@ from __future__ import annotations
import os
import shutil
-import stat
import tempfile
import time
import zipfile
@@ -83,60 +82,25 @@ def _candidate_skill_roots(source_dir: Path) -> List[Path]:
return unique or [source_dir]
-def _safe_extract_zip(zip_path: Path, target: Path) -> None:
- with zipfile.ZipFile(zip_path, "r") as archive:
- for member in archive.infolist():
- name = member.filename
- if not name or name.startswith(("/", "\\")) or "\\" in name:
- raise ValueError(f"Unsafe zip entry path: {name!r}")
-
- mode = member.external_attr >> 16
- if stat.S_ISLNK(mode):
- raise ValueError(f"Refusing to extract symlink from zip: {name!r}")
-
- destination = target / name
- if not _is_within(destination, target):
- raise ValueError(f"Unsafe zip entry path: {name!r}")
-
- archive.extractall(target)
-
-
-def extract_skills_zip(
- zip_path: Path,
- *,
- tmp_subdir: str = "skill_imports",
- prefix: str = "import",
-) -> tuple[Path, Path]:
- """
- Extract a zip into a temp folder inside Agent Zero's tmp directory.
- Returns (source_root, cleanup_root).
- """
- base_tmp = Path(files.get_abs_path("tmp", tmp_subdir))
- base_tmp.mkdir(parents=True, exist_ok=True)
- stamp = time.strftime("%Y%m%d_%H%M%S")
- target = base_tmp / f"{prefix}_{zip_path.stem}_{stamp}"
- target.mkdir(parents=True, exist_ok=True)
-
- try:
- _safe_extract_zip(zip_path, target)
- except Exception:
- shutil.rmtree(target, ignore_errors=True)
- raise
-
- # If zip contains a single top-level folder, treat that as the root
- children = [p for p in target.iterdir()]
- if len(children) == 1 and children[0].is_dir():
- return children[0], target
- return target, target
-
-
def _unzip_to_temp_dir(zip_path: Path) -> Path:
"""
Extract a zip into a temp folder under tmp/skill_imports (inside Agent Zero base dir).
Returns the extraction root folder.
"""
- source_root, _cleanup_root = extract_skills_zip(zip_path)
- return source_root
+ base_tmp = Path(files.get_abs_path("tmp", "skill_imports"))
+ base_tmp.mkdir(parents=True, exist_ok=True)
+ stamp = time.strftime("%Y%m%d_%H%M%S")
+ target = base_tmp / f"import_{zip_path.stem}_{stamp}"
+ target.mkdir(parents=True, exist_ok=True)
+
+ with zipfile.ZipFile(zip_path, "r") as z:
+ z.extractall(target)
+
+ # If zip contains a single top-level folder, treat that as the root
+ children = [p for p in target.iterdir()]
+ if len(children) == 1 and children[0].is_dir():
+ return children[0]
+ return target
def build_import_plan(
@@ -297,3 +261,4 @@ def import_skills(
destination_root=dest_root,
namespace=ns,
)
+
diff --git a/helpers/skills_import.py.dox.md b/helpers/skills_import.py.dox.md
deleted file mode 100644
index e6bc0493e..000000000
--- a/helpers/skills_import.py.dox.md
+++ /dev/null
@@ -1,58 +0,0 @@
-# skills_import.py DOX
-
-## Purpose
-
-- Own the `skills_import.py` helper module.
-- This module plans and imports skill bundles into user, project, or profile scopes.
-- Keep this file-level DOX profile synchronized with `skills_import.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `skills_import.py` owns the runtime implementation.
-- `skills_import.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `ImportPlanItem` (no explicit base class)
-- `ImportResult` (no explicit base class)
-- Top-level functions:
-- `_is_within(child: Path, parent: Path) -> bool`
-- `_derive_namespace(source: Path) -> str`
-- `_candidate_skill_roots(source_dir: Path) -> List[Path]`: Heuristics to find likely skill roots inside a repo/pack:
-- `_safe_extract_zip(zip_path: Path, target: Path) -> None`
-- `extract_skills_zip(zip_path: Path, tmp_subdir: str=..., prefix: str=...) -> tuple[Path, Path]`: Extract a zip into a temp folder inside Agent Zero's tmp directory and return the scan/import root plus cleanup root.
-- `_unzip_to_temp_dir(zip_path: Path) -> Path`: Extract a zip into a temp folder under tmp/skill_imports (inside Agent Zero base dir).
-- `build_import_plan(source: Path, dest_root: Path, namespace: Optional[str]=...) -> Tuple[List[ImportPlanItem], Path]`: Build a copy plan for importing skills from a source folder.
-- `_resolve_conflict(dest: Path, policy: ConflictPolicy) -> Tuple[Path, bool]`: Returns (final_dest_path, should_copy).
-- `get_project_skills_folder(project_name: str) -> Path`: Get the skills folder path for a project.
-- `get_agent_profile_skills_folder(profile_name: str) -> Path`
-- `get_project_agent_profile_skills_folder(project_name: str, profile_name: str) -> Path`
-- `resolve_skills_destination_root(project_name: Optional[str], agent_profile: Optional[str]) -> Path`
-- `import_skills(source_path: str, namespace: Optional[str]=..., conflict: ConflictPolicy=..., dry_run: bool=..., project_name: Optional[str]=..., agent_profile: Optional[str]=...) -> ImportResult`: Import external Skills into usr/skills//...
-- Notable constants/configuration names: `PROJECT_SKILLS_DIR`.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem reads, filesystem writes, filesystem deletion, plugin state.
-- Imported dependency areas include: `__future__`, `dataclasses`, `helpers`, `helpers.skills`, `os`, `pathlib`, `shutil`, `stat`, `tempfile`, `time`, `typing`, `zipfile`.
-
-## Key Concepts
-
-- Zip extraction rejects absolute paths, backslash paths, path traversal, and symlink entries before extraction.
-- Important called helpers/classes observed in the source: `dataclass`, `strip`, `plugins.is_dir`, `Path`, `base_tmp.mkdir`, `time.strftime`, `target.mkdir`, `_candidate_skill_roots`, `Path.expanduser`, `resolve_skills_destination_root`, `dest_root.mkdir`, `build_import_plan`, `ImportResult`, `child.resolve.relative_to`, `direct.is_dir`, `discover_skill_md_files`, `plugins.iterdir`, `files.get_abs_path`, `zipfile.ZipFile`, `archive.extractall`, `shutil.rmtree`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/state_migration.py.dox.md b/helpers/state_migration.py.dox.md
deleted file mode 100644
index 9af4faa8c..000000000
--- a/helpers/state_migration.py.dox.md
+++ /dev/null
@@ -1,45 +0,0 @@
-# state_migration.py DOX
-
-## Purpose
-
-- Own the `state_migration.py` helper module.
-- This module moves retired state tree paths to current runtime locations.
-- Keep this file-level DOX profile synchronized with `state_migration.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `state_migration.py` owns the runtime implementation.
-- `state_migration.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Top-level functions:
-- `migrate_retired_state_tree(source: Path, destination: Path, owner: str, migrated: list[str], warnings: list[str], errors: list[str]) -> None`: Move retired plugin state into its plugin-owned state directory.
-- `_move_path(source: Path, target: Path, migrated: list[str]) -> None`
-- `_next_conflict_path(path: Path) -> Path`
-- `_remove_empty_dir(path: Path, owner: str, warnings: list[str]) -> None`
-- `_same_path(left: Path, right: Path) -> bool`
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem reads, filesystem writes, plugin state, settings/state persistence.
-- Imported dependency areas include: `__future__`, `pathlib`, `shutil`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `_same_path`, `final_target.parent.mkdir`, `shutil.move`, `path.with_name`, `_move_path`, `source.is_dir`, `target.is_dir`, `source.rmdir`, `target.exists`, `target.is_symlink`, `_next_conflict_path`, `candidate.exists`, `candidate.is_symlink`, `path.rmdir`, `source.exists`, `source.is_symlink`, `destination.mkdir`, `_remove_empty_dir`, `source.iterdir`, `left.resolve`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/state_monitor.py.dox.md b/helpers/state_monitor.py.dox.md
deleted file mode 100644
index a84115652..000000000
--- a/helpers/state_monitor.py.dox.md
+++ /dev/null
@@ -1,59 +0,0 @@
-# state_monitor.py DOX
-
-## Purpose
-
-- Own the `state_monitor.py` helper module.
-- This module tracks dirty state and connection projections for WebUI sync.
-- Keep this file-level DOX profile synchronized with `state_monitor.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `state_monitor.py` owns the runtime implementation.
-- `state_monitor.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `ConnectionProjection` (no explicit base class)
-- `StateMonitor` (no explicit base class)
- - `bind_manager(self, manager: 'WsManager', handler_id: str | None=...) -> None`
- - `register_sid(self, namespace: str, sid: str) -> None`
- - `unregister_sid(self, namespace: str, sid: str) -> None`
- - `mark_dirty_all(self, reason: str | None=...) -> None`
- - `mark_dirty_for_context(self, context_id: str, reason: str | None=...) -> None`
- - `update_projection(self, namespace: str, sid: str, request: StateRequestV1, seq_base: int) -> None`
- - `mark_dirty(self, namespace: str, sid: str, reason: str | None=..., wave_id: str | None=...) -> None`
-- Top-level functions:
-- `get_state_monitor() -> StateMonitor`
-- `_reset_state_monitor_for_testing() -> None`
-- Notable constants/configuration names: `_STATE_MONITOR_HOLDER`, `_STATE_MONITOR_LOCK`.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem deletion, WebSocket state, settings/state persistence, scheduler state.
-- Imported dependency areas include: `__future__`, `asyncio`, `dataclasses`, `helpers`, `helpers.print_style`, `helpers.state_snapshot`, `helpers.ws`, `helpers.ws_manager`, `os`, `threading`, `time`, `typing`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `threading.RLock`, `field`, `ws_debug`, `_ws_debug_enabled`, `context_id.strip`, `loop.call_soon_threadsafe`, `self._schedule_debounce_on_loop`, `asyncio.get_running_loop`, `asyncio.current_task`, `loop.is_closed`, `self._debounce_handles.pop`, `self._push_tasks.pop`, `self._projections.pop`, `self.mark_dirty`, `self._mark_dirty_on_loop`, `runtime.is_development`, `loop.call_later`, `StateMonitor`, `ConnectionProjection`, `handle.cancel`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_model_config_api_keys.py`
- - `tests/test_multi_tab_isolation.py`
- - `tests/test_state_monitor.py`
- - `tests/test_state_sync_handler.py`
- - `tests/test_state_sync_welcome_screen.py`
- - `tests/test_ws_handlers.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/state_monitor_integration.py.dox.md b/helpers/state_monitor_integration.py.dox.md
deleted file mode 100644
index 15c7e3530..000000000
--- a/helpers/state_monitor_integration.py.dox.md
+++ /dev/null
@@ -1,43 +0,0 @@
-# state_monitor_integration.py DOX
-
-## Purpose
-
-- Own the `state_monitor_integration.py` helper module.
-- This module bridges dirty-state calls into the shared state monitor.
-- Keep this file-level DOX profile synchronized with `state_monitor_integration.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `state_monitor_integration.py` owns the runtime implementation.
-- `state_monitor_integration.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Top-level functions:
-- `mark_dirty_all(reason: str | None=...) -> None`
-- `mark_dirty_for_context(context_id: str, reason: str | None=...) -> None`
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: settings/state persistence.
-- Imported dependency areas include: `__future__`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `get_state_monitor.mark_dirty_all`, `get_state_monitor.mark_dirty_for_context`, `get_state_monitor`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_model_config_api_keys.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/state_snapshot.py b/helpers/state_snapshot.py
index 9a1db075d..a51f6239e 100644
--- a/helpers/state_snapshot.py
+++ b/helpers/state_snapshot.py
@@ -140,20 +140,6 @@ def _apply_agent_profile_metadata(
context_data["agent_profile_label"] = labels.get(profile, profile) if profile else ""
-def _prune_missing_saved_contexts() -> None:
- from helpers import persist_chat
-
- saved_ids = persist_chat.saved_chat_ids()
- for ctx in AgentContext.all():
- if ctx.type == AgentContextType.BACKGROUND or ctx.is_running():
- continue
- if (
- ctx.data.get(persist_chat.SAVED_CHAT_CONTEXT_DATA_KEY)
- and ctx.id not in saved_ids
- ):
- AgentContext.remove(ctx.id)
-
-
def parse_state_request_payload(payload: Mapping[str, Any]) -> StateRequestV1:
context = payload.get("context")
log_from = payload.get("log_from")
@@ -269,8 +255,6 @@ async def build_snapshot_from_request(*, request: StateRequestV1) -> SnapshotV1:
from_no = _coerce_non_negative_int(request.log_from, default=0)
notifications_from_no = _coerce_non_negative_int(request.notifications_from, default=0)
- _prune_missing_saved_contexts()
-
active_context = AgentContext.get(ctxid) if ctxid else None
if active_context:
diff --git a/helpers/state_snapshot.py.dox.md b/helpers/state_snapshot.py.dox.md
deleted file mode 100644
index 69fccccf2..000000000
--- a/helpers/state_snapshot.py.dox.md
+++ /dev/null
@@ -1,65 +0,0 @@
-# state_snapshot.py DOX
-
-## Purpose
-
-- Own the `state_snapshot.py` helper module.
-- This module builds and validates typed WebUI state snapshots.
-- Keep this file-level DOX profile synchronized with `state_snapshot.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `state_snapshot.py` owns the runtime implementation.
-- `state_snapshot.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `SnapshotV1` (`TypedDict`)
-- `StateRequestV1` (no explicit base class)
-- `StateRequestValidationError` (`ValueError`)
-- Top-level functions:
-- `_annotation_to_isinstance_types(annotation: Any) -> tuple[type, ...]`: Convert type annotation to tuple suitable for isinstance().
-- `_build_schema_from_typeddict(td: type) -> dict[str, tuple[type, ...]]`: Extract field names and isinstance-compatible types from TypedDict.
-- `validate_snapshot_schema_v1(snapshot: Mapping[str, Any]) -> None`
-- `_coerce_non_negative_int(value: Any, default: int=...) -> int`
-- `_get_agent_profile_labels() -> dict[str, str]`
-- `_apply_agent_profile_metadata(context_data: dict[str, Any], ctx: AgentContext, labels: dict[str, str]) -> None`
-- `_prune_missing_saved_contexts() -> None`
-- `parse_state_request_payload(payload: Mapping[str, Any]) -> StateRequestV1`
-- `_coerce_state_request_inputs(context: Any, log_from: Any, notifications_from: Any, timezone: Any) -> StateRequestV1`
-- `advance_state_request_after_snapshot(request: StateRequestV1, snapshot: Mapping[str, Any]) -> StateRequestV1`
-- `async build_snapshot_from_request(request: StateRequestV1) -> SnapshotV1`: Build a poll-shaped snapshot for both /poll and state_push.
-- `_notify_timezone_changed(previous_timezone: str, current_timezone: str) -> None`
-- `async build_snapshot(context: str | None, log_from: int, notifications_from: int, timezone: str | None) -> SnapshotV1`
-- Notable constants/configuration names: `_SNAPSHOT_V1_SCHEMA`, `SNAPSHOT_SCHEMA_V1_KEYS`.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem reads, plugin state, settings/state persistence, secret handling, scheduler state, in-memory context removal.
-- Imported dependency areas include: `__future__`, `agent`, `dataclasses`, `helpers.dotenv`, `helpers.localization`, `helpers.persist_chat`, `helpers.task_scheduler`, `pytz`, `types`, `typing`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `dataclass`, `_build_schema_from_typeddict`, `get_origin`, `timezone.strip`, `StateRequestV1`, `localization.get_timezone`, `localization.set_timezone`, `ctxid.strip`, `_coerce_non_negative_int`, `AgentContext.get_notification_manager`, `notification_manager.output`, `_get_agent_profile_labels`, `ctxs.sort`, `tasks.sort`, `validate_snapshot_schema_v1`, `_coerce_state_request_inputs`, `super.__init__`, `get_args`, `_annotation_to_isinstance_types`, `TypeError`.
-- Snapshot building prunes non-running in-memory contexts that were previously saved but no longer have a `chat.json`, preventing stale sidebar rows after chat files are deleted outside `/chat_remove`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_multi_tab_isolation.py`
- - `tests/test_snapshot_parity.py`
- - `tests/test_snapshot_schema_v1.py`
- - `tests/test_state_monitor.py`
- - `tests/test_state_sync_handler.py`
- - `tests/test_state_sync_welcome_screen.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/strings.py.dox.md b/helpers/strings.py.dox.md
deleted file mode 100644
index ccc055650..000000000
--- a/helpers/strings.py.dox.md
+++ /dev/null
@@ -1,49 +0,0 @@
-# strings.py DOX
-
-## Purpose
-
-- Own the `strings.py` helper module.
-- This module sanitizes, formats, truncates, and expands framework string content.
-- Keep this file-level DOX profile synchronized with `strings.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `strings.py` owns the runtime implementation.
-- `strings.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Top-level functions:
-- `sanitize_string(s: str, encoding: str=...) -> str`
-- `calculate_valid_match_lengths(first: bytes | str, second: bytes | str, deviation_threshold: int=..., deviation_reset: int=..., ignore_patterns: list[bytes | str]=..., debug: bool=...) -> tuple[int, int]`
-- `format_key(key: str) -> str`: Format a key string to be more readable.
-- `dict_to_text(d: dict) -> str`
-- `truncate_text(text: str, length: int, at_end: bool=..., replacement: str=...) -> str`
-- `truncate_text_by_ratio(text: str, threshold: int, replacement: str=..., ratio: float=...) -> str`: Truncate text with replacement at a specified ratio position.
-- `replace_file_includes(text: str, placeholder_pattern: str=...) -> str`
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem reads, filesystem deletion.
-- Imported dependency areas include: `helpers`, `re`, `sys`, `time`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `s.encode.decode`, `join`, `join.rstrip`, `re.sub`, `skip_ignored_patterns`, `match.group`, `s.encode`, `sys.stdout.write`, `sys.stdout.flush`, `time.sleep`, `c.isupper`, `result.islower`, `word.capitalize`, `files.fix_dev_path`, `files.read_file`, `re.match`, `formatted.split`, `c.isalnum`, `format_key`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_model_search.py`
- - `tests/test_whatsapp_number_utils.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/subagents.py b/helpers/subagents.py
index df9d5ae20..f3557a49e 100644
--- a/helpers/subagents.py
+++ b/helpers/subagents.py
@@ -433,4 +433,4 @@ def get_paths(
# end-of-file imports to prevent circular imports
-from helpers import plugins
+from helpers import plugins
\ No newline at end of file
diff --git a/helpers/subagents.py.dox.md b/helpers/subagents.py.dox.md
deleted file mode 100644
index 9b954ff80..000000000
--- a/helpers/subagents.py.dox.md
+++ /dev/null
@@ -1,61 +0,0 @@
-# subagents.py DOX
-
-## Purpose
-
-- Own the `subagents.py` helper module.
-- This module discovers, merges, loads, and saves agent profile definitions.
-- Keep this file-level DOX profile synchronized with `subagents.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `subagents.py` owns the runtime implementation.
-- `subagents.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `SubAgentListItem` (`BaseModel`)
- - `post_validator(self)`
-- `SubAgent` (`SubAgentListItem`)
-- Top-level functions:
-- `get_agents_list(project_name: str | None=...) -> list[SubAgentListItem]`
-- `get_agents_dict(project_name: str | None=...) -> dict[str, SubAgentListItem]`
-- `_get_agents_list_from_dir(dir: str, origin: Origin) -> dict[str, SubAgentListItem]`
-- `load_agent_data(name: str, project_name: str | None=...) -> SubAgent`
-- `save_agent_data(name: str, subagent: SubAgent) -> None`
-- `delete_agent_data(name: str) -> None`
-- `_load_agent_data_from_dir(dir: str, name: str, origin: Origin) -> SubAgent | None`
-- `_merge_agents(base: SubAgent | None, override: SubAgent | None) -> SubAgent | None`
-- `_merge_agent_list_items(base: SubAgentListItem, override: SubAgentListItem) -> SubAgentListItem`
-- `get_agents_roots() -> list[str]`
-- `get_all_agents_list() -> list[dict[str, str]]`
-- `_merge_origins(base: list[Origin], override: list[Origin]) -> list[Origin]`
-- `get_default_promp_file_names() -> list[str]`
-- `get_available_agents_dict(project_name: str | None) -> dict[str, SubAgentListItem]`
-- `get_paths(agent: 'Agent|None', *subpaths, must_exist_completely: bool=..., include_project: bool=..., include_user: bool=..., include_default: bool=..., include_plugins: bool=..., default_root: str=...) -> list[str]`: Returns list of file paths for the given agent and subpaths, searched in order of priority:
-- Notable constants/configuration names: `GLOBAL_DIR`, `USER_DIR`, `DEFAULT_AGENTS_DIR`, `USER_AGENTS_DIR`, `PATHS_CACHE_AREA`.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem reads, filesystem writes, filesystem deletion, plugin state, settings/state persistence.
-- Imported dependency areas include: `helpers`, `json`, `os`, `pydantic`, `typing`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `cache.toggle_area`, `model_validator`, `_get_agents_list_from_dir`, `plugins.get_enabled_plugin_paths`, `_merge_agent_dicts`, `files.get_subdirectories`, `_load_agent_data_from_dir`, `_merge_agent`, `files.write_file`, `files.delete_dir`, `SubAgent`, `SubAgentListItem`, `files.find_existing_paths_by_pattern`, `get_agents_roots`, `files.list_files`, `get_agents_dict`, `cache.determine_cache_key`, `cache.add`, `projects.get_project_meta`, `FileNotFoundError`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_skills_runtime.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/system_packages.py.dox.md b/helpers/system_packages.py.dox.md
deleted file mode 100644
index f1078d3b7..000000000
--- a/helpers/system_packages.py.dox.md
+++ /dev/null
@@ -1,44 +0,0 @@
-# system_packages.py DOX
-
-## Purpose
-
-- Own the `system_packages.py` helper module.
-- This module runs apt operations with retry behavior.
-- Keep this file-level DOX profile synchronized with `system_packages.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `system_packages.py` owns the runtime implementation.
-- `system_packages.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Top-level functions:
-- `run_apt_with_retries(runner: Callable[[], subprocess.CompletedProcess[str]], lock_timeout_seconds: int=..., retry_seconds: int=...) -> subprocess.CompletedProcess[str]`: Run an apt/dpkg command, serializing in-process callers and waiting out apt locks.
-- `is_apt_lock_error(result: subprocess.CompletedProcess[str]) -> bool`
-- Notable constants/configuration names: `APT_LOCK_TIMEOUT_SECONDS`, `APT_LOCK_RETRY_SECONDS`.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: subprocess/runtime control.
-- Imported dependency areas include: `__future__`, `subprocess`, `threading`, `time`, `typing`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `threading.RLock`, `lower`, `time.monotonic`, `runner`, `time.sleep`, `is_apt_lock_error`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_office_document_store.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/tailscale_tunnel.py.dox.md b/helpers/tailscale_tunnel.py.dox.md
deleted file mode 100644
index 90afd506e..000000000
--- a/helpers/tailscale_tunnel.py.dox.md
+++ /dev/null
@@ -1,64 +0,0 @@
-# tailscale_tunnel.py DOX
-
-## Purpose
-
-- Own the `tailscale_tunnel.py` helper module.
-- This module implements Tailscale tunnel install and provider lifecycle behavior.
-- Keep this file-level DOX profile synchronized with `tailscale_tunnel.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `tailscale_tunnel.py` owns the runtime implementation.
-- `tailscale_tunnel.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `TailscaleTunnel` (`CliTunnelHelper`)
- - `stop(self)`
-- Top-level functions:
-- `tailscale_arch()`
-- `tailscale_archive_url()`
-- `install_tailscale(notify=...)`
-- `resolve_tailscaled_binary(binary_path)`
-- `notify_info(notify, message, data=...)`
-- `tailscale_socket_args(socket_path=...)`
-- `tailscale_command(binary_path, args, socket_path=...)`
-- `tailscale_status(binary_path, socket_path=...)`
-- `tailscale_funnel_help(binary_path, socket_path=...)`
-- `compact_output(lines)`
-- `tailscale_daemon_hint(output)`
-- `tailscale_daemon_ready(binary_path, socket_path)`
-- `read_recent_tailscaled_log()`
-- `start_tailscaled(binary_path, notify=...)`
-- `stop_managed_tailscaled()`
-- `tailscale_up_failure_message(output, timed_out=...)`
-- `run_tailscale_up(binary_path, notify=..., timeout=..., socket_path=...)`
-- `ensure_tailscale_funnel_command(binary_path, socket_path=...)`
-- `ensure_tailscale_ready(binary_path, notify=...)`
-- Notable constants/configuration names: `TAILSCALE_URL_RE`, `TAILSCALE_LOGIN_URL_RE`, `TAILSCALE_STABLE_PACKAGES_URL`, `TAILSCALE_UP_TIMEOUT`, `TAILSCALE_FUNNEL_TIMEOUT`, `TAILSCALE_FUNNEL_HTTPS_PORT`, `TAILSCALE_DAEMON_START_TIMEOUT`, `TAILSCALE_RUNTIME_DIR`, `TAILSCALE_STATE_DIR`, `TAILSCALE_SOCKET_PATH`, `TAILSCALE_DAEMON_LOG_PATH`, `TAILSCALE_DAEMON_PID_PATH`.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem reads, filesystem writes, filesystem deletion, network calls, subprocess/runtime control, WebSocket state, settings/state persistence, tunnel state.
-- Imported dependency areas include: `collections`, `flaredantic`, `helpers`, `helpers.cli_tunnel`, `json`, `os`, `pathlib`, `queue`, `re`, `shutil`, `subprocess`, `threading`, `time`, `urllib.parse`, `urllib.request`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `re.compile`, `Path`, `files.get_abs_path`, `cli_tunnel.platform_parts`, `tailscale_arch`, `pattern.search`, `urllib.parse.urljoin`, `shutil.which`, `tailscale_archive_url`, `cli_tunnel.download_file`, `Path.with_name`, `sibling.exists`, `callable`, `subprocess.run`, `join`, `output.lower`, `tailscale_status`, `compact_output`, `resolve_tailscaled_binary`, `tailscale_daemon_ready`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_tunnel_remote_link.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/task_scheduler.py.dox.md b/helpers/task_scheduler.py.dox.md
deleted file mode 100644
index 7292b4faf..000000000
--- a/helpers/task_scheduler.py.dox.md
+++ /dev/null
@@ -1,112 +0,0 @@
-# task_scheduler.py DOX
-
-## Purpose
-
-- Own the `task_scheduler.py` helper module.
-- This module models, serializes, schedules, runs, and persists scheduled tasks.
-- Keep this file-level DOX profile synchronized with `task_scheduler.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `task_scheduler.py` owns the runtime implementation.
-- `task_scheduler.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `TaskState` (`str`, `Enum`)
-- `TaskType` (`str`, `Enum`)
-- `TaskSchedule` (`BaseModel`)
- - `to_crontab(self) -> str`
-- `TaskPlan` (`BaseModel`)
- - `create(cls, todo: list[datetime] | None=..., in_progress: datetime | None=..., done: list[datetime] | None=...)`
- - `add_todo(self, launch_time: datetime)`
- - `set_in_progress(self, launch_time: datetime)`
- - `set_done(self, launch_time: datetime)`
- - `get_next_launch_time(self) -> datetime | None`
- - `should_launch(self) -> datetime | None`
-- `BaseTask` (`BaseModel`)
- - `update(self, name: str | None=..., state: TaskState | None=..., system_prompt: str | None=..., prompt: str | None=..., attachments: list[str] | None=..., last_run: datetime | None=..., last_result: str | None=..., context_id: str | None=..., **kwargs)`
- - `check_schedule(self, frequency_seconds: float=...) -> bool`
- - `get_next_run(self) -> datetime | None`
- - `is_dedicated(self) -> bool`
- - `get_next_run_minutes(self) -> int | None`
- - `async on_run(self)`
- - `async on_finish(self)`
- - `async on_error(self, error: str)`
-- `AdHocTask` (`BaseTask`)
- - `create(cls, name: str, system_prompt: str, prompt: str, token: str, attachments: list[str] | None=..., context_id: str | None=..., project_name: str | None=..., project_color: str | None=...)`
- - `update(self, name: str | None=..., state: TaskState | None=..., system_prompt: str | None=..., prompt: str | None=..., attachments: list[str] | None=..., last_run: datetime | None=..., last_result: str | None=..., context_id: str | None=..., token: str | None=..., **kwargs)`
-- `ScheduledTask` (`BaseTask`)
- - `create(cls, name: str, system_prompt: str, prompt: str, schedule: TaskSchedule, attachments: list[str] | None=..., context_id: str | None=..., timezone: str | None=..., project_name: str | None=..., project_color: str | None=...)`
- - `update(self, name: str | None=..., state: TaskState | None=..., system_prompt: str | None=..., prompt: str | None=..., attachments: list[str] | None=..., last_run: datetime | None=..., last_result: str | None=..., context_id: str | None=..., schedule: TaskSchedule | None=..., **kwargs)`
- - `check_schedule(self, frequency_seconds: float=...) -> bool`
- - `get_next_run(self) -> datetime | None`
-- `PlannedTask` (`BaseTask`)
- - `create(cls, name: str, system_prompt: str, prompt: str, plan: TaskPlan, attachments: list[str] | None=..., context_id: str | None=..., project_name: str | None=..., project_color: str | None=...)`
- - `update(self, name: str | None=..., state: TaskState | None=..., system_prompt: str | None=..., prompt: str | None=..., attachments: list[str] | None=..., last_run: datetime | None=..., last_result: str | None=..., context_id: str | None=..., plan: TaskPlan | None=..., **kwargs)`
- - `check_schedule(self, frequency_seconds: float=...) -> bool`
- - `get_next_run(self) -> datetime | None`
- - `async on_run(self)`
- - `async on_finish(self)`
- - `async on_success(self, result: str)`
- - `async on_error(self, error: str)`
-- `SchedulerTaskList` (`BaseModel`)
- - `get(cls) -> 'SchedulerTaskList'`
- - `async reload(self) -> 'SchedulerTaskList'`
- - `async add_task(self, task: Union[ScheduledTask, AdHocTask, PlannedTask]) -> 'SchedulerTaskList'`
- - `async save(self) -> 'SchedulerTaskList'`
- - `async update_task_by_uuid(self, task_uuid: str, updater_func: Callable[[Union[ScheduledTask, AdHocTask, PlannedTask]], None], verify_func: Callable[[Union[ScheduledTask, AdHocTask, PlannedTask]], bool]=...) -> Union[ScheduledTask, AdHocTask, PlannedTask] | None`
- - `get_tasks(self) -> list[Union[ScheduledTask, AdHocTask, PlannedTask]]`
- - `get_tasks_by_context_id(self, context_id: str, only_running: bool=...) -> list[Union[ScheduledTask, AdHocTask, PlannedTask]]`
- - `async get_due_tasks(self) -> list[Union[ScheduledTask, AdHocTask, PlannedTask]]`
-- `TaskScheduler` (no explicit base class)
- - `get(cls) -> 'TaskScheduler'`
- - `cancel_running_task(self, task_uuid: str, terminate_thread: bool=...) -> bool`
- - `cancel_tasks_by_context(self, context_id: str, terminate_thread: bool=...) -> bool`
- - `async reload(self)`
- - `get_tasks(self) -> list[Union[ScheduledTask, AdHocTask, PlannedTask]]`
- - `get_tasks_by_context_id(self, context_id: str, only_running: bool=...) -> list[Union[ScheduledTask, AdHocTask, PlannedTask]]`
- - `async add_task(self, task: Union[ScheduledTask, AdHocTask, PlannedTask]) -> 'TaskScheduler'`
- - `async remove_task_by_uuid(self, task_uuid: str) -> 'TaskScheduler'`
-- Top-level functions:
-- `normalize_schedule_timezone(timezone_name: str | None) -> str`
-- `_now() -> datetime`
-- `_localize_task_datetime(dt: datetime) -> datetime`
-- `serialize_datetime(dt: Optional[datetime]) -> Optional[str]`: Serialize a datetime object to ISO format string in the user's timezone.
-- `parse_datetime(dt_str: Optional[str]) -> Optional[datetime]`: Parse ISO format datetime string with timezone awareness.
-- `serialize_task_schedule(schedule: TaskSchedule) -> Dict[str, str]`: Convert TaskSchedule to a standardized dictionary format.
-- `parse_task_schedule(schedule_data: Dict[str, str]) -> TaskSchedule`: Parse dictionary into TaskSchedule with validation.
-- `serialize_task_plan(plan: TaskPlan) -> Dict[str, Any]`: Convert TaskPlan to a standardized dictionary format.
-- `parse_task_plan(plan_data: Dict[str, Any]) -> TaskPlan`: Parse dictionary into TaskPlan with validation.
-- `serialize_task(task: Union[ScheduledTask, AdHocTask, PlannedTask]) -> Dict[str, Any]`: Standardized serialization for task objects with proper handling of all complex types.
-- `serialize_tasks(tasks: list[Union[ScheduledTask, AdHocTask, PlannedTask]]) -> list[Dict[str, Any]]`: Serialize a list of tasks to a list of dictionaries.
-- `deserialize_task(task_data: Dict[str, Any], task_class: Optional[Type[T]]=...) -> T`: Deserialize dictionary into appropriate task object with validation.
-- Notable constants/configuration names: `SCHEDULER_FOLDER`, `LOCAL_TIMEZONE_ALIASES`, `T`.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem reads, filesystem writes, filesystem deletion, network calls, settings/state persistence, secret handling, scheduler state.
-- Imported dependency areas include: `agent`, `asyncio`, `crontab`, `datetime`, `enum`, `helpers`, `helpers.defer`, `helpers.files`, `helpers.localization`, `helpers.persist_chat`, `helpers.print_style`, `initialize`, `nest_asyncio`, `os`, `os.path`, `pydantic`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `nest_asyncio.apply`, `TypeVar`, `str.strip`, `callable`, `datetime.now`, `tzinfo.localize`, `Field`, `PrivateAttr`, `Localization.get.serialize_datetime`, `normalize_schedule_timezone`, `Localization.get.get_timezone`, `pytz.timezone`, `now`, `localize`, `cls`, `_localize_task_datetime`, `self.todo.remove`, `self.get_next_launch_time`, `super.__init__`, `threading.RLock`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_task_scheduler_timezone.py`
- - `tests/test_timezone_regressions.py`
- - `tests/test_tool_action_contracts.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/timed_input.py.dox.md b/helpers/timed_input.py.dox.md
deleted file mode 100644
index 5bcc3ce95..000000000
--- a/helpers/timed_input.py.dox.md
+++ /dev/null
@@ -1,40 +0,0 @@
-# timed_input.py DOX
-
-## Purpose
-
-- Own the `timed_input.py` helper module.
-- This module reads terminal input with timeout handling.
-- Keep this file-level DOX profile synchronized with `timed_input.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `timed_input.py` owns the runtime implementation.
-- `timed_input.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Top-level functions:
-- `timeout_input(prompt, timeout=...)`
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Imported dependency areas include: `inputimeout`, `sys`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `inputimeout`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/tokens.py.dox.md b/helpers/tokens.py.dox.md
deleted file mode 100644
index c42637a8e..000000000
--- a/helpers/tokens.py.dox.md
+++ /dev/null
@@ -1,54 +0,0 @@
-# tokens.py DOX
-
-## Purpose
-
-- Own the `tokens.py` helper module.
-- This module counts and approximates tokens and trims text to budgets.
-- Keep this file-level DOX profile synchronized with `tokens.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `tokens.py` owns the runtime implementation.
-- `tokens.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Top-level functions:
-- `count_tokens(text: str, encoding_name=...) -> int`
-- `approximate_tokens(text: str) -> int`
-- `sanitize_embedded_image_data_urls(text: str) -> str`
-- `approximate_prompt_tokens(text: str) -> int`
-- `trim_to_tokens(text: str, max_tokens: int, direction: Literal['start', 'end'], ellipsis: str=...) -> str`
-- Notable constants/configuration names: `APPROX_BUFFER`, `TRIM_BUFFER`, `EMBEDDED_IMAGE_DATA_PLACEHOLDER`, `_EMBEDDED_IMAGE_DATA_URL_PATTERN`.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: secret handling.
-- Imported dependency areas include: `re`, `tiktoken`, `typing`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `re.compile`, `tiktoken.get_encoding`, `encoding.encode`, `_EMBEDDED_IMAGE_DATA_URL_PATTERN.sub`, `approximate_tokens`, `count_tokens`, `sanitize_embedded_image_data_urls`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_chat_compaction.py`
- - `tests/test_default_prompt_budget.py`
- - `tests/test_history_compression_wait.py`
- - `tests/test_mcp_handler_multimodal.py`
- - `tests/test_oauth_codex.py`
- - `tests/test_oauth_gemini_api.py`
- - `tests/test_oauth_xai_grok.py`
- - `tests/test_ws_security.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/tool.py.dox.md b/helpers/tool.py.dox.md
deleted file mode 100644
index 5577d7fbc..000000000
--- a/helpers/tool.py.dox.md
+++ /dev/null
@@ -1,58 +0,0 @@
-# tool.py DOX
-
-## Purpose
-
-- Own the `tool.py` helper module.
-- This module defines the base agent tool class and response contract.
-- Keep this file-level DOX profile synchronized with `tool.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `tool.py` owns the runtime implementation.
-- `tool.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `Response` (no explicit base class)
-- `Tool` (no explicit base class)
- - `async execute(self, **kwargs) -> Response`
- - `async set_progress(self, content: str | None)`
- - `add_progress(self, content: str | None)`
- - `async before_execution(self, **kwargs)`
- - `async after_execution(self, response: Response, **kwargs)`
- - `get_log_object(self)`
- - `nice_key(self, key: str)`
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- `Tool` defines `execute(...)`.
-- Observed side-effect areas: settings/state persistence.
-- Imported dependency areas include: `abc`, `agent`, `dataclasses`, `helpers.extension`, `helpers.print_style`, `helpers.strings`, `typing`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `self.get_log_object`, `sanitize_string`, `self.agent.hist_add_tool_result`, `self.agent.context.log.log`, `key.split`, `join`, `call_extensions_async`, `response.message.strip`, `uuid.uuid4`, `PrintStyle`, `PrintStyle.stream`, `words.capitalize`, `word.lower`, `self.nice_key`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_a0_connector_prompt_gating.py`
- - `tests/test_browser_agent_regressions.py`
- - `tests/test_default_prompt_budget.py`
- - `tests/test_dirty_json.py`
- - `tests/test_document_query_plugin.py`
- - `tests/test_fastmcp_openapi_security.py`
- - `tests/test_host_browser_connector.py`
- - `tests/test_mcp_handler_multimodal.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/tunnel_common.py.dox.md b/helpers/tunnel_common.py.dox.md
deleted file mode 100644
index 2aa35d8ca..000000000
--- a/helpers/tunnel_common.py.dox.md
+++ /dev/null
@@ -1,51 +0,0 @@
-# tunnel_common.py DOX
-
-## Purpose
-
-- Own the `tunnel_common.py` helper module.
-- This module contains shared tunnel provider helper classes and event parsing.
-- Keep this file-level DOX profile synchronized with `tunnel_common.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `tunnel_common.py` owns the runtime implementation.
-- `tunnel_common.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `TunnelHelper` (no explicit base class)
- - `notify_starting(self, label)`
- - `notify_url_ready(self, label, url)`
- - `notify_stopped(self, label)`
-- `FlaredanticTunnelHelper` (`TunnelHelper`)
- - `build_tunnel(self)`
- - `start(self)`
- - `stop(self)`
-- Top-level functions:
-- `event_value(event)`
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: tunnel state.
-- Imported dependency areas include: `flaredantic`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `callable`, `self._notify`, `self.notify_starting`, `self.build_tunnel`, `self.tunnel.start`, `self.notify_stopped`, `self.notify_callback`, `self.notify_url_ready`, `self.tunnel.stop`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_tunnel_remote_link.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/tunnel_manager.py.dox.md b/helpers/tunnel_manager.py.dox.md
deleted file mode 100644
index c90749a20..000000000
--- a/helpers/tunnel_manager.py.dox.md
+++ /dev/null
@@ -1,51 +0,0 @@
-# tunnel_manager.py DOX
-
-## Purpose
-
-- Own the `tunnel_manager.py` helper module.
-- This module selects and coordinates configured tunnel providers.
-- Keep this file-level DOX profile synchronized with `tunnel_manager.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `tunnel_manager.py` owns the runtime implementation.
-- `tunnel_manager.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `TunnelManager` (no explicit base class)
- - `get_instance(cls)`
- - `get_notifications(self)`
- - `get_last_error(self)`
- - `start_tunnel(self, port=..., provider=...)`
- - `stop_tunnel(self)`
- - `get_tunnel_url(self)`
-- Top-level functions:
-- `normalize_provider(provider)`
-- Notable constants/configuration names: `SUPPORTED_TUNNEL_PROVIDERS`, `TUNNEL_PROVIDER_ALIASES`.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: tunnel state.
-- Imported dependency areas include: `collections`, `flaredantic`, `helpers.cloudflare_tunnel`, `helpers.microsoft_tunnel`, `helpers.print_style`, `helpers.serveo_tunnel`, `helpers.tailscale_tunnel`, `helpers.tunnel_common`, `threading`, `time`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `strip.lower`, `threading.Lock`, `join`, `ValueError`, `deque`, `self.notifications.clear`, `ServeoTunnelHelper`, `self._ensure_subscribed`, `strip`, `notifier.subscribe`, `CloudflareTunnel`, `MicrosoftDevTunnel`, `TailscaleTunnel`, `normalize_provider`, `threading.Thread`, `tunnel_thread.start`, `cls`, `event_value`, `PrintStyle.error`, `self._append_notification`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_tunnel_remote_link.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/tunnel_origins.py b/helpers/tunnel_origins.py
deleted file mode 100644
index c24d8e997..000000000
--- a/helpers/tunnel_origins.py
+++ /dev/null
@@ -1,107 +0,0 @@
-import json
-import urllib.request
-from urllib.parse import urlparse
-
-
-_DEFAULT_PORTS = {
- "http": 80,
- "https": 443,
- "ws": 80,
- "wss": 443,
-}
-
-
-def origin_from_url(value):
- """Normalize a URL or Origin header to scheme://host[:port]."""
- if not isinstance(value, str) or not value.strip():
- return None
- parsed = urlparse(value.strip())
- if not parsed.scheme or not parsed.hostname:
- return None
-
- scheme = parsed.scheme.lower()
- host = parsed.hostname.lower()
- try:
- port = parsed.port
- except ValueError:
- return None
-
- origin = f"{scheme}://{host}"
- if port and port != _DEFAULT_PORTS.get(scheme):
- origin += f":{port}"
- return origin
-
-
-def origin_key(value):
- """Return a comparable same-origin tuple including default ports."""
- origin = origin_from_url(value)
- if not origin:
- return None
- parsed = urlparse(origin)
- try:
- port = parsed.port or _DEFAULT_PORTS.get(parsed.scheme)
- except ValueError:
- return None
- if not parsed.scheme or not parsed.hostname or port is None:
- return None
- return parsed.scheme, parsed.hostname.lower(), int(port)
-
-
-def get_active_tunnel_origins():
- """Return normalized origins for currently active Remote Control URLs."""
- origins = []
-
- try:
- from helpers.tunnel_manager import TunnelManager
-
- tunnel_url = TunnelManager.get_instance().get_tunnel_url()
- _append_origin(origins, tunnel_url)
- except Exception:
- pass
-
- try:
- _append_origin(origins, _get_tunnel_service_url())
- except Exception:
- pass
-
- return origins
-
-
-def _append_origin(origins, url):
- origin = origin_from_url(url)
- if origin and origin not in origins:
- origins.append(origin)
-
-
-def _get_tunnel_service_url():
- try:
- from helpers import dotenv, runtime
-
- should_query_service = bool(
- runtime.is_dockerized()
- or runtime.get_arg("tunnel_api_port")
- or dotenv.get_dotenv_value("TUNNEL_API_PORT")
- )
- if not should_query_service:
- return None
-
- port = runtime.get_tunnel_api_port()
- except Exception:
- return None
-
- body = json.dumps({"action": "get"}).encode("utf-8")
- request = urllib.request.Request(
- f"http://localhost:{port}/",
- data=body,
- headers={"Content-Type": "application/json"},
- method="POST",
- )
- try:
- with urllib.request.urlopen(request, timeout=0.35) as response:
- payload = json.loads(response.read().decode("utf-8", errors="replace"))
- except Exception:
- return None
-
- if isinstance(payload, dict) and payload.get("success"):
- return payload.get("tunnel_url")
- return None
diff --git a/helpers/tunnel_origins.py.dox.md b/helpers/tunnel_origins.py.dox.md
deleted file mode 100644
index 83c829d8b..000000000
--- a/helpers/tunnel_origins.py.dox.md
+++ /dev/null
@@ -1,37 +0,0 @@
-# tunnel_origins.py DOX
-
-## Purpose
-
-- Own origin normalization for Remote Control tunnel URLs and CSRF/WebSocket same-origin checks.
-- Provide a small helper boundary between tunnel discovery and security enforcement.
-
-## Ownership
-
-- `tunnel_origins.py` owns the runtime implementation.
-- `tunnel_origins.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Top-level functions:
-- `origin_from_url(value)`
-- `origin_key(value)`
-- `get_active_tunnel_origins()`
-
-## Runtime Contracts
-
-- Normalize URL and Origin header values to `scheme://host[:port]`, omitting default ports.
-- Return comparable origin keys with default ports restored for same-origin checks.
-- Treat invalid, missing, or malformed origins as `None`.
-- Discover active tunnel origins from `TunnelManager` and the Docker tunnel API without raising if either source is unavailable.
-- Keep tunnel service lookups short-timeout and local-only.
-
-## Work Guidance
-
-- Keep parsing based on `urllib.parse` rather than hand-rolled string checks.
-- Preserve defensive exception handling because tunnel services are optional and may not be running.
-- Coordinate security-sensitive changes with CSRF and WebSocket tests.
-
-## Verification
-
-- Run `pytest tests/test_csrf_tunnel_origins.py tests/test_ws_csrf.py -q` after changing tunnel origin behavior.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/ui_server.py b/helpers/ui_server.py
index fdea68b01..0756c6e0d 100644
--- a/helpers/ui_server.py
+++ b/helpers/ui_server.py
@@ -1,7 +1,6 @@
from dataclasses import dataclass, field
from datetime import timedelta
import asyncio
-import json
import logging
import os
import secrets
@@ -27,7 +26,7 @@ from werkzeug.wrappers.request import Request as WerkzeugRequest
import socketio # type: ignore[import-untyped]
from helpers import dotenv, fasta2a_server, files, git, login, mcp_server, runtime
-from helpers.api import get_safe_next_url, register_api_route, requires_auth
+from helpers.api import register_api_route, requires_auth
from helpers.extension import extensible
from helpers.files import get_abs_path
from helpers.print_style import PrintStyle
@@ -38,19 +37,6 @@ from helpers.ws_manager import WsManager, set_shared_ws_manager
UPLOAD_LIMIT_BYTES = 5 * 1024 * 1024 * 1024
-SOCKETIO_PING_INTERVAL_SECONDS = 45
-SOCKETIO_PING_TIMEOUT_SECONDS = 120
-
-
-def _positive_int_env(name: str, default: int) -> int:
- raw_value = os.getenv(name)
- if raw_value is None:
- return default
- try:
- value = int(raw_value)
- except (TypeError, ValueError):
- return default
- return value if value > 0 else default
def configure_process_environment() -> None:
@@ -99,14 +85,8 @@ class UiServerRuntime:
cors_allowed_origins=lambda _origin, environ: validate_ws_origin(environ)[0],
logger=False,
engineio_logger=False,
- ping_interval=_positive_int_env(
- "A0_SOCKETIO_PING_INTERVAL_SECONDS",
- SOCKETIO_PING_INTERVAL_SECONDS,
- ),
- ping_timeout=_positive_int_env(
- "A0_SOCKETIO_PING_TIMEOUT_SECONDS",
- SOCKETIO_PING_TIMEOUT_SECONDS,
- ),
+ ping_interval=25,
+ ping_timeout=20,
max_http_buffer_size=50 * 1024 * 1024,
)
@@ -220,25 +200,19 @@ class UiRouteHandlers:
@extensible
async def login_handler(self):
error = None
- fallback_url = url_for("serve_index")
- next_url = get_safe_next_url(
- request.form.get("next") if request.method == "POST" else request.args.get("next"),
- fallback_url,
- )
-
if request.method == "POST":
user = dotenv.get_dotenv_value("AUTH_LOGIN")
password = dotenv.get_dotenv_value("AUTH_PASSWORD")
if request.form["username"] == user and request.form["password"] == password:
session["authentication"] = login.get_credentials_hash()
- return redirect(next_url or fallback_url)
+ return redirect(url_for("serve_index"))
else:
await asyncio.sleep(1)
error = "Invalid Credentials. Please try again."
login_page_content = files.read_file("webui/login.html")
- return render_template_string(login_page_content, error=error, next=next_url)
+ return render_template_string(login_page_content, error=error)
@extensible
async def logout_handler(self):
@@ -263,13 +237,6 @@ class UiRouteHandlers:
user_time_format_setting = str(settings_helper.get_settings().get("time_format", "12h"))
except Exception:
user_time_format_setting = "12h"
- try:
- user_ui_control_visibility = json.dumps(
- settings_helper.get_settings()["ui_control_visibility"],
- separators=(",", ":"),
- )
- except Exception:
- user_ui_control_visibility = json.dumps(settings_helper.UI_CONTROL_VISIBILITY_DEFAULTS)
index = files.read_file("webui/index.html")
return files.replace_placeholders_text(
@@ -281,7 +248,6 @@ class UiRouteHandlers:
logged_in=("true" if login.get_credentials_hash() else "false"),
user_timezone_setting=user_timezone_setting,
user_time_format_setting=user_time_format_setting,
- user_ui_control_visibility=user_ui_control_visibility,
)
@requires_auth
diff --git a/helpers/ui_server.py.dox.md b/helpers/ui_server.py.dox.md
deleted file mode 100644
index ae430843f..000000000
--- a/helpers/ui_server.py.dox.md
+++ /dev/null
@@ -1,60 +0,0 @@
-# ui_server.py DOX
-
-## Purpose
-
-- Own the `ui_server.py` helper module.
-- This module configures and owns Flask/WebUI runtime route handlers.
-- Keep this file-level DOX profile synchronized with `ui_server.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `ui_server.py` owns the runtime implementation.
-- `ui_server.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `UiServerRuntime` (no explicit base class)
- - `create(cls) -> 'UiServerRuntime'`
- - `refresh_runtime_settings(self) -> None`
- - `register_http_routes(self) -> None`
- - `register_transport_handlers(self) -> None`
- - `build_asgi_app(self, startup_monitor: StartupMonitor)`
- - `access_log_enabled(self) -> bool`
-- `UiRouteHandlers` (no explicit base class)
- - `async login_handler(self)`
- - `async logout_handler(self)`
- - `async serve_index(self)`
- - `async serve_builtin_plugin_asset(self, plugin_name, asset_path)`
- - `async serve_plugin_asset(self, plugin_name, asset_path)`
- - `async serve_extension_asset(self, asset_path)`
-- Top-level functions:
-- `_positive_int_env(name: str, default: int) -> int`
-- `configure_process_environment() -> None`
-- Notable constants/configuration names: `UPLOAD_LIMIT_BYTES`, `SOCKETIO_PING_INTERVAL_SECONDS`, `SOCKETIO_PING_TIMEOUT_SECONDS`, `A0_SOCKETIO_PING_INTERVAL_SECONDS`, `A0_SOCKETIO_PING_TIMEOUT_SECONDS`.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Socket.IO heartbeat defaults are intentionally longer than Engine.IO's short defaults so CLI sessions survive long prompt/context work; environment overrides must remain positive integers and fall back to source defaults when invalid.
-- Observed side-effect areas: filesystem reads, network calls, subprocess/runtime control, WebSocket state, plugin state, settings/state persistence, secret handling.
-- Imported dependency areas include: `asyncio`, `dataclasses`, `datetime`, `flask`, `helpers`, `helpers.api`, `helpers.extension`, `helpers.files`, `helpers.print_style`, `helpers.server_startup`, `helpers.ws`, `helpers.ws_manager`, `logging`, `os`, `secrets`, `socketio`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `logging.getLogger.setLevel`, `Localization.get.apply_process_timezone`, `_positive_int_env`, `field`, `Flask`, `threading.RLock`, `socketio.AsyncServer`, `WsManager`, `set_shared_ws_manager`, `cls`, `server_runtime.refresh_runtime_settings`, `settings_helper.get_settings`, `settings_helper.set_runtime_settings_snapshot`, `self.ws_manager.set_server_restart_broadcast`, `UiRouteHandlers`, `self.webapp.add_url_rule`, `register_api_route`, `register_ws_namespace`, `files.read_file`, `render_template_string`, `session.pop`.
-- `serve_index()` bootstraps the normalized UI control visibility map alongside timezone and time-format preferences so controls render correctly before Settings is opened.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/update_check.py.dox.md b/helpers/update_check.py.dox.md
deleted file mode 100644
index f25ef598a..000000000
--- a/helpers/update_check.py.dox.md
+++ /dev/null
@@ -1,41 +0,0 @@
-# update_check.py DOX
-
-## Purpose
-
-- Own the `update_check.py` helper module.
-- This module checks available Agent Zero updates.
-- Keep this file-level DOX profile synchronized with `update_check.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `update_check.py` owns the runtime implementation.
-- `update_check.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Top-level functions:
-- `async check_version()`
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: network calls, settings/state persistence.
-- Imported dependency areas include: `hashlib`, `helpers`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `git.get_version`, `git.is_official_agent_zero_repo`, `hashlib.sha256.hexdigest`, `httpx.AsyncClient`, `response.json`, `client.post`, `hashlib.sha256`, `runtime.get_persistent_id.encode`, `runtime.get_persistent_id`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/vector_db.py.dox.md b/helpers/vector_db.py.dox.md
deleted file mode 100644
index bc88f84a1..000000000
--- a/helpers/vector_db.py.dox.md
+++ /dev/null
@@ -1,53 +0,0 @@
-# vector_db.py DOX
-
-## Purpose
-
-- Own the `vector_db.py` helper module.
-- This module wraps FAISS vector storage, retrieval, and comparator behavior.
-- Keep this file-level DOX profile synchronized with `vector_db.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `vector_db.py` owns the runtime implementation.
-- `vector_db.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `MyFaiss` (`FAISS`)
- - `get_by_ids(self, ids: Sequence[str]) -> List[Document]`
- - `async aget_by_ids(self, ids: Sequence[str]) -> List[Document]`
- - `get_all_docs(self) -> dict[str, Document]`
-- `VectorDB` (no explicit base class)
- - `async search_by_similarity_threshold(self, query: str, limit: int, threshold: float, filter: str=...)`
- - `async search_by_metadata(self, filter: str, limit: int=...) -> list[Document]`
- - `async insert_documents(self, docs: list[Document])`
- - `async delete_documents_by_ids(self, ids: list[str])`
-- Top-level functions:
-- `format_docs_plain(docs: list[Document]) -> list[str]`
-- `cosine_normalizer(val: float) -> float`
-- `get_comparator(condition: str)`
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem deletion.
-- Imported dependency areas include: `agent`, `faiss`, `helpers`, `langchain.embeddings`, `langchain.storage`, `langchain_community.docstore.in_memory`, `langchain_community.vectorstores`, `langchain_community.vectorstores.utils`, `langchain_core.documents`, `simpleeval`, `typing`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `self.get_by_ids`, `agent.get_embedding_model`, `self._get_embeddings`, `faiss.IndexFlatIP`, `MyFaiss`, `get_comparator`, `self.db.get_all_docs`, `InMemoryByteStore`, `CacheBackedEmbeddings.from_bytes_store`, `self.db.asearch`, `comparator`, `guids.generate_id`, `zip`, `self.db.add_documents`, `self.db.aget_by_ids`, `simple_eval`, `self.embeddings.embed_query`, `InMemoryDocstore`, `self.db.adelete`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/virtual_desktop.py.dox.md b/helpers/virtual_desktop.py.dox.md
deleted file mode 100644
index 34e7b2510..000000000
--- a/helpers/virtual_desktop.py.dox.md
+++ /dev/null
@@ -1,74 +0,0 @@
-# virtual_desktop.py DOX
-
-## Purpose
-
-- Own the `virtual_desktop.py` helper module.
-- This module registers and proxies virtual desktop sessions.
-- Keep this file-level DOX profile synchronized with `virtual_desktop.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `virtual_desktop.py` owns the runtime implementation.
-- `virtual_desktop.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `VirtualDesktopEndpoint` (no explicit base class)
-- `VirtualDesktopRegistry` (no explicit base class)
- - `register(self, endpoint: VirtualDesktopEndpoint) -> None`
- - `unregister(self, token: str) -> None`
- - `proxy_for_token(self, token: str) -> VirtualDesktopEndpoint | None`
- - `resize(self, token: str, width: int, height: int) -> dict[str, Any]`
-- Top-level functions:
-- `register_session(token: str, host: str, port: int, owner: str=..., title: str=..., resize: ResizeCallback | None=...) -> None`
-- `unregister_session(token: str) -> None`
-- `proxy_for_token(token: str) -> VirtualDesktopEndpoint | None`
-- `resize_session(token: str, width: int, height: int) -> dict[str, Any]`
-- `get_registry() -> VirtualDesktopRegistry`
-- `session_url(token: str, title: str=...) -> str`
-- `collect_status() -> dict[str, Any]`
-- `find_xpra_html_root() -> Path | None`
-- `_package_installed(package: str) -> bool`
-- `normalize_size(width: int | float | str, height: int | float | str, max_width: int=..., max_height: int=..., min_width: int=..., min_height: int=...) -> tuple[int, int]`
-- `normalize_desktop_display_size(width: int | float | str, height: int | float | str, max_width: int=..., max_height: int=..., min_width: int=..., min_height: int=..., min_aspect_ratio: float=...) -> tuple[int, int]`
-- `resize_display(display: int, width: int, height: int, max_width: int=..., max_height: int=..., window_class: str=..., keys: tuple[str, ...]=..., xauthority: str=..., home: str=...) -> dict[str, Any]`
-- `_ensure_xrandr_mode(env: dict[str, str], width: int, height: int) -> None`
-- `_select_xrandr_mode(env: dict[str, str], width: int, height: int) -> subprocess.CompletedProcess[str]`
-- `_xrandr_output_modes(env: dict[str, str]) -> tuple[str, set[str]]`
-- `current_display_size(display: int, xauthority: str=..., home: str=...) -> tuple[int, int] | None`
-- `fit_window_until(display: int, width: int, height: int, window_class: str=..., keys: tuple[str, ...]=..., settle_seconds: float=..., timeout_seconds: float=..., process: subprocess.Popen[Any] | None=..., xauthority: str=..., home: str=...) -> None`
-- `fit_window(display: int, width: int, height: int, window_class: str=..., keys: tuple[str, ...]=..., xauthority: str=..., home: str=...) -> bool`
-- `has_window(display: int, window_class: str=..., name: str=..., xauthority: str=..., home: str=...) -> bool`
-- `find_window(display: int, window_class: str=..., name: str=..., xauthority: str=..., home: str=...) -> str`
-- `close_windows(display: int, names: tuple[str, ...]=..., window_class: str=..., xauthority: str=..., home: str=...) -> int`
-- `_find_window(display: int, window_class: str=..., name: str=..., xauthority: str=..., home: str=...) -> str`
-- `_display_env(display: int, xauthority: str=..., home: str=...) -> dict[str, str]`
-- Notable constants/configuration names: `STATE_DIR`, `DEFAULT_WIDTH`, `DEFAULT_HEIGHT`, `MAX_WIDTH`, `MAX_HEIGHT`, `MIN_WIDTH`, `MIN_HEIGHT`, `MIN_DESKTOP_ASPECT_RATIO`, `SESSION_PATH`, `XPRA_HTML_ROOT_CANDIDATES`.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem reads, filesystem writes, network calls, subprocess/runtime control, plugin state, settings/state persistence, secret handling.
-- Imported dependency areas include: `__future__`, `dataclasses`, `helpers`, `helpers.localization`, `math`, `os`, `pathlib`, `re`, `shutil`, `subprocess`, `threading`, `time`, `typing`, `urllib.parse`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `Path`, `files.get_abs_path`, `get_registry.register`, `get_registry.unregister`, `get_registry.proxy_for_token`, `get_registry.resize`, `quote`, `urlencode`, `find_xpra_html_root`, `subprocess.run`, `normalize_size`, `shutil.which`, `_display_env`, `current_display_size`, `_ensure_xrandr_mode`, `_select_xrandr_mode`, `time.sleep`, `strip`, `_xrandr_output_modes`, `result.stdout.splitlines`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_office_canvas_setup.py`
- - `tests/test_office_desktop_state.py`
- - `tests/test_office_document_store.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/virtual_desktop_routes.py.dox.md b/helpers/virtual_desktop_routes.py.dox.md
deleted file mode 100644
index b784fca80..000000000
--- a/helpers/virtual_desktop_routes.py.dox.md
+++ /dev/null
@@ -1,55 +0,0 @@
-# virtual_desktop_routes.py DOX
-
-## Purpose
-
-- Own the `virtual_desktop_routes.py` helper module.
-- This module installs virtual desktop gateway route hooks.
-- Keep this file-level DOX profile synchronized with `virtual_desktop_routes.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `virtual_desktop_routes.py` owns the runtime implementation.
-- `virtual_desktop_routes.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `VirtualDesktopGateway` (no explicit base class)
- - `async http(self, scope: Scope, receive: Receive, send: Send) -> None`
- - `async resize(self, scope: Scope, receive: Receive, send: Send) -> None`
- - `async proxy_http(self, scope: Scope, receive: Receive, send: Send, token: str, upstream_path: str) -> None`
- - `async websocket(self, scope: Scope, receive: Receive, send: Send) -> None`
- - `async open_websocket(self, endpoint: virtual_desktop.VirtualDesktopEndpoint, target: str, subprotocols: tuple[str, ...]) -> tuple[asyncio.StreamReader, asyncio.StreamWriter, WSConnection, str | None]`
- - `async browser_to_xpra(self, websocket: WebSocket, upstream: WSConnection, writer: asyncio.StreamWriter) -> None`
- - `async xpra_to_browser(self, websocket: WebSocket, upstream: WSConnection, reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None`
- - `session_request(self, path: str) -> tuple[str, str] | None`
-- Top-level functions:
-- `install_route_hooks() -> None`
-- `is_installed() -> bool`
-- Notable constants/configuration names: `HOP_BY_HOP_HEADERS`, `XPRA_MENU_CUSTOM_PATCH`, `XPRA_WINDOW_OFFSET_WARNING`, `XPRA_WINDOW_OFFSET_WARNING_PATCH`, `XPRA_WINDOW_SCRIPT`, `XPRA_WINDOW_SCRIPT_PATCH`.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem deletion, network calls, subprocess/runtime control, WebSocket state, settings/state persistence, secret handling.
-- Imported dependency areas include: `__future__`, `asyncio`, `flask.sessions`, `helpers`, `http.client`, `http.cookies`, `starlette.requests`, `starlette.responses`, `starlette.types`, `starlette.websockets`, `urllib.parse`, `wsproto`, `wsproto.events`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `self.relative_path`, `self.session_request`, `self.query`, `virtual_desktop.proxy_for_token`, `WebSocket`, `self.upstream_target`, `WSConnection`, `writer.write`, `rest.partition`, `unquote`, `quote`, `http.client.HTTPConnection`, `query_string.decode`, `urlsplit`, `location.startswith`, `path.startswith`, `parse_qs`, `login.get_credentials_hash`, `SecureCookieSessionInterface.get_signing_serializer`, `dict.get.decode`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_office_canvas_setup.py`
- - `tests/test_office_document_store.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/wait.py.dox.md b/helpers/wait.py.dox.md
deleted file mode 100644
index a647c28ef..000000000
--- a/helpers/wait.py.dox.md
+++ /dev/null
@@ -1,49 +0,0 @@
-# wait.py DOX
-
-## Purpose
-
-- Own the `wait.py` helper module.
-- This module formats and runs managed waits with progress updates.
-- Keep this file-level DOX profile synchronized with `wait.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `wait.py` owns the runtime implementation.
-- `wait.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Top-level functions:
-- `format_remaining_time(total_seconds: float) -> str`
-- `async managed_wait(agent, target_time, is_duration_wait, log, get_heading_callback)`
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Imported dependency areas include: `asyncio`, `helpers.localization`, `helpers.print_style`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `divmod`, `join`, `Localization.get.now`, `total_seconds`, `agent.handle_intervention`, `asyncio.sleep`, `pause_duration.total_seconds`, `PrintStyle.info`, `get_heading_callback`, `format_remaining_time`, `Localization.get.serialize_datetime`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/email_parser_test.py`
- - `tests/rate_limiter_test.py`
- - `tests/test_api_chat_lifetime.py`
- - `tests/test_browser_agent_regressions.py`
- - `tests/test_chat_compaction.py`
- - `tests/test_default_prompt_budget.py`
- - `tests/test_document_query_plugin.py`
- - `tests/test_download_toast_regressions.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/watchdog.py.dox.md b/helpers/watchdog.py.dox.md
deleted file mode 100644
index 920b4703c..000000000
--- a/helpers/watchdog.py.dox.md
+++ /dev/null
@@ -1,72 +0,0 @@
-# watchdog.py DOX
-
-## Purpose
-
-- Own the `watchdog.py` helper module.
-- This module registers filesystem watchdogs with debouncing and path filtering.
-- Keep this file-level DOX profile synchronized with `watchdog.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `watchdog.py` owns the runtime implementation.
-- `watchdog.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `_DispatchHandler` (no explicit base class)
- - `dispatch(self, event: Any)`
-- `_Watch` (no explicit base class)
-- `_PendingBatch` (no explicit base class)
-- `_WatchRegistry` (no explicit base class)
- - `add(self, id: str, roots: list[str], patterns: list[str] | None, ignore_patterns: list[str] | None, events: WatchEvents, debounce: float, handler: WatchHandler) -> None`
- - `remove(self, id: str) -> bool`
- - `clear(self) -> None`
- - `start(self) -> None`
- - `stop(self) -> None`
- - `dispatch(self, scheduled_root: str, event: Any) -> None`
-- Top-level functions:
-- `_normalize_root(root: str) -> str`
-- `_normalize_roots(roots: list[str]) -> list[str]`
-- `_normalize_patterns(patterns: list[str] | None, default: list[str] | None=...) -> list[str]`
-- `_normalize_events(events: WatchEvents) -> frozenset[WatchEvent]`
-- `_map_event_type(event_type: str) -> WatchEvent | None`
-- `_normalize_debounce(debounce: float) -> float`
-- `_covering_roots(roots: Iterable[str]) -> set[str]`
-- `_is_same_or_nested(path: str, root: str) -> bool`
-- `_is_under_watch(path: str, watch: _Watch) -> bool`
-- `_compile_matcher(root: str, patterns: list[str], ignore_patterns: list[str]) -> PatternMatcher`
-- `_compile_single_matcher(root: str, patterns: list[str]) -> PatternMatcher`
-- `add_watchdog(id: str, roots: list[str], patterns: list[str] | None=..., ignore_patterns: list[str] | None=..., events: WatchEvents=..., debounce: float=..., handler: WatchHandler | None=...) -> None`
-- `remove_watchdog(id: str) -> bool`
-- `clear_watchdogs() -> None`
-- `start_watchdog_daemon() -> None`
-- `stop_watchdog_daemon() -> None`
-- Notable constants/configuration names: `_DEFAULT_PATTERNS`, `_DEFAULT_IGNORE_PATTERNS`, `_VALID_EVENTS`, `_EVENT_ALIASES`.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem reads, filesystem writes, filesystem deletion.
-- Imported dependency areas include: `__future__`, `dataclasses`, `os`, `pathlib`, `threading`, `typing`, `watchdog.observers`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `frozenset`, `dataclass`, `_WatchRegistry`, `_registry.start`, `os.path.abspath`, `_compile_single_matcher`, `_registry.add`, `_registry.remove`, `_registry.clear`, `_registry.stop`, `self.registry.dispatch`, `threading.RLock`, `self._ensure_watchdog_available`, `_normalize_roots`, `_normalize_patterns`, `_normalize_events`, `_normalize_debounce`, `self._stop_observer`, `_map_event_type`, `_covering_roots`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_model_config_api_keys.py`
- - `tests/test_model_config_project_presets.py`
- - `tests/test_time_travel.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/ws.py b/helpers/ws.py
index 5ca1cc79c..1150aa8dc 100644
--- a/helpers/ws.py
+++ b/helpers/ws.py
@@ -13,7 +13,6 @@ from flask import Flask, session, request
from helpers import files, cache
from helpers.print_style import PrintStyle
from helpers.errors import format_error
-from helpers.tunnel_origins import get_active_tunnel_origins, origin_key
if TYPE_CHECKING:
from helpers.ws_manager import WsManager
@@ -149,11 +148,6 @@ def validate_ws_origin(environ: dict[str, Any]) -> tuple[bool, str | None]:
if origin_host == host and origin_port == port:
return True, None
- request_origin_key = (origin_parsed.scheme, origin_host, int(origin_port))
- for active_origin in get_active_tunnel_origins():
- if origin_key(active_origin) == request_origin_key:
- return True, None
-
if origin_host not in {host for host, _ in candidates}:
return False, "origin_host_mismatch"
return False, "origin_port_mismatch"
@@ -654,4 +648,4 @@ def _error_response(code: str, message: str,
"ok": False,
"error": {"code": code, "error": message},
}],
- }
+ }
\ No newline at end of file
diff --git a/helpers/ws.py.dox.md b/helpers/ws.py.dox.md
deleted file mode 100644
index 5eb7279f6..000000000
--- a/helpers/ws.py.dox.md
+++ /dev/null
@@ -1,75 +0,0 @@
-# ws.py DOX
-
-## Purpose
-
-- Own the `ws.py` helper module.
-- This module defines WebSocket handler registration, origin validation, and security checks.
-- Keep this file-level DOX profile synchronized with `ws.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `ws.py` owns the runtime implementation.
-- `ws.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `ConnectionNotFoundError` (`RuntimeError`)
-- `_SecurityContext` (no explicit base class)
-- `WsHandler` (no explicit base class)
- - `namespace(self) -> str`
- - `manager(self) -> 'WsManager'`
- - `identifier(self) -> str`
- - `bind_manager(self, manager: 'WsManager', namespace: str | None=...) -> None`
- - `requires_loopback(cls) -> bool`
- - `requires_api_key(cls) -> bool`
- - `requires_auth(cls) -> bool`
- - `requires_csrf(cls) -> bool`
-- Top-level functions:
-- `_ws_debug_enabled() -> bool`: Check A0_WS_DEBUG env var - lightweight, no heavy imports.
-- `ws_debug(message: str) -> None`: Log *message* via :class:`PrintStyle` when ``A0_WS_DEBUG`` is active.
-- `_default_port_for_scheme(scheme: str) -> int | None`
-- `normalize_origin(value: Any) -> str | None`: Normalize an Origin/Referer header value to scheme://host[:port].
-- `_parse_host_header(value: Any) -> tuple[str | None, int | None]`
-- `validate_ws_origin(environ: dict[str, Any]) -> tuple[bool, str | None]`: Validate the browser Origin during the Socket.IO handshake.
-- `_check_security(handler_cls: type[WsHandler], ctx: _SecurityContext) -> dict[str, Any] | None`: Return an error payload dict if the check fails, or ``None`` on success.
-- `register_ws_namespace(socketio_server: socketio.AsyncServer, webapp: Flask, lock: ThreadLockType, manager: 'WsManager | None'=...) -> None`
-- `_error_response(code: str, message: str, correlation_id: str) -> dict[str, Any]`
-- Notable constants/configuration names: `NAMESPACE`, `CACHE_AREA`.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- `WsHandler` defines `process(...)`.
-- `WsHandler` defines `requires_auth(...)`.
-- `WsHandler` defines `requires_csrf(...)`.
-- `WsHandler` defines `requires_api_key(...)`.
-- `WsHandler` defines `requires_loopback(...)`.
-- Observed side-effect areas: filesystem reads, filesystem deletion, network calls, WebSocket state, plugin state, settings/state persistence, secret handling.
-- Imported dependency areas include: `abc`, `dataclasses`, `flask`, `helpers`, `helpers.errors`, `helpers.network`, `helpers.print_style`, `os`, `pathlib`, `socketio`, `threading`, `typing`, `urllib.parse`, `uuid`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `threading.Lock`, `os.getenv.strip.lower`, `_ws_debug_enabled`, `urlparse`, `normalize_origin`, `_parse_host_header`, `handler_cls.requires_loopback`, `handler_cls.requires_auth`, `handler_cls.requires_csrf`, `handler_cls.requires_api_key`, `socketio_server.on`, `PrintStyle.debug`, `value.strip`, `origin_parsed.hostname.lower`, `_default_port_for_scheme`, `req_host.lower`, `forwarded_host_raw.strip`, `forwarded_host_raw.split.strip`, `forwarded_proto_raw.strip`, `forwarded_proto_raw.split.strip.lower`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_a0_connector_computer_use_metadata.py`
- - `tests/test_a0_connector_prompt_gating.py`
- - `tests/test_browser_agent_regressions.py`
- - `tests/test_docker_release_plan.py`
- - `tests/test_download_toast_regressions.py`
- - `tests/test_git_version_label.py`
- - `tests/test_host_browser_connector.py`
- - `tests/test_multi_tab_isolation.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/ws_manager.py.dox.md b/helpers/ws_manager.py.dox.md
deleted file mode 100644
index 3d8f6da61..000000000
--- a/helpers/ws_manager.py.dox.md
+++ /dev/null
@@ -1,70 +0,0 @@
-# ws_manager.py DOX
-
-## Purpose
-
-- Own the `ws_manager.py` helper module.
-- This module manages WebSocket connections, event validation, buffering, and dispatch.
-- Keep this file-level DOX profile synchronized with `ws_manager.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `ws_manager.py` owns the runtime implementation.
-- `ws_manager.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Classes:
-- `WsResult` (no explicit base class)
- - `ok(cls, data: dict[str, Any] | None=..., correlation_id: str | None=..., duration_ms: float | None=...) -> 'WsResult'`
- - `error(cls, code: str, message: str, details: Any | None=..., correlation_id: str | None=..., duration_ms: float | None=...) -> 'WsResult'`
- - `as_result(self, handler_id: str, fallback_correlation_id: str | None, duration_ms: float | None=...) -> dict[str, Any]`
-- `BufferedEvent` (no explicit base class)
-- `ConnectionInfo` (no explicit base class)
-- `_HandlerExecution` (no explicit base class)
-- `WsManager` (no explicit base class)
- - `register_diagnostic_watcher(self, namespace: str, sid: str) -> bool`
- - `unregister_diagnostic_watcher(self, namespace: str, sid: str) -> None`
- - `register_handlers(self, handlers_by_namespace: dict[str, Iterable[WsHandler]]) -> None`
- - `iter_namespaces(self) -> list[str]`
- - `async process_client_event(self, namespace: str, event_type: str, data: dict[str, Any], sid: str, handlers: list[WsHandler]) -> dict[str, Any]`
- - `async handle_connect(self, namespace: str, sid: str, user_id: str | None=...) -> None`
- - `async handle_disconnect(self, namespace: str, sid: str) -> None`
- - `async route_event(self, namespace: str, event_type: str, data: dict[str, Any], sid: str, ack: Optional[Callable[[Any], None]]=..., include_handlers: Set[str] | None=..., exclude_handlers: Set[str] | None=..., allow_exclude: bool=..., handler_id: str | None=...) -> dict[str, Any]`
-- Top-level functions:
-- `validate_event_type(event_type: str) -> str`: Validate an event name: must be lowercase_snake_case and not reserved.
-- `async send_data(event_type: str, data: dict[str, Any], endpoint_name: str=..., connection_id: str | None=...) -> None`: Convenience wrapper around :pymeth:`WsManager.send_data`.
-- `_utcnow() -> datetime`
-- `set_shared_ws_manager(manager: 'WsManager') -> None`
-- `get_shared_ws_manager() -> 'WsManager'`
-- Notable constants/configuration names: `_EVENT_NAME_PATTERN`, `_RESERVED_EVENT_NAMES`, `BUFFER_MAX_SIZE`, `BUFFER_TTL`, `DIAGNOSTIC_EVENT`, `LIFECYCLE_CONNECT_EVENT`, `LIFECYCLE_DISCONNECT_EVENT`, `STATE_PUSH_EVENT`, `SERVER_RESTART_EVENT`, `ERR_NO_HANDLERS`, `ERR_HANDLER_ERROR`, `ERR_INVALID_FILTER`, `ERR_INVALID_EVENT`, `ERR_CONNECTION_NOT_FOUND`, `ERR_TIMEOUT`.
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem deletion, network calls, WebSocket state, settings/state persistence, scheduler state.
-- Imported dependency areas include: `__future__`, `asyncio`, `collections`, `dataclasses`, `datetime`, `helpers`, `helpers.defer`, `helpers.print_style`, `helpers.ws`, `os`, `re`, `socketio`, `threading`, `time`, `typing`, `uuid`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `re.compile`, `timedelta`, `get_shared_ws_manager`, `datetime.now`, `field`, `cls`, `TypeError`, `_EVENT_NAME_PATTERN.fullmatch`, `ValueError`, `manager.send_data`, `RuntimeError`, `defaultdict`, `runtime.is_development`, `ws_debug`, `self._ensure_dispatcher_loop`, `dispatcher_loop.is_closed`, `asyncio.run_coroutine_threadsafe`, `_utcnow.isoformat.replace`, `self._copy_diagnostic_watchers`, `self._lifecycle_tasks.add`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_browser_agent_regressions.py`
- - `tests/test_host_browser_connector.py`
- - `tests/test_state_sync_handler.py`
- - `tests/test_state_sync_welcome_screen.py`
- - `tests/test_tool_action_contracts.py`
- - `tests/test_ws_handlers.py`
- - `tests/test_ws_manager.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/helpers/yaml.py.dox.md b/helpers/yaml.py.dox.md
deleted file mode 100644
index 41f1da445..000000000
--- a/helpers/yaml.py.dox.md
+++ /dev/null
@@ -1,52 +0,0 @@
-# yaml.py DOX
-
-## Purpose
-
-- Own the `yaml.py` helper module.
-- This module wraps YAML loading/dumping and JSON conversion helpers.
-- Keep this file-level DOX profile synchronized with `yaml.py` because this directory is intentionally flat.
-
-## Ownership
-
-- `yaml.py` owns the runtime implementation.
-- `yaml.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
-- Top-level functions:
-- `loads(text: str)`
-- `dumps(obj, **kwargs) -> str`
-- `from_json(text: str, **yaml_dump_kwargs) -> str`
-- `to_json(text: str, **json_dump_kwargs) -> str`
-
-## Runtime Contracts
-
-- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
-- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- Observed side-effect areas: filesystem writes, settings/state persistence.
-- Imported dependency areas include: `json`, `yaml`.
-
-## Key Concepts
-
-- Important called helpers/classes observed in the source: `yaml.safe_load`, `yaml.safe_dump`, `dumps`, `loads`, `json.dumps`, `json.loads`.
-- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
-
-## Work Guidance
-
-- Preserve public helper APIs used by core code and plugins unless every caller is updated.
-- Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded.
-- Prefer adding cohesive helper functions here only when behavior is reused across modules.
-
-## Verification
-
-- Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers.
-- Related tests observed by source search:
- - `tests/test_a0_connector_prompt_gating.py`
- - `tests/test_browser_agent_regressions.py`
- - `tests/test_document_query_plugin.py`
- - `tests/test_model_config_api_keys.py`
- - `tests/test_model_config_project_presets.py`
- - `tests/test_oauth_codex.py`
- - `tests/test_oauth_providers.py`
- - `tests/test_office_canvas_setup.py`
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/knowledge/AGENTS.md b/knowledge/AGENTS.md
index 4ee093f65..8075b4686 100644
--- a/knowledge/AGENTS.md
+++ b/knowledge/AGENTS.md
@@ -13,7 +13,7 @@
## Local Contracts
-- Do not place secrets, chat transcripts, private user data, or local deployment details in repository knowledge.
+- Do not place secrets, chat transcripts, private user memory, or local deployment details in repository knowledge.
- Keep self-knowledge consistent with current architecture, configuration, capabilities, and setup docs.
- Markdown here is for agent runtime context, not general marketing copy.
diff --git a/knowledge/main/about/setup-and-deployment.md b/knowledge/main/about/setup-and-deployment.md
index 6ed3456b7..90fd33f86 100644
--- a/knowledge/main/about/setup-and-deployment.md
+++ b/knowledge/main/about/setup-and-deployment.md
@@ -21,6 +21,7 @@ For local development:
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
+pip install -r requirements2.txt
python run_ui.py
```
diff --git a/models.py b/models.py
index 811fa31e0..8518f0d1e 100644
--- a/models.py
+++ b/models.py
@@ -14,19 +14,19 @@ from typing import (
TypedDict,
)
-from litellm import embedding
+from litellm import completion, acompletion, embedding
import litellm
import openai
+from litellm.types.utils import ModelResponse
from helpers import dotenv
-from helpers import settings, images
+from helpers import settings, dirty_json, images
from helpers.dotenv import load_dotenv
from helpers.providers import ModelType as ProviderModelType, get_provider_config
from helpers.rate_limiter import RateLimiter
from helpers.tokens import approximate_tokens
+from helpers import dirty_json
from helpers.extension import extensible # extensible: allows plugins to intercept get_api_key()
-from helpers.litellm_transport import LiteLLMTransport, ResponsesTransport
-from helpers.llm_result import LLMResult
from langchain_core.language_models.chat_models import SimpleChatModel
from langchain_core.outputs.chat_generation import ChatGenerationChunk
@@ -45,53 +45,6 @@ from sentence_transformers import SentenceTransformer
from pydantic import ConfigDict
-DEFAULT_LITELLM_GLOBAL_KWARGS: dict[str, Any] = {
- "drop_params": True,
-}
-
-# LiteLLM documents drop_params as both a module-level switch and per-call kwarg.
-# Other entries in litellm_global_kwargs, such as timeout or additional_drop_params,
-# are kept as per-call kwargs instead of becoming arbitrary module attributes.
-LITELLM_MODULE_GLOBAL_KEYS = frozenset({"drop_params"})
-
-
-def _normalize_litellm_kwargs(values: dict[str, Any]) -> dict[str, Any]:
- # Normalize .env/UI-style scalar strings into native types for LiteLLM.
- result: dict[str, Any] = {}
- for k, v in values.items():
- if isinstance(v, str):
- stripped = v.strip()
- lowered = stripped.lower()
- if lowered == "true":
- result[k] = True
- elif lowered == "false":
- result[k] = False
- elif lowered in ("none", "null"):
- result[k] = None
- else:
- try:
- result[k] = int(stripped)
- except ValueError:
- try:
- result[k] = float(stripped)
- except ValueError:
- result[k] = v
- else:
- result[k] = v
- return result
-
-
-def get_litellm_global_kwargs() -> dict[str, Any]:
- kwargs = _normalize_litellm_kwargs(DEFAULT_LITELLM_GLOBAL_KWARGS)
- try:
- configured = settings.get_settings().get("litellm_global_kwargs", {}) # type: ignore[union-attr]
- except Exception:
- configured = {}
- if isinstance(configured, dict):
- kwargs.update(_normalize_litellm_kwargs(configured))
- return kwargs
-
-
# keep provider logging quiet in normal operation
def turn_off_logging():
os.environ["LITELLM_LOG"] = "ERROR" # only errors
@@ -102,32 +55,9 @@ def turn_off_logging():
logging.getLogger(name).setLevel(logging.ERROR)
-def set_litellm_params():
- global_kwargs = get_litellm_global_kwargs()
- for key, value in global_kwargs.items():
- if key not in LITELLM_MODULE_GLOBAL_KEYS:
- continue
- setattr(litellm, key, value)
- return global_kwargs
-
-
-def configure_litellm():
- turn_off_logging()
- set_litellm_params()
-
-
-def _merge_litellm_call_kwargs(*overrides: dict[str, Any] | None) -> dict[str, Any]:
- kwargs = get_litellm_global_kwargs()
- for override in overrides:
- if isinstance(override, dict):
- kwargs.update(override)
- return kwargs
-
-
# init
load_dotenv()
-configure_litellm()
-
+turn_off_logging()
class ModelType(Enum):
CHAT = "Chat"
@@ -162,7 +92,6 @@ class ChatChunk(TypedDict):
response_delta: str
reasoning_delta: str
-
class ChatGenerationResult:
"""Chat generation result object"""
def __init__(self, chunk: ChatChunk|None = None):
@@ -442,6 +371,14 @@ class LiteLLMChatWrapper(SimpleChatModel):
result.append(message_dict)
+ if explicit_caching and result:
+ if result[0]["role"] == "system":
+ result[0]["cache_control"] = {"type": "ephemeral"}
+ for i in range(len(result) - 1, -1, -1):
+ if result[i]["role"] == "assistant":
+ result[i]["cache_control"] = {"type": "ephemeral"}
+ break
+
return result
def _call(
@@ -451,20 +388,21 @@ class LiteLLMChatWrapper(SimpleChatModel):
run_manager: Optional[CallbackManagerForLLMRun] = None,
**kwargs: Any,
) -> str:
- configure_litellm()
+ import asyncio
+
msgs = self._convert_messages(messages)
# Apply rate limiting if configured
apply_rate_limiter_sync(self.a0_model_conf, str(msgs))
- call_kwargs = _merge_litellm_call_kwargs(self.kwargs, kwargs)
- transport = LiteLLMTransport(
- model=self.model_name,
- messages=msgs,
- kwargs=call_kwargs,
- stop=stop,
+ # Call the model
+ call_kwargs = _without_stream_kwarg({**self.kwargs, **kwargs})
+ resp = completion(
+ model=self.model_name, messages=msgs, stop=stop, **call_kwargs
)
- parsed = transport.complete()
+
+ # Parse output
+ parsed = _parse_chunk(resp)
output = ChatGenerationResult(parsed).output()
return output["response_delta"]
@@ -475,22 +413,28 @@ class LiteLLMChatWrapper(SimpleChatModel):
run_manager: Optional[CallbackManagerForLLMRun] = None,
**kwargs: Any,
) -> Iterator[ChatGenerationChunk]:
- configure_litellm()
+ import asyncio
+
msgs = self._convert_messages(messages)
# Apply rate limiting if configured
apply_rate_limiter_sync(self.a0_model_conf, str(msgs))
result = ChatGenerationResult()
- call_kwargs = _merge_litellm_call_kwargs(self.kwargs, kwargs)
- transport = LiteLLMTransport(
+ call_kwargs = _without_stream_kwarg({**self.kwargs, **kwargs})
+
+ for chunk in completion(
model=self.model_name,
messages=msgs,
- kwargs=call_kwargs,
+ stream=True,
stop=stop,
- )
- for parsed in transport.stream():
- output = result.add_chunk(parsed)
+ **call_kwargs,
+ ):
+ # parse chunk
+ parsed = _parse_chunk(chunk) # chunk parsing
+ output = result.add_chunk(parsed) # chunk processing
+
+ # Only yield chunks with non-None content
if output["response_delta"]:
yield ChatGenerationChunk(
message=AIMessageChunk(content=output["response_delta"])
@@ -503,22 +447,27 @@ class LiteLLMChatWrapper(SimpleChatModel):
run_manager: Optional[AsyncCallbackManagerForLLMRun] = None,
**kwargs: Any,
) -> AsyncIterator[ChatGenerationChunk]:
- configure_litellm()
msgs = self._convert_messages(messages)
# Apply rate limiting if configured
await apply_rate_limiter(self.a0_model_conf, str(msgs))
result = ChatGenerationResult()
- call_kwargs = _merge_litellm_call_kwargs(self.kwargs, kwargs)
- transport = LiteLLMTransport(
+ call_kwargs = _without_stream_kwarg({**self.kwargs, **kwargs})
+
+ response = await acompletion(
model=self.model_name,
messages=msgs,
- kwargs=call_kwargs,
+ stream=True,
stop=stop,
+ **call_kwargs,
)
- async for parsed in transport.astream():
- output = result.add_chunk(parsed)
+ async for chunk in response: # type: ignore
+ # parse chunk
+ parsed = _parse_chunk(chunk) # chunk parsing
+ output = result.add_chunk(parsed) # chunk processing
+
+ # Only yield chunks with non-None content
if output["response_delta"]:
yield ChatGenerationChunk(
message=AIMessageChunk(content=output["response_delta"])
@@ -539,7 +488,7 @@ class LiteLLMChatWrapper(SimpleChatModel):
**kwargs: Any,
) -> Tuple[str, str]:
- configure_litellm()
+ turn_off_logging()
if not messages:
messages = []
@@ -558,19 +507,10 @@ class LiteLLMChatWrapper(SimpleChatModel):
)
# Prepare call kwargs and retry config (strip A0-only params before calling LiteLLM)
- call_kwargs: dict[str, Any] = _merge_litellm_call_kwargs(
- self.kwargs, kwargs
- )
- if explicit_caching:
- call_kwargs["a0_explicit_prompt_caching"] = True
+ call_kwargs: dict[str, Any] = _without_stream_kwarg({**self.kwargs, **kwargs})
max_retries: int = int(call_kwargs.pop("a0_retry_attempts", 2))
retry_delay_s: float = float(call_kwargs.pop("a0_retry_delay_seconds", 1.5))
stream = reasoning_callback is not None or response_callback is not None or tokens_callback is not None
- transport = LiteLLMTransport(
- model=self.model_name,
- messages=msgs_conv,
- kwargs=call_kwargs,
- )
# results
result = ChatGenerationResult()
@@ -579,45 +519,60 @@ class LiteLLMChatWrapper(SimpleChatModel):
while True:
got_any_chunk = False
try:
- if stream:
- stop_response: str | None = None
- async for parsed in transport.astream():
- got_any_chunk = True
- output = result.add_chunk(parsed)
+ # call model
+ _completion = await acompletion(
+ model=self.model_name,
+ messages=msgs_conv,
+ stream=stream,
+ **call_kwargs,
+ )
- # collect reasoning delta and call callbacks
- if output["reasoning_delta"]:
- if reasoning_callback:
- await reasoning_callback(output["reasoning_delta"], result.reasoning)
- if tokens_callback:
- await tokens_callback(
- output["reasoning_delta"],
- approximate_tokens(output["reasoning_delta"]),
- )
- # Add output tokens to rate limiter if configured
- if limiter:
- limiter.add(output=approximate_tokens(output["reasoning_delta"]))
- # collect response delta and call callbacks
- if output["response_delta"]:
- if response_callback:
- stop_response = await response_callback(
- output["response_delta"], result.response
- )
- if tokens_callback:
- await tokens_callback(
- output["response_delta"],
- approximate_tokens(output["response_delta"]),
- )
- # Add output tokens to rate limiter if configured
- if limiter:
- limiter.add(output=approximate_tokens(output["response_delta"]))
- if stop_response is not None:
- result.response = stop_response
- break
+ if stream:
+ # iterate over chunks
+ stop_response: str | None = None
+ try:
+ async for chunk in _completion: # type: ignore
+ got_any_chunk = True
+ # parse chunk
+ parsed = _parse_chunk(chunk)
+ output = result.add_chunk(parsed)
+
+ # collect reasoning delta and call callbacks
+ if output["reasoning_delta"]:
+ if reasoning_callback:
+ await reasoning_callback(output["reasoning_delta"], result.reasoning)
+ if tokens_callback:
+ await tokens_callback(
+ output["reasoning_delta"],
+ approximate_tokens(output["reasoning_delta"]),
+ )
+ # Add output tokens to rate limiter if configured
+ if limiter:
+ limiter.add(output=approximate_tokens(output["reasoning_delta"]))
+ # collect response delta and call callbacks
+ if output["response_delta"]:
+ if response_callback:
+ stop_response = await response_callback(
+ output["response_delta"], result.response
+ )
+ if tokens_callback:
+ await tokens_callback(
+ output["response_delta"],
+ approximate_tokens(output["response_delta"]),
+ )
+ # Add output tokens to rate limiter if configured
+ if limiter:
+ limiter.add(output=approximate_tokens(output["response_delta"]))
+ if stop_response is not None:
+ result.response = stop_response
+ break
+ finally:
+ if stop_response is not None and hasattr(_completion, "aclose"):
+ await _completion.aclose() # type: ignore[attr-defined]
# non-stream response
else:
- parsed = await transport.acomplete()
+ parsed = _parse_chunk(_completion)
output = result.add_chunk(parsed)
if limiter:
if output["response_delta"]:
@@ -637,151 +592,6 @@ class LiteLLMChatWrapper(SimpleChatModel):
attempt += 1
await asyncio.sleep(retry_delay_s)
- async def unified_turn(
- self,
- system_message="",
- user_message="",
- messages: List[BaseMessage] | None = None,
- response_callback: Callable[[str, str], Awaitable[str | None]] | None = None,
- reasoning_callback: Callable[[str, str], Awaitable[None]] | None = None,
- tokens_callback: Callable[[str, int], Awaitable[None]] | None = None,
- rate_limiter_callback: (
- Callable[[str, str, int, int], Awaitable[bool]] | None
- ) = None,
- explicit_caching: bool = False,
- **kwargs: Any,
- ) -> LLMResult:
- """Canonical internal LLM turn with Responses metadata.
-
- Public plugin-facing callers should keep using ``unified_call``. Core
- orchestration uses this method when it needs response ids, native output
- items, and state/capability metadata.
- """
-
- configure_litellm()
-
- if not messages:
- messages = []
- if system_message:
- messages.insert(0, SystemMessage(content=system_message))
- if user_message:
- messages.append(HumanMessage(content=user_message))
-
- msgs_conv = self._convert_messages(messages, explicit_caching=explicit_caching)
-
- limiter = await apply_rate_limiter(
- self.a0_model_conf, str(msgs_conv), rate_limiter_callback
- )
-
- call_kwargs: dict[str, Any] = _merge_litellm_call_kwargs(
- self.kwargs, kwargs
- )
- if explicit_caching:
- call_kwargs["a0_explicit_prompt_caching"] = True
- max_retries: int = int(call_kwargs.pop("a0_retry_attempts", 2))
- retry_delay_s: float = float(call_kwargs.pop("a0_retry_delay_seconds", 1.5))
- stream = (
- reasoning_callback is not None
- or response_callback is not None
- or tokens_callback is not None
- )
- transport = LiteLLMTransport(
- model=self.model_name,
- messages=msgs_conv,
- kwargs=call_kwargs,
- )
-
- result = ChatGenerationResult()
-
- attempt = 0
- while True:
- got_any_chunk = False
- try:
- if stream:
- stop_response: str | None = None
- async for parsed in transport.astream():
- got_any_chunk = True
- output = result.add_chunk(parsed)
-
- if output["reasoning_delta"]:
- if reasoning_callback:
- await reasoning_callback(
- output["reasoning_delta"], result.reasoning
- )
- if tokens_callback:
- await tokens_callback(
- output["reasoning_delta"],
- approximate_tokens(output["reasoning_delta"]),
- )
- if limiter:
- limiter.add(
- output=approximate_tokens(
- output["reasoning_delta"]
- )
- )
-
- if output["response_delta"]:
- if response_callback:
- stop_response = await response_callback(
- output["response_delta"], result.response
- )
- if tokens_callback:
- await tokens_callback(
- output["response_delta"],
- approximate_tokens(output["response_delta"]),
- )
- if limiter:
- limiter.add(
- output=approximate_tokens(
- output["response_delta"]
- )
- )
- if (
- stop_response is not None
- and not transport.policy.using_responses
- ):
- result.response = stop_response
- break
- if stop_response is not None:
- result.response = stop_response
- else:
- parsed = await transport.acomplete()
- output = result.add_chunk(parsed)
- if limiter:
- if output["response_delta"]:
- limiter.add(
- output=approximate_tokens(output["response_delta"])
- )
- if output["reasoning_delta"]:
- limiter.add(
- output=approximate_tokens(output["reasoning_delta"])
- )
-
- llm_result = transport.last_result or LLMResult.from_chat(
- response=result.output()["response_delta"],
- reasoning=result.output()["reasoning_delta"],
- input_items=ResponsesTransport.input_from_messages(msgs_conv),
- provider_model_key=self.model_name,
- capability=transport._capability_metadata(),
- )
- if result.output()["response_delta"]:
- llm_result.response = result.output()["response_delta"]
- if result.output()["reasoning_delta"]:
- llm_result.reasoning = result.output()["reasoning_delta"]
- return llm_result
-
- except Exception as e:
- import asyncio
-
- if (
- got_any_chunk
- or not _is_transient_litellm_error(e)
- or attempt >= max_retries
- ):
- raise
- attempt += 1
- await asyncio.sleep(retry_delay_s)
-
class LiteLLMEmbeddingWrapper(Embeddings):
model_name: str
@@ -800,30 +610,20 @@ class LiteLLMEmbeddingWrapper(Embeddings):
self.a0_model_conf = model_config
def embed_documents(self, texts: List[str]) -> List[List[float]]:
- configure_litellm()
# Apply rate limiting if configured
apply_rate_limiter_sync(self.a0_model_conf, " ".join(texts))
- resp = embedding(
- model=self.model_name,
- input=texts,
- **_merge_litellm_call_kwargs(self.kwargs),
- )
+ resp = embedding(model=self.model_name, input=texts, **self.kwargs)
return [
item.get("embedding") if isinstance(item, dict) else item.embedding # type: ignore
for item in resp.data # type: ignore
]
def embed_query(self, text: str) -> List[float]:
- configure_litellm()
# Apply rate limiting if configured
apply_rate_limiter_sync(self.a0_model_conf, text)
- resp = embedding(
- model=self.model_name,
- input=[text],
- **_merge_litellm_call_kwargs(self.kwargs),
- )
+ resp = embedding(model=self.model_name, input=[text], **self.kwargs)
item = resp.data[0] # type: ignore
return item.get("embedding") if isinstance(item, dict) else item.embedding # type: ignore
@@ -936,6 +736,38 @@ def _get_litellm_embedding(
)
+def _parse_chunk(chunk: Any) -> ChatChunk:
+ delta = chunk["choices"][0].get("delta", {})
+ message = chunk["choices"][0].get("message", {}) or chunk["choices"][0].get(
+ "model_extra", {}
+ ).get("message", {})
+ response_delta = (
+ delta.get("content", "")
+ if isinstance(delta, dict)
+ else getattr(delta, "content", "")
+ ) or (
+ message.get("content", "")
+ if isinstance(message, dict)
+ else getattr(message, "content", "")
+ ) or ""
+ reasoning_delta = (
+ delta.get("reasoning_content", "")
+ if isinstance(delta, dict)
+ else getattr(delta, "reasoning_content", "")
+ ) or (
+ message.get("reasoning_content", "")
+ if isinstance(message, dict)
+ else getattr(message, "reasoning_content", "")
+ ) or ""
+
+ return ChatChunk(reasoning_delta=reasoning_delta, response_delta=response_delta)
+
+
+def _without_stream_kwarg(kwargs: dict[str, Any]) -> dict[str, Any]:
+ kwargs.pop("stream", None)
+ return kwargs
+
+
def _adjust_call_args(provider_name: str, model_name: str, kwargs: dict):
@@ -949,6 +781,22 @@ def _adjust_call_args(provider_name: str, model_name: str, kwargs: dict):
def _merge_provider_defaults(
provider_type: ProviderModelType, original_provider: str, kwargs: dict
) -> tuple[str, dict]:
+ # Normalize .env-style numeric strings (e.g., "timeout=30") into ints/floats for LiteLLM
+ def _normalize_values(values: dict) -> dict:
+ result: dict[str, Any] = {}
+ for k, v in values.items():
+ if isinstance(v, str):
+ try:
+ result[k] = int(v)
+ except ValueError:
+ try:
+ result[k] = float(v)
+ except ValueError:
+ result[k] = v
+ else:
+ result[k] = v
+ return result
+
provider_name = original_provider # default: unchanged
cfg = get_provider_config(provider_type, original_provider)
if cfg:
@@ -966,6 +814,15 @@ def _merge_provider_defaults(
if key and key not in ("None", "NA"):
kwargs["api_key"] = key
+ # Merge LiteLLM global kwargs (timeouts, stream_timeout, etc.)
+ try:
+ global_kwargs = settings.get_settings().get("litellm_global_kwargs", {}) # type: ignore[union-attr]
+ except Exception:
+ global_kwargs = {}
+ if isinstance(global_kwargs, dict):
+ for k, v in _normalize_values(global_kwargs).items():
+ kwargs.setdefault(k, v)
+
return provider_name, kwargs
diff --git a/plugins/AGENTS.md b/plugins/AGENTS.md
index 0d42f9124..ba9e518d3 100644
--- a/plugins/AGENTS.md
+++ b/plugins/AGENTS.md
@@ -30,7 +30,6 @@
- `hooks.py` runs in the framework runtime. Explicitly target another runtime if a plugin must prepare the agent execution environment.
- `execute.py` is manual user-triggered setup, maintenance, repair, migration, or refresh work; automatic framework behavior belongs in hooks or lifecycle extensions.
- Plugin routes are `GET /plugins//`, `POST /api/plugins//`, and `POST /api/plugins` for management actions.
-- `_a0_connector` WebSocket history replay must stay bounded: emit large chat history as paged `connector_context_snapshot` payloads, keep `last_sequence` as the Agent Zero log-output cursor, and avoid sending an entire long transcript in one frame.
- Frontend plugin HTML extensions live under `extensions/webui//`, include a root Alpine scope, and use `x-move-*` directives when targeting static breakpoints.
- Frontend plugin JS extensions live under `extensions/webui//` and export a default function.
- Plugin UI must use the A0 notification system for errors, warnings, success, and info instead of inline success/error boxes.
@@ -49,7 +48,6 @@
- Keep plugin README and docs current when user-visible plugin behavior changes.
- Check configuration before injecting setup or discovery banners so configured plugins do not keep advertising setup.
- Use highly unique banner IDs prefixed by plugin name.
-- Browser tool prompts must preserve the existing-tab workflow: when a user refers to an already-open URL, tab, or page title, guide agents to `list` and then `set_active` or `navigate` by `browser_id` instead of blindly opening a new tab.
- When preparing community plugins, keep plugin contents at the standalone repository root with `plugin.yaml`, `README.md`, and a root `LICENSE`.
- Plugin Index submissions use a separate `index.yaml` under `a0-plugins/plugins//`; do not confuse it with runtime `plugin.yaml`.
@@ -62,39 +60,4 @@
## Child DOX Index
-Direct child DOX files:
-
-| Child | Scope |
-| --- | --- |
-| [_a0_connector/AGENTS.md](_a0_connector/AGENTS.md) | HTTP and WebSocket connector integration with remote tools and runtime bridges. |
-| [_browser/AGENTS.md](_browser/AGENTS.md) | Playwright browser tool, helpers, viewer, and browser panel UI. |
-| [_chat_branching/AGENTS.md](_chat_branching/AGENTS.md) | Chat branching from an existing message. |
-| [_chat_compaction/AGENTS.md](_chat_compaction/AGENTS.md) | Full-chat compaction into a summary message. |
-| [_commands/AGENTS.md](_commands/AGENTS.md) | Built-in slash command manager, command file discovery, and chat composer slash picker. |
-| [_code_execution/AGENTS.md](_code_execution/AGENTS.md) | Terminal, Python, and Node.js execution tools and shell runtimes. |
-| [_desktop/AGENTS.md](_desktop/AGENTS.md) | Linux desktop runtime, sessions, and desktop surface. |
-| [_discovery/AGENTS.md](_discovery/AGENTS.md) | Welcome-screen plugin discovery cards and promotions. |
-| [_document_query/AGENTS.md](_document_query/AGENTS.md) | Document parsing, indexing, and Q&A tools. |
-| [_editor/AGENTS.md](_editor/AGENTS.md) | Native Markdown editor surface and sessions. |
-| [_email_integration/AGENTS.md](_email_integration/AGENTS.md) | IMAP/Exchange polling and SMTP reply integration. |
-| [_error_retry/AGENTS.md](_error_retry/AGENTS.md) | Critical exception retry lifecycle hooks. |
-| [_goal/AGENTS.md](_goal/AGENTS.md) | Built-in chat goal strip, `/goal` slash command, and agent-facing goal tools. |
-| [_infection_check/AGENTS.md](_infection_check/AGENTS.md) | Prompt-injection safety analysis before tool execution. |
-| [_kokoro_tts/AGENTS.md](_kokoro_tts/AGENTS.md) | Kokoro text-to-speech integration. |
-| [_memory/AGENTS.md](_memory/AGENTS.md) | Optional persistent recall plugin, knowledge import, tools, and dashboard; do not assume it is enabled outside this plugin. |
-| [_model_config/AGENTS.md](_model_config/AGENTS.md) | Model selection, presets, API-key checks, and scoped overrides. |
-| [_oauth/AGENTS.md](_oauth/AGENTS.md) | OAuth-backed model-provider connections and local proxy routes. |
-| [_office/AGENTS.md](_office/AGENTS.md) | LibreOffice office artifacts and office canvas sessions. |
-| [_onboarding/AGENTS.md](_onboarding/AGENTS.md) | First-time model onboarding wizard. |
-| [_orchestrator/AGENTS.md](_orchestrator/AGENTS.md) | External terminal coding-agent orchestration skill, adapter status, and settings UI. |
-| [_plugin_installer/AGENTS.md](_plugin_installer/AGENTS.md) | Plugin install and update flows from ZIP, Git, and Plugin Index. |
-| [_plugin_scan/AGENTS.md](_plugin_scan/AGENTS.md) | LLM-guided security scanner for third-party plugins. |
-| [_plugin_validator/AGENTS.md](_plugin_validator/AGENTS.md) | Plugin manifest, structure, convention, and security validator. |
-| [_promptinclude/AGENTS.md](_promptinclude/AGENTS.md) | Promptinclude scanning and prompt injection. |
-| [_skills/AGENTS.md](_skills/AGENTS.md) | Active and hidden skill configuration and prompt injection. |
-| [_telegram_integration/AGENTS.md](_telegram_integration/AGENTS.md) | Telegram bot integration and per-user chat sessions. |
-| [_text_editor/AGENTS.md](_text_editor/AGENTS.md) | Native text read, write, and patch tool. |
-| [_time_travel/AGENTS.md](_time_travel/AGENTS.md) | Workspace history, diff, travel, snapshot, and revert flows. |
-| [_whatsapp_integration/AGENTS.md](_whatsapp_integration/AGENTS.md) | WhatsApp Baileys bridge integration. |
-| [_whats_new/AGENTS.md](_whats_new/AGENTS.md) | Version-gated What's New showcase modal, card list, and startup trigger. |
-| [_whisper_stt/AGENTS.md](_whisper_stt/AGENTS.md) | Whisper speech-to-text integration. |
+No child DOX files.
diff --git a/plugins/_a0_connector/AGENTS.md b/plugins/_a0_connector/AGENTS.md
deleted file mode 100644
index b81567a24..000000000
--- a/plugins/_a0_connector/AGENTS.md
+++ /dev/null
@@ -1,39 +0,0 @@
-# A0 Connector Plugin DOX
-
-## Purpose
-
-- Own the current Agent Zero connector plugin for HTTP and WebSocket integration.
-- Provide remote execution, text-editing freshness, and connector runtime bridges.
-
-## Ownership
-
-- `plugin.yaml` owns plugin metadata and settings scope.
-- `api/` owns connector WebSocket and API entry points.
-- `helpers/` owns chat context, event bridge, execution config, freshness, version, and WebSocket runtime helpers.
-- `tools/`, `prompts/`, `skills/`, `extensions/`, and `webui/` own connector-facing agent and UI contributions.
-
-## Local Contracts
-
-- Preserve session-auth and `auth.handlers` activation assumptions.
-- Keep remote tool prompts synchronized with remote tool behavior and disclose
- them only from connected CLI metadata: no connected CLI hides all remote tool
- prompts, remote file metadata enables `text_editor_remote`, F4-enabled remote
- execution metadata enables `code_execution_remote`, and supported enabled
- Computer Use that does not need re-arming enables `computer_use_remote`.
-- Do not bypass WebSocket authentication or leak connector session data.
-- File operation results may arrive as chunked JSON/base64
- `connector_file_op_result` frames; resolve the pending file operation only
- after all chunks for the `op_id` are assembled.
-- Host browser status metadata may advertise `available_browsers` entries with browser ids, labels, CDP endpoints, status, and enabled state; keep older CLI payloads without those fields compatible.
-
-## Work Guidance
-
-- Coordinate connector runtime changes with API, tools, prompts, and WebUI viewer behavior together.
-
-## Verification
-
-- Run connector-specific tests or smoke-test HTTP and `/ws` integration when changing runtime behavior.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/plugins/_a0_connector/api/v1/browser_runtime.py b/plugins/_a0_connector/api/v1/browser_runtime.py
index ae9b63829..72467e954 100644
--- a/plugins/_a0_connector/api/v1/browser_runtime.py
+++ b/plugins/_a0_connector/api/v1/browser_runtime.py
@@ -24,11 +24,6 @@ def _normalize_requested_backend(value: object) -> str:
return ""
-def _normalize_host_browser_selection(value: object) -> str:
- raw = _string(value).lower().replace(" ", "_")
- return "".join(ch for ch in raw if ch.isalnum() or ch in {"_", "-", ":", ".", "/"})[:200]
-
-
def _normalize_profile_mode(value: object) -> str:
normalized = _string(value).lower().replace("-", "_").replace(" ", "_")
if normalized in {"agent", "clean", "clean_agent", "a0", "dedicated"}:
@@ -73,10 +68,6 @@ class BrowserRuntime(connector_base.ProtectedConnectorApiHandler):
mimetype="application/json",
)
settings["runtime_backend"] = runtime_backend
- if "host_browser_selection" in input or "browser_selection" in input:
- settings["host_browser_selection"] = _normalize_host_browser_selection(
- input.get("host_browser_selection", input.get("browser_selection"))
- )
if "host_browser_profile_mode" in input or "profile_mode" in input:
profile_mode = _normalize_profile_mode(
input.get("host_browser_profile_mode", input.get("profile_mode"))
@@ -95,13 +86,11 @@ class BrowserRuntime(connector_base.ProtectedConnectorApiHandler):
runtime_backend = settings.get("runtime_backend") or "container"
profile_mode = _normalize_profile_mode(settings.get("host_browser_profile_mode")) or "existing"
- browser_selection = _normalize_host_browser_selection(settings.get("host_browser_selection"))
return {
"ok": True,
"runtime_backend": runtime_backend,
"host_browser_profile_mode": profile_mode,
- "host_browser_selection": browser_selection,
"label": _runtime_label(runtime_backend),
"project_name": project_name,
"agent_profile": "",
diff --git a/plugins/_a0_connector/api/ws_connector.py b/plugins/_a0_connector/api/ws_connector.py
index 0152eeb4c..2f15c671f 100644
--- a/plugins/_a0_connector/api/ws_connector.py
+++ b/plugins/_a0_connector/api/ws_connector.py
@@ -62,9 +62,6 @@ WS_FEATURES = [
"connector_browser_op",
]
-_SNAPSHOT_REPLAY_PAGE_SIZE = 50
-_LIVE_STREAM_PAGE_SIZE = 100
-
class WsConnector(WsHandler):
_streaming_tasks: ClassVar[dict[tuple[str, str], asyncio.Task[None]]] = {}
@@ -256,25 +253,19 @@ class WsConnector(WsHandler):
)
subscribe_sid_to_context(sid, context_id)
- events, last_sequence = get_context_log_entries(
- context_id,
- after=from_sequence,
- limit=_SNAPSHOT_REPLAY_PAGE_SIZE,
- )
- await self._emit_context_snapshot(
+ events, last_sequence = get_context_log_entries(context_id, after=from_sequence)
+ await self.emit_to(
sid,
- context_id=context_id,
- events=events,
- last_sequence=last_sequence,
- context=context,
+ "connector_context_snapshot",
+ {
+ "context_id": context_id,
+ "events": events,
+ "last_sequence": last_sequence,
+ "message_queue": self._queue_items_for_context(context),
+ },
correlation_id=data.get("correlationId"),
)
- self._start_streaming(
- sid,
- context_id,
- from_sequence=last_sequence,
- replay_history=True,
- )
+ self._start_streaming(sid, context_id, from_sequence=last_sequence)
return {
"context_id": context_id,
@@ -358,25 +349,19 @@ class WsConnector(WsHandler):
if context_id not in subscribed_contexts_for_sid(sid):
subscribe_sid_to_context(sid, context_id)
- events, last_sequence = get_context_log_entries(
- context_id,
- after=0,
- limit=_SNAPSHOT_REPLAY_PAGE_SIZE,
- )
- await self._emit_context_snapshot(
+ events, last_sequence = get_context_log_entries(context_id, after=0)
+ await self.emit_to(
sid,
- context_id=context_id,
- events=events,
- last_sequence=last_sequence,
- context=context,
+ "connector_context_snapshot",
+ {
+ "context_id": context_id,
+ "events": events,
+ "last_sequence": last_sequence,
+ "message_queue": self._queue_items_for_context(context),
+ },
correlation_id=data.get("correlationId"),
)
- self._start_streaming(
- sid,
- context_id,
- from_sequence=last_sequence,
- replay_history=True,
- )
+ self._start_streaming(sid, context_id, from_sequence=last_sequence)
message_id = client_message_id or data.get("correlationId") or ""
context.log.log(
@@ -880,48 +865,14 @@ class WsConnector(WsHandler):
f"[a0-connector] failed to emit connector_context_complete to {target_sid}: {exc}"
)
- async def _emit_context_snapshot(
- self,
- sid: str,
- *,
- context_id: str,
- events: list[dict[str, Any]],
- last_sequence: int,
- context: AgentContext | None = None,
- correlation_id: str | None = None,
- ) -> None:
- await self.emit_to(
- sid,
- "connector_context_snapshot",
- {
- "context_id": context_id,
- "events": events,
- "last_sequence": last_sequence,
- "message_queue": self._queue_items_for_context(context),
- },
- correlation_id=correlation_id,
- )
-
- def _start_streaming(
- self,
- sid: str,
- context_id: str,
- *,
- from_sequence: int,
- replay_history: bool = False,
- ) -> None:
+ def _start_streaming(self, sid: str, context_id: str, *, from_sequence: int) -> None:
key = (sid, context_id)
task = self._streaming_tasks.get(key)
if task is not None and not task.done():
return
task = asyncio.create_task(
- self._stream_events(
- sid,
- context_id,
- from_sequence=from_sequence,
- replay_history=replay_history,
- )
+ self._stream_events(sid, context_id, from_sequence=from_sequence)
)
self._streaming_tasks[key] = task
@@ -936,26 +887,14 @@ class WsConnector(WsHandler):
context_id: str,
*,
from_sequence: int,
- replay_history: bool = False,
) -> None:
# `from_sequence` is a log-output cursor (not an event sequence number).
cursor = max(int(from_sequence or 0), 0)
last_queue_signature, _ = self._queue_state_for_context_id(context_id)
was_running = self._context_is_running(context_id)
try:
- if replay_history:
- cursor = await self._replay_history_snapshots(
- sid,
- context_id,
- from_sequence=cursor,
- )
-
while context_id in subscribed_contexts_for_sid(sid):
- events, next_cursor = get_context_log_entries(
- context_id,
- after=cursor,
- limit=_LIVE_STREAM_PAGE_SIZE,
- )
+ events, next_cursor = get_context_log_entries(context_id, after=cursor)
for event in events:
await self.emit_to(sid, "connector_context_event", event)
cursor = max(cursor, int(next_cursor or cursor))
@@ -981,7 +920,7 @@ class WsConnector(WsHandler):
},
)
was_running = is_running
- await asyncio.sleep(0 if events else 0.5)
+ await asyncio.sleep(0.5)
except asyncio.CancelledError:
raise
except Exception as exc:
@@ -990,41 +929,3 @@ class WsConnector(WsHandler):
)
finally:
self._streaming_tasks.pop((sid, context_id), None)
-
- async def _replay_history_snapshots(
- self,
- sid: str,
- context_id: str,
- *,
- from_sequence: int,
- ) -> int:
- cursor = max(int(from_sequence or 0), 0)
-
- while context_id in subscribed_contexts_for_sid(sid):
- events, next_cursor = get_context_log_entries(
- context_id,
- after=cursor,
- limit=_SNAPSHOT_REPLAY_PAGE_SIZE,
- )
- next_cursor = max(cursor, int(next_cursor or cursor))
- if not events:
- return next_cursor
-
- _, queue_items = self._queue_state_for_context_id(context_id)
- await self.emit_to(
- sid,
- "connector_context_snapshot",
- {
- "context_id": context_id,
- "events": events,
- "last_sequence": next_cursor,
- "message_queue": queue_items,
- },
- )
-
- if next_cursor == cursor:
- return cursor + len(events)
- cursor = next_cursor
- await asyncio.sleep(0)
-
- return cursor
diff --git a/plugins/_a0_connector/extensions/python/_functions/_11_tools_prompt/build_prompt/end/_70_include_remote_tool_stubs.py b/plugins/_a0_connector/extensions/python/_functions/_11_tools_prompt/build_prompt/end/_70_include_remote_tool_stubs.py
deleted file mode 100644
index 5f8fa398f..000000000
--- a/plugins/_a0_connector/extensions/python/_functions/_11_tools_prompt/build_prompt/end/_70_include_remote_tool_stubs.py
+++ /dev/null
@@ -1,66 +0,0 @@
-from __future__ import annotations
-
-import re
-from typing import Any
-
-from helpers.extension import Extension
-from plugins._a0_connector.helpers.remote_tool_prompts import (
- REMOTE_TOOL_PROMPTS,
- remote_tool_prompt_availability,
-)
-
-
-_TOOL_MARKERS = {
- tool_name: f'"tool_name": "{tool_name}"' for tool_name in REMOTE_TOOL_PROMPTS
-}
-
-
-class IncludeRemoteToolStubs(Extension):
- def execute(self, data: dict[str, Any] = {}, **kwargs: Any) -> None:
- if self.agent is None:
- return
- if not isinstance(data, dict):
- return
- result = data.get("result")
- if not isinstance(result, str):
- return
-
- context_id = str(
- getattr(getattr(self.agent, "context", None), "id", "") or ""
- ).strip()
- if not context_id:
- return
-
- available = remote_tool_prompt_availability(context_id)
- for tool_name, prompt_file in REMOTE_TOOL_PROMPTS.items():
- try:
- prompt = self.agent.read_prompt(prompt_file).strip()
- except Exception:
- continue
- if not prompt:
- continue
-
- if available.get(tool_name):
- marker = _TOOL_MARKERS[tool_name]
- if marker not in result:
- result = f"{result.rstrip()}\n\n{prompt}"
- continue
-
- result = _remove_prompt(result, prompt)
-
- data["result"] = result
-
-
-def _remove_prompt(result: str, prompt: str) -> str:
- if prompt not in result:
- return result
-
- for needle, replacement in (
- (f"\n\n{prompt}\n\n", "\n\n"),
- (f"\n\n{prompt}", ""),
- (f"{prompt}\n\n", ""),
- (prompt, ""),
- ):
- result = result.replace(needle, replacement)
-
- return re.sub(r"\n{3,}", "\n\n", result).rstrip()
diff --git a/plugins/_a0_connector/extensions/python/_functions/extensions/python/system_prompt/_11_tools_prompt/build_prompt/end/_70_include_remote_tool_stubs.py b/plugins/_a0_connector/extensions/python/_functions/extensions/python/system_prompt/_11_tools_prompt/build_prompt/end/_70_include_remote_tool_stubs.py
new file mode 100644
index 000000000..1fe05f0d5
--- /dev/null
+++ b/plugins/_a0_connector/extensions/python/_functions/extensions/python/system_prompt/_11_tools_prompt/build_prompt/end/_70_include_remote_tool_stubs.py
@@ -0,0 +1,31 @@
+from __future__ import annotations
+
+from typing import Any
+
+from helpers.extension import Extension
+
+
+_COMPUTER_USE_PROMPT = "agent.system.tool.computer_use_remote.md"
+_COMPUTER_USE_TOOL_MARKER = '"tool_name": "computer_use_remote"'
+
+
+class IncludeRemoteToolStubs(Extension):
+ def execute(self, data: dict[str, Any] = {}, **kwargs: Any) -> None:
+ if self.agent is None:
+ return
+ if not isinstance(data, dict):
+ return
+ result = data.get("result")
+ if not isinstance(result, str):
+ return
+ if _COMPUTER_USE_TOOL_MARKER in result:
+ return
+
+ try:
+ prompt = self.agent.read_prompt(_COMPUTER_USE_PROMPT).strip()
+ except Exception:
+ return
+ if not prompt:
+ return
+
+ data["result"] = f"{result.rstrip()}\n\n{prompt}"
diff --git a/plugins/_a0_connector/helpers/event_bridge.py b/plugins/_a0_connector/helpers/event_bridge.py
index c453a6e78..9a35e6729 100644
--- a/plugins/_a0_connector/helpers/event_bridge.py
+++ b/plugins/_a0_connector/helpers/event_bridge.py
@@ -79,7 +79,6 @@ def log_entry_to_connector_event(
def get_context_log_entries(
context_id: str,
after: int = 0,
- limit: int | None = None,
) -> tuple[list[dict[str, Any]], int]:
"""Return connector events plus the next log cursor for the context."""
try:
@@ -89,20 +88,7 @@ def get_context_log_entries(
if context is None:
return [], 0
- start = max(int(after or 0), 0)
- end: int | None = None
- if limit is not None:
- limit = max(int(limit or 0), 0)
- if limit > 0:
- log_lock = getattr(context.log, "_lock", None)
- log_updates = getattr(context.log, "updates", None)
- if log_lock is not None and isinstance(log_updates, list):
- with log_lock:
- end = min(start + limit, len(log_updates))
- else:
- end = start + limit
-
- log_output = context.log.output(start=start, end=end)
+ log_output = context.log.output(start=max(int(after or 0), 0))
events = [
log_entry_to_connector_event(entry, context_id)
for entry in log_output.items
diff --git a/plugins/_a0_connector/helpers/remote_tool_prompts.py b/plugins/_a0_connector/helpers/remote_tool_prompts.py
deleted file mode 100644
index 65472fabe..000000000
--- a/plugins/_a0_connector/helpers/remote_tool_prompts.py
+++ /dev/null
@@ -1,60 +0,0 @@
-from __future__ import annotations
-
-from typing import Any
-
-from plugins._a0_connector.helpers import ws_runtime
-
-
-REMOTE_TOOL_PROMPTS: dict[str, str] = {
- "code_execution_remote": "agent.system.tool.code_execution_remote.md",
- "computer_use_remote": "agent.system.tool.computer_use_remote.md",
- "text_editor_remote": "agent.system.tool.text_editor_remote.md",
-}
-
-
-def remote_tool_prompt_availability(context_id: str) -> dict[str, bool]:
- """Return which connector remote tools should be advertised in prompts."""
- candidates = ws_runtime.remote_tool_sids_for_context(context_id)
- return {
- "code_execution_remote": any(
- _remote_exec_prompt_available(sid) for sid in candidates
- ),
- "computer_use_remote": any(
- _computer_use_prompt_available(sid) for sid in candidates
- ),
- "text_editor_remote": any(
- _remote_file_prompt_available(sid) for sid in candidates
- ),
- }
-
-
-def should_include_remote_tool_prompt(agent: Any, tool_name: str) -> bool:
- if tool_name not in REMOTE_TOOL_PROMPTS:
- return True
-
- context = getattr(agent, "context", None)
- context_id = str(getattr(context, "id", "") or "").strip()
- if not context_id:
- return False
-
- return bool(remote_tool_prompt_availability(context_id).get(tool_name))
-
-
-def _remote_file_prompt_available(sid: str) -> bool:
- metadata = ws_runtime.remote_file_metadata_for_sid(sid) or {}
- return bool(metadata.get("enabled"))
-
-
-def _remote_exec_prompt_available(sid: str) -> bool:
- metadata = ws_runtime.remote_exec_metadata_for_sid(sid) or {}
- return bool(metadata.get("enabled"))
-
-
-def _computer_use_prompt_available(sid: str) -> bool:
- metadata = ws_runtime.computer_use_metadata_for_sid(sid) or {}
- status = str(metadata.get("status", "") or "").strip().lower()
- return bool(
- metadata.get("supported")
- and metadata.get("enabled")
- and status != "rearm required"
- )
diff --git a/plugins/_a0_connector/helpers/ws_runtime.py b/plugins/_a0_connector/helpers/ws_runtime.py
index e35948359..1987e5350 100644
--- a/plugins/_a0_connector/helpers/ws_runtime.py
+++ b/plugins/_a0_connector/helpers/ws_runtime.py
@@ -1,13 +1,10 @@
from __future__ import annotations
import asyncio
-import base64
-import binascii
import copy
-import json
import threading
import time
-from dataclasses import dataclass, field
+from dataclasses import dataclass
from typing import Any
@@ -17,8 +14,6 @@ class PendingFileOperation:
loop: asyncio.AbstractEventLoop
future: asyncio.Future[dict[str, Any]]
context_id: str | None = None
- chunk_count: int | None = None
- chunks: dict[int, bytes] = field(default_factory=dict)
@dataclass
@@ -80,9 +75,6 @@ class HostBrowserMetadata:
profile_label: str
profile_path: str
cdp_endpoint: str
- browser_id: str
- browser_label: str
- available_browsers: tuple[dict[str, Any], ...]
content_helper_sha256: str
features: tuple[str, ...]
support_reason: str
@@ -419,9 +411,6 @@ def store_sid_host_browser_metadata(sid: str, payload: dict[str, Any]) -> HostBr
profile_label=str(payload.get("profile_label", "") or "").strip(),
profile_path=str(payload.get("profile_path", "") or "").strip(),
cdp_endpoint=str(payload.get("cdp_endpoint", "") or "").strip(),
- browser_id=str(payload.get("browser_id", payload.get("browser_selection", "")) or "").strip(),
- browser_label=str(payload.get("browser_label", "") or "").strip(),
- available_browsers=_normalize_available_host_browsers(payload.get("available_browsers")),
content_helper_sha256=str(payload.get("content_helper_sha256", "") or "").strip().lower(),
features=features,
support_reason=support_reason,
@@ -451,32 +440,6 @@ def _host_browser_can_prepare(
)
-def _normalize_available_host_browsers(value: Any) -> tuple[dict[str, Any], ...]:
- if not isinstance(value, (list, tuple)):
- return ()
- browsers: list[dict[str, Any]] = []
- for item in value:
- if not isinstance(item, dict):
- continue
- browser_id = str(item.get("id", item.get("browser_id", item.get("selection", ""))) or "").strip()
- family = str(item.get("family", item.get("browser_family", "")) or "").strip()
- label = str(item.get("label", item.get("name", "")) or "").strip()
- cdp_endpoint = str(item.get("cdp_endpoint", "") or "").strip()
- status = str(item.get("status", "") or "").strip()
- enabled = bool(item.get("enabled", True))
- if not any((browser_id, family, label, cdp_endpoint)):
- continue
- browsers.append({
- "id": browser_id or family or cdp_endpoint,
- "family": family,
- "label": label or family or browser_id or cdp_endpoint,
- "cdp_endpoint": cdp_endpoint,
- "status": status,
- "enabled": enabled,
- })
- return tuple(browsers)
-
-
def clear_sid_host_browser_metadata(sid: str) -> None:
with _state_lock:
_sid_host_browser_metadata.pop(sid, None)
@@ -496,9 +459,6 @@ def host_browser_metadata_for_sid(sid: str) -> dict[str, Any] | None:
"profile_label": metadata.profile_label,
"profile_path": metadata.profile_path,
"cdp_endpoint": metadata.cdp_endpoint,
- "browser_id": metadata.browser_id,
- "browser_label": metadata.browser_label,
- "available_browsers": copy.deepcopy(list(metadata.available_browsers)),
"content_helper_sha256": metadata.content_helper_sha256,
"features": list(metadata.features),
"support_reason": metadata.support_reason,
@@ -564,10 +524,6 @@ def all_host_browser_metadata() -> list[dict[str, Any]]:
"browser_family": metadata.browser_family,
"profile_label": metadata.profile_label,
"profile_path": metadata.profile_path,
- "cdp_endpoint": metadata.cdp_endpoint,
- "browser_id": metadata.browser_id,
- "browser_label": metadata.browser_label,
- "available_browsers": copy.deepcopy(list(metadata.available_browsers)),
"content_helper_sha256": metadata.content_helper_sha256,
"features": list(metadata.features),
"support_reason": metadata.support_reason,
@@ -614,101 +570,9 @@ def resolve_pending_file_op(
sid: str,
payload: dict[str, Any],
) -> bool:
- if payload.get("chunked") is True:
- return _resolve_pending_file_chunk(op_id, sid=sid, payload=payload)
return _resolve_pending(_pending_file_ops, op_id, sid=sid, payload=payload)
-def _resolve_pending_file_chunk(
- op_id: str,
- *,
- sid: str,
- payload: dict[str, Any],
-) -> bool:
- error = _validate_file_chunk_payload(payload)
- if error:
- return _fail_pending(
- _pending_file_ops,
- op_id,
- sid=sid,
- error=f"Invalid chunked file operation result: {error}",
- )
-
- chunk_index = int(payload["chunk_index"])
- chunk_count = int(payload["chunk_count"])
- encoded = str(payload.get("data") or "")
- try:
- chunk = base64.b64decode(encoded.encode("ascii"), validate=True)
- except (UnicodeEncodeError, binascii.Error) as exc:
- return _fail_pending(
- _pending_file_ops,
- op_id,
- sid=sid,
- error=f"Invalid chunked file operation result: {exc}",
- )
-
- with _state_lock:
- pending = _pending_file_ops.get(op_id)
- if pending is None or pending.sid != sid:
- return False
-
- if pending.chunk_count is None:
- pending.chunk_count = chunk_count
- elif pending.chunk_count != chunk_count:
- _pending_file_ops.pop(op_id, None)
- pending.loop.call_soon_threadsafe(
- _set_future_result,
- pending.future,
- {
- "op_id": op_id,
- "ok": False,
- "error": "Invalid chunked file operation result: chunk_count changed",
- },
- )
- return True
-
- pending.chunks[chunk_index] = chunk
- if len(pending.chunks) < chunk_count:
- return True
-
- ordered = [pending.chunks[index] for index in range(chunk_count)]
- _pending_file_ops.pop(op_id, None)
-
- try:
- assembled = b"".join(ordered).decode("utf-8")
- result = json.loads(assembled)
- if not isinstance(result, dict):
- raise ValueError("decoded result is not an object")
- except Exception as exc:
- result = {
- "op_id": op_id,
- "ok": False,
- "error": f"Invalid chunked file operation result: {exc}",
- }
-
- pending.loop.call_soon_threadsafe(_set_future_result, pending.future, result)
- return True
-
-
-def _validate_file_chunk_payload(payload: dict[str, Any]) -> str:
- if payload.get("encoding") != "json+base64":
- return "encoding must be json+base64"
-
- try:
- chunk_index = int(payload.get("chunk_index"))
- chunk_count = int(payload.get("chunk_count"))
- except (TypeError, ValueError):
- return "chunk_index and chunk_count must be integers"
-
- if chunk_count <= 0:
- return "chunk_count must be positive"
- if chunk_index < 0 or chunk_index >= chunk_count:
- return "chunk_index out of range"
- if not isinstance(payload.get("data"), str):
- return "data must be a string"
- return ""
-
-
def fail_pending_file_op(
op_id: str,
*,
diff --git a/plugins/_a0_connector/prompts/agent.system.tool.code_execution_remote.md b/plugins/_a0_connector/prompts/agent.system.tool.code_execution_remote.md
index 8c334d0a8..2e1251153 100644
--- a/plugins/_a0_connector/prompts/agent.system.tool.code_execution_remote.md
+++ b/plugins/_a0_connector/prompts/agent.system.tool.code_execution_remote.md
@@ -1,10 +1,9 @@
# code_execution_remote tool
-Shown when a connected A0 CLI advertises remote execution, which the user enables
-with F4 in the CLI. Runs shell-backed execution on the machine where that CLI is
-running. Use this tool, not `code_execution_tool`, when the user asks for the
-connected local terminal, the A0 CLI host, their local machine, or explicitly
-says not to use Docker/server/container execution.
+Runs shell-backed execution on the machine where a connected A0 CLI is running.
+Use this tool, not `code_execution_tool`, when the user asks for the connected
+local terminal, the A0 CLI host, their local machine, or explicitly says not to
+use Docker/server/container execution.
For complex local project work, optionally load skill `host-code-execution`.
Availability and permissions are checked when the tool runs. If no CLI is
diff --git a/plugins/_a0_connector/prompts/agent.system.tool.computer_use_remote.md b/plugins/_a0_connector/prompts/agent.system.tool.computer_use_remote.md
index 558208ee0..163f23db4 100644
--- a/plugins/_a0_connector/prompts/agent.system.tool.computer_use_remote.md
+++ b/plugins/_a0_connector/prompts/agent.system.tool.computer_use_remote.md
@@ -1,6 +1,6 @@
### computer_use_remote
-Shown when a connected A0 CLI advertises enabled Computer Use (`/computer-use on`) and does not need re-arming. Runtime-gated beta desktop control through that CLI on the user's host machine. Availability, backend support, and trust mode are checked when the tool runs, together with CLI presence, local enablement, and re-arm state. Computer Use enablement is scoped to the current CLI session, not scoped to a single chat context.
+Runtime-gated beta desktop control through a connected A0 CLI on the user's host machine. The callable contract is available in the tool prompt. Availability, backend support, and trust mode are checked when the tool runs, together with CLI presence, local enablement, and re-arm state. Computer Use enablement is scoped to the current CLI session, not scoped to a single chat context.
Use this for native host desktop UI inspection, screenshots, background-safe window/element actions when supported, clicking, scrolling, typing, key presses, and status checks. Do not use it for ordinary web-page navigation or host-browser control; use the browser tool for web pages unless browser automation cannot express the task. For complex desktop workflows, load and follow skill `host-computer-use` before proceeding.
diff --git a/plugins/_a0_connector/prompts/agent.system.tool.text_editor_remote.md b/plugins/_a0_connector/prompts/agent.system.tool.text_editor_remote.md
index 0c2b0341f..514108b8b 100644
--- a/plugins/_a0_connector/prompts/agent.system.tool.text_editor_remote.md
+++ b/plugins/_a0_connector/prompts/agent.system.tool.text_editor_remote.md
@@ -1,10 +1,9 @@
# text_editor_remote tool
-Shown when a connected A0 CLI advertises remote file access. Reads, writes, and
-patches files on the machine where that CLI is running. Use this tool, not
-server-side file tools, when the user asks for files on the connected local
-machine, A0 CLI host, or explicitly says not to use Docker/server files. For
-complex remote edits, optionally load skill `host-file-editing`.
+Reads, writes, and patches files on the machine where a connected A0 CLI is
+running. Use this tool, not server-side file tools, when the user asks for files
+on the connected local machine, A0 CLI host, or explicitly says not to use
+Docker/server files. For complex remote edits, optionally load skill `host-file-editing`.
Availability and permissions are checked when the tool runs. If no CLI is
connected, remote file access is disabled, or a write/patch needs Read&Write,
diff --git a/plugins/_browser/AGENTS.md b/plugins/_browser/AGENTS.md
deleted file mode 100644
index a0e570a9d..000000000
--- a/plugins/_browser/AGENTS.md
+++ /dev/null
@@ -1,50 +0,0 @@
-# Browser Plugin DOX
-
-## Purpose
-
-- Own the built-in Playwright browser tool and WebUI browser viewer.
-- Bridge browser automation, page inspection helpers, and browser panel UI.
-
-## Ownership
-
-- `plugin.yaml` and `default_config.yaml` own metadata and browser settings defaults.
-- `tools/browser.py` owns the agent-facing browser tool.
-- `helpers/` owns Playwright runtime, selectors, URL helpers, extension management, and connector runtime logic.
-- `api/` owns status, extension, and browser WebSocket handlers.
-- `assets/`, `prompts/`, `skills/`, `extensions/`, and `webui/` own browser scripts, prompts, skill guidance, hook contributions, and UI.
-
-## Local Contracts
-
-- Keep browser actions safe around external pages, credentials, and user data.
-- Preserve Playwright lifecycle cleanup and WebSocket viewer compatibility across regular host browsers and Electron WebContentsView embedding.
-- Keep the WebUI Browser inside its own modal/canvas affordance; do not replace it with page-level navigation.
-- Default the visible WebUI Browser to live CDP screencast for responsiveness. Keep lightweight CDP/DOM state snapshots as the fallback transport.
-- Paint live screencast frames through the Browser panel canvas/ImageBitmap path when available; keep the ``/data URL path for snapshots and fallback rendering.
-- Push internal screencast frames from the runtime to the WebSocket consumer after subscription; keep `read/pop_screencast_frame` as fallback/tooling APIs, not the WebUI hot path.
-- Keep Browser viewer frame transport capability-negotiated: updated clients may request binary/slim screencast frames, while older clients must keep the base64/full-metadata fallback. Do not let the WebUI advertise binary frames unless its Socket.IO client reconstructs attachments as real `Blob`, `ArrayBuffer`, or typed-array values.
-- Keep WebUI Browser tabs scoped to the active chat context by default; aggregate tabs from other AgentContext runtimes only when the Browser settings tab scope is `shared`.
-- Keep narrow WebUI Browser controls usable by grouping navigation with Annotate/settings above a full-width address bar.
-- For Bring Your Own Browser with an existing host profile, `host_browser_selection` may target automatic CLI selection, a browser family/id, or an explicit CDP endpoint and must be forwarded to the connector runtime as `browser_selection`.
-- Browser Settings must refresh connected A0 CLI host-browser inventory while the settings view is open so newly authorized endpoints appear without saving or reopening.
-- Browser Settings keeps the Host browser dropdown focused on automatic selection, advertised debug endpoints, and a validated Custom endpoint field instead of listing every installed local profile.
-- Browser URL-intent handling must only claim web URL schemes and leave custom Agent Zero schemes to their owning surfaces.
-- Prefer DOM/CDP browser actions with refs, selectors, frame-chain refs, and screenshots over viewport coordinate input. Coordinates remain a visual fallback.
-- Do not hardcode user-specific browser paths or secrets.
-
-## Work Guidance
-
-- Coordinate tool, helper, and panel changes so browser state shown in the UI matches tool behavior.
-- Do not depend on nested Electron `` support or launcher-specific preload bridges unless the launcher exposes that bridge as an explicit contract.
-- Keep `prompts/agent.system.tool.browser.md` as a compact callable contract; move detailed browser workflows into `skills/browser-automation/SKILL.md`.
-- Keep `skills/browser-automation/SKILL.md` frontmatter triggers current with rendered browsing, host-browser, screenshot, and web-interaction user phrasing so relevant-skill recall can surface the skill before the full browser workflow is needed.
-- Keep fragile form guidance progressively disclosed through `skills/browser-form-workflows/SKILL.md`, linked from the browser prompt through `browser-automation`.
-
-## Verification
-
-- Smoke-test browser launch, navigation, DOM capture, and WebUI viewer after runtime changes.
-- For viewer render-path changes, verify the live Browser panel paints a screencast frame on canvas with `frameSrc` empty and snapshots still falling back to the image path.
-- Run browser prompt/skill regression tests after changing browser prompt or Browser plugin skills.
-
-## Child DOX Index
-
-No child DOX files.
diff --git a/plugins/_browser/api/ws_browser.py b/plugins/_browser/api/ws_browser.py
index be7092826..a061c406a 100644
--- a/plugins/_browser/api/ws_browser.py
+++ b/plugins/_browser/api/ws_browser.py
@@ -1,7 +1,6 @@
from __future__ import annotations
import asyncio
-import base64
import contextlib
import time
from typing import Any, ClassVar
@@ -9,23 +8,13 @@ from typing import Any, ClassVar
from agent import AgentContext
from helpers.ws import WsHandler
from helpers.ws_manager import WsResult
-from plugins._browser.helpers.config import (
- DEFAULT_BROWSER_TAB_SCOPE,
- TAB_SCOPE_KEY,
- get_browser_config,
-)
from plugins._browser.helpers.runtime import get_runtime, list_runtime_sessions
-FRAME_READ_TIMEOUT_SECONDS = 0.5
+FRAME_IDLE_POLL_SECONDS = 0.05
FRAME_RETRY_DELAY_SECONDS = 0.5
FRAME_STATE_REFRESH_SECONDS = 0.75
-SNAPSHOT_STATE_POLL_SECONDS = 0.75
-SCREENCAST_STREAM_QUALITY = 80
-SCREENSHOT_QUALITY = 92
-VIEWER_TRANSPORT_SCREENCAST = "screencast"
-VIEWER_TRANSPORT_SNAPSHOT = "snapshot"
-VIEWER_TRANSPORTS = {VIEWER_TRANSPORT_SCREENCAST, VIEWER_TRANSPORT_SNAPSHOT}
+SCREENCAST_QUALITY = 92
class WsBrowser(WsHandler):
@@ -103,41 +92,21 @@ class WsBrowser(WsHandler):
if existing:
existing.cancel()
viewer_id = str(data.get("viewer_id") or "")
- viewer_transport = self._viewer_transport(data)
- binary_frames = self._bool(data.get("binary_frames", data.get("binaryFrames")))
- slim_frames = self._bool(data.get("slim_frames", data.get("slimFrames", binary_frames)))
- capture_scale = self._capture_scale_from_data(data)
snapshot = None
if runtime:
- if viewer_transport == VIEWER_TRANSPORT_SCREENCAST:
- stream_task = self._stream_frames(
- sid,
- context_id,
- active_id,
- viewer_id,
- binary_frames=binary_frames,
- slim_frames=slim_frames,
- capture_scale=capture_scale,
- )
- else:
- stream_task = self._stream_state(sid, context_id, active_id, viewer_id)
- self._streams[stream_key] = asyncio.create_task(stream_task)
+ self._streams[stream_key] = asyncio.create_task(
+ self._stream_frames(sid, context_id, active_id, viewer_id)
+ )
snapshot = await self._snapshot_for_browser(runtime, active_id)
- browsers, all_browsers, tab_scope = await self._tabs_for_scope(context_id, browsers)
-
return {
"context_id": context_id,
"active_browser_context_id": context_id,
"active_browser_id": active_id,
"snapshot": snapshot,
- "browsers": browsers,
- "all_browsers": all_browsers,
- "tab_scope": tab_scope,
+ "browsers": await self._all_browser_tabs(),
+ "all_browsers": True,
"viewer_id": viewer_id,
- "viewer_transport": viewer_transport,
- "binary_frames": binary_frames,
- "slim_frames": slim_frames,
}
def _unsubscribe(self, data: dict[str, Any], sid: str) -> dict[str, Any] | WsResult:
@@ -150,23 +119,10 @@ class WsBrowser(WsHandler):
return {"context_id": context_id, "unsubscribed": True}
async def _sessions(self, data: dict[str, Any]) -> dict[str, Any]:
- context_id = self._context_id(data)
- tab_scope = self._tab_scope()
- if tab_scope == "shared":
- return {
- "context_id": context_id,
- "browsers": await self._all_browser_tabs(),
- "all_browsers": True,
- "tab_scope": tab_scope,
- }
-
- runtime = await get_runtime(context_id, create=False) if context_id else None
- listing = await runtime.call("list") if runtime else {}
return {
- "context_id": context_id,
- "browsers": listing.get("browsers") or [],
- "all_browsers": False,
- "tab_scope": tab_scope,
+ "context_id": self._context_id(data),
+ "browsers": await self._all_browser_tabs(),
+ "all_browsers": True,
}
async def _snapshot(self, data: dict[str, Any]) -> dict[str, Any] | WsResult:
@@ -178,15 +134,13 @@ class WsBrowser(WsHandler):
runtime = await get_runtime(context_id, create=False)
if not runtime:
- browsers, all_browsers, tab_scope = await self._tabs_for_scope(context_id, [])
return {
"context_id": context_id,
"active_browser_context_id": context_id,
"active_browser_id": None,
"snapshot": None,
- "browsers": browsers,
- "all_browsers": all_browsers,
- "tab_scope": tab_scope,
+ "browsers": await self._all_browser_tabs(),
+ "all_browsers": True,
}
listing = await runtime.call("list")
@@ -195,9 +149,9 @@ class WsBrowser(WsHandler):
snapshot = None
if active_id:
try:
- quality = int(data.get("quality") or SCREENSHOT_QUALITY)
+ quality = int(data.get("quality") or SCREENCAST_QUALITY)
except (TypeError, ValueError):
- quality = SCREENSHOT_QUALITY
+ quality = SCREENCAST_QUALITY
with contextlib.suppress(Exception):
snapshot = await runtime.call(
"screenshot",
@@ -205,16 +159,13 @@ class WsBrowser(WsHandler):
quality=quality,
)
- browsers, all_browsers, tab_scope = await self._tabs_for_scope(context_id, browsers)
-
return {
"context_id": context_id,
"active_browser_context_id": context_id,
"active_browser_id": active_id,
"snapshot": snapshot,
- "browsers": browsers,
- "all_browsers": all_browsers,
- "tab_scope": tab_scope,
+ "browsers": await self._all_browser_tabs(),
+ "all_browsers": True,
}
async def _command(self, data: dict[str, Any], sid: str) -> dict[str, Any] | WsResult:
@@ -249,10 +200,7 @@ class WsBrowser(WsHandler):
listing = await runtime.call("list")
last_interacted_browser_id = listing.get("last_interacted_browser_id")
snapshot = await self._snapshot_for_result(runtime, result)
- browsers, all_browsers, tab_scope = await self._tabs_for_scope(
- context_id,
- listing.get("browsers") or [],
- )
+ all_browsers = await self._all_browser_tabs()
await self.emit_to(
sid,
"browser_viewer_state",
@@ -264,26 +212,22 @@ class WsBrowser(WsHandler):
"browser_id": browser_id,
"result": result,
"snapshot": snapshot,
- "browsers": browsers,
- "all_browsers": all_browsers,
- "tab_scope": tab_scope,
+ "browsers": all_browsers,
+ "all_browsers": True,
"last_interacted_browser_id": last_interacted_browser_id,
- "viewer_transport": self._viewer_transport(data),
},
correlation_id=data.get("correlationId"),
)
return {
"result": result,
"snapshot": snapshot,
- "browsers": browsers,
- "all_browsers": all_browsers,
- "tab_scope": tab_scope,
+ "browsers": all_browsers,
+ "all_browsers": True,
"active_browser_context_id": context_id,
"last_interacted_browser_id": last_interacted_browser_id,
"command": command,
"browser_id": browser_id,
"viewer_id": viewer_id,
- "viewer_transport": self._viewer_transport(data),
}
async def _input(self, data: dict[str, Any], sid: str) -> dict[str, Any] | WsResult:
@@ -393,7 +337,7 @@ class WsBrowser(WsHandler):
if not browser_id:
return None
with contextlib.suppress(Exception):
- return await runtime.call("screenshot", browser_id, quality=SCREENSHOT_QUALITY)
+ return await runtime.call("screenshot", browser_id, quality=SCREENCAST_QUALITY)
return None
async def _snapshot_for_browser(
@@ -404,27 +348,9 @@ class WsBrowser(WsHandler):
if not browser_id:
return None
with contextlib.suppress(Exception):
- return await runtime.call("screenshot", browser_id, quality=SCREENSHOT_QUALITY)
+ return await runtime.call("screenshot", browser_id, quality=SCREENCAST_QUALITY)
return None
- @staticmethod
- def _tab_scope() -> str:
- scope = str(
- (get_browser_config() or {}).get(TAB_SCOPE_KEY, DEFAULT_BROWSER_TAB_SCOPE)
- or DEFAULT_BROWSER_TAB_SCOPE
- ).strip().lower().replace("-", "_")
- return "shared" if scope == "shared" else DEFAULT_BROWSER_TAB_SCOPE
-
- async def _tabs_for_scope(
- self,
- context_id: str,
- browsers: list[dict[str, Any]] | None,
- ) -> tuple[list[dict[str, Any]], bool, str]:
- tab_scope = self._tab_scope()
- if tab_scope == "shared":
- return await self._all_browser_tabs(), True, tab_scope
- return browsers or [], False, tab_scope
-
async def _all_browser_tabs(self) -> list[dict[str, Any]]:
browsers: list[dict[str, Any]] = []
for session in await list_runtime_sessions():
@@ -441,10 +367,6 @@ class WsBrowser(WsHandler):
context_id: str,
browser_id: int | str | None,
viewer_id: str = "",
- *,
- binary_frames: bool = False,
- slim_frames: bool = False,
- capture_scale: float = 1.0,
) -> None:
runtime = None
stream_id = None
@@ -452,12 +374,7 @@ class WsBrowser(WsHandler):
try:
runtime = await get_runtime(context_id, create=False)
if not runtime:
- await self._emit_empty_frame(
- sid,
- context_id,
- viewer_id=viewer_id,
- frame_source=VIEWER_TRANSPORT_SCREENCAST,
- )
+ await self._emit_empty_frame(sid, context_id, viewer_id=viewer_id)
await asyncio.sleep(FRAME_RETRY_DELAY_SECONDS)
continue
@@ -465,75 +382,36 @@ class WsBrowser(WsHandler):
browsers = listing.get("browsers") or []
active_id = self._active_browser_id(listing, browser_id)
if not active_id:
- await self._emit_viewer_state(
- sid,
- context_id,
- active_id,
- browsers=browsers,
- viewer_id=viewer_id,
- state=None,
- viewer_transport=VIEWER_TRANSPORT_SCREENCAST,
- )
+ await self._emit_empty_frame(sid, context_id, browsers=browsers, viewer_id=viewer_id)
await asyncio.sleep(FRAME_RETRY_DELAY_SECONDS)
continue
screencast = await runtime.call(
"start_screencast",
active_id,
- quality=SCREENCAST_STREAM_QUALITY,
+ quality=SCREENCAST_QUALITY,
every_nth_frame=1,
- capture_scale=capture_scale,
)
stream_id = screencast["stream_id"]
active_id = screencast["browser_id"]
state = screencast.get("state")
- await self._emit_viewer_state(
+ await self.emit_to(
sid,
- context_id,
- active_id,
- browsers=browsers,
- viewer_id=viewer_id,
- state=state,
- viewer_transport=VIEWER_TRANSPORT_SCREENCAST,
+ "browser_viewer_frame",
+ {
+ "context_id": context_id,
+ "viewer_id": viewer_id,
+ "browser_id": active_id,
+ "browsers": browsers,
+ "image": "",
+ "mime": "",
+ "state": state,
+ "frame_source": "state",
+ },
)
last_state_refresh = 0.0
- last_state_signature = self._state_signature(active_id, browsers)
- frame_sequence = 0
- server_loop = asyncio.get_running_loop()
- stop_event = asyncio.Event()
-
- async def emit_frame(frame: dict[str, Any]) -> None:
- nonlocal frame_sequence
- try:
- frame_sequence += 1
- payload = self._frame_payload(
- frame,
- context_id=context_id,
- viewer_id=viewer_id,
- browser_id=active_id,
- sequence=frame_sequence,
- binary_frames=binary_frames,
- )
- if not slim_frames:
- payload["browsers"] = browsers
- payload["state"] = state
- await self._emit_to_connected_viewer(sid, "browser_viewer_frame", payload)
- except BaseException:
- stop_event.set()
- raise
-
- def frame_consumer(frame: dict[str, Any]):
- return asyncio.run_coroutine_threadsafe(emit_frame(frame), server_loop)
-
- def stop_consumer() -> None:
- server_loop.call_soon_threadsafe(stop_event.set)
-
- await runtime.call("attach_screencast_consumer", stream_id, frame_consumer, stop_consumer)
-
while True:
- if stop_event.is_set():
- break
now = time.monotonic()
if now - last_state_refresh >= FRAME_STATE_REFRESH_SECONDS:
listing = await runtime.call("list")
@@ -542,25 +420,23 @@ class WsBrowser(WsHandler):
if str(active_id) not in browser_ids:
break
state = self._state_for_browser(browsers, active_id, state)
- state_signature = self._state_signature(active_id, browsers)
- if state_signature != last_state_signature:
- await self._emit_viewer_state(
- sid,
- context_id,
- active_id,
- browsers=browsers,
- viewer_id=viewer_id,
- state=state,
- viewer_transport=VIEWER_TRANSPORT_SCREENCAST,
- )
- last_state_signature = state_signature
last_state_refresh = now
try:
- await asyncio.wait_for(stop_event.wait(), timeout=FRAME_READ_TIMEOUT_SECONDS)
+ frame = await runtime.call("pop_screencast_frame", stream_id)
+ except KeyError:
break
- except TimeoutError:
+ if frame is None:
+ await asyncio.sleep(FRAME_IDLE_POLL_SECONDS)
continue
+
+ frame["context_id"] = context_id
+ frame["viewer_id"] = viewer_id
+ frame["browser_id"] = active_id
+ frame["browsers"] = browsers
+ frame["state"] = state
+ frame["frame_source"] = "screencast"
+ await self.emit_to(sid, "browser_viewer_frame", frame)
except asyncio.CancelledError:
raise
except Exception:
@@ -571,64 +447,6 @@ class WsBrowser(WsHandler):
await runtime.call("stop_screencast", stream_id)
stream_id = None
- async def _stream_state(
- self,
- sid: str,
- context_id: str,
- browser_id: int | str | None,
- viewer_id: str = "",
- ) -> None:
- last_signature = None
- while True:
- try:
- runtime = await get_runtime(context_id, create=False)
- if not runtime:
- signature = (None, ())
- if signature != last_signature:
- await self._emit_empty_frame(
- sid,
- context_id,
- viewer_id=viewer_id,
- frame_source=VIEWER_TRANSPORT_SNAPSHOT,
- )
- last_signature = signature
- await asyncio.sleep(FRAME_RETRY_DELAY_SECONDS)
- continue
-
- listing = await runtime.call("list")
- browsers = listing.get("browsers") or []
- active_id = self._active_browser_id(listing, browser_id)
- state = self._state_for_browser(browsers, active_id, None) if active_id else None
- signature = (
- str(active_id or ""),
- tuple(
- (
- str(browser.get("context_id") or context_id),
- str(browser.get("id") or ""),
- str(browser.get("currentUrl") or ""),
- str(browser.get("title") or ""),
- bool(browser.get("loading")),
- )
- for browser in browsers
- ),
- )
- if signature != last_signature:
- await self._emit_viewer_state(
- sid,
- context_id,
- active_id,
- browsers=browsers,
- viewer_id=viewer_id,
- state=state,
- viewer_transport=VIEWER_TRANSPORT_SNAPSHOT,
- )
- last_signature = signature
- await asyncio.sleep(SNAPSHOT_STATE_POLL_SECONDS)
- except asyncio.CancelledError:
- raise
- except Exception:
- await asyncio.sleep(FRAME_RETRY_DELAY_SECONDS)
-
@staticmethod
def _active_browser_id(
listing: dict[str, Any],
@@ -659,107 +477,6 @@ class WsBrowser(WsHandler):
return browser
return current_state
- @staticmethod
- def _state_signature(
- active_id: int | str | None,
- browsers: list[dict[str, Any]],
- ) -> tuple[str, tuple[tuple[str, str, str, str, bool], ...]]:
- return (
- str(active_id or ""),
- tuple(
- (
- str(browser.get("context_id") or ""),
- str(browser.get("id") or ""),
- str(browser.get("currentUrl") or ""),
- str(browser.get("title") or ""),
- bool(browser.get("loading")),
- )
- for browser in browsers
- ),
- )
-
- @staticmethod
- def _frame_payload(
- frame: dict[str, Any],
- *,
- context_id: str,
- viewer_id: str,
- browser_id: int | str,
- sequence: int,
- binary_frames: bool,
- ) -> dict[str, Any]:
- image = str(frame.get("image") or "")
- payload: dict[str, Any] = {
- "context_id": context_id,
- "viewer_id": viewer_id,
- "browser_id": browser_id,
- "seq": sequence,
- "mime": frame.get("mime") or "image/jpeg",
- "frame_source": VIEWER_TRANSPORT_SCREENCAST,
- "viewer_transport": VIEWER_TRANSPORT_SCREENCAST,
- }
- dimensions = WsBrowser._frame_dimensions(frame.get("metadata"))
- if dimensions:
- payload.update(dimensions)
- if binary_frames:
- try:
- payload["image"] = base64.b64decode(image, validate=False)
- payload["encoding"] = "binary"
- except Exception:
- payload["image"] = image
- payload["encoding"] = "base64"
- else:
- payload["image"] = image
- payload["encoding"] = "base64"
- return payload
-
- @staticmethod
- def _frame_dimensions(metadata: Any) -> dict[str, int]:
- if not isinstance(metadata, dict):
- return {}
- for width_key, height_key in (
- ("expectedWidth", "expectedHeight"),
- ("deviceWidth", "deviceHeight"),
- ("jpegWidth", "jpegHeight"),
- ):
- try:
- width = int(metadata.get(width_key) or 0)
- height = int(metadata.get(height_key) or 0)
- except (TypeError, ValueError):
- continue
- if width > 0 and height > 0:
- return {"width": width, "height": height}
- return {}
-
- async def _emit_viewer_state(
- self,
- sid: str,
- context_id: str,
- browser_id: int | str | None,
- *,
- browsers: list[dict[str, Any]] | None = None,
- viewer_id: str = "",
- state: dict[str, Any] | None = None,
- viewer_transport: str = VIEWER_TRANSPORT_SNAPSHOT,
- ) -> None:
- browsers, all_browsers, tab_scope = await self._tabs_for_scope(context_id, browsers or [])
- await self._emit_to_connected_viewer(
- sid,
- "browser_viewer_state",
- {
- "context_id": context_id,
- "active_browser_context_id": context_id,
- "viewer_id": viewer_id,
- "browser_id": browser_id,
- "active_browser_id": browser_id,
- "browsers": browsers or [],
- "state": state,
- "all_browsers": all_browsers,
- "tab_scope": tab_scope,
- "viewer_transport": viewer_transport,
- },
- )
-
async def _emit_empty_frame(
self,
sid: str,
@@ -767,10 +484,8 @@ class WsBrowser(WsHandler):
*,
browsers: list[dict[str, Any]] | None = None,
viewer_id: str = "",
- frame_source: str = "",
) -> None:
- browsers, all_browsers, tab_scope = await self._tabs_for_scope(context_id, browsers or [])
- await self._emit_to_connected_viewer(
+ await self.emit_to(
sid,
"browser_viewer_frame",
{
@@ -778,47 +493,12 @@ class WsBrowser(WsHandler):
"viewer_id": viewer_id,
"browser_id": None,
"browsers": browsers or [],
- "all_browsers": all_browsers,
- "tab_scope": tab_scope,
"image": "",
"mime": "",
"state": None,
- "frame_source": frame_source,
- "viewer_transport": frame_source or VIEWER_TRANSPORT_SNAPSHOT,
},
)
- async def _emit_to_connected_viewer(
- self,
- sid: str,
- event: str,
- data: dict[str, Any],
- ) -> None:
- manager = getattr(self, "_manager", None)
- if manager is not None:
- with manager.lock:
- connected = (getattr(self, "namespace", "/ws"), sid) in manager.connections
- if not connected:
- raise asyncio.CancelledError()
- await self.emit_to(sid, event, data)
-
- @staticmethod
- def _viewer_transport(data: dict[str, Any]) -> str:
- raw = (
- data.get("viewer_transport")
- or data.get("viewerTransport")
- or data.get("surface_transport")
- or data.get("surfaceTransport")
- or data.get("transport")
- or ""
- )
- normalized = str(raw or "").strip().lower().replace("-", "_")
- if normalized == "live":
- normalized = VIEWER_TRANSPORT_SCREENCAST
- if normalized in VIEWER_TRANSPORTS:
- return normalized
- return VIEWER_TRANSPORT_SNAPSHOT
-
@staticmethod
def _viewport_from_data(data: dict[str, Any]) -> dict[str, int] | None:
try:
@@ -833,20 +513,6 @@ class WsBrowser(WsHandler):
"height": max(200, min(4096, height)),
}
- @staticmethod
- def _capture_scale_from_data(data: dict[str, Any]) -> float:
- try:
- scale = float(
- data.get("device_pixel_ratio")
- or data.get("devicePixelRatio")
- or data.get("pixel_ratio")
- or data.get("pixelRatio")
- or 1
- )
- except (TypeError, ValueError):
- return 1.0
- return max(1.0, min(2.0, scale))
-
@staticmethod
def _context_id(data: dict[str, Any]) -> str:
return str(data.get("context_id") or data.get("context") or "").strip()
diff --git a/plugins/_browser/assets/browser-page-content.js b/plugins/_browser/assets/browser-page-content.js
index f583b4493..65a798ac6 100644
--- a/plugins/_browser/assets/browser-page-content.js
+++ b/plugins/_browser/assets/browser-page-content.js
@@ -1,7 +1,7 @@
(() => {
const GLOBAL_KEY = "__spaceBrowserPageContent__";
const DOM_HELPER_KEY = "__spaceBrowserDomHelper__";
- const VERSION = "13";
+ const VERSION = "12";
const REQUIRED_API_NAMES = Object.freeze([
"annotate",
"boundingBoxFor",
@@ -3103,12 +3103,8 @@
function buildActionResult(entry, extra = {}) {
return {
- actionStrategy: entry.helperBacked ? "frame_chain_ref" : "dom_ref",
captureId: state.captureId,
descriptorTags: Array.isArray(entry?.descriptorTags) ? entry.descriptorTags.slice() : [],
- frameChain: Array.isArray(entry?.frameChain) ? entry.frameChain.slice() : [],
- frameId: entry.frameId || "",
- nodeId: entry.nodeId || "",
referenceId: entry.referenceId,
semanticTags: Array.isArray(entry?.semanticTags) ? entry.semanticTags.slice() : [],
state: entry.state || collectElementStateMetadata(entry.element, state.captureOptions),
@@ -3120,7 +3116,6 @@
function buildHelperBackedActionResult(entry, helperResult, extra = {}) {
return {
- actionStrategy: "frame_chain_ref",
captureId: state.captureId,
descriptorTags: Array.isArray(helperResult?.descriptorTags) ? helperResult.descriptorTags : (entry.descriptorTags || []),
frameChain: entry.frameChain.slice(),
diff --git a/plugins/_browser/default_config.yaml b/plugins/_browser/default_config.yaml
index 896239304..b87c040e1 100644
--- a/plugins/_browser/default_config.yaml
+++ b/plugins/_browser/default_config.yaml
@@ -8,11 +8,6 @@ default_homepage: "about:blank"
# When the Browser surface is already open, keep it synced to agent Browser tool results.
autofocus_active_page: true
-# Browser tab visibility in the WebUI:
-# - per_context: each chat shows only its own Browser tabs.
-# - shared: show Browser tabs from all active chats.
-browser_tab_scope: "per_context"
-
# Maximum number of Browser tabs/pages a single chat context may keep open.
# Raise this only for deliberate parallel browsing workflows.
max_open_tabs: 32
@@ -33,11 +28,6 @@ host_browser_privacy_policy: "allow"
# - agent: use a clean A0-controlled browser profile on the host.
host_browser_profile_mode: "existing"
-# Optional host browser target when using an existing browser.
-# Empty means A0 CLI chooses the first supported/active browser.
-# Values may be browser family ids (chrome, edge, chromium) or CLI-advertised ids/endpoints.
-host_browser_selection: ""
-
# Optional _model_config preset used by Browser-owned model helpers.
# Empty uses the effective Main Model.
model_preset: ""
diff --git a/plugins/_browser/helpers/config.py b/plugins/_browser/helpers/config.py
index 5279f6215..f39e52e51 100644
--- a/plugins/_browser/helpers/config.py
+++ b/plugins/_browser/helpers/config.py
@@ -11,17 +11,13 @@ PLUGIN_NAME = "_browser"
MODEL_PRESET_KEY = "model_preset"
DEFAULT_HOMEPAGE_KEY = "default_homepage"
AUTOFOCUS_ACTIVE_PAGE_KEY = "autofocus_active_page"
-TAB_SCOPE_KEY = "browser_tab_scope"
MAX_OPEN_TABS_KEY = "max_open_tabs"
RUNTIME_BACKEND_KEY = "runtime_backend"
HOST_BROWSER_PRIVACY_POLICY_KEY = "host_browser_privacy_policy"
HOST_BROWSER_PROFILE_MODE_KEY = "host_browser_profile_mode"
-HOST_BROWSER_SELECTION_KEY = "host_browser_selection"
RUNTIME_BACKENDS = {"container", "host_required"}
-BROWSER_TAB_SCOPES = {"per_context", "shared"}
HOST_BROWSER_PRIVACY_POLICIES = {"enforce_local", "warn", "allow"}
HOST_BROWSER_PROFILE_MODES = {"existing", "agent"}
-DEFAULT_BROWSER_TAB_SCOPE = "per_context"
DEFAULT_MAX_OPEN_TABS = 32
MIN_MAX_OPEN_TABS = 1
HARD_MAX_OPEN_TABS = 50
@@ -59,15 +55,6 @@ def _normalize_model_preset(value: Any) -> str:
return str(value or "").strip()
-def _normalize_host_browser_selection(value: Any) -> str:
- raw = str(value or "").strip()
- if not raw:
- return ""
- normalized = raw.lower().replace(" ", "_")
- # Keep explicit CLI ids/ports/endpoints usable while avoiding control characters.
- return "".join(ch for ch in normalized if ch.isalnum() or ch in {"_", "-", ":", ".", "/"})[:200]
-
-
def _normalize_default_homepage(value: Any) -> str:
homepage = str(value or "").strip()
return homepage or "about:blank"
@@ -130,11 +117,6 @@ def normalize_browser_config(settings: dict[str, Any] | None) -> dict[str, Any]:
raw.get(AUTOFOCUS_ACTIVE_PAGE_KEY, True),
default=True,
),
- TAB_SCOPE_KEY: _normalize_choice(
- raw.get(TAB_SCOPE_KEY, DEFAULT_BROWSER_TAB_SCOPE),
- allowed=BROWSER_TAB_SCOPES,
- default=DEFAULT_BROWSER_TAB_SCOPE,
- ),
MAX_OPEN_TABS_KEY: _normalize_int(
raw.get(MAX_OPEN_TABS_KEY, DEFAULT_MAX_OPEN_TABS),
default=DEFAULT_MAX_OPEN_TABS,
@@ -154,9 +136,6 @@ def normalize_browser_config(settings: dict[str, Any] | None) -> dict[str, Any]:
allowed=HOST_BROWSER_PROFILE_MODES,
default="existing",
),
- HOST_BROWSER_SELECTION_KEY: _normalize_host_browser_selection(
- raw.get(HOST_BROWSER_SELECTION_KEY, raw.get("host_browser_choice", ""))
- ),
MODEL_PRESET_KEY: _normalize_model_preset(raw.get(MODEL_PRESET_KEY, "")),
}
diff --git a/plugins/_browser/helpers/connector_runtime.py b/plugins/_browser/helpers/connector_runtime.py
index d5f05cb71..82107aa7b 100644
--- a/plugins/_browser/helpers/connector_runtime.py
+++ b/plugins/_browser/helpers/connector_runtime.py
@@ -57,13 +57,8 @@ HOST_BROWSER_PROFILE_MODE_KEY = getattr(
"HOST_BROWSER_PROFILE_MODE_KEY",
"host_browser_profile_mode",
)
-HOST_BROWSER_SELECTION_KEY = getattr(
- browser_config,
- "HOST_BROWSER_SELECTION_KEY",
- "host_browser_selection",
-)
get_browser_config = browser_config.get_browser_config
-_LOCAL_PROVIDERS = {"ollama", "lm_studio", "llama_cpp", "omlx", "vllm"}
+_LOCAL_PROVIDERS = {"ollama", "lm_studio"}
_LOCAL_HOSTS = {"localhost", "127.0.0.1", "::1", "host.docker.internal"}
_SENSITIVE_ACTIONS = {"content", "detail", "evaluate", "screenshot", "screenshot_file"}
_KEY_ALIASES = {
@@ -83,8 +78,7 @@ _REQUIRED_API_NAMES_RE = re.compile(
re.S,
)
_HOST_BROWSER_REMOTE_DEBUGGING_HELP = (
- "For an already-open Chromium-family browser, open its inspect page, such as "
- "`chrome://inspect/#remote-debugging` or `opera://inspect/#remote-debugging`, "
+ 'For an already-open Chrome-family browser, open `chrome://inspect/#remote-debugging`, '
'enable "Allow remote debugging for this browser instance", run `/browser host on`, '
"and retry."
)
@@ -129,7 +123,6 @@ class ConnectorBrowserRuntime:
"context_id": self.context_id,
"action": action,
"profile_mode": self._host_browser_profile_mode(),
- "browser_selection": self._host_browser_selection(),
}
if action == "open":
@@ -290,7 +283,6 @@ class ConnectorBrowserRuntime:
"context_id": self.context_id,
"action": "ensure",
"profile_mode": self._host_browser_profile_mode(),
- "browser_selection": self._host_browser_selection(),
},
),
)
@@ -303,10 +295,6 @@ class ConnectorBrowserRuntime:
mode = str(config.get(HOST_BROWSER_PROFILE_MODE_KEY) or "existing").strip().lower()
return "agent" if mode == "agent" else "existing"
- def _host_browser_selection(self) -> str:
- config = get_browser_config(self.agent)
- return str(config.get(HOST_BROWSER_SELECTION_KEY) or "").strip()
-
def _with_content_helper(self, sid: str, payload: dict[str, Any]) -> dict[str, Any]:
return self._with_browser_helpers(sid, payload)
@@ -520,10 +508,7 @@ class ConnectorBrowserRuntime:
if not message:
message = "Host browser operation failed"
normalized = message.lower()
- if (
- "chrome://inspect/#remote-debugging" in normalized
- or "opera://inspect/#remote-debugging" in normalized
- ):
+ if "chrome://inspect/#remote-debugging" in normalized:
return _append_docker_browser_recovery(message)
if any(token in normalized for token in _REMOTE_DEBUGGING_ERROR_TOKENS):
return _append_docker_browser_recovery(
diff --git a/plugins/_browser/helpers/runtime.py b/plugins/_browser/helpers/runtime.py
index 994c6f04e..e7455d81a 100644
--- a/plugins/_browser/helpers/runtime.py
+++ b/plugins/_browser/helpers/runtime.py
@@ -317,11 +317,8 @@ class _BrowserScreencast:
self.browser_id = browser_id
self.session = session
self.mime = mime
- self.frame_consumer: Any | None = None
- self.stop_callback: Any | None = None
self.queue = asyncio.Queue(maxsize=1)
self.stopped = False
- self._closed = False
self._ack_tasks: set[asyncio.Task] = set()
self._expected_width = 0
self._expected_height = 0
@@ -332,14 +329,10 @@ class _BrowserScreencast:
quality: int,
every_nth_frame: int,
viewport: dict[str, int],
- capture_scale: float = 1.0,
) -> None:
self.session.on("Page.screencastFrame", self._on_frame)
width = max(320, min(4096, int(viewport.get("width") or DEFAULT_VIEWPORT["width"])))
height = max(200, min(4096, int(viewport.get("height") or DEFAULT_VIEWPORT["height"])))
- scale = max(1.0, min(2.0, float(capture_scale or 1.0)))
- max_width = max(320, min(SCREENCAST_MAX_WIDTH, int(round(width * scale))))
- max_height = max(200, min(SCREENCAST_MAX_HEIGHT, int(round(height * scale))))
self._expected_width = width
self._expected_height = height
with contextlib.suppress(Exception):
@@ -350,8 +343,8 @@ class _BrowserScreencast:
{
"format": "jpeg",
"quality": max(20, min(95, int(quality))),
- "maxWidth": max_width,
- "maxHeight": max_height,
+ "maxWidth": SCREENCAST_MAX_WIDTH,
+ "maxHeight": SCREENCAST_MAX_HEIGHT,
"everyNthFrame": max(1, int(every_nth_frame)),
},
)
@@ -401,21 +394,10 @@ class _BrowserScreencast:
raise RuntimeError("Browser screencast stopped.")
return frame
- async def attach_consumer(self, frame_consumer: Any, stop_callback: Any | None = None) -> None:
- self.frame_consumer = frame_consumer
- self.stop_callback = stop_callback
- frame = await self.pop_frame()
- if frame:
- await self._deliver_frame(frame)
-
async def stop(self) -> None:
- if self._closed:
+ if self.stopped:
return
- was_stopped = self.stopped
- self._closed = True
self.stopped = True
- if not was_stopped:
- self._notify_stopped()
self._drop_queued_frames()
with contextlib.suppress(asyncio.QueueFull):
self.queue.put_nowait(None)
@@ -437,8 +419,6 @@ class _BrowserScreencast:
task.add_done_callback(self._ack_tasks.discard)
async def _handle_frame(self, params: dict[str, Any]) -> None:
- stop_after_ack = False
- notify_stop = False
try:
data = params.get("data") or ""
if data:
@@ -448,7 +428,7 @@ class _BrowserScreencast:
metadata["jpegWidth"], metadata["jpegHeight"] = size
metadata["expectedWidth"] = self._expected_width
metadata["expectedHeight"] = self._expected_height
- await self._deliver_frame(
+ self._queue_latest(
{
"browser_id": self.browser_id,
"mime": self.mime,
@@ -456,14 +436,6 @@ class _BrowserScreencast:
"metadata": metadata,
}
)
- except asyncio.CancelledError:
- stop_after_ack = True
- except Exception:
- if self.frame_consumer:
- stop_after_ack = True
- notify_stop = True
- else:
- raise
finally:
session_id = params.get("sessionId")
if session_id is not None and not self.stopped:
@@ -472,24 +444,6 @@ class _BrowserScreencast:
"Page.screencastFrameAck",
{"sessionId": int(session_id)},
)
- if stop_after_ack:
- self.stopped = True
- if notify_stop:
- self._notify_stopped()
-
- def _notify_stopped(self) -> None:
- if not self.stop_callback:
- return
- with contextlib.suppress(Exception):
- self.stop_callback()
-
- async def _deliver_frame(self, frame: dict[str, Any]) -> None:
- if not self.frame_consumer:
- self._queue_latest(frame)
- return
- future = self.frame_consumer(frame)
- if future is not None:
- await asyncio.wrap_future(future)
def _queue_latest(self, frame: dict[str, Any]) -> None:
self._drop_queued_frames()
@@ -1662,7 +1616,6 @@ class _BrowserRuntimeCore:
*,
quality: int = 78,
every_nth_frame: int = 1,
- capture_scale: float = 1.0,
) -> dict[str, Any]:
await self.ensure_started()
resolved_id = self._resolve_browser_id(browser_id)
@@ -1681,7 +1634,6 @@ class _BrowserRuntimeCore:
quality=quality,
every_nth_frame=every_nth_frame,
viewport=page.viewport_size or DEFAULT_VIEWPORT,
- capture_scale=capture_scale,
)
except Exception:
self.screencasts.pop(stream_id, None)
@@ -1711,17 +1663,6 @@ class _BrowserRuntimeCore:
raise KeyError("Browser screencast is not active.")
return await screencast.pop_frame()
- async def attach_screencast_consumer(
- self,
- stream_id: str,
- frame_consumer: Any,
- stop_callback: Any | None = None,
- ) -> None:
- screencast = self.screencasts.get(str(stream_id or ""))
- if not screencast:
- raise KeyError("Browser screencast is not active.")
- await screencast.attach_consumer(frame_consumer, stop_callback)
-
async def stop_screencast(self, stream_id: str) -> None:
screencast = self.screencasts.pop(str(stream_id or ""), None)
if screencast:
diff --git a/plugins/_browser/prompts/agent.system.tool.browser.md b/plugins/_browser/prompts/agent.system.tool.browser.md
index 402ef6efa..258a5233a 100644
--- a/plugins/_browser/prompts/agent.system.tool.browser.md
+++ b/plugins/_browser/prompts/agent.system.tool.browser.md
@@ -3,15 +3,35 @@ Rendered browser automation for pages that need interaction, JavaScript, forms,
Prefer `search_engine` or `document_query` for plain text research. The tool must not open a Browser surface automatically. Use the tool headlessly unless the user opens the Browser surface or asks for the optional visible WebUI viewer.
-When the user asks for "my browser", "host browser", "local browser", a local Chromium browser, or opening a URL in their host browser, use this `browser` tool. Do not substitute `computer_use_remote`, `code_execution_remote`, `xdg-open`, `sensible-browser`, or Python `webbrowser.open`. If setup fails and mentions remote debugging, tell the user to open the browser inspect page, such as `chrome://inspect/#remote-debugging` or `opera://inspect/#remote-debugging`, enable "Allow remote debugging for this browser instance", run `/browser host on`, and retry.
+The browser may run in Docker container mode or A0 CLI host-browser mode depending on settings. Container-mode paths resolve inside Agent Zero; host-mode paths resolve on the connected A0 CLI host.
-For rendered browsing workflows, multi-step interaction, screenshots, downloads, uploads, forms, or host/container mode decisions, first load `browser-automation` with `skills_tool:load`, then call this tool using the loaded instructions. For fragile forms, `browser-automation` links to `browser-form-workflows`; load it when selects, checkboxes, radios, uploads, contenteditable fields, validation, or submission state are central.
+When the user asks for "my browser", "host browser", "local browser", local Chrome, or opening a URL in their host browser, use this `browser` tool. Do not substitute `computer_use_remote`, `code_execution_remote`, or host shell launchers such as `xdg-open`, `sensible-browser`, or Python `webbrowser.open`. If host-browser setup fails and mentions remote debugging, stop and tell the user to open `chrome://inspect/#remote-debugging`, enable "Allow remote debugging for this browser instance", run `/browser host on`, and retry.
-Actions: tabs `open`, `list`, `state`, `set_active`, `navigate`, `back`, `forward`, `reload`, `close`, `close_all`; inspect `content`, `detail`, `screenshot`; interact `click`, `hover`, `double_click`, `right_click`, `drag`, `type`, `submit`, `type_submit`, `scroll`, `select_option`, `set_checked`, `upload_file`; advanced `evaluate`, `key_chord`, `mouse`, `wheel`, `keyboard`, `clipboard`, `set_viewport`, `multi`.
+For complex browser workflows, load skill `browser-automation`. For fragile forms, load skill `browser-form-workflows`.
-Rules:
-- If the user asks for an existing tab, page title, or already-open URL, call `list` first, match by `title` or `currentUrl`, then use `set_active` or `navigate` on that `browser_id`.
-- Prefer DOM/CDP actions: use refs from the latest `content`, including frame-chain refs, or same-page `selector`; use coordinates only when refs/selectors are unavailable or the user explicitly asks for visual manual control.
-- Screenshots are explicit only; the browser does not automatically load screenshots. Call `vision_load` with the returned `vision_load.tool_args.paths` value before reasoning visually.
+Actions: `open`, `list`, `state`, `set_active`, `navigate`, `back`, `forward`, `reload`, `content`, `detail`, `screenshot`, `click`, `hover`, `double_click`, `right_click`, `drag`, `type`, `submit`, `type_submit`, `scroll`, `evaluate`, `key_chord`, `mouse`, `wheel`, `keyboard`, `clipboard`, `set_viewport`, `select_option`, `set_checked`, `upload_file`, `multi`, `close`, `close_all`.
+
+Common args: `action`, `browser_id`, `url`, `ref`, `target_ref`, `text`, `selector`, `selectors`, `script`, `modifiers`, `keys`, `key`, `include_content`, `focus_popup`, `event_type`, `x`, `y`, `to_x`, `to_y`, `delta_x`, `delta_y`, `button`, `quality`, `full_page`, `path`, `paths`, `value`, `values`, `checked`, `width`, `height`, `calls`.
+
+Workflow:
+- `open` creates a tab and returns id/state.
+- `content` returns markdown with refs like `[link 3]`, `[button 6]`, `[input text 8]`.
+- Interactions use refs from the latest `content` capture.
+- For same-page controls that are easier to identify structurally, `click`, `type`, `submit`, `type_submit`, `scroll`, `select_option`, `set_checked`, and `upload_file` may use `selector` instead of `ref`; the tool resolves the selector through `content` first.
+- `click` with `x`/`y` and no `ref` is treated as a coordinate mouse click. `type` with text and no `ref` types into the currently focused element. `key_chord` accepts either `["Control", "A"]` or `"CTRL+A"`.
+- `navigate` reuses an existing `browser_id` and is preferred for serial browsing.
+- Screenshots are explicit only; the browser does not automatically load screenshots. Call `vision_load` with the returned `vision_load.tool_args.paths` value before reasoning visually. When no `path` is requested, browser screenshots are saved as chat-scoped artifacts; explicit `path` requests remain user-owned files.
- Keep the tab set small; close pages after extracting what you need.
-- `multi` is only a browser action: use `tool_name: "browser"` with `tool_args.action: "multi"`. Never use `tool_name: "multi"`.
+
+`multi` is only a browser action: use `tool_name: "browser"` with `tool_args.action: "multi"`. Never use `tool_name: "multi"`.
+
+Example:
+~~~json
+{
+ "tool_name": "browser",
+ "tool_args": {
+ "action": "open",
+ "url": "https://example.com"
+ }
+}
+~~~
diff --git a/plugins/_browser/skills/browser-automation/SKILL.md b/plugins/_browser/skills/browser-automation/SKILL.md
index 6a09f56a6..b79d0e42b 100644
--- a/plugins/_browser/skills/browser-automation/SKILL.md
+++ b/plugins/_browser/skills/browser-automation/SKILL.md
@@ -1,39 +1,19 @@
---
name: browser-automation
description: Use for complex Agent Zero browser automation, including multi-tab browsing, screenshots, forms, uploads, raw pointer/keyboard actions, host-vs-container browser mode, and visual verification workflows.
-triggers:
- - "browser automation"
- - "web automation"
- - "open website"
- - "open URL"
- - "navigate browser"
- - "interact with web page"
- - "JavaScript page"
- - "browser screenshot"
- - "screenshot webpage"
- - "visual verification"
- - "multi-tab browsing"
- - "download file from website"
- - "upload file in browser"
- - "host browser"
- - "local browser"
- - "my browser"
---
# Browser Tool
-Use this skill after the compact `browser` tool prompt points you here. It is the progressive-disclosure workflow guide for rendered pages, multi-step browser work, logins, downloads, JavaScript-heavy sites, screenshots, host/container browser mode, and visual inspection. Prefer `search_engine` or `document_query` for plain text research.
-
-For fragile forms, load `browser-form-workflows` with `skills_tool:load` before acting when selects, checkboxes, radios, file uploads, contenteditable fields, validation, or final submission state are central to the task.
+Use the `browser` tool for rendered pages, forms, logins, downloads, JavaScript-heavy sites, screenshots, and visual inspection. Prefer `search_engine` or `document_query` for plain text research.
## Core Workflow
1. `open` creates a browser tab and returns a `browser_id`.
2. `content` returns readable markdown plus typed refs like `[link 3]`, `[button 6]`, `[input text 8]`.
-3. Interact with refs using `click`, `type`, `submit`, `scroll`, etc.; iframe/shadow targets may return frame-chain metadata in action results.
+3. Interact with refs using `click`, `type`, `submit`, `scroll`, etc.
4. Use `navigate` on an existing `browser_id` for serial browsing.
5. Keep only a small working tab set; close pages when finished.
-6. If the user asks for an existing tab, page title, or already-open URL, call `list` first, match by `title` or `currentUrl`, then use `set_active` or `navigate` on that `browser_id` instead of opening a new tab.
## Modes
@@ -59,13 +39,12 @@ Screenshot args include `quality`, `full_page`, and optional `path`. Without `pa
- `select_option` works for native selects and detectable ARIA listbox/combobox controls.
- `set_checked` works for checkbox, radio, switch, and toggle-like refs.
- `upload_file` works for file input refs or associated labels; verify the file exists in the active browser environment.
-- For fragile forms, call `skills_tool` with `action: "load"` and `skill_name: "browser-form-workflows"`, then follow that form-specific workflow before filling or submitting.
+- For fragile forms, load skill `browser-form-workflows`.
## Pointer And Keyboard
-- Prefer refs/selectors and DOM/CDP actions over viewport coordinates.
-- `hover`, `double_click`, `right_click`, and `drag` accept refs or viewport coordinates when no reliable ref exists.
-- Coordinates are Chromium viewport CSS pixels and match screenshots; treat them as visual fallback, not the default interaction path.
+- `hover`, `double_click`, `right_click`, and `drag` accept refs or viewport coordinates.
+- Coordinates are Chromium viewport CSS pixels and match screenshots.
- `key_chord` presses keys in order and releases in reverse.
- `clipboard` actions are copy, cut, or paste.
- `set_viewport` resizes the page viewport.
diff --git a/plugins/_browser/skills/browser-form-workflows/SKILL.md b/plugins/_browser/skills/browser-form-workflows/SKILL.md
index 0a199d4bb..4dffd483d 100644
--- a/plugins/_browser/skills/browser-form-workflows/SKILL.md
+++ b/plugins/_browser/skills/browser-form-workflows/SKILL.md
@@ -1,24 +1,11 @@
---
name: browser-form-workflows
description: Use for complex Agent Zero Browser form workflows involving selects, checkboxes, radios, file uploads, contenteditable fields, multi-step validation, or visually verified submission.
-triggers:
- - "browser form"
- - "web form"
- - "fill form"
- - "fill web form"
- - "submit form"
- - "select option"
- - "dropdown"
- - "checkbox"
- - "radio button"
- - "file upload"
- - "contenteditable"
- - "form validation"
---
# Browser Forms
-Use this skill as the form-specific extension of `browser-automation`. Load it after, or alongside, `browser-automation` when the page state may depend on selects, checkboxes, radios, file uploads, contenteditable fields, validation, or visual confirmation.
+Use this skill for complex Browser form workflows where the page state may depend on selects, checkboxes, radios, file uploads, contenteditable fields, validation, or visual confirmation.
Start with `browser:content` to capture current refs, then use `browser:detail` on ambiguous fields before acting. Prefer ref-based form actions before coordinates.
diff --git a/plugins/_browser/webui/browser-config-store.js b/plugins/_browser/webui/browser-config-store.js
index 4f32fe0ac..81fee2e85 100644
--- a/plugins/_browser/webui/browser-config-store.js
+++ b/plugins/_browser/webui/browser-config-store.js
@@ -4,14 +4,8 @@ import { callJsonApi } from "/js/api.js";
const BROWSER_EXTENSIONS_API = "/plugins/_browser/extensions";
const BROWSER_STATUS_API = "/plugins/_browser/status";
const RUNTIME_BACKENDS = new Set(["container", "host_required"]);
-const BROWSER_TAB_SCOPES = new Set(["per_context", "shared"]);
const HOST_PRIVACY_POLICIES = new Set(["enforce_local", "warn", "allow"]);
const HOST_PROFILE_MODES = new Set(["existing", "agent"]);
-const DEFAULT_MAX_OPEN_TABS = 32;
-const MIN_MAX_OPEN_TABS = 1;
-const HARD_MAX_OPEN_TABS = 50;
-const HOST_BROWSER_STATUS_REFRESH_MS = 1000;
-const CUSTOM_HOST_BROWSER_SELECTION = "__custom_endpoint__";
function normalizePathList(value) {
const source = Array.isArray(value)
@@ -33,8 +27,6 @@ function ensureConfig(config) {
config.extension_paths = normalizePathList(config.extension_paths);
config.default_homepage = String(config.default_homepage || "about:blank").trim() || "about:blank";
config.autofocus_active_page = normalizeBoolean(config.autofocus_active_page, true);
- config.browser_tab_scope = normalizeChoice(config.browser_tab_scope, BROWSER_TAB_SCOPES, "per_context");
- config.max_open_tabs = normalizeInt(config.max_open_tabs, DEFAULT_MAX_OPEN_TABS, MIN_MAX_OPEN_TABS, HARD_MAX_OPEN_TABS);
config.runtime_backend = normalizeRuntimeBackend(config.runtime_backend);
config.host_browser_privacy_policy = normalizeChoice(
config.host_browser_privacy_policy,
@@ -46,7 +38,6 @@ function ensureConfig(config) {
HOST_PROFILE_MODES,
"existing",
);
- config.host_browser_selection = normalizeHostBrowserSelection(config.host_browser_selection);
config.model_preset = String(config.model_preset || "").trim();
delete config.model;
return config;
@@ -57,55 +48,12 @@ function normalizeChoice(value, allowed, fallback) {
return allowed.has(normalized) ? normalized : fallback;
}
-function normalizeInt(value, fallback, minimum, maximum) {
- const number = Number.parseInt(value, 10);
- if (!Number.isFinite(number)) return fallback;
- return Math.max(minimum, Math.min(maximum, number));
-}
-
function normalizeRuntimeBackend(value) {
const normalized = String(value || "").trim().toLowerCase().replace(/-/g, "_");
if (normalized === "host_when_available") return "host_required";
return RUNTIME_BACKENDS.has(normalized) ? normalized : "container";
}
-function normalizeHostBrowserSelection(value) {
- return String(value || "").trim().toLowerCase().replace(/\s+/g, "_").slice(0, 200);
-}
-
-function normalizeCustomHostBrowserEndpoint(value) {
- const raw = String(value || "").trim();
- if (!raw) return "";
- const candidate = raw.includes("://") ? raw : `ws://${raw}`;
- try {
- const url = new URL(candidate);
- if (!["ws:", "wss:"].includes(url.protocol) || !url.host || !url.pathname.startsWith("/devtools/browser/")) {
- return "";
- }
- return normalizeHostBrowserSelection(`${url.protocol}//${url.host}${url.pathname}${url.search || ""}`);
- } catch (_error) {
- return "";
- }
-}
-
-function isCustomHostBrowserEndpoint(value) {
- return Boolean(normalizeCustomHostBrowserEndpoint(value));
-}
-
-function debugPortVersionUrl(value) {
- const raw = String(value || "").trim();
- if (!raw) return "";
- const candidate = raw.includes("://") ? raw : `ws://${raw}`;
- try {
- const url = new URL(candidate);
- if (!["ws:", "wss:"].includes(url.protocol) || !url.host) return "";
- if (url.pathname && url.pathname !== "/") return "";
- return `http://${url.host}/json/version`;
- } catch (_error) {
- return "";
- }
-}
-
function normalizeBoolean(value, fallback = true) {
if (value === undefined || value === null || value === "") return fallback;
if (typeof value === "boolean") return value;
@@ -126,9 +74,6 @@ function hostBrowserFamilyLabel(value) {
chromium: "Chromium",
edge: "Edge",
"edge-dev": "Edge Dev",
- brave: "Brave",
- opera: "Opera",
- vivaldi: "Vivaldi",
};
const label = labels[base] || "Host browser";
if (remoteDebugging) return `${label} (allowed)`;
@@ -154,18 +99,13 @@ export const store = createStore("browserConfig", {
extensionDeleteLoadingPath: "",
hostBrowserStatus: null,
hostBrowserStatusLoading: false,
- hostBrowserStatusRefreshTimer: null,
- hostBrowserCustomEndpoint: "",
- hostBrowserCustomMode: false,
async init(config) {
this.bindConfig(config);
await Promise.all([this.loadExtensionsList(), this.loadHostBrowserStatus()]);
- this.startHostBrowserStatusRefresh();
},
cleanup() {
- this.stopHostBrowserStatusRefresh();
this.config = null;
this.extensionsList = [];
this.extensionsError = "";
@@ -173,22 +113,6 @@ export const store = createStore("browserConfig", {
this.extensionDeleteLoadingPath = "";
this.hostBrowserStatus = null;
this.hostBrowserStatusLoading = false;
- this.hostBrowserCustomEndpoint = "";
- this.hostBrowserCustomMode = false;
- },
-
- startHostBrowserStatusRefresh() {
- this.stopHostBrowserStatusRefresh();
- this.hostBrowserStatusRefreshTimer = window.setInterval(
- () => this.loadHostBrowserStatus(),
- HOST_BROWSER_STATUS_REFRESH_MS,
- );
- },
-
- stopHostBrowserStatusRefresh() {
- if (!this.hostBrowserStatusRefreshTimer) return;
- window.clearInterval(this.hostBrowserStatusRefreshTimer);
- this.hostBrowserStatusRefreshTimer = null;
},
bindConfig(config) {
@@ -196,9 +120,6 @@ export const store = createStore("browserConfig", {
if (!safeConfig) return;
if (this.config === safeConfig) return;
this.config = safeConfig;
- if (isCustomHostBrowserEndpoint(safeConfig.host_browser_selection)) {
- this.hostBrowserCustomEndpoint = safeConfig.host_browser_selection;
- }
},
setAutofocusActivePage(enabled) {
@@ -211,27 +132,6 @@ export const store = createStore("browserConfig", {
return this.config?.autofocus_active_page === false ? "Off" : "On";
},
- setBrowserTabScope(value) {
- const safeConfig = ensureConfig(this.config);
- if (!safeConfig) return;
- safeConfig.browser_tab_scope = normalizeChoice(value, BROWSER_TAB_SCOPES, "per_context");
- },
-
- browserTabScopeLabel() {
- return this.config?.browser_tab_scope === "shared" ? "Shared" : "Per chat";
- },
-
- normalizeMaxOpenTabs() {
- const safeConfig = ensureConfig(this.config);
- if (!safeConfig) return;
- safeConfig.max_open_tabs = normalizeInt(
- safeConfig.max_open_tabs,
- DEFAULT_MAX_OPEN_TABS,
- MIN_MAX_OPEN_TABS,
- HARD_MAX_OPEN_TABS,
- );
- },
-
runtimeBackendLabel() {
const value = this.config?.runtime_backend || "container";
if (value === "host_required") return "Bring Your Own Browser";
@@ -245,93 +145,6 @@ export const store = createStore("browserConfig", {
return "Local Models Only";
},
- hostBrowserOptions() {
- const connectors = Array.isArray(this.hostBrowserStatus?.connectors)
- ? this.hostBrowserStatus.connectors
- : [];
- const options = [{ value: "", label: "Automatic (A0 CLI chooses)" }];
- const seen = new Set([""]);
- for (const connector of connectors) {
- const advertised = Array.isArray(connector?.available_browsers)
- ? connector.available_browsers
- : [];
- for (const browser of advertised) {
- const value = normalizeCustomHostBrowserEndpoint(browser?.cdp_endpoint || browser?.id);
- if (!value || seen.has(value)) continue;
- seen.add(value);
- const label = browser?.label || hostBrowserFamilyLabel(browser?.family || value);
- const status = browser?.status ? ` - ${hostBrowserStatusLabel(browser.status)}` : "";
- options.push({ value, label: `${label}${status}` });
- }
- const fallbackValue = normalizeCustomHostBrowserEndpoint(connector?.cdp_endpoint || connector?.browser_id);
- if (fallbackValue && !seen.has(fallbackValue)) {
- seen.add(fallbackValue);
- const label = connector?.browser_label || hostBrowserFamilyLabel(connector?.browser_family || fallbackValue);
- options.push({ value: fallbackValue, label });
- }
- }
- const selected = normalizeHostBrowserSelection(this.config?.host_browser_selection);
- if (selected && !seen.has(selected) && !isCustomHostBrowserEndpoint(selected)) {
- seen.add(selected);
- options.push({ value: selected, label: `Saved: ${selected}` });
- }
- options.push({ value: CUSTOM_HOST_BROWSER_SELECTION, label: "Custom endpoint" });
- return options;
- },
-
- hostBrowserSelectValue() {
- if (this.hostBrowserCustomMode) return CUSTOM_HOST_BROWSER_SELECTION;
- const selected = normalizeHostBrowserSelection(this.config?.host_browser_selection);
- if (!selected) return "";
- if (this.hostBrowserOptions().some((option) => option.value === selected)) return selected;
- if (isCustomHostBrowserEndpoint(selected)) return CUSTOM_HOST_BROWSER_SELECTION;
- return selected;
- },
-
- setHostBrowserSelection(value) {
- const safeConfig = ensureConfig(this.config);
- if (!safeConfig) return;
- if (value === CUSTOM_HOST_BROWSER_SELECTION) {
- this.hostBrowserCustomMode = true;
- if (isCustomHostBrowserEndpoint(safeConfig.host_browser_selection)) {
- this.hostBrowserCustomEndpoint = safeConfig.host_browser_selection;
- } else {
- safeConfig.host_browser_selection = "";
- }
- return;
- }
- this.hostBrowserCustomMode = false;
- safeConfig.host_browser_selection = normalizeHostBrowserSelection(value);
- },
-
- showCustomHostBrowserEndpoint() {
- return this.hostBrowserSelectValue() === CUSTOM_HOST_BROWSER_SELECTION;
- },
-
- setCustomHostBrowserEndpoint(value) {
- this.hostBrowserCustomMode = true;
- this.hostBrowserCustomEndpoint = String(value || "").trim();
- const safeConfig = ensureConfig(this.config);
- if (!safeConfig) return;
- const endpoint = normalizeCustomHostBrowserEndpoint(this.hostBrowserCustomEndpoint);
- if (endpoint || !this.hostBrowserCustomEndpoint) {
- safeConfig.host_browser_selection = endpoint;
- }
- },
-
- customHostBrowserEndpointDiagnostic() {
- if (!this.hostBrowserCustomEndpoint) {
- return "Paste a ws://.../devtools/browser/... endpoint from the browser inspect page.";
- }
- const endpoint = normalizeCustomHostBrowserEndpoint(this.hostBrowserCustomEndpoint);
- if (endpoint) return `Using ${endpoint}`;
- const versionUrl = debugPortVersionUrl(this.hostBrowserCustomEndpoint);
- if (versionUrl) {
- return `This looks like a debug port. Open ${versionUrl} and copy webSocketDebuggerUrl.`;
- }
- return "Endpoint must be a ws:// or wss:// URL ending in /devtools/browser/...";
- },
-
hostBrowserProfileModeLabel() {
const value = this.config?.host_browser_profile_mode || "existing";
if (value === "agent") return "Clean Agent Profile";
diff --git a/plugins/_browser/webui/browser-panel.html b/plugins/_browser/webui/browser-panel.html
index c92a1acec..c72112064 100644
--- a/plugins/_browser/webui/browser-panel.html
+++ b/plugins/_browser/webui/browser-panel.html
@@ -16,7 +16,7 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
Chrome Extensions
+
+
+
+
+
+
+
+
+
+ warning
+
+ Extensions run inside the Docker browser sandbox, but malicious or buggy extensions can still
+ damage that environment. Review what you install.
+
+
+
+
+
+ Installed extensions
+ progress_activity
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -61,119 +173,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
Chrome Extensions
-
-
-
-
-
-
-
-
-
- warning
-
- Extensions run inside the Docker browser sandbox, but malicious or buggy extensions can still
- damage that environment. Review what you install.
-
-