13 KiB
Integrations
Last updated: dev@df2fad2 | 2026-07-12
Scope
This spec covers external integration surfaces in:
routes/codex_routes.py;integrations/codex/*andintegrations/claude/*;routes/api_token_routes.pyand bearer-token handling inapp.py;routes/auth_routes.pyintegration CRUD/test routes;src/integrations.pyanddata/integrations.json;routes/webhook_routes.pyandsrc/webhook_manager.py;- task webhook generation/triggering in
routes/task_routes.py,app.py, andstatic/js/tasks.js; - companion/mobile pairing in
companion/routes.pyandcompanion/pairing.py; - provider OAuth/device-flow endpoint links in
routes/copilot_routes.py,routes/chatgpt_subscription_routes.py,routes/device_flow.py, andProviderAuthSessionrows; - integration UI surfaces in
static/js/settings.jsandstatic/js/admin.js; - database models
ApiTokenandWebhook.
The SQLAlchemy Integration model exists in core/database.py, but current Settings generic integration CRUD uses src/integrations.py and data/integrations.json.
Scoped Agent Runtime
/api/codex/* is the canonical scoped HTTP surface for external coding agents. Claude Code uses the same runtime endpoints; /api/claude/plugin.zip only delivers the Claude skill bundle.
routes.codex_routes owns:
/api/codex/capabilities;- todos list/manage through
do_manage_notes(); - email list/read/draft/send;
- memory list/add/delete;
- calendar list/create/delete;
- document list/read/create/delete;
- Cookbook task/server/output/cached-model/preset/serve/adopt/stop controls.
_scope_owner() owns scope checks and token-owner resolution. _as_owner() temporarily runs borrowed route handlers as the scoped owner and restores request state afterward. Borrowed email, memory, calendar, and document route handlers own their domain behavior; Codex routes only adapt them behind scoped access.
Runtime behavior:
- missing scopes return 403;
- invalid payloads return 400;
- unavailable borrowed route surfaces return 503;
- capabilities expose scope-derived booleans and partial availability flags;
- email send and destructive actions remain described as confirmation-required behavior in bundled agent instructions.
- Cookbook adopt/stop paths validate stored remote SSH host and port before interpolating them into SSH commands.
The local integration skill/helper files require ODYSSEUS_URL and ODYSSEUS_API_TOKEN. They must use /api/codex/* and must not bypass Settings/token scopes through SSH, Docker, direct DB access, local files, MCP internals, or app imports. Helper scripts refuse non-/api/codex/* paths.
Bundle Distribution
/api/codex/plugin.zip ships the Codex plugin tree from integrations/codex/. /api/claude/plugin.zip ships only the Claude skills/ subtree from integrations/claude/skills/. These routes require an authenticated browser/user request and do not embed an API token.
Setup instructions are duplicated in integration READMEs and static/js/settings.js; they need to stay aligned with live route surfaces and /api/codex/capabilities.
API Tokens
routes.api_token_routes owns token profiles, allowed scopes, scope normalization, token creation/update/revocation, and profile metadata shown in Settings. Partial updates preserve existing scopes unless new scopes are supplied, owner checks apply to update/delete, and write scopes auto-include their read scope where applicable.
app.py owns bearer-token validation. It accepts Bearer ody_..., checks a bcrypt hash through a prefix cache, updates last_used_at asynchronously, and stamps:
request.state.current_user = "api";request.state.api_token = True;request.state.api_token_owner;request.state.api_token_scopes.
The raw token is returned only on creation. Stored state is hash, prefix, owner, scopes, active flag, and timestamps. Token create/update/delete invalidates the auth middleware cache. Companion pairing also mints chat-scoped ApiToken rows and invalidates that cache.
Current API-token consumers include:
/api/codex/*scoped agent routes;/api/v1/chatsynchronous external chat;/api/modelscatalog reads forchat-scoped token owners;- companion read endpoints;
- selected session and owner-attribution helpers described in
auth-security.md.
The Cookbook scoped-agent surface currently exposes cookbook:read and cookbook:launch in Settings and checks them in Codex routes; those scope names must stay reconciled with routes.api_token_routes.ALLOWED_SCOPES.
Generic API Integrations
src.integrations owns generic API integration presets, data/integrations.json, API-key encryption/decryption, secret masking, plaintext-key migration, enabled integration prompt text, and execute_api_call().
routes.auth_routes owns admin-only HTTP CRUD/test routes for these integrations. Presets are public metadata. The ntfy test route is special: it publishes a real test notification to the configured reminder topic instead of only probing server health.
api_call is the agent/tool execution path for configured integrations. It is blocked for non-admin/public users by tool security, accepts only relative paths, uses the admin-configured base URL/auth settings, and returns truncated external responses to the model, including a sentinel when long JSON lists are shortened. Admin-authored integration descriptions are prompt context; external responses remain untrusted data.
execute_api_call() normalizes base URLs to HTTP(S) scheme, hostname, and
path-only values, rejects request paths that are not relative absolute paths
(/...) or that carry schemes/fragments, treats / as the base URL without
appending an extra slash, and checks the final URL through src.url_safety.
Link-local/metadata targets are always rejected; setting
INTEGRATION_API_BLOCK_PRIVATE_IPS=true also rejects loopback/RFC1918/private
addresses for operators who do not need LAN integrations.
Current call sites include:
src.agent_loopinjecting enabled integration descriptions;src.tool_implementations.do_api_call();- task scheduler discovery/check-ins;
- note reminder delivery through ntfy integrations and the generic webhook reminder channel.
Webhooks And External Chat
Outgoing webhooks are admin-managed Webhook rows. routes.webhook_routes owns CRUD/test/toggle/delete and /api/v1/chat. src.webhook_manager owns allowed event validation, public URL validation, delivery-time URL revalidation, DNS-rebinding-safe pinned-IP delivery, HMAC signing, fire-and-forget delivery, in-flight task references, and delivery status/error persistence. Sanitized delivery errors redact IPv6-style address details.
Allowed outgoing events are:
session.created;chat.message;chat.completed;webhook.test.
Current webhook event emitters include session creation, chat message/completion paths, and /api/v1/chat completion.
/api/v1/chat is an inbound external chat endpoint. It requires a chat API token, checks session ownership before resume, can create a session from a direct API key, and otherwise falls back to the first owner-visible enabled model endpoint. Token-supplied direct base_url values use public-URL validation; configured endpoints remain admin-trusted. Logs and delivery/error text that include endpoint URLs should pass through URL redaction helpers before persistence or diagnostics.
Task Webhooks And Event Triggers
Task webhook triggers are separate inbound webhooks. app.py exempts only /api/tasks/{task_id}/webhook/{token} from normal auth so external callers can trigger tasks without cookies. routes.task_routes owns token generation/regeneration and validates task id, token, and active status before queueing a run.
static/js/tasks.js displays the live task webhook URL. scripts/odysseus-webhook is stale against this route shape and is not authoritative.
Event-triggered tasks use src.event_bus; task execution and scheduling ownership lives in calendar-tasks-notes.md.
Companion Pairing
companion.routes owns companion/mobile HTTP routes:
/api/companion/ping;/api/companion/info;/api/companion/models;/api/companion/pair.
Read endpoints accept session or bearer-token callers and resolve the effective owner for visible rows. Model responses omit API keys. Pairing GET renders the admin form; pairing POST is admin-cookie only, mints a normal chat-scoped API token, invalidates the auth token cache, and returns a host/port/token payload as HTML or JSON.
companion.pairing owns LAN host detection, pairing payload shape, token minting, and optional QR generation. QR rendering depends on optional qrcode; if unavailable or failing, pairing still returns the text payload.
Unified Settings Surface
The Settings Integrations view aggregates several subsystem surfaces:
- generic API integrations;
- Codex/Claude agent token setup;
- CalDAV, CardDAV, email accounts including Google Workspace/.edu OAuth connect flows, MCP/OAuth links, provider device-flow links, and agent tokens.
- provider-auth backed model endpoints such as ChatGPT Subscription and Copilot, where device-flow credentials live in provider auth rows rather than endpoint API-key fields.
Vault and companion/mobile setup are separate settings/route surfaces today, not entries in the unified add-integration list.
This spec owns the cross-integration framing and agent/token/webhook surfaces. Domain internals stay with their subsystem specs: calendar, email/contacts, shell-MCP, vault/auth, and settings-admin.
Degraded And Compatibility Behavior
- 403 from scoped APIs means a settings/scope restriction.
- 503 from Codex borrowed routes means the domain route surface is unavailable.
- Missing or corrupt
data/integrations.jsonloads as an empty list; non-object rows are ignored. - Plaintext generic integration API keys migrate to encrypted storage on load.
- Webhook delivery has no retry/backoff queue; the persisted state is last status or sanitized last error.
- Webhook URLs are validated at create and delivery time, redirects are disabled, and delivery connects to the IP set validated immediately before the request.
- Companion LAN detection is best-effort and falls back to local host/port defaults.
ODYSSEUS_URLmust be reachable from the external coding agent; no Docker/native URL rewrite is performed.
Security And Provenance
- API-token routes must either enforce a relevant scope or document an explicit exception.
- Codex/Claude plugin zips must not expose secrets beyond source instructions and helper files.
- Webhook list responses expose
has_secret, not the secret value. - Webhook secrets are encrypted when an API key manager is available; plaintext fallback is legacy/degraded behavior.
- Outgoing webhook signatures use
X-Odysseus-Signature. - Generic integration API keys are encrypted at rest and masked in API responses.
- Generic integration base URLs are admin-configured and not the same public-only policy as webhook URLs.
api_calloutput and remote integration responses are untrusted model context.- Pairing payloads expose the raw chat token once through HTML/JSON/QR; persisted token storage is hash/prefix only.
Testing Notes
Current targeted coverage includes API-token CRUD basics, chat-scoped /api/models token access, companion pairing/read-only owner scoping, webhook SSRF validation, webhook auth-exempt source checks, webhook CLI token masking, integration-store shape/encryption migration, Google email OAuth route/helper behavior, Cookbook API-token scopes, Cookbook adopt SSH host validation, and /api/v1/chat base-url/fallback owner scoping.
The integration audit also ran the targeted venv subset covering those areas with 52 passing tests and one warning.
Current Gaps
- Codex/Claude scoped routes, owner restoration, degraded 503 behavior, plugin zip contents, and helper-script path refusal need focused regression tests.
- Token profile/update behavior and Settings agent-token scope toggles need direct coverage.
- Codex Cookbook scopes need continued Settings, route-check, and
ALLOWED_SCOPESregression coverage. - Generic integration HTTP CRUD/test routes,
execute_api_call()auth modes, response shaping, and frontend Settings/Admin flows need direct coverage. do_manage_tokens()does not match/api/tokenssemantics forody_prefix, owner, scopes, and cache invalidation.do_manage_webhooks()bypasses route behavior and does not cover signing-secret parity.- Companion read endpoints should either require
chatscope or be documented as an explicit scope-policy exception. - Decide whether webhook secret plaintext fallback should remain accepted when the API key manager is unavailable.
- Decide whether generic integration base URLs should stay LAN-capable by
default or make
INTEGRATION_API_BLOCK_PRIVATE_IPS=truethe default. - Decide whether admin-authored integration descriptions and
api_callresults need a shared untrusted-context wrapper. - The dormant SQLAlchemy
Integrationmodel should be removed, migrated into use, or documented as legacy. scripts/odysseus-webhookstill emits the removed/api/webhook/{token}path.