* docs(specs): restore bootstrap after dev rewrite
* docs(specs): remove runtime inventory snapshot
* docs(specs): reconcile current dev truth
* docs(specs): document scheduled task actions as an owner-attribution source
Owner Attribution covered cookie, bearer-token and internal-loopback
requests. Scheduled task actions are a fourth source and behave
differently: _execute_action passes owner=task.owner off the stored
ScheduledTask row, so no request and no resolved principal are in
flight, and route-level require_user() never runs.
Webhook triggers are the sharp case. They are unauthenticated by
design with the token as the only credential and execute under the
stored task.owner.
Paths cite routes/task/task_routes.py, the canonical location after
the task subpackage move (#6081); routes/task_routes.py on current dev
is the backward-compat shim.
* docs(specs): add chained tasks to the trigger list, refresh dev stamp
Review feedback from RaresKeY on the previous commit.
"Every trigger path" was too broad: success-chained tasks are another
path into _execute_action. Added them with their own citation, and
noted that chaining additionally requires the target task to share
task.owner and rejects cycles, which is stricter than the trigger-side
checks. Softened the lead-in to "these trigger paths".
Line 56 still pointed at routes/task_routes.py for webhook credential
validation. That path is the backward-compat shim on current dev after
the task subpackage move (#6081); repointed to the canonical
routes/task/task_routes.py.
Stamp moved to dev@2a6b09b. Inspection backing that bump was scoped:
every file path cited in this spec was mechanically checked to resolve
on 2a6b09b, and every file:line in the Owner Attribution additions was
read against it. Behavioral claims elsewhere in the file were not
re-audited.
* docs(specs): correct SECURE_COOKIES description to match current behavior
Third of the stale details RaresKeY enumerated. The cookie section
described SECURE_COOKIES as purely opt-in, which stopped being true.
_secure_cookie() (routes/auth_routes.py:89) treats an explicit true or
false as authoritative and derives the Secure attribute from the
request otherwise, including when the variable is unset and when
docker-compose injects it present-but-empty. Either the connection
scheme or the first X-Forwarded-Proto hop being https is enough.
* docs(specs): refresh current dev truth
---------
Co-authored-by: StressTestor <212606152+StressTestor@users.noreply.github.com>
14 KiB
Integrations
Last updated: dev@e71f8ce | 2026-08-25
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;- canonical
routes/webhook/webhook_routes.pyplus its top-level compatibility shim, andsrc/webhook_manager.py; - task webhook generation/triggering in canonical
routes/task/task_routes.py, its top-level compatibility shim,app.py,static/js/tasks.js, andscripts/odysseus-webhook; - 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.
After validation, execute_api_call() pins the outbound connection to the validated IP snapshot while preserving the configured URL, Host header, TLS server name, and redirect policy. DNS cannot select a different destination between SSRF validation and transport.
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.task_routes owns token generation/regeneration and validates task id, token, and active status before queueing a run; the top-level route module is a compatibility alias.
static/js/tasks.js displays the live task webhook URL. scripts/odysseus-webhook url now emits the same route with percent-encoded task/token path segments; the CLI still reads and mutates task rows directly for list/show/rotate/revoke rather than delegating to HTTP route policy.
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.
When COMPANION_BASE_URL is set, pairing advertises that validated operator-selected v1 address instead of container/request auto-detection. The accepted form is a canonical ASCII http:// LAN/Tailscale IPv4, single-label hostname, or *.local origin with optional valid port and no credentials/path/query/fragment; HTTPS, public/misleading numeric host spellings, percent/backslash/control characters, and unsupported hosts fail closed. Auth-disabled model inventory retains the normal single-user all-endpoints view instead of filtering every ownerless request to legacy-null rows.
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 unless a valid
COMPANION_BASE_URLis configured. 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. - Admin-authored integration descriptions and
api_callresults enter the untrusted-result/gated-action pipeline, but their product-level trust presentation still needs continued review. - The dormant SQLAlchemy
Integrationmodel should be removed, migrated into use, or documented as legacy.