Files
oai-web/server/web/templates/help.html
Rune Olsen 7b0a9ccc2b Settings: add dedicated DAV/Pushover tabs, fix CalDAV/CardDAV bugs
- Add admin DAV tab (rename from CalDAV/CardDAV) and Pushover tab
  - Add per-user Pushover tab (User Key only; App Token stays admin-managed)
  - Remove system-wide CalDAV/CardDAV fallback — per-user config only
  - Rewrite contacts_tool.py using httpx directly (caldav 2.x dropped AddressBook)
  - Fix CardDAV REPORT/PROPFIND using SOGo URL pattern
  - Fix CalDAV/CardDAV test endpoints (POST method, URL scheme normalization)
  - Fix Show Password button — API now returns actual credential values
  - Convert Credentials tab to generic key-value store; dedicated keys
    (CalDAV, Pushover, trusted_proxy) excluded via _DEDICATED_CRED_KEYS
2026-04-10 12:06:23 +02:00

1042 lines
75 KiB
HTML

{% extends "base.html" %}
{% block title %}{{ agent_name }} - Help{% endblock %}
{% block content %}
<div class="help-layout">
<!-- ── Left: TOC sidebar ─────────────────────────────────────────────── -->
<aside class="help-toc">
<input
type="search"
class="form-input"
placeholder="Search docs…"
oninput="helpSearch(this.value)"
style="margin-bottom:16px;font-size:12px;"
>
<ul class="toc-list">
<li><a href="#getting-started">Getting Started</a></li>
<li>
<a href="#chat">Chat Interface</a>
<ul>
<li><a href="#chat-attachments">File Attachments</a></li>
<li><a href="#chat-model-picker">Model Picker</a></li>
<li><a href="#chat-badges">Capability Badges</a></li>
</ul>
</li>
<li><a href="#files">Files</a></li>
<li>
<a href="#agents">Agents</a>
<ul>
<li><a href="#agents-creating">Creating Agents</a></li>
<li><a href="#agents-schedule">Scheduling</a></li>
<li><a href="#agents-prompt-modes">Prompt Modes</a></li>
<li><a href="#agents-tools">Tool Restrictions</a></li>
<li><a href="#agents-subagents">Sub-agents</a></li>
</ul>
</li>
<li><a href="#mcp">MCP Servers</a></li>
<li>
<a href="#settings">Settings</a>
<ul>
{% if current_user and current_user.is_admin %}
<li><a href="#settings-general">General</a></li>
<li><a href="#settings-whitelists">Whitelists</a></li>
<li><a href="#settings-credentials">Credentials</a></li>
<li><a href="#settings-dav">DAV</a></li>
<li><a href="#settings-pushover">Pushover</a></li>
{% endif %}
<li><a href="#settings-inbox">Inbox</a></li>
<li><a href="#settings-emailaccounts">Email Accounts</a></li>
<li><a href="#settings-telegram">Telegram</a></li>
{% if not (current_user and current_user.is_admin) %}
<li><a href="#settings-caldav">CalDAV / CardDAV</a></li>
<li><a href="#settings-pushover">Pushover</a></li>
{% endif %}
<li><a href="#settings-webhooks">Webhooks</a></li>
<li><a href="#settings-profile">Profile</a></li>
<li><a href="#settings-personality">Personality</a></li>
<li><a href="#settings-brain">2nd Brain</a></li>
<li><a href="#settings-mcp">MCP Servers</a></li>
{% if current_user and current_user.is_admin %}
<li><a href="#settings-security">Security</a></li>
<li><a href="#settings-branding">Branding</a></li>
<li><a href="#settings-apikey">API Key</a></li>
{% endif %}
</ul>
</li>
{% if current_user and current_user.is_admin %}
<li><a href="#user-management">User Management</a></li>
<li><a href="#credentials">Credential Key Reference</a></li>
{% endif %}
<li><a href="#api">REST API Reference</a></li>
<li><a href="#security">Security Model</a></li>
<li><a href="#messaging">Telegram &amp; Email Inbox</a></li>
</ul>
</aside>
<!-- ── Right: content ────────────────────────────────────────────────── -->
<div class="help-content">
<!-- ── 1. Getting Started ──────────────────────────────────────────── -->
<section id="getting-started" data-section>
<h1>Getting Started</h1>
<h2>What is oAI-Web?</h2>
<p>
oAI-Web (agent name: <strong>{{ agent_name }}</strong>) is a secure, self-hosted personal AI agent
built on the Claude API with full tool-use support. It runs on your home server and exposes a clean
web interface for use inside your local network. The agent can read email, browse the web, manage
calendar events, read and write files, send push notifications, generate images, and more — all via
a structured tool-use loop with optional confirmation prompts before side-effects.
</p>
<h2>System Requirements</h2>
<ul>
<li>An API key for at least one AI provider: <strong>Anthropic</strong> or <strong>OpenRouter</strong></li>
<li>Python 3.12+ (or Docker)</li>
<li><strong>PostgreSQL</strong> (with asyncpg) — the main application database</li>
<li>PostgreSQL + <strong>pgvector</strong> extension only required if you use the <em>2nd Brain</em> feature (can be the same server)</li>
</ul>
<h2>First-Time Setup Checklist</h2>
<ol>
<li>Copy <code>.env.example</code> to <code>.env</code> and set <code>ANTHROPIC_API_KEY</code> (or <code>OPENROUTER_API_KEY</code>), <code>AIDE_DB_URL</code> (PostgreSQL connection string), and <code>DB_MASTER_PASSWORD</code></li>
<li>Start the server: <code>python -m uvicorn server.main:app --host 0.0.0.0 --port 8080 --reload</code></li>
<li>On first boot with zero users, you are redirected to <code>/setup</code> to create the first admin account</li>
<li>Open <a href="/settings">Settings</a><strong>Credentials</strong> and add any additional credentials (CalDAV, email, Pushover, etc.)</li>
<li>Add email recipients via <strong>Settings → Whitelists → Email Whitelist</strong></li>
<li>Add filesystem directories via <strong>Settings → Whitelists → Filesystem Sandbox</strong> — the agent cannot touch any path outside these directories</li>
<li>Optionally set <code>system:users_base_folder</code> in Credentials to enable per-user file storage (e.g. <code>/data/users</code>)</li>
<li>Optionally configure email accounts and Telegram via their respective Settings tabs</li>
</ol>
<h2>Key Concepts</h2>
<dl>
<dt>Agent</dt>
<dd>A configured AI persona with a model, system prompt, optional schedule, and restricted tool set. Agents run headlessly — no confirmation prompts, results logged in run history.</dd>
<dt>Tool</dt>
<dd>A capability the AI can invoke: read a file, send an email, fetch a web page, generate an image, etc. Every tool call is logged in the Audit Log.</dd>
<dt>Confirmation</dt>
<dd>Before any side-effect tool (send email, write file, delete calendar event) executes in interactive chat, a modal asks you to approve or deny. Agents skip confirmations.</dd>
<dt>Audit Log</dt>
<dd>An append-only record of every tool call, its arguments, and outcome. Never auto-deleted unless you configure a retention period.</dd>
<dt>Credential Store</dt>
<dd>An AES-256-GCM encrypted key-value store in PostgreSQL. All secrets (API keys, passwords) live here — never in the agent's context window.</dd>
<dt>User Folder</dt>
<dd>When <code>system:users_base_folder</code> is set, each user gets a personal folder at <code>{base}/{username}/</code>. Agents and the Files page scope all file access to this folder automatically.</dd>
</dl>
<h2>Quick Start</h2>
<p>Navigate to the <a href="/">Chat</a> page, type a message and press <kbd>Enter</kbd>. The agent responds, uses tools as needed (you'll see spinning indicators), and may ask for confirmation before sending email or writing files.</p>
</section>
<!-- ── 2. Chat Interface ───────────────────────────────────────────── -->
<section id="chat" data-section>
<h1>Chat Interface</h1>
<h2>Sending Messages</h2>
<p>
Press <kbd>Enter</kbd> to send. Use <kbd>Shift+Enter</kbd> for a newline within your message.
The <strong>Clear History</strong> button (✕) in the status bar wipes the in-memory conversation for the current session — the agent starts fresh.
</p>
<h2 id="chat-attachments">File Attachments</h2>
<p>
The <strong>paperclip button</strong> (📎) in the input bar opens a file picker. Only shown when the active model supports vision or documents. Supported formats:
</p>
<ul>
<li><strong>Images</strong>: JPEG, PNG, GIF, WebP, AVIF — shown as thumbnails in the preview strip</li>
<li><strong>PDF</strong>: shown as a file chip with the filename in the preview strip</li>
</ul>
<p>
You can also <strong>paste images</strong> directly from the clipboard (Ctrl/Cmd+V in the chat input). Multiple files can be attached in one message. Remove any attachment by clicking the ✕ on its preview chip.
</p>
<p class="help-note">
Attachments are sent inline with the message as base64-encoded data. Large files (especially PDFs) will increase the token count and cost of the request.
</p>
<h2 id="chat-model-picker">Model Picker</h2>
<p>
The button in the status bar (bottom of the chat area) shows the currently active model. Click it to open a searchable modal listing all available models from all configured providers. Use arrow keys to navigate, <kbd>Enter</kbd> to select, <kbd>Esc</kbd> to close. Your selection is persisted in <code>localStorage</code> across page loads.
</p>
<h2 id="chat-badges">Capability Badges</h2>
<p>Small badges in the status bar show what the active model supports:</p>
<ul>
<li>🎨 <strong>Image Gen</strong> — can generate images (use via the <code>image_gen</code> tool in agents)</li>
<li>👁 <strong>Vision</strong> — can read images and PDFs; the attachment button is shown</li>
<li>🔧 <strong>Tools</strong> — supports tool/function calling</li>
<li>🌐 <strong>Online</strong> — has live web access built in</li>
</ul>
<h2>Tool Indicators</h2>
<p>While the agent is working, small badges appear below each message:</p>
<ul>
<li><span style="color:var(--accent)"></span> <strong>Pulsing blue</strong> — tool is currently running</li>
<li><span style="color:var(--green)"></span> <strong>Solid green</strong> — tool completed successfully</li>
<li><span style="color:var(--red)"></span> <strong>Solid red</strong> — tool failed or returned an error</li>
</ul>
<h2>Confirmation Modal</h2>
<p>
When the agent wants to execute a side-effect tool (send email, write/delete a file, send a push notification), a yellow modal appears showing the tool name and arguments. Click <strong>Approve</strong> to proceed or <strong>Deny</strong> to block the action. The agent receives your decision and continues.
</p>
<h2>Pausing the Agent</h2>
<p>
The <strong>Pause</strong> button in the sidebar is a global kill switch. While paused, no agent runs, scheduled tasks, inbox processing, or Telegram responses will execute. The button turns green and shows <strong>Resume</strong> when paused. Click it again to re-enable everything.
</p>
</section>
<!-- ── 3. Files ────────────────────────────────────────────────────── -->
<section id="files" data-section>
<h1>Files</h1>
<p>
The <a href="/files">Files</a> page is a browser for your personal data folder (provisioned automatically when <code>system:users_base_folder</code> is configured by your admin). It lets you navigate, download, and delete files directly from the web UI.
</p>
<h2>Browsing</h2>
<ul>
<li>Click a folder to enter it. Use the breadcrumb trail at the top to navigate back.</li>
<li>Hidden files (names starting with <code>.</code>) are not shown.</li>
<li>Columns show file size and last-modified timestamp.</li>
</ul>
<h2>Downloading</h2>
<ul>
<li><strong>Download</strong> — downloads an individual file.</li>
<li><strong>↓ ZIP</strong> — downloads an entire folder (and its contents) as a ZIP archive. The <strong>Download folder as ZIP</strong> button in the header always downloads the current folder.</li>
</ul>
<h2>Deleting Files</h2>
<p>
A red <strong>Delete</strong> button appears next to downloadable files. Clicking it shows a confirmation dialog before the file is permanently removed. Deletion is instant and cannot be undone.
</p>
<p class="help-note">
<strong>Protected files</strong>: files whose names start with <code>memory_</code> or <code>reasoning_</code> cannot be deleted from the UI. These are agent memory and decision logs maintained by email handling agents — deleting them would disrupt the agent's continuity.
</p>
<h2>No Folder Configured?</h2>
<p>
If the Files page shows "No files folder configured", ask your administrator to set the <code>system:users_base_folder</code> credential to a base path (e.g. <code>/data/users</code>). Your personal folder at <code>{base}/{username}/</code> is created automatically.
</p>
</section>
<!-- ── 4. Agents ───────────────────────────────────────────────────── -->
<section id="agents" data-section>
<h1>Agents</h1>
<p>
Agents are headless AI personas with a fixed system prompt, model, and optional cron schedule. Unlike interactive chat, agents run without confirmation modals — their allowed tools are declared at creation time. Results and token usage are logged per-run in the <a href="/agents">Agents</a> page.
</p>
<p class="help-note">
Email handling agents (created automatically by Email Accounts setup) are hidden from the Agents list and Status tab. They are managed exclusively via <strong>Settings → Email Accounts</strong>.
</p>
<h2 id="agents-creating">Creating an Agent</h2>
<p>Click <strong>New Agent</strong> on the Agents page. Required fields:</p>
<ul>
<li><strong>Name</strong> — displayed in the UI and logs</li>
<li><strong>Model</strong> — any model from a configured provider</li>
<li><strong>Prompt</strong> — the agent's task description or system prompt (see Prompt Modes below)</li>
</ul>
<p>Optional fields:</p>
<ul>
<li><strong>Description</strong> — shown in the agent list for reference</li>
<li><strong>Schedule</strong> — cron expression for automatic runs</li>
<li><strong>Allowed Tools</strong> — restrict which tools the agent may use</li>
<li><strong>Max Tool Calls</strong> — per-run limit (overrides the system default)</li>
<li><strong>Sub-agents</strong> — toggle to allow this agent to create child agents</li>
<li><strong>Prompt Mode</strong> — controls how the prompt is composed (see below)</li>
</ul>
<h2 id="agents-schedule">Scheduling</h2>
<p>Enter a cron expression in the <strong>Schedule</strong> field. The format is:</p>
<pre>minute hour day-of-month month day-of-week</pre>
<p>Examples:</p>
<ul>
<li><code>0 8 * * 1-5</code> — weekdays at 08:00</li>
<li><code>*/15 * * * *</code> — every 15 minutes</li>
<li><code>0 9 * * 1</code> — every Monday at 09:00</li>
<li><code>30 18 * * *</code> — every day at 18:30</li>
</ul>
<p>
Use the <strong>Enable / Disable</strong> toggle to pause a schedule without deleting the agent.
The <strong>Run Now</strong> button triggers an immediate run regardless of schedule.
</p>
<h2 id="agents-prompt-modes">Prompt Modes</h2>
<p>Three modes control how the agent prompt is combined with the standard system prompt (SOUL.md + security rules):</p>
<dl>
<dt>Combined <em>(default)</em></dt>
<dd>The agent prompt is prepended as the highest-priority instruction. The standard system prompt (SOUL.md, date/time, USER.md, security rules) is appended after. Best for most agents.</dd>
<dt>System only</dt>
<dd>The standard system prompt is used as-is; the agent prompt becomes the task message sent to the agent. Useful when you want {{ agent_name }}'s full personality but just need to specify a recurring task.</dd>
<dt>Agent only</dt>
<dd>The agent prompt <em>fully replaces</em> the system prompt — no SOUL.md, no security rules, no USER.md context. Use with caution. Suitable for specialized agents with a completely different persona.</dd>
</dl>
<h2 id="agents-tools">Tool Restrictions</h2>
<p>
Leave <strong>Allowed Tools</strong> blank to give the agent access to all tools. Select specific tools to restrict — only those tool schemas are sent to the model, making it structurally impossible to use undeclared tools.
</p>
<p>
MCP server tools appear as a single server-level toggle (e.g. <code>Gitea MCP</code>), which enables all tools from that server. Individual built-in tools are listed separately.
</p>
<p>Follow the <strong>least-privilege principle</strong>: give each agent only the tools it actually needs.</p>
<h2 id="agents-subagents">Sub-agents</h2>
<p>
Enable the <strong>Sub-agents</strong> toggle to give an agent access to the <code>create_subagent</code> tool. This allows the agent to spin up child agents to handle parallel or specialized tasks. Sub-agents run synchronously (the parent waits for the child to finish) and are logged separately in run history with <code>parent_agent_id</code> set.
</p>
<h2>Image Generation in Agents</h2>
<p>
Agents can generate images using the <code>image_gen</code> tool. Important: the <strong>agent model must be a text/tool-use model</strong> (e.g. Claude Sonnet), not an image-generation model. The <code>image_gen</code> tool calls the image-gen model internally, saves the result to disk, and returns the file path. The default image-gen model is <code>openrouter:openai/gpt-5-image</code> — override via the <code>system:default_image_gen_model</code> credential.
</p>
<p>
Generated images are saved to the agent's user folder. The file path is returned as the tool result so the agent can reference it.
</p>
</section>
<!-- ── 5. MCP Servers ─────────────────────────────────────────────── -->
<section id="mcp" data-section>
<h1>MCP Servers</h1>
<p>
<strong>MCP (Model Context Protocol)</strong> is an open protocol for exposing tools to AI models over a network. oAI-Web can connect to external MCP servers and use their tools exactly like built-in tools. Tool names are namespaced as <code>mcp__{server}__{tool}</code>.
</p>
<h2>Requirements for a Compatible MCP Server</h2>
<p>To be compatible with oAI-Web, an MCP server must:</p>
<ul>
<li>Expose an SSE endpoint at <code>/sse</code></li>
<li>Use <strong>SSE transport</strong> (not stdio)</li>
<li>Be compatible with <code>mcp==1.26.*</code></li>
<li>If built with Python FastMCP: use <code>uvicorn.run(mcp.sse_app(), host=..., port=...)</code><strong>not</strong> <code>mcp.run(host=..., port=...)</code> (the latter ignores <code>host</code>/<code>port</code> in mcp 1.26)</li>
<li>If connecting from a non-localhost IP (e.g. <code>192.168.x.x</code>): disable DNS rebinding protection:
<pre>from mcp.server.transport_security import TransportSecuritySettings
mcp = FastMCP(
"my-server",
transport_security=TransportSecuritySettings(
enable_dns_rebinding_protection=False
),
)</pre>
Without this, the server rejects requests with a <code>421 Misdirected Request</code> error.
</li>
<li>oAI-Web connects per-call (open → use → close), <em>not</em> persistent — the server must handle this gracefully</li>
</ul>
<h2>Adding an MCP Server</h2>
<ol>
<li>Go to <strong>Settings → MCP Servers</strong></li>
<li>Click <strong>Add Server</strong></li>
<li>Enter:
<ul>
<li><strong>Name</strong> — display name; also used for tool namespacing (slugified)</li>
<li><strong>URL</strong> — full SSE endpoint, e.g. <code>http://192.168.1.72:8812/sse</code></li>
<li><strong>Transport</strong> — select <code>sse</code></li>
<li><strong>API Key</strong> — optional bearer token if the server requires authentication</li>
</ul>
</li>
<li>Click <strong>Save</strong></li>
</ol>
<p>oAI-Web will immediately attempt to connect and discover tools. The tool count is shown in the server list.</p>
<h2>Tool Namespacing</h2>
<p>
A server named <code>Gitea MCP</code> (slugified: <code>gitea_mcp</code>) exposes tools as <code>mcp__gitea_mcp__list_repos</code>, <code>mcp__gitea_mcp__create_issue</code>, etc.
In the agent tool picker, the entire server appears as a single toggle — enabling it grants access to all of its tools.
</p>
<h2>Refreshing Tool Discovery</h2>
<p>
Click <strong>Refresh</strong> on any server in <strong>Settings → MCP Servers</strong> to re-discover tools without restarting oAI-Web. Useful after adding new tools to an MCP server.
</p>
</section>
<!-- ── 6. Settings Walkthrough ────────────────────────────────────── -->
<section id="settings" data-section>
<h1>Settings</h1>
{% if current_user and current_user.is_admin %}
<h2 id="settings-general">General <small style="font-weight:400;font-size:12px;color:var(--text-dim)">(Admin)</small></h2>
<ul>
<li><strong>Agent Control</strong>: Pause / Resume the global kill switch</li>
<li><strong>Runtime Limits</strong>: Max Tool Calls (per run) and Max Autonomous Runs per Hour — stored in the credential store for live override without restart</li>
<li><strong>Trusted Proxy IPs</strong>: Comma-separated IPs for <code>X-Forwarded-For</code> trust (requires restart)</li>
<li><strong>Users Base Folder</strong>: Set <code>system:users_base_folder</code> to an absolute path (e.g. <code>/data/users</code>) to enable per-user file storage. Each user's folder at <code>{base}/{username}/</code> is created automatically.</li>
<li><strong>Audit Log Retention</strong>: Set a retention period in days (0 = keep forever); manual clear available</li>
</ul>
<h2 id="settings-whitelists">Whitelists <small style="font-weight:400;font-size:12px;color:var(--text-dim)">(Admin)</small></h2>
<ul>
<li><strong>Email Whitelist</strong>: Only addresses on this list can receive emails from {{ agent_name }}. Supports per-address daily send limits.</li>
<li><strong>Web Whitelist (Tier 1)</strong>: Domains always accessible to the agent, regardless of session type. Subdomains are automatically included (e.g. <code>wikipedia.org</code> covers <code>en.wikipedia.org</code>).</li>
<li><strong>Filesystem Sandbox</strong>: Absolute paths the agent may read/write. The agent cannot access any path outside these directories (unless it falls within a user's personal folder). Add directories before using filesystem tools.</li>
</ul>
<h2 id="settings-credentials">Credentials <small style="font-weight:400;font-size:12px;color:var(--text-dim)">(Admin)</small></h2>
<p>
A generic AES-256-GCM encrypted key-value store for API keys and other secrets. Keys use a <code>namespace:key</code> convention. Service-specific credentials (CalDAV, CardDAV, Pushover) are managed in their own dedicated tabs — they do not appear here. See the <a href="#credentials">Credential Key Reference</a> for a full list of system keys.
</p>
<h2 id="settings-dav">DAV (CalDAV &amp; CardDAV) <small style="font-weight:400;font-size:12px;color:var(--text-dim)">(Admin)</small></h2>
<p>
Configure CalDAV and CardDAV for the admin user. There is no system-wide fallback — every user configures their own credentials independently via this tab (admin) or the <strong>CalDAV / CardDAV</strong> tab (regular users).
</p>
<ul>
<li><strong>CalDAV</strong>: server URL, username, password, and calendar name. Bare hostnames (e.g. <code>mail.example.com</code>) are accepted — <code>https://</code> is prepended automatically.</li>
<li><strong>CardDAV</strong>: tick <em>Same server as CalDAV</em> to reuse the same credentials, or enter a separate URL, username, and password. The SOGo URL pattern (<code>/SOGo/dav/{user}/Contacts/personal/</code>) is built automatically.</li>
<li><strong>Allow contact writes</strong>: when enabled, agents can create, update, and delete contacts (not just read them). This is per-user — enabling it for your account does not affect other users.</li>
<li><strong>Test buttons</strong>: verify CalDAV and CardDAV connectivity without saving.</li>
</ul>
<h2 id="settings-pushover">Pushover <small style="font-weight:400;font-size:12px;color:var(--text-dim)">(Admin)</small></h2>
<p>
Pushover sends push notifications to iOS and Android devices.
</p>
<ul>
<li><strong>App Token</strong>: registered once at <a href="https://pushover.net" target="_blank">pushover.net</a> for this oAI-Web installation. Shared by all users — they cannot see or change it.</li>
<li><strong>User Key</strong>: the admin's personal Pushover user key, shown on your pushover.net dashboard. Each user sets their own User Key in <strong>Settings → Pushover</strong>.</li>
</ul>
{% endif %}
<h2 id="settings-inbox">Inbox</h2>
<p>
The Inbox tab manages <strong>trigger rules</strong> for the legacy global IMAP/SMTP account. For per-user or multi-account email handling, use the <strong>Email Accounts</strong> tab instead.
</p>
<ul>
<li><strong>Trigger Rules</strong>: keyword phrases that, when matched in an incoming email subject/body, dispatch a specific agent and optionally send an auto-reply</li>
<li>Matching is case-insensitive and order-independent — all tokens in the phrase must appear somewhere in the message</li>
</ul>
<h2 id="settings-emailaccounts">Email Accounts</h2>
<p>
Email Accounts is the main email management area, separate from the legacy Inbox tab. Each account is independently configured with its own IMAP/SMTP credentials and an account type:
</p>
<dl>
<dt>Trigger account</dt>
<dd>Uses IMAP IDLE (instant push). Dispatches agents based on keyword trigger rules in incoming emails.</dd>
<dt>Handling account</dt>
<dd>Polls every 60 seconds. A dedicated AI agent reads and handles each email. The agent gets access to email, Telegram (optional), and filesystem tools scoped to the user's data folder.</dd>
</dl>
<p>For handling accounts, you can also configure:</p>
<ul>
<li><strong>Extra tools</strong>: optionally give the handling agent access to Telegram and/or Pushover for notifications</li>
<li><strong>Telegram keyword</strong>: a short slug (e.g. <code>work</code>) that creates a <code>/work</code> Telegram command. Send <code>/work &lt;message&gt;</code> to interact with the email agent via Telegram. Built-in sub-commands: <code>/work pause</code>, <code>/work resume</code>, <code>/work status</code></li>
<li><strong>Pause / Resume</strong>: temporarily suspend a handling account without disabling it entirely. Also available via the Telegram <code>/keyword pause</code> command</li>
</ul>
<p class="help-note">
The handling agent uses memory files (<code>memory_&lt;username&gt;.md</code>) and reasoning logs (<code>reasoning_&lt;username&gt;.md</code>) stored in the user's data folder to maintain continuity across email sessions. These files are visible in the Files page but cannot be deleted there.
</p>
<h2 id="settings-telegram">Telegram</h2>
<ul>
<li><strong>Bot Token</strong>: your Telegram bot's API token (from @BotFather). Admins set a global token here; non-admin users can set their own per-user token in their Profile tab.</li>
<li><strong>Chat ID Whitelist</strong>: only messages from listed chat IDs are processed</li>
<li><strong>Default Agent</strong>: agent dispatched for messages that don't match any trigger rule</li>
<li><strong>Trigger Rules</strong>: same keyword-matching logic as email inbox</li>
</ul>
{% if not (current_user and current_user.is_admin) %}
<h2 id="settings-caldav">CalDAV / CardDAV</h2>
<p>
Configure your personal CalDAV and CardDAV connection. There is no system-wide fallback — if you don't configure it, the tools are unavailable to you.
</p>
<ul>
<li><strong>CalDAV</strong>: server URL, username, password, and calendar name. Bare hostnames are accepted — <code>https://</code> is added automatically.</li>
<li><strong>CardDAV</strong>: tick <em>Same server as CalDAV</em> to reuse credentials, or enter separate details.</li>
<li><strong>Allow contact writes</strong>: when enabled, agents can create, update, and delete contacts.</li>
<li><strong>Test buttons</strong>: verify connectivity before saving.</li>
</ul>
<h2 id="settings-pushover">Pushover</h2>
<p>
Set your personal <strong>User Key</strong> to receive push notifications on your Pushover-connected devices. Your User Key is shown on your <a href="https://pushover.net" target="_blank">pushover.net</a> dashboard. The App Token (the shared application credential) is managed by the admin — you only need your own User Key.
</p>
{% endif %}
<h2 id="settings-webhooks">Webhooks</h2>
<p>
Inbound webhooks let external services trigger agents via HTTP — useful for iOS Shortcuts, GitHub actions, Home Assistant automations, or any tool that can send an HTTP request.
</p>
<ul>
<li><strong>Create a webhook</strong>: assign a name, description, and target agent. The secret token is shown <strong>once</strong> at creation — copy it immediately. Use <em>Rotate Token</em> to generate a new one if it is ever compromised.</li>
<li><strong>Trigger via POST</strong>: <code>POST /webhook/{token}</code> with body <code>{"message": "..."}</code></li>
<li><strong>Trigger via GET</strong>: <code>GET /webhook/{token}?q=your+message</code> — useful for iOS Shortcuts URL actions</li>
<li><strong>Enable/disable</strong>: toggle a webhook on/off without deleting it</li>
</ul>
<p>The <strong>Outbound Targets</strong> section (same tab) manages named URLs that agents can send JSON payloads to via the <code>webhook</code> tool.</p>
<h2 id="settings-profile">Profile</h2>
<p>Available to all users. Contains:</p>
<ul>
<li><strong>Theme</strong>: choose from 7 built-in themes (Dark, Darker, Light, Nord, Solarized Dark, Gruvbox, Catppuccin Mocha). Applied immediately server-side with no flash of wrong theme.</li>
<li><strong>Account Info</strong>: username and email (read-only); editable display name</li>
<li><strong>Change Password</strong>: update your login password</li>
<li><strong>Two-Factor Authentication (TOTP)</strong>: enable/disable TOTP-based MFA. On setup, a QR code is shown to scan with any authenticator app (e.g. Aegis, Google Authenticator). Once enabled, every login requires a 6-digit code.</li>
<li><strong>Data Folder</strong>: shows the path of your auto-provisioned personal folder (set by admin via <code>system:users_base_folder</code>). This folder is where the Files page browses and where agent memory files are stored.</li>
<li><strong>Telegram Bot Token</strong>: per-user Telegram bot token (optional). Overrides the global token for your sessions.</li>
</ul>
<h2 id="settings-personality">Personality</h2>
<p>
Edit <strong>SOUL.md</strong> (agent identity, values, communication style) and <strong>USER.md</strong> (owner context: name, location, preferences) directly in the browser. Changes take effect immediately — no restart required.
Both files are injected into every system prompt in order: SOUL.md → date/time → USER.md → security rules.
</p>
<h2 id="settings-brain">2nd Brain</h2>
<p>
Requires PostgreSQL + pgvector (<code>BRAIN_DB_URL</code> env var). When connected, shows connection status, recent captured thoughts, and a manual capture form. The brain MCP server is exposed at <code>/brain-mcp/sse</code> and requires the <code>brain:mcp_key</code> credential for authentication.
</p>
<h2 id="settings-mcp">MCP Servers</h2>
<p>Add, edit, remove, enable/disable, and refresh external MCP servers. See the <a href="#mcp">MCP Servers</a> section for full setup details.</p>
{% if current_user and current_user.is_admin %}
<h2 id="settings-security">Security <small style="font-weight:400;font-size:12px;color:var(--text-dim)">(Admin)</small></h2>
<p>Five independently toggleable security options:</p>
<ol>
<li><strong>Enhanced Sanitization</strong>: strips extended prompt-injection patterns from all external content</li>
<li><strong>Canary Token</strong>: a daily-rotating secret injected into the system prompt; if it appears in any tool argument, the call is blocked and you receive a Pushover alert</li>
<li><strong>LLM Content Screening</strong>: after fetching web/email/file content, sends it to a cheap screening model for a SAFE/UNSAFE verdict before returning it to the agent</li>
<li><strong>Output Validation</strong>: blocks email auto-replies from inbox sessions back to the trigger sender (prevents exfiltration loops)</li>
<li><strong>Content Truncation</strong>: caps content length from web fetch, email, and file reads to configurable character limits</li>
</ol>
<p>See the <a href="#security">Security Model</a> section for the broader security architecture.</p>
<h2 id="settings-branding">Branding <small style="font-weight:400;font-size:12px;color:var(--text-dim)">(Admin)</small></h2>
<p>
Customize the sidebar brand name (default: <code>{{ agent_name }}</code>) and logo (default: <code>logo.png</code>). Upload a PNG/JPG/GIF/WebP/SVG logo (max 2 MB). Changes take effect immediately. Reset to defaults by clearing the name field or deleting the logo.
</p>
<h2 id="settings-apikey">API Key <small style="font-weight:400;font-size:12px;color:var(--text-dim)">(Admin)</small></h2>
<p>
Protects the REST API for external programmatic access (scripts, home automations, other services, Swagger).
The <strong>web UI always works without a key</strong> — a signed session cookie is set automatically on login.
The API key is only required for:
</p>
<ul>
<li>External tools and scripts calling <code>/api/*</code> directly</li>
<li>Swagger UI (<a href="/docs">/docs</a>) — click <strong>Authorize</strong> and enter the key</li>
</ul>
<p>
The raw key is shown <strong>once</strong> at generation time — copy it to your external tool. Only a SHA-256 hash is stored server-side. Regenerating invalidates the previous key immediately.
</p>
<p>
Use header <code>X-API-Key: &lt;key&gt;</code> or <code>Authorization: Bearer &lt;key&gt;</code> in external requests.
If no key is configured, the API is open (home-network default).
</p>
{% endif %}
</section>
<!-- ── 7. User Management ──────────────────────────────────────────── -->
{% if current_user and current_user.is_admin %}
<section id="user-management" data-section>
<h1>User Management <small style="font-weight:400;font-size:14px;color:var(--text-dim)">(Admin)</small></h1>
<p>
oAI-Web supports multiple users with role-based access. Manage users at <a href="/admin/users">Admin → Users</a>.
</p>
<h2>Roles</h2>
<dl>
<dt>Admin</dt>
<dd>Full access to all settings, whitelists, credentials, and user management. Can see and manage all agents and audit logs across all users.</dd>
<dt>User</dt>
<dd>Access to chat, agents, files, and their own settings (Profile, CalDAV, Telegram, Email Accounts, MCP Servers). Cannot access system-wide whitelists, credentials, branding, or security settings.</dd>
</dl>
<h2>Creating Users</h2>
<ol>
<li>Go to <a href="/admin/users">Admin → Users</a></li>
<li>Click <strong>New User</strong></li>
<li>Enter username, email, password, and select a role</li>
<li>If <code>system:users_base_folder</code> is configured, a personal folder is created automatically at <code>{base}/{username}/</code></li>
</ol>
<h2>MFA Management</h2>
<p>
Users set up their own TOTP in <strong>Settings → Profile → Two-Factor Authentication</strong>. As admin, you can clear any user's MFA from the Users page (useful if they lose their authenticator). The <strong>Clear MFA</strong> button resets their TOTP secret — they must set it up again on next login.
</p>
<h2>User Filesystem Scoping</h2>
<p>
Non-admin users' agents automatically receive a <strong>scoped filesystem tool</strong> limited to their personal folder (<code>{base}/{username}/</code>). They cannot access paths outside their folder, even if those paths are in the global filesystem whitelist. Admin agents continue to use the global whitelist-based sandbox.
</p>
</section>
{% endif %}
<!-- ── 8. Credential Key Reference ───────────────────────────────── -->
{% if current_user and current_user.is_admin %}
<section id="credentials" data-section>
<h1>Credential Key Reference</h1>
<p>All keys are stored in the encrypted credential store. System keys use the <code>system:</code> prefix.</p>
<div class="table-wrap">
<table class="help-api-table">
<thead><tr><th>Key</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>system:paused</code></td><td>Kill switch — set to <code>"1"</code> to pause all agent activity</td></tr>
<tr><td><code>system:max_tool_calls</code></td><td>Live override of MAX_TOOL_CALLS env var</td></tr>
<tr><td><code>system:max_autonomous_runs_per_hour</code></td><td>Live override of MAX_AUTONOMOUS_RUNS_PER_HOUR</td></tr>
<tr><td><code>system:audit_retention_days</code></td><td>Days to keep audit entries (0 = keep forever)</td></tr>
<tr><td><code>system:trusted_proxy_ips</code></td><td>Comma-separated IPs for X-Forwarded-For trust (requires restart)</td></tr>
<tr><td><code>system:users_base_folder</code></td><td>Base path for per-user folders (e.g. <code>/data/users</code>). Each user's folder is created at <code>{base}/{username}/</code>.</td></tr>
<tr><td><code>system:default_image_gen_model</code></td><td>Model used by the <code>image_gen</code> tool (default: <code>openrouter:openai/gpt-5-image</code>)</td></tr>
<tr><td><code>system:brand_name</code></td><td>Custom sidebar brand name</td></tr>
<tr><td><code>system:brand_logo_filename</code></td><td>Custom sidebar logo filename in /static/</td></tr>
<tr><td><code>system:security_sanitize_enhanced</code></td><td>Option 1: enhanced injection pattern sanitization</td></tr>
<tr><td><code>system:security_canary_enabled</code></td><td>Option 2: canary token detection enabled</td></tr>
<tr><td><code>system:canary_token</code></td><td>Auto-generated daily canary token (read-only)</td></tr>
<tr><td><code>system:canary_rotated_at</code></td><td>Timestamp of last canary rotation (read-only)</td></tr>
<tr><td><code>system:security_llm_screen_enabled</code></td><td>Option 3: LLM content screening enabled</td></tr>
<tr><td><code>system:security_llm_screen_model</code></td><td>Model for LLM screening (default: google/gemini-flash-1.5)</td></tr>
<tr><td><code>system:security_llm_screen_block</code></td><td>Option 3 block mode — block vs flag on UNSAFE verdict</td></tr>
<tr><td><code>system:security_output_validation_enabled</code></td><td>Option 4: output validation for inbox sessions</td></tr>
<tr><td><code>system:security_truncation_enabled</code></td><td>Option 5: content truncation</td></tr>
<tr><td><code>system:security_max_web_chars</code></td><td>Max chars from web fetch (default: 20 000)</td></tr>
<tr><td><code>system:security_max_email_chars</code></td><td>Max chars from email body (default: 6 000)</td></tr>
<tr><td><code>system:security_max_file_chars</code></td><td>Max chars from file read (default: 20 000)</td></tr>
<tr><td><code>system:security_max_subject_chars</code></td><td>Max chars of email subject (default: 200)</td></tr>
<tr><td><code>telegram:bot_token</code></td><td>Global Telegram bot API token</td></tr>
<tr><td><code>telegram:default_agent_id</code></td><td>UUID of agent for unmatched Telegram messages</td></tr>
<tr><td><code>pushover_app_token</code></td><td>Pushover App Token — managed via <strong>Settings → Pushover</strong>, not this tab</td></tr>
<tr><td><code>brain:mcp_key</code></td><td>2nd Brain MCP authentication key</td></tr>
<tr><td><code>system:api_key_hash</code></td><td>SHA-256 hash of the external API key (raw key never stored)</td></tr>
<tr><td><code>system:api_key_created_at</code></td><td>Timestamp of last API key generation</td></tr>
<tr><td><code>system:session_secret</code></td><td>HMAC secret for signing web UI session cookies (auto-generated)</td></tr>
</tbody>
</table>
</div>
</section>
{% endif %}
<!-- ── 9. REST API Reference ──────────────────────────────────────── -->
<section id="api" data-section>
<h1>REST API Reference</h1>
<p>
All endpoints are prefixed with <code>/api</code>. Responses are JSON.
If an API key is configured (Settings → General → API Key), external requests must include
<code>X-API-Key: &lt;key&gt;</code>. The web UI is exempt via session cookie.
</p>
<p class="help-note">
<strong>API Explorer:</strong> Browse and test endpoints interactively via Swagger UI at
<a href="/docs" target="_blank"><code>/docs</code></a> or ReDoc at
<a href="/redoc" target="_blank"><code>/redoc</code></a>.
Click <strong>Authorize</strong> in Swagger and enter your API key to make authenticated calls.
</p>
<h3>Credentials</h3>
<div class="table-wrap">
<table class="help-api-table">
<thead><tr><th>Method</th><th>Path</th><th>Description</th></tr></thead>
<tbody>
<tr><td><span class="http-get">GET</span></td><td><code>/api/credentials</code></td><td>List all credential keys (values not returned)</td></tr>
<tr><td><span class="http-post">POST</span></td><td><code>/api/credentials</code></td><td>Set a credential <code>{key, value, description}</code></td></tr>
<tr><td><span class="http-get">GET</span></td><td><code>/api/credentials/{key}</code></td><td>Get a single credential value</td></tr>
<tr><td><span class="http-del">DELETE</span></td><td><code>/api/credentials/{key}</code></td><td>Delete a credential</td></tr>
</tbody>
</table>
</div>
<h3>Audit Log</h3>
<div class="table-wrap">
<table class="help-api-table">
<thead><tr><th>Method</th><th>Path</th><th>Description</th></tr></thead>
<tbody>
<tr><td><span class="http-get">GET</span></td><td><code>/api/audit</code></td><td>Paginated audit log; params: <code>start</code>, <code>end</code>, <code>tool</code>, <code>session_id</code>, <code>task_id</code>, <code>confirmed</code>, <code>page</code>, <code>page_size</code></td></tr>
<tr><td><span class="http-del">DELETE</span></td><td><code>/api/audit</code></td><td>Delete audit entries older than <code>?older_than_days=N</code> (0 = all)</td></tr>
</tbody>
</table>
</div>
<h3>API Key</h3>
<div class="table-wrap">
<table class="help-api-table">
<thead><tr><th>Method</th><th>Path</th><th>Description</th></tr></thead>
<tbody>
<tr><td><span class="http-get">GET</span></td><td><code>/api/settings/api-key</code></td><td>Returns <code>{configured: bool, created_at}</code> — never returns the raw key</td></tr>
<tr><td><span class="http-post">POST</span></td><td><code>/api/settings/api-key</code></td><td>Generate a new key — returns <code>{key}</code> once only; invalidates previous key</td></tr>
<tr><td><span class="http-del">DELETE</span></td><td><code>/api/settings/api-key</code></td><td>Revoke the current key</td></tr>
</tbody>
</table>
</div>
<h3>Settings</h3>
<div class="table-wrap">
<table class="help-api-table">
<thead><tr><th>Method</th><th>Path</th><th>Description</th></tr></thead>
<tbody>
<tr><td><span class="http-get">GET</span></td><td><code>/api/settings/limits</code></td><td>Current runtime limits</td></tr>
<tr><td><span class="http-post">POST</span></td><td><code>/api/settings/limits</code></td><td>Update <code>{max_tool_calls, max_autonomous_runs_per_hour}</code></td></tr>
<tr><td><span class="http-get">GET</span></td><td><code>/api/settings/security</code></td><td>Current security option states</td></tr>
<tr><td><span class="http-post">POST</span></td><td><code>/api/settings/security</code></td><td>Update security options</td></tr>
<tr><td><span class="http-get">GET</span></td><td><code>/api/settings/branding</code></td><td>Current brand name and logo URL</td></tr>
<tr><td><span class="http-post">POST</span></td><td><code>/api/settings/branding</code></td><td>Update <code>{brand_name}</code></td></tr>
<tr><td><span class="http-post">POST</span></td><td><code>/api/settings/branding/logo/upload</code></td><td>Upload a logo file (multipart)</td></tr>
<tr><td><span class="http-del">DELETE</span></td><td><code>/api/settings/branding/logo</code></td><td>Reset logo to default</td></tr>
<tr><td><span class="http-get">GET</span></td><td><code>/api/settings/audit-retention</code></td><td>Current audit retention setting</td></tr>
<tr><td><span class="http-post">POST</span></td><td><code>/api/settings/audit-retention</code></td><td>Update <code>{days}</code></td></tr>
<tr><td><span class="http-get">GET</span></td><td><code>/api/settings/caldav</code></td><td>Admin CalDAV &amp; CardDAV config (same as <code>/api/my/caldav/config</code>)</td></tr>
<tr><td><span class="http-post">POST</span></td><td><code>/api/settings/caldav</code></td><td>Save admin CalDAV &amp; CardDAV config</td></tr>
<tr><td><span class="http-get">GET</span></td><td><code>/api/settings/pushover</code></td><td>Current Pushover App Token and admin User Key</td></tr>
<tr><td><span class="http-post">POST</span></td><td><code>/api/settings/pushover</code></td><td>Save App Token and admin User Key</td></tr>
</tbody>
</table>
</div>
<h3>Whitelists</h3>
<div class="table-wrap">
<table class="help-api-table">
<thead><tr><th>Method</th><th>Path</th><th>Description</th></tr></thead>
<tbody>
<tr><td><span class="http-get">GET</span></td><td><code>/api/email-whitelist</code></td><td>List whitelisted email recipients</td></tr>
<tr><td><span class="http-post">POST</span></td><td><code>/api/email-whitelist</code></td><td>Add/update <code>{email, daily_limit}</code></td></tr>
<tr><td><span class="http-del">DELETE</span></td><td><code>/api/email-whitelist/{email}</code></td><td>Remove a recipient</td></tr>
<tr><td><span class="http-get">GET</span></td><td><code>/api/web-whitelist</code></td><td>List Tier-1 web domains</td></tr>
<tr><td><span class="http-post">POST</span></td><td><code>/api/web-whitelist</code></td><td>Add <code>{domain, note}</code></td></tr>
<tr><td><span class="http-del">DELETE</span></td><td><code>/api/web-whitelist/{domain}</code></td><td>Remove a domain</td></tr>
<tr><td><span class="http-get">GET</span></td><td><code>/api/filesystem-whitelist</code></td><td>List sandbox directories</td></tr>
<tr><td><span class="http-post">POST</span></td><td><code>/api/filesystem-whitelist</code></td><td>Add <code>{path, note}</code></td></tr>
<tr><td><span class="http-del">DELETE</span></td><td><code>/api/filesystem-whitelist/{path}</code></td><td>Remove a directory</td></tr>
<tr><td><span class="http-get">GET</span></td><td><code>/api/filesystem-browser</code></td><td>Server-side directory listing; param: <code>?path=</code></td></tr>
</tbody>
</table>
</div>
<h3>Agents &amp; Runs</h3>
<div class="table-wrap">
<table class="help-api-table">
<thead><tr><th>Method</th><th>Path</th><th>Description</th></tr></thead>
<tbody>
<tr><td><span class="http-get">GET</span></td><td><code>/api/agents</code></td><td>List agents (excludes email handling agents)</td></tr>
<tr><td><span class="http-post">POST</span></td><td><code>/api/agents</code></td><td>Create an agent</td></tr>
<tr><td><span class="http-get">GET</span></td><td><code>/api/agents/{id}</code></td><td>Get agent details</td></tr>
<tr><td><span class="http-put">PUT</span></td><td><code>/api/agents/{id}</code></td><td>Update agent</td></tr>
<tr><td><span class="http-del">DELETE</span></td><td><code>/api/agents/{id}</code></td><td>Delete agent</td></tr>
<tr><td><span class="http-post">POST</span></td><td><code>/api/agents/{id}/run</code></td><td>Trigger immediate run</td></tr>
<tr><td><span class="http-post">POST</span></td><td><code>/api/agents/{id}/toggle</code></td><td>Enable / disable schedule</td></tr>
<tr><td><span class="http-get">GET</span></td><td><code>/api/agents/{id}/runs</code></td><td>List runs for an agent</td></tr>
<tr><td><span class="http-get">GET</span></td><td><code>/api/agent-runs</code></td><td>List recent runs across all agents (excludes email handlers)</td></tr>
<tr><td><span class="http-get">GET</span></td><td><code>/api/agent-runs/{run_id}</code></td><td>Get a specific run including full result text</td></tr>
<tr><td><span class="http-post">POST</span></td><td><code>/api/runs/{run_id}/stop</code></td><td>Stop a running agent</td></tr>
</tbody>
</table>
</div>
<h3>Models</h3>
<div class="table-wrap">
<table class="help-api-table">
<thead><tr><th>Method</th><th>Path</th><th>Description</th></tr></thead>
<tbody>
<tr><td><span class="http-get">GET</span></td><td><code>/api/models</code></td><td>Available model IDs + default</td></tr>
<tr><td><span class="http-get">GET</span></td><td><code>/api/models/info</code></td><td>Full model metadata: name, context, pricing, capabilities</td></tr>
</tbody>
</table>
</div>
<h3>MCP Servers</h3>
<div class="table-wrap">
<table class="help-api-table">
<thead><tr><th>Method</th><th>Path</th><th>Description</th></tr></thead>
<tbody>
<tr><td><span class="http-get">GET</span></td><td><code>/api/mcp-servers</code></td><td>List all MCP servers</td></tr>
<tr><td><span class="http-post">POST</span></td><td><code>/api/mcp-servers</code></td><td>Add a server</td></tr>
<tr><td><span class="http-put">PUT</span></td><td><code>/api/mcp-servers/{id}</code></td><td>Update a server</td></tr>
<tr><td><span class="http-del">DELETE</span></td><td><code>/api/mcp-servers/{id}</code></td><td>Remove a server</td></tr>
<tr><td><span class="http-post">POST</span></td><td><code>/api/mcp-servers/{id}/toggle</code></td><td>Enable / disable a server</td></tr>
<tr><td><span class="http-post">POST</span></td><td><code>/api/mcp-servers/{id}/refresh</code></td><td>Re-discover tools</td></tr>
</tbody>
</table>
</div>
<h3>Telegram &amp; Inbox</h3>
<div class="table-wrap">
<table class="help-api-table">
<thead><tr><th>Method</th><th>Path</th><th>Description</th></tr></thead>
<tbody>
<tr><td><span class="http-get">GET</span></td><td><code>/api/telegram/config</code></td><td>Global Telegram bot config</td></tr>
<tr><td><span class="http-post">POST</span></td><td><code>/api/telegram/config</code></td><td>Save bot token + default agent</td></tr>
<tr><td><span class="http-get">GET</span></td><td><code>/api/telegram/whitelist</code></td><td>Chat ID whitelist</td></tr>
<tr><td><span class="http-post">POST</span></td><td><code>/api/telegram/whitelist</code></td><td>Add chat ID</td></tr>
<tr><td><span class="http-del">DELETE</span></td><td><code>/api/telegram/whitelist/{chat_id}</code></td><td>Remove chat ID</td></tr>
<tr><td><span class="http-get">GET</span></td><td><code>/api/telegram/triggers</code></td><td>List trigger rules</td></tr>
<tr><td><span class="http-post">POST</span></td><td><code>/api/telegram/triggers</code></td><td>Create trigger rule</td></tr>
<tr><td><span class="http-del">DELETE</span></td><td><code>/api/telegram/triggers/{id}</code></td><td>Delete trigger rule</td></tr>
<tr><td><span class="http-get">GET</span></td><td><code>/api/inbox/config</code></td><td>Legacy IMAP/SMTP configuration</td></tr>
<tr><td><span class="http-post">POST</span></td><td><code>/api/inbox/config</code></td><td>Save legacy IMAP/SMTP credentials</td></tr>
<tr><td><span class="http-get">GET</span></td><td><code>/api/inbox/triggers</code></td><td>List email trigger rules</td></tr>
<tr><td><span class="http-post">POST</span></td><td><code>/api/inbox/triggers</code></td><td>Create email trigger rule</td></tr>
<tr><td><span class="http-del">DELETE</span></td><td><code>/api/inbox/triggers/{id}</code></td><td>Delete email trigger rule</td></tr>
<tr><td><span class="http-get">GET</span></td><td><code>/api/inbox/status</code></td><td>Status of all inbox listeners</td></tr>
</tbody>
</table>
</div>
<h3>Email Accounts</h3>
<div class="table-wrap">
<table class="help-api-table">
<thead><tr><th>Method</th><th>Path</th><th>Description</th></tr></thead>
<tbody>
<tr><td><span class="http-get">GET</span></td><td><code>/api/my/email-accounts</code></td><td>List user's email accounts</td></tr>
<tr><td><span class="http-post">POST</span></td><td><code>/api/my/email-accounts</code></td><td>Create an email account</td></tr>
<tr><td><span class="http-put">PUT</span></td><td><code>/api/my/email-accounts/{id}</code></td><td>Update email account</td></tr>
<tr><td><span class="http-del">DELETE</span></td><td><code>/api/my/email-accounts/{id}</code></td><td>Delete email account</td></tr>
<tr><td><span class="http-post">POST</span></td><td><code>/api/my/email-accounts/{id}/pause</code></td><td>Pause a handling account</td></tr>
<tr><td><span class="http-post">POST</span></td><td><code>/api/my/email-accounts/{id}/resume</code></td><td>Resume a paused handling account</td></tr>
<tr><td><span class="http-post">POST</span></td><td><code>/api/my/email-accounts/list-folders-preview</code></td><td>List IMAP folders using raw credentials (without saving an account)</td></tr>
<tr><td><span class="http-get">GET</span></td><td><code>/api/my/email-accounts/available-extra-tools</code></td><td>Which notification tools are available for handling accounts</td></tr>
</tbody>
</table>
</div>
<h3>Files</h3>
<div class="table-wrap">
<table class="help-api-table">
<thead><tr><th>Method</th><th>Path</th><th>Description</th></tr></thead>
<tbody>
<tr><td><span class="http-get">GET</span></td><td><code>/api/my/files</code></td><td>List files/folders in the user's data folder; param: <code>?path=</code></td></tr>
<tr><td><span class="http-del">DELETE</span></td><td><code>/api/my/files</code></td><td>Delete a file; param: <code>?path=</code>. Protected names (<code>memory_*</code>, <code>reasoning_*</code>) return 403.</td></tr>
<tr><td><span class="http-get">GET</span></td><td><code>/api/my/files/download</code></td><td>Download a single file; param: <code>?path=</code></td></tr>
<tr><td><span class="http-get">GET</span></td><td><code>/api/my/files/download-zip</code></td><td>Download a folder as ZIP; param: <code>?path=</code></td></tr>
<tr><td><span class="http-get">GET</span></td><td><code>/api/my/data-folder</code></td><td>Return the user's provisioned data folder path</td></tr>
</tbody>
</table>
</div>
<h3>User Profile &amp; Settings</h3>
<div class="table-wrap">
<table class="help-api-table">
<thead><tr><th>Method</th><th>Path</th><th>Description</th></tr></thead>
<tbody>
<tr><td><span class="http-get">GET</span></td><td><code>/api/my/profile</code></td><td>Get display name</td></tr>
<tr><td><span class="http-post">POST</span></td><td><code>/api/my/profile</code></td><td>Update display name</td></tr>
<tr><td><span class="http-get">GET</span></td><td><code>/api/my/theme</code></td><td>Get current theme</td></tr>
<tr><td><span class="http-post">POST</span></td><td><code>/api/my/theme</code></td><td>Set theme <code>{theme_id}</code></td></tr>
<tr><td><span class="http-get">GET</span></td><td><code>/api/my/mfa/status</code></td><td>Whether MFA is enabled for the current user</td></tr>
<tr><td><span class="http-post">POST</span></td><td><code>/api/my/mfa/setup/begin</code></td><td>Start MFA setup — returns QR code PNG (base64) and provisioning URI</td></tr>
<tr><td><span class="http-post">POST</span></td><td><code>/api/my/mfa/setup/confirm</code></td><td>Confirm setup with a valid TOTP code <code>{code}</code></td></tr>
<tr><td><span class="http-del">DELETE</span></td><td><code>/api/my/mfa/disable</code></td><td>Disable MFA for the current user</td></tr>
<tr><td><span class="http-get">GET</span></td><td><code>/api/my/caldav/config</code></td><td>Get per-user CalDAV &amp; CardDAV config</td></tr>
<tr><td><span class="http-post">POST</span></td><td><code>/api/my/caldav/config</code></td><td>Save per-user CalDAV &amp; CardDAV credentials</td></tr>
<tr><td><span class="http-del">DELETE</span></td><td><code>/api/my/caldav/config</code></td><td>Remove per-user CalDAV &amp; CardDAV config</td></tr>
<tr><td><span class="http-post">POST</span></td><td><code>/api/my/caldav/test</code></td><td>Test CalDAV connectivity with current saved config</td></tr>
<tr><td><span class="http-post">POST</span></td><td><code>/api/my/caldav/test-carddav</code></td><td>Test CardDAV connectivity with current saved config</td></tr>
<tr><td><span class="http-get">GET</span></td><td><code>/api/my/pushover</code></td><td>Get current user's Pushover User Key (masked)</td></tr>
<tr><td><span class="http-post">POST</span></td><td><code>/api/my/pushover</code></td><td>Save personal User Key <code>{user_key}</code></td></tr>
<tr><td><span class="http-del">DELETE</span></td><td><code>/api/my/pushover</code></td><td>Remove personal User Key</td></tr>
<tr><td><span class="http-get">GET</span></td><td><code>/api/my/telegram/whitelisted-chats</code></td><td>List Telegram chat IDs whitelisted for the current user</td></tr>
</tbody>
</table>
</div>
<h3>Webhooks</h3>
<div class="table-wrap">
<table class="help-api-table">
<thead><tr><th>Method</th><th>Path</th><th>Description</th></tr></thead>
<tbody>
<tr><td><span class="http-get">GET</span></td><td><code>/api/webhooks</code></td><td>List inbound webhook endpoints (admin)</td></tr>
<tr><td><span class="http-post">POST</span></td><td><code>/api/webhooks</code></td><td>Create endpoint — returns token once (admin)</td></tr>
<tr><td><span class="http-put">PUT</span></td><td><code>/api/webhooks/{id}</code></td><td>Update name/description/agent/enabled (admin)</td></tr>
<tr><td><span class="http-del">DELETE</span></td><td><code>/api/webhooks/{id}</code></td><td>Delete endpoint (admin)</td></tr>
<tr><td><span class="http-post">POST</span></td><td><code>/api/webhooks/{id}/rotate</code></td><td>Regenerate token — returns new token once (admin)</td></tr>
<tr><td><span class="http-get">GET</span></td><td><code>/api/my/webhooks</code></td><td>List current user's webhook endpoints</td></tr>
<tr><td><span class="http-post">POST</span></td><td><code>/api/my/webhooks</code></td><td>Create personal webhook endpoint</td></tr>
<tr><td><span class="http-put">PUT</span></td><td><code>/api/my/webhooks/{id}</code></td><td>Update personal webhook endpoint</td></tr>
<tr><td><span class="http-del">DELETE</span></td><td><code>/api/my/webhooks/{id}</code></td><td>Delete personal webhook endpoint</td></tr>
<tr><td><span class="http-get">GET</span></td><td><code>/webhook/{token}</code></td><td>Trigger via GET — param: <code>?q=message</code> (no auth)</td></tr>
<tr><td><span class="http-post">POST</span></td><td><code>/webhook/{token}</code></td><td>Trigger via POST — body: <code>{"message": "...", "async": true}</code> (no auth)</td></tr>
<tr><td><span class="http-get">GET</span></td><td><code>/api/webhook-targets</code></td><td>List outbound webhook targets (admin)</td></tr>
<tr><td><span class="http-post">POST</span></td><td><code>/api/webhook-targets</code></td><td>Create outbound target (admin)</td></tr>
<tr><td><span class="http-put">PUT</span></td><td><code>/api/webhook-targets/{id}</code></td><td>Update outbound target (admin)</td></tr>
<tr><td><span class="http-del">DELETE</span></td><td><code>/api/webhook-targets/{id}</code></td><td>Delete outbound target (admin)</td></tr>
</tbody>
</table>
</div>
<h3>Monitors</h3>
<div class="table-wrap">
<table class="help-api-table">
<thead><tr><th>Method</th><th>Path</th><th>Description</th></tr></thead>
<tbody>
<tr><td><span class="http-get">GET</span></td><td><code>/api/watched-pages</code></td><td>List page-change monitors</td></tr>
<tr><td><span class="http-post">POST</span></td><td><code>/api/watched-pages</code></td><td>Create page monitor</td></tr>
<tr><td><span class="http-put">PUT</span></td><td><code>/api/watched-pages/{id}</code></td><td>Update page monitor</td></tr>
<tr><td><span class="http-del">DELETE</span></td><td><code>/api/watched-pages/{id}</code></td><td>Delete page monitor</td></tr>
<tr><td><span class="http-post">POST</span></td><td><code>/api/watched-pages/{id}/check-now</code></td><td>Force an immediate check</td></tr>
<tr><td><span class="http-get">GET</span></td><td><code>/api/rss-feeds</code></td><td>List RSS feed monitors</td></tr>
<tr><td><span class="http-post">POST</span></td><td><code>/api/rss-feeds</code></td><td>Create RSS feed monitor</td></tr>
<tr><td><span class="http-put">PUT</span></td><td><code>/api/rss-feeds/{id}</code></td><td>Update RSS feed monitor</td></tr>
<tr><td><span class="http-del">DELETE</span></td><td><code>/api/rss-feeds/{id}</code></td><td>Delete RSS feed monitor</td></tr>
<tr><td><span class="http-post">POST</span></td><td><code>/api/rss-feeds/{id}/fetch-now</code></td><td>Force an immediate fetch</td></tr>
</tbody>
</table>
</div>
<h3>User Management <small style="color:var(--text-dim)">(Admin)</small></h3>
<div class="table-wrap">
<table class="help-api-table">
<thead><tr><th>Method</th><th>Path</th><th>Description</th></tr></thead>
<tbody>
<tr><td><span class="http-get">GET</span></td><td><code>/api/users</code></td><td>List all users</td></tr>
<tr><td><span class="http-post">POST</span></td><td><code>/api/users</code></td><td>Create a user <code>{username, email, password, role}</code></td></tr>
<tr><td><span class="http-put">PUT</span></td><td><code>/api/users/{id}</code></td><td>Update user (role, active status, etc.)</td></tr>
<tr><td><span class="http-del">DELETE</span></td><td><code>/api/users/{id}</code></td><td>Delete a user</td></tr>
<tr><td><span class="http-del">DELETE</span></td><td><code>/api/users/{id}/mfa</code></td><td>Clear a user's MFA secret (admin reset)</td></tr>
</tbody>
</table>
</div>
<h3>System Prompt, Tools &amp; Control</h3>
<div class="table-wrap">
<table class="help-api-table">
<thead><tr><th>Method</th><th>Path</th><th>Description</th></tr></thead>
<tbody>
<tr><td><span class="http-get">GET</span></td><td><code>/api/system-prompt/soul</code></td><td>Read SOUL.md content</td></tr>
<tr><td><span class="http-post">POST</span></td><td><code>/api/system-prompt/soul</code></td><td>Save SOUL.md content</td></tr>
<tr><td><span class="http-get">GET</span></td><td><code>/api/system-prompt/user</code></td><td>Read USER.md content</td></tr>
<tr><td><span class="http-post">POST</span></td><td><code>/api/system-prompt/user</code></td><td>Save USER.md content</td></tr>
<tr><td><span class="http-get">GET</span></td><td><code>/api/tools</code></td><td>List all registered tools with schemas</td></tr>
<tr><td><span class="http-post">POST</span></td><td><code>/api/pause</code></td><td>Pause all agent activity</td></tr>
<tr><td><span class="http-post">POST</span></td><td><code>/api/resume</code></td><td>Resume agent activity</td></tr>
<tr><td><span class="http-get">GET</span></td><td><code>/api/status</code></td><td>Pause state + pending confirmations</td></tr>
<tr><td><span class="http-get">GET</span></td><td><code>/health</code></td><td>Health check</td></tr>
</tbody>
</table>
</div>
</section>
<!-- ── 10. Security Model ──────────────────────────────────────────── -->
<section id="security" data-section>
<h1>Security Model</h1>
<h2>Core Principle</h2>
<p>
<strong>External input is data, never instructions.</strong> Email body text, calendar content, web page content, and file contents are all passed as <em>tool results</em> — they are never injected into the system prompt where they could alter {{ agent_name }}'s instructions.
</p>
<h2>Three DB-Managed Whitelists</h2>
<ul>
<li><strong>Email whitelist</strong> — {{ agent_name }} can only send email to addresses explicitly approved here</li>
<li><strong>Web whitelist (Tier 1)</strong> — domains always accessible; subdomains included automatically</li>
<li><strong>Filesystem sandbox</strong> — {{ agent_name }} can only read/write within declared directories (or a user's personal folder)</li>
</ul>
<p>Tier 2 web access (any URL) is only available in user-initiated chat sessions, never in autonomous agent runs.</p>
<h2>User Filesystem Isolation</h2>
<p>
Non-admin users' agents use a <strong>scoped filesystem tool</strong> restricted to their personal folder (<code>{base}/{username}/</code>). This is enforced at the tool level regardless of what the agent prompt says. Admin agents use the global whitelist-based sandbox as before.
</p>
<h2>Confirmation Flow</h2>
<p>
In interactive chat, any tool with side effects (send email, write/delete files, send notifications, create/delete calendar events) triggers a confirmation modal. The agent pauses until you approve or deny. Agents running headlessly skip confirmations — their scope is declared at creation time.
</p>
<h2>Five Security Options</h2>
<ol>
<li><strong>Enhanced Sanitization</strong> — removes known prompt-injection patterns from all external content before it reaches the agent</li>
<li><strong>Canary Token</strong> — a daily-rotating secret in the system prompt; any tool call argument containing the canary is blocked and triggers a Pushover alert, detecting prompt-injection exfiltration attempts</li>
<li><strong>LLM Content Screening</strong> — a cheap secondary model screens fetched content for malicious instructions; operates in flag or block mode</li>
<li><strong>Output Validation</strong> — prevents inbox auto-reply loops by blocking outbound emails back to the triggering sender</li>
<li><strong>Content Truncation</strong> — enforces maximum character limits on web fetch, email, and file content to limit the attack surface of large malicious documents</li>
</ol>
<h2>Audit Log</h2>
<p>
Every tool call — arguments, result summary, confirmation status, session ID, task ID — is written to an append-only audit log. Logs are never auto-deleted unless you configure a retention period. View them at <a href="/audit">Audit Log</a>.
</p>
<h2>Kill Switch</h2>
<p>
The <strong>Pause</strong> button in the sidebar immediately halts all agent activity: no new runs, no inbox processing, no Telegram responses. The <code>system:paused</code> credential stores the state and is checked before every operation. Individual email handling accounts can also be paused independently via their Telegram keyword command or the Email Accounts UI.
</p>
<h2>No Credentials in Agent Context</h2>
<p>
API keys, passwords, and tokens are only accessed by the server-side tool implementations. The agent itself never sees a raw credential — it only receives structured results (e.g. a list of calendar events, a fetched page).
</p>
</section>
<!-- ── 11. Telegram & Email Inbox ──────────────────────────────────── -->
<section id="messaging" data-section>
<h1>Telegram &amp; Email Inbox</h1>
<h2>Telegram</h2>
<p>
oAI-Web connects to Telegram via long-polling (no webhook required). Admin setup:
</p>
<ol>
<li>Create a bot via @BotFather and copy the bot token</li>
<li>Go to <strong>Settings → Telegram</strong> and save the bot token</li>
<li>Add your Telegram chat ID to the whitelist (messages from unlisted IDs are silently dropped)</li>
<li>Optionally set a default agent for messages that don't match any trigger rule</li>
<li>Add trigger rules to route specific messages to specific agents</li>
</ol>
<p>
Non-admin users can also set their own Telegram bot token under <strong>Settings → Profile → Telegram Bot Token</strong>. This creates a personal bot that routes to agents and email accounts scoped to that user.
</p>
<p>
Each chat maintains its own conversation history (session ID: <code>telegram:{chat_id}</code>), persisted in memory and reset on server restart.
</p>
<h2>Telegram Keyword Commands (Email Handling)</h2>
<p>
When an email handling account has a <strong>Telegram keyword</strong> set (e.g. <code>work</code>), Telegram messages starting with <code>/work</code> are routed directly to that email account's agent. This allows you to interact with the email agent via Telegram without any trigger rules.
</p>
<p>Built-in sub-commands (e.g. for keyword <code>work</code>):</p>
<ul>
<li><code>/work pause</code> — temporarily pause the email account's listener</li>
<li><code>/work resume</code> — resume the listener</li>
<li><code>/work status</code> — show the account's current status</li>
<li><code>/work &lt;any message&gt;</code> — pass the message to the handling agent</li>
</ul>
<p class="help-note">
Only the Telegram chat ID associated with the email account can use its keyword commands. Other chat IDs are rejected.
</p>
<h2>Email Inbox — Trigger Accounts</h2>
<p>
Trigger accounts use IMAP IDLE for instant push notification. When a new email arrives:
</p>
<ol>
<li>Subject + body matched against trigger rules; the first match wins</li>
<li>If matched, the corresponding agent is dispatched; an auto-reply is sent if configured</li>
<li>If no trigger matches, the email is silently ignored (no default agent for trigger accounts)</li>
</ol>
<p>
Sender whitelist check behavior:
</p>
<ul>
<li>Whitelisted sender + matching trigger → agent runs, reply sent</li>
<li>Whitelisted sender + no trigger → "no trigger word" reply</li>
<li>Non-whitelisted sender + matching trigger → agent runs, but Output Validation blocks the reply</li>
<li>Non-whitelisted sender + no trigger → <strong>silently dropped</strong> (reveals nothing to the sender)</li>
</ul>
<h2>Email Inbox — Handling Accounts</h2>
<p>
Handling accounts poll every 60 seconds. A dedicated AI agent reads each new email and decides how to handle it. The agent has access to:
</p>
<ul>
<li><strong>Email tool</strong> — list, read, mark as read, move, create folders</li>
<li><strong>Filesystem tool</strong> — scoped to the user's data folder (if configured)</li>
<li><strong>Memory files</strong><code>memory_&lt;username&gt;.md</code> (persistent notes) and <code>reasoning_&lt;username&gt;.md</code> (append-only decision log) are injected into each run</li>
<li><strong>Telegram tool</strong> (optional) — bound to the account's associated chat ID; reply messages automatically include a <code>/keyword &lt;reply&gt;</code> footer for easy follow-up</li>
<li><strong>Pushover tool</strong> (optional, admin only)</li>
</ul>
<h2>Trigger Rule Matching</h2>
<p>
Both Telegram and email inbox use the same trigger-matching algorithm:
</p>
<ul>
<li><strong>Case-insensitive</strong><code>URGENT</code> matches <code>urgent</code></li>
<li><strong>Order-independent</strong> — all tokens in the trigger phrase must appear somewhere in the message, but not necessarily in sequence</li>
<li>Example: trigger phrase <code>daily report</code> matches <em>"Send me the report for the daily standup"</em> but also <em>"Daily summary report please"</em></li>
</ul>
</section>
</div><!-- .help-content -->
</div><!-- .help-layout -->
{% endblock %}