mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-09-10 18:22:20 +02:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb8842526b |
@@ -30,8 +30,6 @@ secrets.env~
|
||||
.idea/
|
||||
dev-docs/
|
||||
docs/
|
||||
website/
|
||||
assets/branding/
|
||||
*.md
|
||||
*.db
|
||||
*.sqlite
|
||||
|
||||
+1
-1
@@ -6,4 +6,4 @@
|
||||
# A per-area ownership map (security/auth, CI, frontend, agent internals, with
|
||||
# multiple named owners per line) is being worked out in issue #593; once
|
||||
# agreed it replaces this file. Until then, required reviews and the security
|
||||
# CI gate (website/security-ci.md) remain in force via branch protection.
|
||||
# CI gate (docs/security-ci.md) remain in force via branch protection.
|
||||
|
||||
+10
-11
@@ -21,7 +21,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
@@ -73,10 +73,10 @@ jobs:
|
||||
name: Python syntax (compileall)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.11"
|
||||
# Byte-compile sources — catches syntax errors without installing deps.
|
||||
@@ -86,10 +86,10 @@ jobs:
|
||||
name: JS syntax (node --check)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: "20"
|
||||
# Syntax-check our own JS (skip vendored libs in static/lib).
|
||||
@@ -105,12 +105,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
# Make Python test validation authoritative for the configured scope.
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
# Detect whether this PR only touches repository prose outside the Pages site.
|
||||
# Detect whether this PR only touches documentation files.
|
||||
# If so, skip the expensive pytest run while still reporting a passing check.
|
||||
- name: Check for docs-only changes
|
||||
id: docs-check
|
||||
@@ -122,10 +122,9 @@ jobs:
|
||||
BASE="${{ github.event.before }}"
|
||||
HEAD="${{ github.sha }}"
|
||||
fi
|
||||
# Keep website/ and assets/branding/ out of this bypass: pytest owns
|
||||
# regression guards for their published-file and orphan-asset contracts.
|
||||
# List all changed files; if every file matches docs/markdown patterns, skip pytest.
|
||||
changed=$(git diff --name-only "$BASE" "$HEAD" 2>/dev/null || git diff --name-only HEAD~1 HEAD)
|
||||
non_docs=$(echo "$changed" | grep -Ev '^(docs/|[^/]+\.md$|\.github/[^/]+\.md$)' || true)
|
||||
non_docs=$(echo "$changed" | grep -Ev '^(docs/|.*\.md$|\.github/[^/]+\.md$)' || true)
|
||||
if [ -z "$non_docs" ]; then
|
||||
echo "docs_only=true" >> "$GITHUB_OUTPUT"
|
||||
echo "Docs-only change detected — skipping pytest."
|
||||
@@ -133,7 +132,7 @@ jobs:
|
||||
echo "docs_only=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
if: steps.docs-check.outputs.docs_only != 'true'
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
@@ -27,15 +27,15 @@ jobs:
|
||||
language: [actions, javascript-typescript, python]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
|
||||
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: none
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
|
||||
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||
with:
|
||||
category: "/language:${{ matrix.language }}"
|
||||
|
||||
@@ -37,12 +37,12 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Lint Dockerfile
|
||||
uses: hadolint/hadolint-action@2a66e89f53d0771bb131a7fa31f3136336094aa6 # v3.4.0
|
||||
uses: hadolint/hadolint-action@2332a7b74a6de0dda2e2221d575162eba76ba5e5 # v3.3.0
|
||||
with:
|
||||
dockerfile: Dockerfile
|
||||
# DL3008: pinning apt package versions is impractical on a -slim base
|
||||
|
||||
@@ -23,16 +23,12 @@ on:
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- 'docs/**'
|
||||
- 'website/**'
|
||||
- 'assets/branding/**'
|
||||
- '.github/ISSUE_TEMPLATE/**'
|
||||
push:
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- 'docs/**'
|
||||
- 'website/**'
|
||||
- 'assets/branding/**'
|
||||
- '.github/ISSUE_TEMPLATE/**'
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -56,17 +52,17 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Buildx
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||
|
||||
# Build without pushing so a broken Dockerfile is caught here, and the
|
||||
# exact image we ship is what gets scanned.
|
||||
- name: Build image
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
@@ -97,15 +93,15 @@ jobs:
|
||||
security-events: write # upload SARIF to the Security tab
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Buildx
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||
|
||||
- name: Build image
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
@@ -123,7 +119,7 @@ jobs:
|
||||
TRIVY_DB_REPOSITORY: ghcr.io/aquasecurity/trivy-db:2
|
||||
|
||||
- name: Upload Trivy results
|
||||
uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
|
||||
uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||
with:
|
||||
sarif_file: trivy-results.sarif
|
||||
category: trivy-image
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -55,12 +55,12 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
name: Deploy GitHub Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'website/**'
|
||||
- '.github/workflows/deploy-pages.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: pages
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Package static site
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pages: read
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
|
||||
- uses: actions/jekyll-build-pages@44a6e6beabd48582f863aeeb6cb2151cc1716697 # v1.0.13
|
||||
with:
|
||||
source: website
|
||||
destination: _site
|
||||
- uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
|
||||
with:
|
||||
path: _site
|
||||
|
||||
deploy:
|
||||
name: Deploy static site
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pages: write
|
||||
id-token: write
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
|
||||
@@ -14,8 +14,6 @@ on:
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- 'docs/**'
|
||||
- 'website/**'
|
||||
- 'assets/branding/**'
|
||||
- '.github/ISSUE_TEMPLATE/**'
|
||||
|
||||
concurrency:
|
||||
@@ -47,20 +45,20 @@ jobs:
|
||||
arch: arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Buildx
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
||||
with:
|
||||
context: .
|
||||
platforms: ${{ matrix.platform }}
|
||||
@@ -88,7 +86,7 @@ jobs:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Read APP_VERSION + short sha
|
||||
@@ -105,16 +103,16 @@ jobs:
|
||||
pattern: digest-*
|
||||
merge-multiple: true
|
||||
- name: Set up Buildx
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Compute tags
|
||||
id: meta
|
||||
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
|
||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
# Skip bots (Dependabot, release-drafter, etc.)
|
||||
if: ${{ github.event.issue.user.type != 'Bot' }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
sparse-checkout: .github/scripts
|
||||
persist-credentials: false
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
# Skip bots: they open PRs programmatically and have their own process.
|
||||
if: github.event.pull_request.user.type != 'Bot'
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
ref: ${{ github.base_ref }}
|
||||
sparse-checkout: .github/scripts
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
# Full history so a secret committed in an earlier commit (and later
|
||||
# deleted) is still caught -- deletion does not remove it from Git.
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -61,12 +61,12 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
|
||||
-18
@@ -85,24 +85,6 @@ output.txt.txt
|
||||
!docs/**/*.gif
|
||||
!docs/**/*.webp
|
||||
|
||||
# …except shipped website and branding media.
|
||||
!website/**/*.jpg
|
||||
!website/**/*.jpeg
|
||||
!website/**/*.png
|
||||
!website/**/*.gif
|
||||
!website/**/*.bmp
|
||||
!website/**/*.webp
|
||||
!website/**/*.tiff
|
||||
!website/**/*.pdf
|
||||
!assets/branding/**/*.jpg
|
||||
!assets/branding/**/*.jpeg
|
||||
!assets/branding/**/*.png
|
||||
!assets/branding/**/*.gif
|
||||
!assets/branding/**/*.bmp
|
||||
!assets/branding/**/*.webp
|
||||
!assets/branding/**/*.tiff
|
||||
!assets/branding/**/*.pdf
|
||||
|
||||
# Reports and temp files
|
||||
reports/
|
||||
tasks/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<p align="center">
|
||||
<img src="assets/branding/odysseus-wordmark.png" alt="Odysseus" width="238">
|
||||
<img src="docs/odysseus-wordmark.png" alt="Odysseus" width="238">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
<p align="center">
|
||||
<a href="#quick-start">Quick Start</a> ·
|
||||
<a href="website/setup.md">Setup Guide</a> ·
|
||||
<a href="docs/setup.md">Setup Guide</a> ·
|
||||
<a href="CONTRIBUTING.md">Contributing</a> ·
|
||||
<a href="ROADMAP.md">Roadmap</a>
|
||||
</p>
|
||||
@@ -18,7 +18,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="assets/branding/odysseus-browser.jpg" alt="Odysseus interface">
|
||||
<img src="docs/odysseus-browser.jpg" alt="Odysseus interface">
|
||||
</p>
|
||||
|
||||
---
|
||||
@@ -36,7 +36,7 @@ docker compose up -d --build
|
||||
|
||||
Open `http://localhost:7000` when the containers are healthy. The first admin password is printed in `docker compose logs odysseus`.
|
||||
|
||||
Native installs, GPU notes, Windows/macOS instructions, HTTPS, and configuration live in the [setup guide](website/setup.md).
|
||||
Native installs, GPU notes, Windows/macOS instructions, HTTPS, and configuration live in the [setup guide](docs/setup.md).
|
||||
|
||||
## Features
|
||||
|
||||
@@ -51,7 +51,7 @@ Native installs, GPU notes, Windows/macOS instructions, HTTPS, and configuration
|
||||
|
||||
## Demo
|
||||
|
||||
A full hover-to-play tour lives on the [Odysseus landing page](https://odysseus-dev.github.io/odysseus/). Its source lives under [`website/`](website/).
|
||||
A full hover-to-play tour lives on the landing page: [`docs/index.html`](docs/index.html).
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -64,7 +64,7 @@ Odysseus is a self-hosted workspace with powerful local tools. Keep auth enabled
|
||||
- Keep `AUTH_ENABLED=true` for any network-accessible deployment.
|
||||
- Keep `LOCALHOST_BYPASS=false` outside local development.
|
||||
|
||||
Deployment details are in the [setup guide](website/setup.md#security-notes).
|
||||
Deployment details are in the [setup guide](docs/setup.md#security-notes).
|
||||
|
||||
## Star History
|
||||
|
||||
|
||||
+4
-4
@@ -27,13 +27,13 @@ echo " port: $PORT"
|
||||
rm -rf "$APP"
|
||||
mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources"
|
||||
|
||||
# ── Icon (best effort) — center-crop the branding image to a square .icns ──
|
||||
if [ -f "$REPO_DIR/assets/branding/odysseus.jpg" ] && command -v sips >/dev/null 2>&1; then
|
||||
# ── Icon (best effort) — center-crop docs/odysseus.jpg to a square .icns ──
|
||||
if [ -f "$REPO_DIR/docs/odysseus.jpg" ] && command -v sips >/dev/null 2>&1; then
|
||||
TMPIMG="$(mktemp -d)"
|
||||
# Center-crop to a square, scale to 512 (sips' icns encoder caps at 512), and
|
||||
# let sips emit the .icns directly — more robust across macOS versions than
|
||||
# building an .iconset by hand.
|
||||
sips -c 720 720 "$REPO_DIR/assets/branding/odysseus.jpg" --out "$TMPIMG/sq.png" >/dev/null 2>&1 || cp "$REPO_DIR/assets/branding/odysseus.jpg" "$TMPIMG/sq.png"
|
||||
sips -c 720 720 "$REPO_DIR/docs/odysseus.jpg" --out "$TMPIMG/sq.png" >/dev/null 2>&1 || cp "$REPO_DIR/docs/odysseus.jpg" "$TMPIMG/sq.png"
|
||||
sips -z 512 512 "$TMPIMG/sq.png" --out "$TMPIMG/icon.png" >/dev/null 2>&1
|
||||
if sips -s format icns "$TMPIMG/icon.png" --out "$APP/Contents/Resources/odysseus.icns" >/dev/null 2>&1; then
|
||||
echo " icon: odysseus.icns"
|
||||
@@ -42,7 +42,7 @@ if [ -f "$REPO_DIR/assets/branding/odysseus.jpg" ] && command -v sips >/dev/null
|
||||
fi
|
||||
rm -rf "$TMPIMG"
|
||||
else
|
||||
echo " icon: (skipped — no assets/branding/odysseus.jpg)"
|
||||
echo " icon: (skipped — no docs/odysseus.jpg)"
|
||||
fi
|
||||
|
||||
# ── Info.plist ──
|
||||
|
||||
@@ -11,8 +11,6 @@ from typing import Dict, List, Any, Optional, TYPE_CHECKING
|
||||
from src.tool_approval_scopes import (
|
||||
CHAT_SESSION_APPROVAL_CONTEXT_MARKER,
|
||||
CHAT_SESSION_APPROVAL_DECISION,
|
||||
CHAT_SESSION_APPROVAL_SIGNATURE_FIELD,
|
||||
verify_chat_session_grant,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -62,14 +60,6 @@ def _history_grants_chat_session_approval(
|
||||
ask_user.get("kind") == "tool_approval"
|
||||
and ask_user.get("resolved") == CHAT_SESSION_APPROVAL_DECISION
|
||||
and str(ask_user.get("session_id") or "") == expected_session
|
||||
# Shape proves nothing here: routes that accept a
|
||||
# caller-supplied metadata blob write into this same history.
|
||||
and verify_chat_session_grant(
|
||||
ask_user.get(CHAT_SESSION_APPROVAL_SIGNATURE_FIELD),
|
||||
expected_session,
|
||||
ask_user.get("approval_id"),
|
||||
CHAT_SESSION_APPROVAL_DECISION,
|
||||
)
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
+1
-10
@@ -96,16 +96,7 @@ repair_bind_mount_ownership() {
|
||||
# Repair image-owned writable paths without walking into bind-mounted host
|
||||
# trees, then repair the app-owned mount roots separately.
|
||||
repair_app_tree_ownership
|
||||
# Docker creates the parent of the HuggingFace bind mount as root before this
|
||||
# entrypoint runs. Repair only the parent directory itself so app-user caches
|
||||
# such as /app/.cache/vllm and /app/.cache/flashinfer can be created without
|
||||
# recursively walking the mounted model cache.
|
||||
chown "$PUID:$PGID" /app/.cache 2>/dev/null || true
|
||||
# The Hugging Face cache can contain hundreds of gigabytes and is a nested
|
||||
# mount with its own ownership contract. Repair its mount root so new cache
|
||||
# entries are writable, but never traverse or rewrite existing model files.
|
||||
chown "$PUID:$PGID" /app/.cache/huggingface 2>/dev/null || true
|
||||
for dir in /app/data /app/logs /app/.ssh /app/.local; do
|
||||
for dir in /app/data /app/logs /app/.ssh /app/.cache/huggingface /app/.local; do
|
||||
repair_bind_mount_ownership "$dir"
|
||||
done
|
||||
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Agent migration manifests
|
||||
|
||||
Odysseus should be able to learn from another agent without blindly trusting
|
||||
@@ -1,7 +1,3 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Attachment References and Upload Storage
|
||||
|
||||
Odysseus stores uploaded bytes once under the configured upload directory and
|
||||
@@ -1,7 +1,3 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Backup & Restore
|
||||
|
||||
Odysseus keeps all of your state in the `data/` directory — the SQLite database
|
||||
@@ -1,7 +1,3 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Outlook / Office 365 email accounts
|
||||
|
||||
Odysseus email accounts currently use IMAP and SMTP with username/password
|
||||
|
Before Width: | Height: | Size: 185 KiB After Width: | Height: | Size: 185 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 79 KiB |
@@ -1,7 +1,3 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# PR Blocker Audit
|
||||
|
||||
`scripts/pr_blocker_audit.py` is a small, read-only triage helper for maintainers who need to inspect open pull request overlap before reviewing or starting related work.
|
||||
@@ -1,7 +1,3 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Security CI guide
|
||||
|
||||
This project runs a set of automated security checks on pull requests and
|
||||
@@ -1,7 +1,3 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Odysseus Setup Guide
|
||||
|
||||
This page keeps the detailed install, deployment, troubleshooting, and configuration notes out of the front README.
|
||||
@@ -19,7 +15,8 @@ On first setup, Odysseus creates an admin account (`admin` unless
|
||||
For Docker installs, the same line is in `docker compose logs odysseus`.
|
||||
Use that for the first login, then change it in **Settings**.
|
||||
|
||||
Contributing? See [CONTRIBUTING.md](https://github.com/odysseus-dev/odysseus/blob/dev/CONTRIBUTING.md) for setup, testing, and pull request guidelines.
|
||||
Contributing? See [CONTRIBUTING.md](../CONTRIBUTING.md) for setup, testing, and
|
||||
pull request guidelines.
|
||||
|
||||
### Docker (recommended)
|
||||
```bash
|
||||
@@ -208,11 +205,9 @@ failed to fulfil mount request: open /usr/lib/wsl/lib/libdxcore.so: no such file
|
||||
|
||||
Check with `snap list docker` or:
|
||||
|
||||
<!-- {% raw %} -->
|
||||
```bash
|
||||
docker info --format '{{.DockerRootDir}}'
|
||||
```
|
||||
<!-- {% endraw %} -->
|
||||
|
||||
A Docker root under `/var/snap/docker/` means snap confinement can prevent
|
||||
Docker from seeing WSL2's `/usr/lib/wsl/lib` GPU libraries even when the files
|
||||
@@ -481,7 +476,7 @@ uv pip sync requirements.lock # reproduce it exactly la
|
||||
### Outlook / Office 365 email
|
||||
Odysseus email accounts currently use IMAP/SMTP username-password auth. Outlook
|
||||
and Microsoft 365 generally require OAuth instead, so normal Microsoft mailbox
|
||||
passwords will fail. See [the Outlook email guide](email-outlook.md) for the
|
||||
passwords will fail. See [docs/email-outlook.md](docs/email-outlook.md) for the
|
||||
current limitation and the planned integration direction.
|
||||
|
||||
## Security Notes
|
||||
@@ -738,7 +733,7 @@ src/ llm_core, agent_loop, agent_tools, chat_processor, search/
|
||||
routes/ chat, session, document, memory, model … endpoints
|
||||
services/ docs, memory, search, hwfit (Cookbook) …
|
||||
static/ index.html + app.js + style.css + js/ (modular front-end)
|
||||
website/ landing page (index.html) + preview clips
|
||||
docs/ landing page (index.html) + preview clips
|
||||
```
|
||||
|
||||
## Data
|
||||
@@ -14,13 +14,6 @@ import threading
|
||||
import time
|
||||
import webbrowser
|
||||
|
||||
# PyInstaller multiprocessing children re-enter this executable with a private
|
||||
# bootstrap argument. Consume it before splash/UI or application imports so a
|
||||
# spawn-based worker does not relaunch the full desktop application.
|
||||
if __name__ == "__main__":
|
||||
import multiprocessing
|
||||
multiprocessing.freeze_support()
|
||||
|
||||
# Define a dummy NullWriter to suppress standard stream crashes (isatty etc.) in GUI mode
|
||||
class NullWriter:
|
||||
def write(self, text):
|
||||
|
||||
Generated
+4
-4
@@ -5,13 +5,13 @@
|
||||
"packages": {
|
||||
"": {
|
||||
"devDependencies": {
|
||||
"@antithesishq/bombadil": "^0.7.0"
|
||||
"@antithesishq/bombadil": "^0.6.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@antithesishq/bombadil": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@antithesishq/bombadil/-/bombadil-0.7.0.tgz",
|
||||
"integrity": "sha512-alJmnphJ/iUoL5mCsnV3DwtajGy/sEQ3NJJCiMhgjqXshSq2BUtAs0vqdXEiiSkB8HbsOX5CLrAcaogYdwfAJg==",
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@antithesishq/bombadil/-/bombadil-0.6.1.tgz",
|
||||
"integrity": "sha512-d1iufG3MI7gSMSiSmMeNdcMW+qR0yQXL2zdkVynC3n3DYgFJYlYXKUQzygmqU12m4RWlR5iOdQU1hsx5UT6+IA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
|
||||
+1
-1
@@ -4,6 +4,6 @@
|
||||
"url": "https://github.com/odysseus-dev/odysseus.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antithesishq/bombadil": "^0.7.0"
|
||||
"@antithesishq/bombadil": "^0.6.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,8 +51,3 @@ pytest-asyncio
|
||||
# TestClient import when only classic httpx is present. Runtime code keeps
|
||||
# using `httpx` above; this is test-client only.
|
||||
httpx2
|
||||
# DATABASE_URL defaults to sqlite (core/database.py), but when pointed at an
|
||||
# external Postgres, SQLAlchemy's postgresql dialect imports psycopg2 inside
|
||||
# create_engine() and raises ModuleNotFoundError if missing. -binary avoids
|
||||
# needing libpq-dev/pg_config on the host/image to compile it.
|
||||
psycopg2-binary
|
||||
|
||||
+3
-46
@@ -9,7 +9,7 @@ import logging
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, AsyncGenerator, List, Optional
|
||||
|
||||
from fastapi import APIRouter, Request, HTTPException, Form, Query, Depends
|
||||
from fastapi import APIRouter, Request, HTTPException, Form, Query
|
||||
from fastapi.responses import StreamingResponse
|
||||
from pydantic import ValidationError
|
||||
|
||||
@@ -40,13 +40,7 @@ from src.foreground_model_routing import (
|
||||
from src.session_search import search_session_messages
|
||||
from src.prompt_security import untrusted_context_message
|
||||
from core.exceptions import SessionNotFoundError
|
||||
from src.auth_helpers import (
|
||||
effective_user,
|
||||
get_current_user,
|
||||
is_delegated_credential,
|
||||
require_api_token_scope,
|
||||
require_chat_api_token_scope,
|
||||
)
|
||||
from src.auth_helpers import effective_user, get_current_user
|
||||
from routes.session_routes import _verify_session_owner
|
||||
from routes.document_helpers import _owner_session_filter
|
||||
from core.database import SessionLocal, get_session_mode, set_session_mode
|
||||
@@ -74,8 +68,6 @@ from src.tool_policy import (
|
||||
web_search_enabled_for_turn,
|
||||
)
|
||||
from src.tool_approvals import tool_approval_store
|
||||
from src.tool_approval_scopes import stamp_chat_session_grant
|
||||
from src.tool_security import delegated_credential_blocked_tools
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -97,23 +89,6 @@ def _stream_failure_status(chunk: str) -> Optional[int]:
|
||||
return None
|
||||
|
||||
|
||||
def _reject_delegated_tool_approval(request: Request) -> None:
|
||||
"""Refuse an approval answered by a bearer API token.
|
||||
|
||||
A tool approval records that a HUMAN authorized one dangerous action. A
|
||||
token is a delegated credential handed to an integration, so when it
|
||||
answers the prompt it triggered, nobody is asked and the gate collapses
|
||||
into an extra round trip. Owner and session already match here: the token
|
||||
is answering on behalf of the account that minted it.
|
||||
"""
|
||||
if is_delegated_credential(request):
|
||||
raise HTTPException(
|
||||
403,
|
||||
"Tool approvals require an interactive session. "
|
||||
"API tokens cannot authorize a gated action.",
|
||||
)
|
||||
|
||||
|
||||
def _mark_tool_approval_resolved(sess, approval_id: Any, decision: Any) -> bool:
|
||||
"""Persist a consumed approval decision on its existing tool event."""
|
||||
|
||||
@@ -138,11 +113,6 @@ def _mark_tool_approval_resolved(sess, approval_id: Any, decision: Any) -> bool:
|
||||
if str(ask_user.get("approval_id") or "") != approval_key:
|
||||
continue
|
||||
ask_user["resolved"] = normalized_decision
|
||||
stamp_chat_session_grant(
|
||||
ask_user,
|
||||
getattr(sess, "id", ""),
|
||||
normalized_decision,
|
||||
)
|
||||
message_id = metadata.get("_db_id")
|
||||
resolved_metadata = {
|
||||
key: value for key, value in metadata.items() if key != "_db_id"
|
||||
@@ -760,17 +730,13 @@ def setup_chat_routes(
|
||||
webhook_manager=None,
|
||||
skills_manager=None,
|
||||
) -> APIRouter:
|
||||
router = APIRouter(
|
||||
tags=["chat"],
|
||||
dependencies=[Depends(require_chat_api_token_scope)],
|
||||
)
|
||||
router = APIRouter(tags=["chat"])
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# POST /api/chat (non-streaming)
|
||||
# ------------------------------------------------------------------ #
|
||||
@router.post("/api/chat", response_model=Dict[str, Any])
|
||||
async def chat_endpoint(request: Request, chat_request: ChatRequest) -> Dict[str, Any]:
|
||||
require_api_token_scope(request, "chat")
|
||||
_set_user_time_from_request(request)
|
||||
|
||||
message = chat_request.message
|
||||
@@ -961,7 +927,6 @@ def setup_chat_routes(
|
||||
# ------------------------------------------------------------------ #
|
||||
@router.post("/api/chat_stream")
|
||||
async def chat_stream(request: Request) -> StreamingResponse:
|
||||
require_api_token_scope(request, "chat")
|
||||
body = None
|
||||
try:
|
||||
if request.headers.get("content-type", "").startswith("application/json"):
|
||||
@@ -1160,7 +1125,6 @@ def setup_chat_routes(
|
||||
sess = session_manager.get_session(session)
|
||||
owner = effective_user(request)
|
||||
if tool_approval_id:
|
||||
_reject_delegated_tool_approval(request)
|
||||
pending_tool_approval = tool_approval_store.peek(tool_approval_id)
|
||||
normalized_owner = str(owner or "").strip().casefold()
|
||||
if (
|
||||
@@ -1478,12 +1442,6 @@ def setup_chat_routes(
|
||||
|
||||
# Build disabled-tools set from frontend toggles + user privileges
|
||||
disabled_tools = set()
|
||||
# Minting is admin-only, so every owner-keyed check below answers
|
||||
# "admin" for a token. Cap it at the non-admin policy instead.
|
||||
# stream_agent_loop repeats this from delegated_credential.
|
||||
_delegated_credential = is_delegated_credential(request)
|
||||
if _delegated_credential:
|
||||
disabled_tools.update(delegated_credential_blocked_tools())
|
||||
# Only disable bash when the caller *explicitly* set it to a falsy
|
||||
# value. When unset (None), defer to per-user privilege checks below.
|
||||
# Web search is per-turn opt-in: either the chat pre-search setting
|
||||
@@ -2369,7 +2327,6 @@ def setup_chat_routes(
|
||||
uploaded_files=ctx.uploaded_files,
|
||||
defer_context_shaping=_foreground_policy.enabled,
|
||||
external_untrusted_context_seen=external_untrusted_context_seen,
|
||||
delegated_credential=_delegated_credential,
|
||||
exact_approval=exact_tool_approval,
|
||||
):
|
||||
if chunk.startswith("data: ") and not chunk.startswith("data: [DONE]"):
|
||||
|
||||
@@ -6,14 +6,13 @@ import logging
|
||||
import re
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
from fastapi import APIRouter, Request, HTTPException, Depends
|
||||
from fastapi import APIRouter, Request, HTTPException
|
||||
|
||||
from core.models import ChatMessage
|
||||
from core.database import SessionLocal, ChatMessage as DbChatMessage, Session as DbSession
|
||||
from src.auth_helpers import effective_user, require_chat_api_token_scope
|
||||
from src.auth_helpers import effective_user
|
||||
from src.topic_analyzer import analyze_topics
|
||||
from src.upload_handler import reserve_message_upload_references
|
||||
from src.tool_approval_scopes import sanitize_client_message_metadata
|
||||
from routes.session_routes import (
|
||||
_message_role,
|
||||
_message_text,
|
||||
@@ -102,10 +101,7 @@ def _merge_continue_rows_to_delete(db_messages, db1, db2):
|
||||
|
||||
|
||||
def setup_history_routes(session_manager, upload_handler=None) -> APIRouter:
|
||||
router = APIRouter(
|
||||
tags=["history"],
|
||||
dependencies=[Depends(require_chat_api_token_scope)],
|
||||
)
|
||||
router = APIRouter(tags=["history"])
|
||||
|
||||
def _reserve_message_uploads(
|
||||
request: Request,
|
||||
@@ -272,7 +268,7 @@ def setup_history_routes(session_manager, upload_handler=None) -> APIRouter:
|
||||
content = body.get("content", "")
|
||||
if not content:
|
||||
raise HTTPException(400, "content is required")
|
||||
metadata = sanitize_client_message_metadata(body.get("metadata"))
|
||||
metadata = body.get("metadata")
|
||||
_reserve_message_uploads(request, content, metadata)
|
||||
msg = ChatMessage(role=role, content=content, metadata=metadata)
|
||||
session_manager.add_message(session_id, msg)
|
||||
|
||||
@@ -4,24 +4,17 @@ import html
|
||||
import json
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from fastapi import APIRouter, Form, HTTPException, Response, Request, Depends
|
||||
from fastapi import APIRouter, Form, HTTPException, Response, Request
|
||||
import logging
|
||||
|
||||
from core.session_manager import SessionManager
|
||||
from core.models import ChatMessage
|
||||
from src.request_models import SessionResponse
|
||||
from core.database import Session as DbSession, SessionLocal, Document, GalleryImage, utcnow_naive
|
||||
from src.auth_helpers import (
|
||||
effective_user,
|
||||
_auth_disabled,
|
||||
owner_filter,
|
||||
is_delegated_credential,
|
||||
require_chat_api_token_scope,
|
||||
)
|
||||
from src.auth_helpers import effective_user, _auth_disabled, owner_filter
|
||||
from src.session_image_cleanup import _generated_image_path_for_cleanup, session_image_refs
|
||||
from src.session_actions import is_session_recently_active
|
||||
from src.upload_handler import reserve_message_upload_references
|
||||
from src.tool_approval_scopes import sanitize_client_message_metadata
|
||||
|
||||
|
||||
def _sanitize_export_filename(name: str) -> str:
|
||||
@@ -131,15 +124,9 @@ def _verify_session_owner(request: Request, session_id: str, session_manager=Non
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/api",
|
||||
tags=["sessions"],
|
||||
dependencies=[Depends(require_chat_api_token_scope)],
|
||||
)
|
||||
router = APIRouter(prefix="/api", tags=["sessions"])
|
||||
|
||||
def _current_user_is_admin(request: Request, user: str | None) -> bool:
|
||||
if is_delegated_credential(request):
|
||||
return False
|
||||
if not user:
|
||||
return False
|
||||
auth_mgr = getattr(request.app.state, "auth_manager", None)
|
||||
@@ -170,22 +157,6 @@ def _reject_raw_endpoint_url_for_non_admin(
|
||||
raise HTTPException(403, "Choose a registered model endpoint")
|
||||
|
||||
|
||||
def _reject_delegated_session_options(
|
||||
request: Request,
|
||||
*,
|
||||
skip_validation: bool = False,
|
||||
api_key: str | None = None,
|
||||
) -> None:
|
||||
"""Keep bearer credentials from exercising interactive-admin options."""
|
||||
if is_delegated_credential(request) and (
|
||||
skip_validation or bool((api_key or "").strip())
|
||||
):
|
||||
raise HTTPException(
|
||||
403,
|
||||
"API tokens cannot supply endpoint credentials or skip endpoint validation",
|
||||
)
|
||||
|
||||
|
||||
def _persist_session_headers(session_id: str, headers: dict | None) -> None:
|
||||
"""Persist endpoint auth headers for DB-backed session metadata."""
|
||||
db = SessionLocal()
|
||||
@@ -369,11 +340,6 @@ def setup_session_routes(
|
||||
):
|
||||
skip_val = str(skip_validation).lower() == "true"
|
||||
user = effective_user(request)
|
||||
_reject_delegated_session_options(
|
||||
request,
|
||||
skip_validation=skip_val,
|
||||
api_key=api_key,
|
||||
)
|
||||
endpoint_api_key = ""
|
||||
endpoint_base_url = ""
|
||||
_reject_raw_endpoint_url_for_non_admin(request, user, endpoint_id, endpoint_url)
|
||||
@@ -598,11 +564,7 @@ def setup_session_routes(
|
||||
except (AttributeError, TypeError, ValueError) as exc:
|
||||
raise HTTPException(400, "Invalid message attachment metadata") from exc
|
||||
for m in messages:
|
||||
sess.add_message(ChatMessage(
|
||||
m["role"],
|
||||
m["content"],
|
||||
metadata=sanitize_client_message_metadata(m.get("metadata")),
|
||||
))
|
||||
sess.add_message(ChatMessage(m["role"], m["content"], metadata=m.get("metadata")))
|
||||
session_manager.save_sessions()
|
||||
return {"ok": True, "count": len(messages)}
|
||||
|
||||
@@ -944,8 +906,6 @@ def setup_session_routes(
|
||||
model: str = Form("gpt-4o"),
|
||||
rag: str = Form(None)
|
||||
):
|
||||
if is_delegated_credential(request):
|
||||
raise HTTPException(403, "This session type requires an interactive session")
|
||||
if not OPENAI_API_KEY:
|
||||
raise HTTPException(400, "Server missing OPENAI_API_KEY")
|
||||
sid = str(uuid.uuid4())
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# Encode a source screen-recording (.mkv) into web-optimized preview clips for
|
||||
# the landing page: website/<name>.webm (VP9) + website/<name>.mp4 (H.264).
|
||||
# the landing page: docs/<name>.webm (VP9) + docs/<name>.mp4 (H.264).
|
||||
#
|
||||
# ./encode_previews.sh <input> <name> [max_secs]
|
||||
#
|
||||
@@ -13,7 +13,7 @@ set -euo pipefail
|
||||
IN="${1:?input file}"
|
||||
NAME="${2:?output basename}"
|
||||
MAX="${3:-30}"
|
||||
OUT_DIR="$(cd "$(dirname "$0")/../website" && pwd)"
|
||||
OUT_DIR="$(cd "$(dirname "$0")/../docs" && pwd)"
|
||||
|
||||
dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$IN" | cut -d. -f1)
|
||||
dur=${dur:-0}
|
||||
|
||||
@@ -16,7 +16,7 @@ sys.path.insert(0, BASE_DIR)
|
||||
from src.constants import (
|
||||
DATA_DIR, AUTH_FILE, UPLOAD_DIR, PERSONAL_DIR, PERSONAL_UPLOADS_DIR,
|
||||
TTS_CACHE_DIR, GENERATED_IMAGES_DIR, DEEP_RESEARCH_DIR, CHROMA_DIR,
|
||||
RAG_DIR, MEMORY_VECTORS_DIR, AGENT_WORKSPACE_DIR, PASSWORD_MIN_LENGTH,
|
||||
RAG_DIR, MEMORY_VECTORS_DIR, PASSWORD_MIN_LENGTH,
|
||||
)
|
||||
from core.auth import RESERVED_USERNAMES
|
||||
|
||||
@@ -31,7 +31,6 @@ DIRS = [
|
||||
CHROMA_DIR,
|
||||
RAG_DIR,
|
||||
MEMORY_VECTORS_DIR,
|
||||
AGENT_WORKSPACE_DIR,
|
||||
os.path.join(BASE_DIR, "logs"),
|
||||
]
|
||||
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
# Specs DocumentMap
|
||||
|
||||
Last updated: dev@e71f8ce | 2026-08-25
|
||||
|
||||
This folder is the compact implementation-truth map for humans and coding agents working on Odysseus. Read this file first, then open only the subsystem specs that match the work.
|
||||
|
||||
Specs are living notes about current code shape and intended contracts. They are not product marketing, not PR planning, not templates, and not a replacement for source inspection or tests.
|
||||
|
||||
This `_readme.md` is the DocumentMap and control document. It is intentionally exempt from subsystem `Scope` and `Current Gaps` sections; keep it limited to the quality contract, working rules, subsystem map, and cross-cutting update triggers.
|
||||
|
||||
## Quality Contract
|
||||
|
||||
Each subsystem spec should stay compact and useful under context pressure:
|
||||
|
||||
- Start with `Last updated: dev@<short-sha> | YYYY-MM-DD`, using the
|
||||
upstream `dev` commit the spec text was inspected against.
|
||||
- Use a concrete `Scope` section that names real files, route surfaces, frontend modules, data stores, and integration points.
|
||||
- Use domain-specific sections. Do not force every spec into the same headings when the subsystem needs `Streaming`, `Tool Results`, `Optional Dependencies`, `Current Gaps`, or another focused section.
|
||||
- State ownership clearly: which file owns a mapping, which layer only forwards state, and which caller requests behavior without owning implementation.
|
||||
- Include runtime behavior bullets for flows that matter.
|
||||
- Include "Current call sites include" when behavior is spread across many files.
|
||||
- Record transitional compatibility notes, especially `src/` versus `services/` duplication.
|
||||
- Record degraded, optional, or platform behavior where it changes runtime expectations.
|
||||
- Record policy/provenance where relevant: untrusted context, encrypted secrets, API token scopes, optional dependency/license implications, generated media, or user data.
|
||||
- End with `Current Gaps` only when there is a real known gap, not as filler.
|
||||
|
||||
If code and specs disagree, treat code as ground truth. Update specs only when
|
||||
the current task explicitly includes spec maintenance or the PR intentionally
|
||||
includes specs; otherwise report the drift in the relevant issue, PR review, or
|
||||
project documentation.
|
||||
|
||||
## Working Rules
|
||||
|
||||
- Start here before substantial work.
|
||||
- Read the related subsystem spec before changing code in that area. For cross-cutting work, include the owning domain spec plus route/runtime, auth/security, persistence, frontend, tool/context, integration, and testing/devops specs as applicable.
|
||||
- Treat specs as read-only context during ordinary project work, PR review, and code review. Do not edit specs unless the user explicitly asks for spec work or the current PR intentionally includes spec changes.
|
||||
- During explicit spec-maintenance work, update the related spec when source inspection shows behavior, ownership, security boundaries, data shape, import paths, or implementation contracts have changed.
|
||||
- During ordinary work, record source/spec drift in the relevant issue, PR review, or project documentation instead of mutating specs.
|
||||
- Keep specs dense but readable. Prefer current facts and invariants over broad explanation.
|
||||
- Every non-index `specs/*.md` file should appear exactly once in the Subsystem Map with a one-line description and no dead link.
|
||||
- Specs contain implementation truth. Planning, research, branch notes, and decisions belong in tracked project docs. Drafts, audit reports, raw exports, and exploratory gap lists are not authoritative until promoted into tracked docs or specs.
|
||||
- Use repo source and these specs as the authority for Odysseus architecture. Do not treat global skill registries or external agent metadata as repo ground truth.
|
||||
|
||||
## Subsystem Map
|
||||
|
||||
- [runtime.md](runtime.md): FastAPI startup, router registration, static serving, lifespan, app-wide middleware.
|
||||
- [auth-security.md](auth-security.md): auth, privileges, API tokens, security headers, untrusted data, SSRF and admin boundaries.
|
||||
- [persistence.md](persistence.md): SQLite models, startup migrations, encrypted columns, ownership columns, data directory rules.
|
||||
- [chat.md](chat.md): chat routes, sessions, streaming, uploads-in-chat, compare handoff, research/chat mode dispatch.
|
||||
- [compare.md](compare.md): model A/B comparison runs, voting/history, compare frontend panes, compare ownership.
|
||||
- [llm-models.md](llm-models.md): LLM provider calls, endpoint discovery, model context length, fallbacks, model endpoints.
|
||||
- [model-capability-canonical.md](model-capability-canonical.md): canonical provider/model capability shapes, evidence, payload resolution, and safe fallback.
|
||||
- [model-quirks.md](model-quirks.md): model-specific behavior observations, evidence, and promotion gates.
|
||||
- [model-providers/_readme.md](model-providers/_readme.md): provider-by-provider API/catalog shape index and compatibility status.
|
||||
- [agent-tools.md](agent-tools.md): agent loop, tool schemas, tool execution, tool retrieval, tool security, MCP tool exposure.
|
||||
- [context-building.md](context-building.md): URL/search/RAG/memory/skills/YouTube/email/tool-output context, untrusted wrapping, unavailable context, intent boundaries.
|
||||
- [search.md](search.md): web search providers, ranking, cache/analytics, URL fetch/content extraction, `src.search`/`services.search` split.
|
||||
- [documents-rag-uploads.md](documents-rag-uploads.md): uploads, documents, PDF/form handling, personal docs, RAG/vector stores.
|
||||
- [memory-skills.md](memory-skills.md): memory storage, semantic memory, skill extraction/formatting, owner isolation.
|
||||
- [research.md](research.md): deep research jobs, synthesis, sources, research library, research UI panel.
|
||||
- [calendar-tasks-notes.md](calendar-tasks-notes.md): CalDAV calendars, scheduled tasks, reminders, assistant runs, notes/todos.
|
||||
- [email-contacts.md](email-contacts.md): IMAP/SMTP email, email library, scheduled mail, contacts/CardDAV.
|
||||
- [gallery-editor-media.md](gallery-editor-media.md): gallery, generated media, image editor drafts, signatures, emoji/font helpers.
|
||||
- [cookbook-hwfit.md](cookbook-hwfit.md): model downloads, local/remote model serving, hardware detection, fit ranking.
|
||||
- [speech.md](speech.md): STT and TTS services, routes, settings, optional dependencies.
|
||||
- [frontend.md](frontend.md): static SPA, module loading, UI conventions, major JS areas, no-build frontend shape.
|
||||
- [integrations.md](integrations.md): Codex/Claude scoped APIs, companion pairing, webhooks, external agent access.
|
||||
- [shell-mcp.md](shell-mcp.md): shell execution, background jobs, MCP manager, built-in MCP servers.
|
||||
- [settings-admin.md](settings-admin.md): settings, preferences, presets, backup/import/export, diagnostics, admin wipe.
|
||||
- [testing-devops.md](testing-devops.md): pytest, JS tests, Docker, scripts, requirements, local dev expectations.
|
||||
|
||||
## Cross-Cutting Spec Update Triggers
|
||||
|
||||
Use these triggers only during explicit spec-maintenance work or a PR that
|
||||
intentionally includes specs. For ordinary work and code review, use the same
|
||||
list to choose which specs to read and where to report drift.
|
||||
|
||||
- New route file or route prefix: update [runtime.md](runtime.md) and the owning subsystem spec.
|
||||
- New SQLAlchemy model, column migration, durable JSON/local store, data directory, backup/import domain, or non-SQL persistence behavior: update [persistence.md](persistence.md) and the owning subsystem spec.
|
||||
- New tool, tool schema, agent prompt rule, or tool security behavior: update [agent-tools.md](agent-tools.md) and [context-building.md](context-building.md) if it adds model context.
|
||||
- New MCP runtime/config/built-in behavior: update [shell-mcp.md](shell-mcp.md), [agent-tools.md](agent-tools.md), and [context-building.md](context-building.md) when MCP tool results enter model context.
|
||||
- New external content source, tool result, MCP/app API result, or integration result shown to an LLM: update [context-building.md](context-building.md) and [auth-security.md](auth-security.md).
|
||||
- New API-token scope, scoped external API, webhook, companion/pairing route, generic integration provider, or external-agent helper bundle: update [integrations.md](integrations.md), [auth-security.md](auth-security.md), and the owning subsystem spec.
|
||||
- New secret store, decrypted-secret return path, settings backup/import/export behavior, diagnostics/log output, vault/tool secret flow, `.env*` policy change, or credential-bearing CLI output: update [auth-security.md](auth-security.md), [settings-admin.md](settings-admin.md), [testing-devops.md](testing-devops.md), and the owning subsystem spec.
|
||||
- New optional dependency, degraded fallback, platform/Docker/native/launcher difference, GPU overlay behavior, or retired compatibility shim: update [testing-devops.md](testing-devops.md) and the owning subsystem spec; also update [runtime.md](runtime.md), [llm-models.md](llm-models.md), [shell-mcp.md](shell-mcp.md), [cookbook-hwfit.md](cookbook-hwfit.md), or [persistence.md](persistence.md) when that layer owns the behavior.
|
||||
- New frontend module or modal/tool surface: update [frontend.md](frontend.md) and the owning subsystem spec.
|
||||
- New static/PWA/service-worker/cache/CSP behavior: update [frontend.md](frontend.md), [runtime.md](runtime.md), and [auth-security.md](auth-security.md) when headers or trust boundaries change.
|
||||
- New CLI script: update [testing-devops.md](testing-devops.md) and the owning subsystem spec.
|
||||
@@ -1,157 +0,0 @@
|
||||
# Agent Tools
|
||||
|
||||
Last updated: dev@e71f8ce | 2026-08-25
|
||||
|
||||
## Scope
|
||||
|
||||
This spec covers agent/tool behavior in:
|
||||
|
||||
- `src/agent_loop.py`;
|
||||
- `src/llm_core.py`;
|
||||
- `src/tool_schemas.py`;
|
||||
- `src/tool_execution.py`;
|
||||
- `src/tool_policy.py`;
|
||||
- `src/tool_index.py`;
|
||||
- `src/tool_parsing.py`;
|
||||
- `src/tool_security.py`;
|
||||
- `src/tool_capabilities.py`;
|
||||
- `src/tool_approval_scopes.py`;
|
||||
- `src/tool_approvals.py`;
|
||||
- `src/attachment_refs.py` and shared upload lifecycle helpers in
|
||||
`src/upload_handler.py` / `src/tool_utils.py`;
|
||||
- `src/tool_implementations.py`;
|
||||
- `src/tools/*.py`;
|
||||
- `src/builtin_actions.py`;
|
||||
- `src/ai_interaction.py`;
|
||||
- `src/action_intents.py`;
|
||||
- `src/goal_based_extractor.py`;
|
||||
- `src/teacher_escalation.py`;
|
||||
- `src/agent_tools/` modules and compatibility facade;
|
||||
- `src/mcp_manager.py`;
|
||||
- `src/builtin_mcp.py`;
|
||||
- `src/bg_jobs.py` and `src/bg_monitor.py`;
|
||||
- `routes/chat_routes.py`, `routes/chat_helpers.py`, `routes/model_routes.py`, `routes/skills_routes.py`, canonical `routes/mcp/mcp_routes.py` plus its shim, and `routes/workspace_routes.py`;
|
||||
- `mcp_servers/*.py`;
|
||||
- frontend stream/admin/settings files that display tool events, workspaces, and disabled tools;
|
||||
- `tests/test_agent_loop.py`, `tests/test_tool_*`, and focused MCP/public-policy/schema tests.
|
||||
|
||||
## Agent Loop
|
||||
|
||||
`src.agent_loop` owns agent prompt assembly, request-local current date/time insertion, tool retrieval, prompted tool-block handling, native tool-call consumption after `llm_core` normalizes provider events, multi-round execution, tool result insertion, final metrics, and fallback responses. It requests context from documents, skills, tool retrieval, and messages; it should not own domain-specific business logic for every tool. Its prompt rules now bias structured/long-form writing toward living documents, route active compose/email drafts back into existing email documents, and prefer first-class `web_search`/`web_fetch` tools over shell/Python/curl for current web lookups when web tools are enabled.
|
||||
|
||||
`src.llm_core` owns provider payloads, native tool-schema emission, and provider stream parsing. `agent_loop` consumes normalized tool-call events and decides whether and how to execute them.
|
||||
|
||||
Agent mode enters through chat routes, including auto-escalation from intent helpers, detached `agent_runs` streaming, resume/stop behavior, and frontend tool-event rendering.
|
||||
|
||||
Guide-only/no-tools turns are runtime policy, not prompt advice. `src.tool_policy` detects strong latest-turn directives such as guide-only mode, no-tools mode, and explicit requests not to use tools; it builds a `ToolPolicy` that hides schemas, disables known native tools, disables MCP for that turn, skips tool retrieval, suppresses local/workspace context injection, blocks document streaming/teacher escalation, and gives `tool_execution` a final execution backstop.
|
||||
|
||||
Plan mode is a read-only investigation path inside the same loop. It adds a denylist for known mutating tools, filters write/unknown MCP tools, prepends plan-mode instructions, and uses the `update_plan` tool only after a plan is approved for execution. The backend path still exists for compatibility, but current browser chat forces incoming `plan_mode` off and the old plan-window UI module is gone.
|
||||
|
||||
Workspace mode is request-scoped. Admin chat can send a workspace directory selected through `static/js/workspace.js`; `agent_loop` injects that fact early in the prompt and `tool_execution` confines bash, python, read/write/edit-file, and code-navigation tools to that root. `routes.workspace_routes` owns admin-only browse/vet APIs, skips hidden/symlink directory traversal, caps listings, and rejects sensitive/root paths before a workspace reaches chat.
|
||||
|
||||
## Tool Registry
|
||||
|
||||
Tool registration is split:
|
||||
|
||||
- `src.agent_tools` is now a package/facade. `TOOL_HANDLERS` maps native tool names to handler functions across filesystem, subprocess, web, document, interaction, model-interaction, background-job, session, and admin modules, while `TOOL_TAGS` keeps compatibility metadata and the global MCP manager handle;
|
||||
- `src.tools` owns domain do_* implementations for calendar, contacts, Cookbook, image, notes, research, search, system, and vault tools. `src.tool_implementations` is now a compatibility facade that re-exports those symbols and lazy-loads admin manage_* symbols to avoid circular imports;
|
||||
- `src.agent_tools.admin_tools` owns admin manage_* tools for endpoints, MCP, webhooks, tokens, and settings, including command validation for `manage_mcp`;
|
||||
- `src.tool_parsing._TOOL_NAME_MAP` owns aliases and prompted-block parsing;
|
||||
- `src.tool_schemas.FUNCTION_TOOL_SCHEMAS` and `function_call_to_tool_block()` own native schema and native-call conversion;
|
||||
- `src.tool_index.BUILTIN_TOOL_DESCRIPTIONS` owns retrieval text;
|
||||
- `src.tool_execution.execute_tool_block()` owns dispatch and hard execution gates;
|
||||
- `routes.model_routes.py` and frontend settings/admin surfaces expose global disabled-tool controls.
|
||||
|
||||
When adding, removing, or renaming a tool, update the registry chain, execution dispatch, retrieval text, prompt wording, disabled-tool UI, and tests together.
|
||||
|
||||
`src.tool_index.ALWAYS_AVAILABLE` is the retrieval catalog for high-frequency tools such as shell/python, web search/fetch, read/write/edit-file, code-nav, `manage_memory`, `ask_user`, `update_plan`, selected Cookbook serve controls, and `app_api`. Current prompt/schema assembly preserves only selected base tools unconditionally, then adds intent-, skill-, and retrieval-relevant tools so unrelated schemas do not flood small contexts.
|
||||
|
||||
## Tool Retrieval And Execution
|
||||
|
||||
`src.tool_index.ToolIndex` owns candidate retrieval using embeddings/keywords and cached index data. Security filtering is not its hard boundary: `agent_loop` hides unavailable schemas, and `tool_execution` blocks disabled, admin-only, and public-restricted calls before dispatch.
|
||||
|
||||
`src.tool_execution` owns built-in tool execution, MCP dispatch, path confinement, background markers, output truncation, internal HTTP loopback, owner/admin checks, policy-blocked execution results, and formatting tool results for the model/UI. File tools support exact edit diffs, full-file writes, read line ranges, and workspace confinement. Code-navigation tools (`grep`, `glob`, `ls`) prefer `rg`/structured filesystem traversal over ad hoc shell commands. Uploaded-file context uses stable `attachment_ref` manifests and owner-checked URIs; a compatibility local path is exposed only after upload-root and tool-root confinement. Shared truncation, upload-handler registration, and MCP manager compatibility helpers live in `src.tool_utils`.
|
||||
|
||||
Tool retrieval has domain-specific hooks beyond generic similarity: contact queries can surface `resolve_contact`/`manage_contact`; matched skills can add `manage_skills` and their required toolsets to the relevant tool set; explicit admin intents can include admin schemas so prompt text and native schema emission match.
|
||||
|
||||
Interaction/session/model helper tools are native first-class tools, not prompt-only conventions. `ask_user` and `update_plan` live in `src.agent_tools.interaction_tools`, model delegation/listing helpers live in `model_interaction_tools`, session creation/list/send/manage helpers live in `session_tools`, and `manage_bg_jobs` lives in `bg_job_tools`.
|
||||
|
||||
Prompted-tool parsing includes recovery paths for local/provider text leaks: bare JSON after a web-tool mention, OpenAI-style raw `{"function": ...}` payloads, StepFun/Gemma/DSML markup, Hermes/Qwen JSON bodies nested inside `tool_call` wrappers, and `<function_model><function_call>...</function_call><parameters>...</parameters></function_model>` wrappers from local MLX/Exo models. The Qwen bare end marker requires its pipe delimiter so ordinary text cannot terminate a tool block. Non-dict JSON arguments are rejected back to empty args instead of crashing the turn, common `tex` typos normalize to `text`, and delimiter scans are forward-only so unterminated tool markup cannot drive quadratic rescans. Executed raw tool JSON is stripped from assistant text afterward; this is still not a general-purpose JSON-command parser.
|
||||
|
||||
Current call sites include:
|
||||
|
||||
- agent mode tool calls from `src.agent_loop`;
|
||||
- MCP route configuration and built-in MCP registration;
|
||||
- background job monitoring and auto-continue;
|
||||
- skill tests, teacher escalation, scheduled tasks, and background follow-up loops;
|
||||
- UI-control and AI interaction helpers.
|
||||
|
||||
## Streaming And Continuations
|
||||
|
||||
Agent streaming emits normal content plus tool progress/output, document stream/update, ask-user choices, plan updates, budget, round exhaustion, loop-breaker, intent-nudge exhaustion, metrics, teacher escalation, research anchor, and finish/error events. Frontend chat stream code and detached replay depend on stable event names. If the stream generator closes while awaiting an in-flight tool, the loop cancels and awaits that tool task so subprocess-backed work is not left orphaned.
|
||||
|
||||
Long-running bash jobs can be detached with background markers. `src.bg_jobs` owns persistent job state/result files; `src.bg_monitor` owns auto-continuation when jobs finish. Detached chat runs are in-memory and do not survive server restart, while background job state is disk-backed.
|
||||
|
||||
Loop-breaker final-answer rounds, explicit repeated-tool/intent-nudge guard events, round-cap continuation signals, optional verifier retries, and teacher escalation are recovery behavior owned by `agent_loop` and `src.teacher_escalation`.
|
||||
|
||||
Approval replay injects the sealed first tool result before the resumed model round. If that replay round has neither assistant prose nor reasoning, `_append_tool_results()` omits the empty assistant spacer so Anthropic-compatible payloads do not contain a rejected non-final empty assistant message; reasoning-only carriers remain a documented compatibility edge.
|
||||
|
||||
## Security And Policy
|
||||
|
||||
- `src.tool_security` owns non-admin blocked-tool decisions.
|
||||
- Non-admin users must not reach admin tools through agent mode, MCP, retrieval, or loopback calls.
|
||||
- Agent owner is passed from chat route `get_current_user(request)`. In `AUTH_ENABLED=false` mode this is `None`, not the `""` value returned by route dependencies. `blocked_tools_for_owner()`, schema hiding, and `execute_tool_block()` all use that owner.
|
||||
- Current dev tool security treats explicit `AUTH_ENABLED=false` as single-user even when an auth store exists, while auth-enabled pre-setup callers remain non-admin.
|
||||
- Path-based tools must remain confined to allowed roots and reject sensitive paths. Sensitive-path checks are case-insensitive and apply to direct file tools and code-navigation tools; `grep`/`glob`/`ls` must not become existence or content oracles for `.env`, SSH/GPG material, `id_rsa`, and similar denylisted paths.
|
||||
- Tool output is bounded/truncated where native execution owns the path, including displayed agent-tool output through the shared truncation helper. MCP output must be treated as untrusted; central MCP-output truncation before model re-entry remains a gap.
|
||||
- Provider-emitted native tool calls are requests, not authorization. `tool_execution` and route-level policy remain the authority.
|
||||
- `src.tool_capabilities` classifies each tool's effects and result integrity. Once external/workspace-untrusted content becomes model-visible, the request/session security context permits only explicitly low-impact tools without interruption and requires exact approval for high-impact, unknown, and arbitrary MCP calls.
|
||||
- `src.tool_approvals` seals an opaque, expiring exact first action plus server-only selected tools and continuation query to owner, session, origin run, tool content, workspace, capability snapshot, and—when relevant—document id/version/content digest. Chat choices grant the resumed task or the same chat session; both consume the exact first action, task scope bypasses the gate only during that resumed run, and chat scope is reconstructed only from a resolved card bound to the exact session id. The browser never receives selected tools/query and submits only task/chat/deny. Non-chat callers retain single-action behavior; new normal turns and superseding actions retire unresolved approvals without clearing taint.
|
||||
- Tool results that expose remote or stored untrusted content arm the gate even when their tool status is failed. Content-free failures and server-generated policy/approval placeholders do not. Native/provider tool messages and fenced results carry model-visible untrusted metadata/wrapping instead of relying on prompt wording alone.
|
||||
- Attachment-bearing document, note, and calendar tools owner-reserve internal
|
||||
upload references before durable writes and fail without mutation when the
|
||||
referenced upload is unavailable.
|
||||
- Guide-only/no-tools mode blocks tools before prompt assembly, before execution, and in chat preprocessing paths that would otherwise fetch context or start tool-backed research.
|
||||
- Plan mode is policy, not prompt advice: mutating native tools are disabled through schema-derived detection plus a static backstop, and write/unknown MCP tools are hidden and runtime-blocked for that turn.
|
||||
|
||||
## Internal Loopback
|
||||
|
||||
`do_app_api()` is implemented in `src.tools.system` and re-exported by `src.tool_implementations`. It owns generic app API loopback, OpenAPI discovery, method/path blocklists, and fixed local target behavior. `_internal_headers()` adds the process-secret internal-tool token and optional `X-Odysseus-Owner`; `core.middleware.require_admin()` and auth middleware own the corresponding bypass and owner-stamping rules. Route-specific owner handling must still be audited.
|
||||
|
||||
## MCP
|
||||
|
||||
`src.mcp_manager` owns configured MCP server lifecycle, discovered tool state, qualified MCP names, OpenAI schema conversion, call routing, generation invalidation, and connect/disconnect status. It supports stdio, SSE, and Streamable HTTP transports; Streamable HTTP can publish a `needs_auth` state and uses `src.mcp_oauth` for OAuth/OIDC-style authorization, token refresh, and encrypted token storage. Arbitrary MCP tools classify fail-high for approvals. `src.builtin_mcp` owns built-in server registration and the native-vs-MCP split. `mcp_servers/` owns server-specific tools for email, image generation, memory, RAG, and optional browser tooling.
|
||||
|
||||
Native bash, python, file, web search, and web fetch tools continue through native fallback even when MCP is unavailable. Browser MCP is optional and can be skipped when cached Playwright/NPX packages are missing. Public users get no MCP schemas, and any `mcp__*` execution attempt must be blocked.
|
||||
|
||||
MCP prompt/schema rendering includes server-provided input schemas, but names, types, and parameter hint text are sanitized and length-capped before entering the prompt. Per-server disabled tools filter listings, prompt descriptions, and function schemas; execution-time disabled-tool enforcement remains a separate hardening item.
|
||||
|
||||
## Intent And Recovery Helpers
|
||||
|
||||
`src.action_intents` owns deterministic chat-to-agent promotion hints and returns a category/reason so route logs can explain auto-escalation decisions. Explicit web-search language is category `web`; it can promote the turn into agent mode and narrow tools toward web search/fetch, but route policy requires explicit web-search enablement and honors explicit denial. It must avoid promoting explanatory questions into agent mode. `src.builtin_actions` owns scheduler/background actions outside the normal live agent loop. `src.teacher_escalation` owns recovery/escalation and skill-creation flows. `src.goal_based_extractor` is research-adjacent and should stay cross-referenced from research behavior rather than treated as ordinary tool execution.
|
||||
|
||||
When an email reader is active, browser chat passes active email metadata and the agent loop injects it as protected, untrusted context so default reply/draft behavior targets the selected message. Active email compose documents are handled as existing email drafts rather than generic new-document requests.
|
||||
|
||||
## Degraded Behavior
|
||||
|
||||
- ToolIndex can degrade to keyword selection when embeddings, Chroma, index
|
||||
warmup, or vector retrieval timeouts fail.
|
||||
- Agent mode can degrade from native function schemas to prompted fenced-block parsing based on provider/tool-support heuristics. Local Ollama `/v1` and native `/api` endpoints default to text tools unless the endpoint explicitly advertises `supports_tools`; `gpt-oss` remains text-tool by default unless the endpoint opts in.
|
||||
- MCP startup failure is non-critical; route/status surfaces expose per-server errors.
|
||||
- `ODYSSEUS_DISABLE_MCP`, missing `mcp`, uncached browser MCP packages, and per-server disabled tools can remove tools without blocking the app.
|
||||
- Global `builtin_browser` disable behavior may not currently match qualified `mcp__builtin_browser__*` tool names.
|
||||
|
||||
## Current Gaps
|
||||
|
||||
- Tool descriptions are duplicated across `FUNCTION_TOOL_SCHEMAS`, agent prompt sections, and `BUILTIN_TOOL_DESCRIPTIONS`.
|
||||
- Agent prompts remain heavy for small local context windows.
|
||||
- Some AI-control helpers are still globally wired from app startup rather than a narrower service layer.
|
||||
- Tool registry consistency is manual across handler maps, tags, aliases, schemas, retrieval descriptions, execution dispatch, settings/model routes, and frontend toggles.
|
||||
- MCP disabled-tool changes can stale-cache tool retrieval because disabled maps are not always an index generation input.
|
||||
- External MCP output still needs a single central size cap before model re-entry; untrusted-result metadata and the post-external-context action gate now cover the prompt-injection/authorization boundary.
|
||||
- Auth-disabled/no-login owner propagation is inconsistent between route dependencies and chat/agent execution, so tool-security and native tool storage behavior need dedicated regression coverage.
|
||||
- Agent tests mostly cover helpers and targeted regressions, including round-cap
|
||||
and disconnect cancellation paths, but not an end-to-end fake-LLM
|
||||
`stream_agent_loop` path with retrieval, native schemas, prompted blocks,
|
||||
disabled/admin hiding, MCP tools, plan/workspace state, user-time context, and
|
||||
tool-result SSE.
|
||||
@@ -0,0 +1,412 @@
|
||||
# Architecture Runtime Inventory
|
||||
|
||||
> **Purpose**: Phase 0 planning baseline for codebase readability improvements (#4071).
|
||||
> **Parent issue**: [#4082](https://github.com/odysseus-dev/odysseus/issues/4082)
|
||||
> **Last updated**: dev@b58af42 | 2026-06-16
|
||||
> **Status**: Draft — to be reviewed before follow-up slices open.
|
||||
> **Snapshot basis**: Importer / file / import-line counts are refreshed to `dev@b58af42` (2026-06-16) and are recomputable via the commands in §3.4. **Line counts** in §2.1 / §2.2 are a snapshot from an earlier baseline and drift as `dev` moves — recompute any of them with `wc -l <file>`. This inventory tracks structure and risk, not live metrics.
|
||||
|
||||
This document maps the current runtime module structure, identifies high-risk boundaries, and recommends safe first refactor slices. It does **not** move files, change imports, or alter runtime behavior.
|
||||
|
||||
---
|
||||
|
||||
## 1. Current Structure Overview
|
||||
|
||||
### 1.1 Top-Level Layout
|
||||
|
||||
```
|
||||
odysseus/
|
||||
├── app.py # FastAPI app entrypoint (1,145 lines)
|
||||
├── conf/ # Configuration (config.py, settings.py, settings_scrub.py)
|
||||
├── src/ # 95 flat .py files + 2 subdirectories
|
||||
│ ├── agent_tools/ # Tool helpers: document, filesystem, subprocess, web
|
||||
│ └── search/ # Search subsystem
|
||||
├── routes/ # 54 flat .py files — HTTP route handlers
|
||||
├── core/ # 10 files — database models, auth, middleware, session
|
||||
├── mcp_servers/ # 5 files — MCP server implementations
|
||||
├── scripts/ # CLI tools and one-shot scripts
|
||||
├── static/ # Frontend HTML/CSS/JS
|
||||
├── tests/ # 583 test files (~54,800 lines)
|
||||
└── services/ # (exists as needed)
|
||||
```
|
||||
|
||||
### 1.2 Directory Flatness Metric
|
||||
|
||||
| Directory | Flat `.py` Files | Subdirectories | Concern |
|
||||
|-----------|-----------------|----------------|---------|
|
||||
| `src/` | **95** | 2 (`agent_tools/`, `search/`) | No domain grouping; 95 files in one directory |
|
||||
| `routes/` | **54** | 0 | All route handlers in one flat directory |
|
||||
| `core/` | 10 | 0 | Manageable, but `database.py` is oversized |
|
||||
|
||||
---
|
||||
|
||||
## 2. Largest Runtime Modules
|
||||
|
||||
### 2.1 Python Backend
|
||||
|
||||
| Rank | File | Lines | Classes | Functions | Risk |
|
||||
|------|------|-------|---------|-----------|------|
|
||||
| 1 | `src/tool_implementations.py` | **4,032** | 0 | ~48 | **HIGH** |
|
||||
| 2 | `routes/email_routes.py` | **3,245** | — | — | **MEDIUM** |
|
||||
| 3 | `routes/cookbook_routes.py` | **2,969** | — | — | **MEDIUM** |
|
||||
| 4 | `src/agent_loop.py` | **2,961** | 0 | ~24 | **HIGH** |
|
||||
| 5 | `src/task_scheduler.py` | **2,330** | — | 5 | MEDIUM |
|
||||
| 6 | `routes/model_routes.py` | **2,266** | — | — | MEDIUM |
|
||||
| 7 | `core/database.py` | **2,265** | 28 | ~59 helpers | **HIGH** |
|
||||
| 8 | `src/builtin_actions.py` | **2,262** | 2 | ~24 | MEDIUM |
|
||||
| 9 | `src/llm_core.py` | **2,164** | — | — | MEDIUM |
|
||||
| 10 | `mcp_servers/email_server.py` | 2,197 | — | — | LOW (separate process) |
|
||||
| 11 | `src/visual_report.py` | 1,918 | — | — | LOW |
|
||||
| 12 | `routes/gallery_routes.py` | 1,896 | — | — | LOW |
|
||||
| 13 | `src/ai_interaction.py` | 1,846 | — | — | MEDIUM |
|
||||
| 14 | `routes/document_routes.py` | 1,717 | — | — | LOW |
|
||||
| 15 | `routes/skills_routes.py` | 1,648 | — | — | LOW |
|
||||
|
||||
**Heuristic**: Files > 2,000 lines with 20+ public symbols and many importers are the highest-risk splits. Files 1,000–2,000 lines are medium-risk if tightly coupled.
|
||||
|
||||
### 2.2 Frontend
|
||||
|
||||
| File | Lines | Concern |
|
||||
|------|-------|---------|
|
||||
| `static/style.css` | **36,653** | Entire app CSS in one file (tracked separately in #2617) |
|
||||
| `static/js/document.js` | **9,776** | Single JS file for document functionality |
|
||||
| `static/js/slashCommands.js` | 6,498 | |
|
||||
| `static/js/settings.js` | 5,266 | |
|
||||
| `static/js/emailLibrary.js` | 5,217 | |
|
||||
| `static/js/notes.js` | 5,124 | |
|
||||
| `static/js/chat.js` | 4,985 | |
|
||||
| `static/app.js` | 4,090 | |
|
||||
|
||||
**Note**: Frontend modularization is tracked separately in #2617 (CSS) and is not the focus of this Phase 0 inventory. Frontend is listed here for completeness but follow-up slices should target Python backend boundaries first.
|
||||
|
||||
---
|
||||
|
||||
## 3. Import Dependency Graph
|
||||
|
||||
### 3.1 Who Depends on `core/database.py`
|
||||
|
||||
**102 files** import from `core.database` — this is the most depended-upon module:
|
||||
|
||||
- All route handlers (`routes/*.py`)
|
||||
- Most `src/*.py` files
|
||||
- `core/session_manager.py`, `core/auth.py`
|
||||
- Multiple test files
|
||||
|
||||
**Implication**: Any split of `core/database.py` is the highest-risk refactor. It should be tackled **last**, never first.
|
||||
|
||||
### 3.2 Who Depends on `src/tool_implementations.py`
|
||||
|
||||
**17 files** import from `src.tool_implementations`:
|
||||
- `src/agent_loop.py`, `src/builtin_actions.py`, `src/tool_index.py`
|
||||
- `src/task_scheduler.py`, `src/tool_policy.py`
|
||||
- Various tests
|
||||
|
||||
### 3.3 Who Depends on `src/agent_loop.py`
|
||||
|
||||
**22 files** import from `src.agent_loop`:
|
||||
|
||||
- `src/tool_policy.py`, `src/teacher_escalation.py`, `src/bg_monitor.py`
|
||||
- `src/task_scheduler.py`
|
||||
- Multiple test files
|
||||
|
||||
### 3.4 Cross-Layer Import Violations
|
||||
|
||||
**`src/` importing from `routes/`** (backwards dependency — domain logic depending on HTTP layer):
|
||||
|
||||
```
|
||||
src/tool_implementations.py ──→ routes/calendar_routes.py
|
||||
src/tool_implementations.py ──→ routes/cookbook_helpers.py
|
||||
src/tool_implementations.py ──→ routes/email_helpers.py
|
||||
src/tool_implementations.py ──→ routes/email_pollers.py
|
||||
src/tool_implementations.py ──→ routes/email_routes.py
|
||||
src/tool_implementations.py ──→ routes/model_routes.py
|
||||
src/tool_implementations.py ──→ routes/note_routes.py
|
||||
src/tool_implementations.py ──→ routes/prefs_routes.py
|
||||
```
|
||||
|
||||
> These are **runtime imports** (inside function bodies, not at module top), which mitigates circular import risk but indicates fuzzy layer boundaries. Function-level inline imports from the HTTP layer into business logic are a code smell.
|
||||
|
||||
**Import counts (top-level)**:
|
||||
| Direction | Count | Notes |
|
||||
|-----------|-------|-------|
|
||||
| `routes/` → `src/` | **374** | Expected: HTTP handlers call domain logic |
|
||||
| `routes/` → `core/` | **126** | Expected: handlers access DB models |
|
||||
| `src/` → `routes/` | **31** | **Unexpected**: domain logic reaching into HTTP layer (direct grep of import lines referencing `routes/`) |
|
||||
| `src/` → `core/` | **106** | Acceptable but could be reduced with a data-access layer |
|
||||
|
||||
> **How the metrics in this document are computed** — recompute against current `dev` before treating any count as authoritative (the tree drifts; these numbers are a snapshot, not a live value):
|
||||
> - `src/` flat `.py` files: `find src -maxdepth 1 -name '*.py' | wc -l`
|
||||
> - `tests/` test files: `find tests -name 'test_*.py' | wc -l`
|
||||
> - `core.database` importers: `grep -rlE '(from|import) +core\.database' --include='*.py' . | grep -v core/database.py | wc -l`
|
||||
> - `src.agent_loop` importers: `grep -rlE '(from|import) +src\.agent_loop' --include='*.py' . | grep -v src/agent_loop.py | wc -l`
|
||||
> - Cross-layer import lines: `grep -rhE '(from|import) +<pkg>' --include='*.py' <dir>/ | wc -l` (e.g. `(from|import) +routes` over `src/`)
|
||||
|
||||
---
|
||||
|
||||
## 4. Route Ownership Map
|
||||
|
||||
Routes can be grouped into logical feature domains. Current flat structure obscures these boundaries:
|
||||
|
||||
| Domain | Route Files | Total Lines | Review Complexity |
|
||||
|--------|-------------|-------------|-------------------|
|
||||
| **Email** | `email_routes.py`, `email_helpers.py`, `email_pollers.py` | 5,936 | HIGH — most complex domain |
|
||||
| **Chat / Agent** | `chat_routes.py`, `chat_helpers.py`, `shell_routes.py`, `codex_routes.py`, `skills_routes.py` | 6,365 | HIGH — core interaction surface |
|
||||
| **Cookbook** | `cookbook_routes.py`, `cookbook_helpers.py`, `cookbook_output.py` | 4,110 | MEDIUM |
|
||||
| **Model / LLM** | `model_routes.py`, `assistant_routes.py`, `copilot_routes.py` | 2,764 | MEDIUM |
|
||||
| **Calendar / Contacts** | `calendar_routes.py`, `contacts_routes.py` | 2,336 | MEDIUM |
|
||||
| **Documents** | `document_routes.py`, `document_helpers.py` | 1,954 | LOW |
|
||||
| **Auth** | `auth_routes.py`, `api_token_routes.py`, `device_flow.py` | 1,171 | LOW |
|
||||
| **Tasks** | `task_routes.py` (standalone) | 1,157 | LOW |
|
||||
| **Session** | `session_routes.py` (standalone) | 1,287 | LOW |
|
||||
| **Gallery** | `gallery_routes.py`, `gallery_helpers.py` | 1,896 | LOW |
|
||||
| **Memory** | `memory_routes.py` | — | LOW |
|
||||
| **Research** | `research_routes.py` | — | LOW |
|
||||
| **MCP** | `mcp_routes.py` | — | LOW |
|
||||
| **Notes** | `note_routes.py` | — | LOW |
|
||||
| **Other** | `prefs_routes.py`, `upload_routes.py`, `vault_routes.py`, `webhook_routes.py`, `workspace_routes.py`, `search_routes.py`, `history_routes.py`, `hwfit_routes.py`, `preset_routes.py`, `signature_routes.py`, `backup_routes.py`, `cleanup_routes.py`, `diagnostics_routes.py`, `embedding_routes.py`, `emoji_routes.py`, `font_routes.py`, `stt_routes.py`, `tts_routes.py`, `compare_routes.py`, `personal_routes.py`, `editor_draft_routes.py`, `admin_wipe_routes.py`, `chatgpt_subscription_routes.py` | 2,000+ | LOW individual, HIGH cumulative |
|
||||
|
||||
---
|
||||
|
||||
## 5. Tool Registry & Implementation Boundaries
|
||||
|
||||
### 5.1 Current Tool Architecture
|
||||
|
||||
| Component | File | Lines | Role |
|
||||
|-----------|------|-------|------|
|
||||
| Tool schemas | `src/tool_schemas.py` | 1,392 | JSON Schema tool definitions (Duck-TypedDict) |
|
||||
| Tool index | `src/tool_index.py` | 542 | RAG-based tool retrieval from ChromaDB |
|
||||
| Tool implementations | `src/tool_implementations.py` | 4,032 | 33 `do_*` functions — all tool execution logic |
|
||||
| Tool security | `src/tool_security.py` | — | Owner-scoped tool blocking |
|
||||
| Tool policy | `src/tool_policy.py` | — | Guide-only directive, plan-mode disabled tools |
|
||||
| Tool utils | `src/tool_utils.py` | — | Shared tool helpers |
|
||||
|
||||
### 5.2 Tool Implementation Categories
|
||||
|
||||
The 33 `do_*` functions in `tool_implementations.py` fall into natural domain groups — the basis for slice 1's split in §6.2:
|
||||
|
||||
| Category | `do_*` functions | Count |
|
||||
|----------|------------------|-------|
|
||||
| **System / config** | `do_manage_skills`, `do_manage_tasks`, `do_manage_endpoints`, `do_manage_mcp`, `do_manage_webhooks`, `do_manage_tokens`, `do_manage_settings`, `do_api_call`, `do_app_api` | 9 |
|
||||
| **Cookbook / model serving** | `do_download_model`, `do_serve_model`, `do_list_served_models`, `do_stop_served_model`, `do_tail_serve_output`, `do_list_downloads`, `do_cancel_download`, `do_search_hf_models`, `do_adopt_served_model`, `do_list_cookbook_servers`, `do_list_serve_presets`, `do_serve_preset`, `do_list_cached_models` | 13 |
|
||||
| **Notes** | `do_manage_notes` | 1 |
|
||||
| **Calendar** | `do_manage_calendar` | 1 |
|
||||
| **Search** | `do_search_chats` | 1 |
|
||||
| **Research** | `do_manage_research`, `do_trigger_research` | 2 |
|
||||
| **Contacts** | `do_resolve_contact`, `do_manage_contact` | 2 |
|
||||
| **Vault** | `do_vault_search`, `do_vault_get`, `do_vault_unlock` | 3 |
|
||||
| **Image** | `do_edit_image` | 1 |
|
||||
| | **Total** | **33** |
|
||||
|
||||
> Low-level tools (filesystem, subprocess, web fetch, document parsing) live in `src/agent_tools/`, **not** in `tool_implementations.py` — out of scope for this split.
|
||||
|
||||
---
|
||||
|
||||
## 6. Risk Assessment & Candidate Slice Ranking
|
||||
|
||||
> **Candidate proposals, not a committed plan.** The rankings, package shapes (e.g. `src/pkg/`, `src/domain/`, `src/infra/`, `src/api/`), split ordering, and route-grouping strategy below are **options for maintainer discussion**. Per #4082/#4071, slice ownership and order are settled by maintainers before any follow-up PR. §1–§3 above are the factual current-state inventory.
|
||||
|
||||
### 6.1 Risk Scale
|
||||
|
||||
| Level | Criteria |
|
||||
|-------|----------|
|
||||
| **LOW** | File has ≤3 importers AND ≤500 lines, OR is a pure refactor with clear boundaries |
|
||||
| **MEDIUM** | File has 4–15 importers OR 500–1,500 lines |
|
||||
| **HIGH** | File has 16+ importers OR >2,000 lines, OR has cross-layer import violations |
|
||||
|
||||
### 6.2 Ranked Split Candidates
|
||||
|
||||
| Priority | Target | Risk | Rationale |
|
||||
|----------|--------|------|-----------|
|
||||
| **1** | `src/tool_implementations.py` → `src/tools/*.py` | **MEDIUM** | 4,032 lines → ~10 files by tool category. Already has natural boundaries. 17 importers, tracked in #3629. Use `__init__.py` shim to keep existing imports working. |
|
||||
| **2** | `routes/` → domain subdirectories (one domain per PR) | **MEDIUM** | 54 flat files. Done **one domain at a time** (e.g. a standalone PR for the email domain, then chat, …), not a broad reorganization — route modules carry helper imports, registration assumptions, and test import paths. |
|
||||
| **3** | `src/agent_loop.py` → `src/agent/loop.py` + submodules | **MEDIUM-HIGH** | 2,961 lines, 24 functions. Can extract prompt building, classification, verification, and runaway detection. Tracked in #3266. |
|
||||
| **4** | `src/` → `src/pkg/`, `src/domain/`, `src/infra/`, `src/api/` | **MEDIUM** | Structural reorganization. Split flat `src/` into layered packages. Must come after routes and tools are stable. |
|
||||
| **5** | `routes/email_*.py` consolidation | **LOW** | Already grouped by filename prefix. Low-risk cleanup within the email domain. |
|
||||
| **6** | `core/database.py` → `src/infra/database/models/*.py` | **HIGH** | 28 classes, 102 importers. Highest-risk split. Must be **last** in any sequence. Requires careful import shim strategy. |
|
||||
| **7** | Frontend CSS modularization | **MEDIUM** | 36,653 lines. Tracked in #2617. Separate timeline from backend work. |
|
||||
| **8** | Frontend JS modularization | **MEDIUM** | 9,776 lines in `document.js`. Introduce ES modules at minimum. |
|
||||
|
||||
### 6.3 Candidate First 3 Behavior-Preserving Slices
|
||||
|
||||
**Slice 1: Split `tool_implementations.py`** (Lowest-risk high-impact)
|
||||
|
||||
- Create `src/tools/` package with one file per tool category
|
||||
- Add `src/tools/__init__.py` re-exporting all symbols with current names
|
||||
- Update 17 importers to use new paths (can be deferred via shim)
|
||||
- Validation: `python -m pytest tests/ -x -q` + manual smoke test of tool execution
|
||||
- Reference: #3629
|
||||
|
||||
**Slice 2: Group `routes/` by domain** (one domain per PR, not a broad sweep)
|
||||
|
||||
Route modules carry helper imports, router registration assumptions, and test import paths, so this must be done **one domain at a time** rather than as a single reorganization PR. Example sequence (each its own PR):
|
||||
|
||||
- PR 2a: move the **email** domain (`email_routes.py`, `email_helpers.py`, `email_pollers.py`) → `routes/email/` + shim
|
||||
- PR 2b: move the **chat/agent** domain → `routes/chat/` + shim
|
||||
- PR 2c: move the **cookbook** domain → `routes/cookbook/` + shim
|
||||
- …and so on per domain from §4
|
||||
|
||||
Each PR: add `__init__.py` re-exporting old names, update `app.py` router imports, validation `python app.py` starts clean. **No behavior change** — pure file reorganization.
|
||||
|
||||
**Slice 3: Extract `agent_loop.py` submodules** (Improve reviewability)
|
||||
|
||||
- Move prompt assembly → `src/agent/prompt.py`
|
||||
- Move request classification → `src/agent/classifier.py`
|
||||
- Move sub-agent verification → `src/agent/verifier.py`
|
||||
- Move runaway detection → `src/agent/runaway.py`
|
||||
- Move context management → `src/agent/context.py`
|
||||
- Keep `src/agent/loop.py` as the main orchestration module
|
||||
- Validation: `python -m pytest tests/test_agent_loop.py tests/test_loop_breaker_runaway.py -v`
|
||||
|
||||
---
|
||||
|
||||
## 7. Safety Guardrails for Follow-Up Work
|
||||
|
||||
Per maintainer guidance in #4082 and #4071:
|
||||
|
||||
- [ ] **One domain/slice per PR** — never mix multiple reorganizations
|
||||
- [ ] **No behavior changes** mixed with file moves — pure reorganization only
|
||||
- [ ] **Keep compatibility shims** — `__init__.py` re-exports for all existing import paths
|
||||
- [ ] **Add or identify focused tests** before risky splits
|
||||
- [ ] **Do not start with `core/database.py`** or broad route movement unless this inventory shows a safe boundary
|
||||
- [ ] **Prefer small, reviewable slices** over large restructures
|
||||
- [ ] **No packaging/runtime/tooling migration** mixed into file moves
|
||||
- [ ] **No frontend framework migration** inside this stabilization lane
|
||||
- [ ] **Validate with `python -m compileall`** — every PR must pass CI checks
|
||||
- [ ] **Validate with `pytest`** — run the full test suite before opening each PR
|
||||
|
||||
---
|
||||
|
||||
## 8. Validation Commands
|
||||
|
||||
Each follow-up PR should be verifiable with these commands before submission:
|
||||
|
||||
```bash
|
||||
# Syntax check — must pass with zero errors
|
||||
python -m compileall src/ routes/ core/ conf/
|
||||
|
||||
# Full test suite — must match baseline pass rate
|
||||
python -m pytest tests/ -x -q
|
||||
|
||||
# Import shim verification — existing import paths must still work
|
||||
python -c "from src.tool_implementations import do_search_chats; print('OK')"
|
||||
|
||||
# App startup smoke test (if backend touched)
|
||||
timeout 5 python app.py 2>&1 | head -5 || true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Open Questions
|
||||
|
||||
1. Is `#2538` (specs ground truth) the canonical behavior map baseline, and should this inventory be kept in sync with those specs once merged?
|
||||
2. Should route grouping follow the domain map proposed here, or is there a different taxonomy preferred by maintainers?
|
||||
3. For the `tool_implementations.py` split (#3629), is the tool categorization in §5.2 acceptable, or should it follow a different grouping?
|
||||
4. Should compatibility shims (`__init__.py`) be temporary (removed in a follow-up wave) or permanent?
|
||||
5. Should an ADR (Architecture Decision Record) document be started to track decisions made during this process?
|
||||
|
||||
---
|
||||
|
||||
## 10. Future Direction (NOT current state)
|
||||
|
||||
The following are **future refactor targets** (candidate directions **pending maintainer agreement**, not committed), recorded here so this inventory does not imply they exist today. None of them are present in the current `dev` tree:
|
||||
|
||||
- `main.py` — proposed rename of the `app.py` entrypoint. Today the app boots via `app.py`.
|
||||
- `src/agent/` — proposed package to hold `agent_loop.py` submodules (prompt/classifier/verifier/runaway/context). Today `agent_loop.py` is a single flat file in `src/`.
|
||||
- `src/infra/`, `src/domain/`, `src/pkg/`, `src/api/` — proposed layered reorganization of the flat `src/` directory (slice 4 in §6).
|
||||
|
||||
These become real only when the corresponding slices land.
|
||||
|
||||
---
|
||||
|
||||
## Appendix A: File Listing
|
||||
|
||||
### `src/` (95 files — 61 shown; run `ls src/*.py` for the full list)
|
||||
|
||||
```
|
||||
agent_loop.py tool_implementations.py tool_schemas.py
|
||||
tool_index.py tool_security.py tool_policy.py
|
||||
tool_utils.py builtin_actions.py task_scheduler.py
|
||||
llm_core.py model_context.py model_discovery.py
|
||||
session_search.py context_budget.py context_compactor.py
|
||||
ai_interaction.py action_intents.py agent_runs.py
|
||||
app_helpers.py app_initializer.py config.py
|
||||
database.py memory.py memory_provider.py
|
||||
secret_storage.py prompt_security.py url_security.py
|
||||
url_safety.py rate_limiter.py cleanup_service.py
|
||||
readiness.py service_health.py exceptions.py
|
||||
request_models.py assistant_log.py bg_monitor.py
|
||||
builtin_mcp.py chat_helpers.py chroma_client.py
|
||||
document_processor.py embedding_lanes.py deep_research.py
|
||||
research_handler.py research_utils.py personal_docs.py
|
||||
rag_manager.py rag_singleton.py topic_analyzer.py
|
||||
visual_report.py youtube_handler.py pdf_forms.py
|
||||
pdf_form_doc.py pdf_runtime.py caldav_writeback.py
|
||||
email_thread_parser.py text_helpers.py user_time.py
|
||||
teacher_escalation.py cookbook_serve_lifecycle.py
|
||||
chatgpt_subscription.py mcp_manager.py
|
||||
```
|
||||
|
||||
### `routes/` (54 files)
|
||||
|
||||
```
|
||||
__init__.py _validators.py
|
||||
auth_routes.py api_token_routes.py device_flow.py
|
||||
chat_routes.py chat_helpers.py shell_routes.py
|
||||
codex_routes.py skills_routes.py
|
||||
email_routes.py email_helpers.py email_pollers.py
|
||||
cookbook_routes.py cookbook_helpers.py cookbook_output.py
|
||||
model_routes.py assistant_routes.py copilot_routes.py
|
||||
calendar_routes.py contacts_routes.py
|
||||
document_routes.py document_helpers.py
|
||||
gallery_routes.py gallery_helpers.py
|
||||
task_routes.py session_routes.py
|
||||
note_routes.py memory_routes.py research_routes.py
|
||||
mcp_routes.py search_routes.py history_routes.py
|
||||
webhook_routes.py workspace_routes.py upload_routes.py
|
||||
vault_routes.py prefs_routes.py preset_routes.py
|
||||
signature_routes.py personal_routes.py hwfit_routes.py
|
||||
backup_routes.py cleanup_routes.py diagnostics_routes.py
|
||||
embedding_routes.py emoji_routes.py font_routes.py
|
||||
stt_routes.py tts_routes.py compare_routes.py
|
||||
editor_draft_routes.py chatgpt_subscription_routes.py admin_wipe_routes.py
|
||||
```
|
||||
|
||||
### `core/` (10 files)
|
||||
|
||||
```
|
||||
__init__.py constants.py database.py models.py
|
||||
auth.py middleware.py session_manager.py exceptions.py
|
||||
atomic_io.py platform_compat.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Appendix B: Key Import Relationships
|
||||
|
||||
```
|
||||
core/database.py ←── 102 importers (routes/*, src/*, core/*, tests/*)
|
||||
↑
|
||||
├── routes/auth_routes.py
|
||||
├── routes/email_routes.py
|
||||
├── src/builtin_actions.py
|
||||
├── src/task_scheduler.py
|
||||
├── src/tool_implementations.py (inline)
|
||||
└── ...97 more
|
||||
|
||||
src/tool_implementations.py ←── 17 importers
|
||||
↑
|
||||
├── src/agent_loop.py
|
||||
├── src/builtin_actions.py
|
||||
├── src/tool_index.py
|
||||
├── src/task_scheduler.py
|
||||
├── src/tool_policy.py
|
||||
└── ...12 more (mostly tests)
|
||||
|
||||
src/agent_loop.py ←── 22 importers
|
||||
↑
|
||||
├── src/tool_policy.py
|
||||
├── src/teacher_escalation.py
|
||||
├── src/bg_monitor.py
|
||||
├── src/task_scheduler.py
|
||||
└── 18 more (incl. tests)
|
||||
```
|
||||
@@ -1,169 +0,0 @@
|
||||
# Auth And Security
|
||||
|
||||
Last updated: dev@e71f8ce | 2026-08-25
|
||||
|
||||
## Scope
|
||||
|
||||
This spec covers current security and trust-boundary behavior in:
|
||||
|
||||
- `core/auth.py`;
|
||||
- `core/middleware.py`;
|
||||
- `core/log_safety.py`;
|
||||
- `core/database.py`;
|
||||
- `app.py` auth middleware and token cache;
|
||||
- `src/auth_helpers.py`;
|
||||
- `src/owner_identity.py`;
|
||||
- `src/tool_approval_scopes.py`, `src/tool_approvals.py`, and `src/tool_capabilities.py`;
|
||||
- `src/tool_security.py`;
|
||||
- `src/tool_execution.py`;
|
||||
- `src/task_action_policy.py`;
|
||||
- `src/prompt_security.py`;
|
||||
- `src/url_safety.py` and `src/url_security.py`;
|
||||
- `src/host_docker_access.py`;
|
||||
- `src/attachment_refs.py` and upload lifecycle enforcement in
|
||||
`src/upload_handler.py` / `routes/upload_routes.py`;
|
||||
- `src/secret_storage.py`;
|
||||
- `src/api_key_manager.py`;
|
||||
- `src/integrations.py`;
|
||||
- `src/webhook_manager.py`;
|
||||
- `src/generated_images.py`;
|
||||
- `scripts/diffusion_server.py`;
|
||||
- `scripts/mlx_image_server.py`;
|
||||
- `companion/routes.py` and `companion/pairing.py`;
|
||||
- `routes/auth_routes.py`, `routes/api_token_routes.py`, and canonical `routes/vault/vault_routes.py` plus its top-level compatibility shim;
|
||||
- admin-gated call sites in route files;
|
||||
- `THREAT_MODEL.md` and `SECURITY.md`.
|
||||
|
||||
## Trust Boundary
|
||||
|
||||
Odysseus is a trusted-user private-network app. Admins intentionally have powerful local capabilities: shell, files, email, calendar, MCP, model serving, vault, settings, and API token management. The security model prevents unauthenticated access, non-admin escalation, prompt-injection through untrusted content, and accidental exposure of internal services.
|
||||
|
||||
`THREAT_MODEL.md` owns high-level security framing, but implementation claims here should be verified against current code when the threat model is stale. This spec records the implementation map that contributors should check before changing auth or untrusted-context flows. Security-header runtime details live in `runtime.md`.
|
||||
|
||||
## Auth Ownership
|
||||
|
||||
- `core.auth.AuthManager` owns users, password hashing, TOTP/backup codes, reserved usernames, privilege defaults, admin promote/demote state, and auth settings stored in `data/auth.json`. Auth config/setup mutations are lock-guarded, and session tokens are persisted separately in `data/sessions.json` behind their own lock.
|
||||
- `app.py` owns request-time auth middleware, token-cache rebuild/invalidation, auth exemptions, API-token verification, and internal-tool identity stamping.
|
||||
- `routes/auth_routes.py` owns HTTP endpoints for setup, signup/login/logout, 2FA, users, privileges, auth features, and integration settings.
|
||||
- `core.middleware.require_admin()` owns the normal admin gate. Local wrappers must document and test any intentional divergence from that boundary.
|
||||
- `src.auth_helpers.effective_user()` owns cookie/API-token owner attribution for selected route code. `require_user()` owns route-level degraded user resolution, `require_privilege()` owns privilege checks, and `owner_filter()` owns shared/null-owner query compatibility.
|
||||
|
||||
Reserved usernames include request-only sentinels `internal-tool`, `api`, `demo`, and `system`, plus the storage-only Default/Local owner `__odysseus_local__`. Loaded auth data drops reserved user records, and create/rename flows must reject real users with those names. `src.owner_identity` is the canonical owner vocabulary and `auth_disabled()` parser.
|
||||
|
||||
## Auth Runtime Flow
|
||||
|
||||
`AuthMiddleware` is the outer request gate because FastAPI middleware executes in reverse add order. It can return API `401` JSON or browser `/login` redirects before timeout/security-header middleware reaches the route.
|
||||
|
||||
Public/auth-exempt surfaces are limited to setup, signup/login/logout/status, feature/settings/integration preset reads, health/version/login, `/static/*`, and task webhook trigger paths. `routes/task/task_routes.py` owns validation of `POST /api/tasks/{task_id}/webhook/{token}` path credentials.
|
||||
|
||||
Login issues an `HttpOnly`, `SameSite=Lax` cookie with a seven-day max age when "remember" is enabled. `_secure_cookie()` (`routes/auth_routes.py:89`) decides the `Secure` attribute: an explicit `SECURE_COOKIES` of `true` or `false` is authoritative, and any other value, including unset and the present-but-empty value docker-compose injects, derives it from the request, marking the cookie `Secure` when the connection scheme or the first `X-Forwarded-Proto` hop is https. TOTP is checked before session issuance. Logout, password changes, user deletion, rename flows, expired sessions, and deleted-user sessions must keep revocation/migration behavior intact.
|
||||
|
||||
Deleting a user revokes that user's browser sessions and API-token rows, then the admin delete route invalidates the in-memory bearer-token cache so already-cached tokens stop authenticating.
|
||||
|
||||
Rename first changes the auth username, then migrates owner-bearing DB rows and disk-backed stores. Current rename coverage includes user preferences, active/disk research state, `memory.json`, upload metadata and owner-qualified upload index keys, skills frontmatter/usage state, cached browser sessions, and API-token cache invalidation. If owner migration fails after the auth rename, the route attempts to roll auth back to the old username instead of leaving a split identity.
|
||||
|
||||
Admin promotion/demotion is a live auth flag change through `AuthManager.set_admin()` and `PUT /api/auth/users/{username}/admin`. Demotion refuses to remove the last admin, permits self-demotion when another admin remains, restores the pre-admin privilege map when available, and does not revoke sessions or API tokens because later admin checks read the current `is_admin` flag.
|
||||
|
||||
## Owner Attribution
|
||||
|
||||
Cookie requests use the real username. Bearer-token requests are stamped as `request.state.current_user = "api"` plus `api_token_owner`, `api_token_scopes`, and token id. Routes that support API-token access must explicitly use `effective_user()` or route-local scope helpers instead of treating `"api"` as an owner.
|
||||
|
||||
Internal loopback calls may stamp `current_user = "internal-tool"` or a validated `X-Odysseus-Owner` username. Network/proxy validation for that bypass lives in `app.py`; `require_admin()` trusts the stamped sentinel or raw internal header and should be used behind equivalent middleware control.
|
||||
|
||||
Missing-owner values remain state-dependent at legacy call sites, but new storage-facing code has one normalization contract:
|
||||
|
||||
- Auth-enabled, configured auth with no `current_user` is unauthenticated and should fail closed at route dependencies.
|
||||
- `AUTH_ENABLED=false` is an explicit local single-user/no-login mode. Existing route dependencies can still return `""`, and admin gates allow the local operator. `effective_storage_owner()` and `storage_owner_for_request()` normalize an absent owner to `__odysseus_local__` only in this mode.
|
||||
- Chat/agent code that reads `get_current_user(request)` directly gets `None` when auth middleware is disabled, because no middleware stamps request state.
|
||||
- SQL `NULL`/JSON missing owners remain legacy/shared compatibility data, not the same thing as a logged-out authenticated caller.
|
||||
- `"api"` and `"internal-tool"` are request sentinels. They must not be persisted as normal storage owners unless a route explicitly defines that behavior.
|
||||
- `__odysseus_local__` is a valid storage owner but never a login or request sentinel. Adoption is incremental: callers that do not use the storage-owner helper can still expose older `None`/empty/null compatibility behavior.
|
||||
|
||||
Authenticated `manage_tasks` mutations require an exact stored task-owner
|
||||
match and reject both cross-owner and legacy null-owner rows. The `owner=None`
|
||||
agent path keeps deliberate auth-disabled single-user compatibility, including
|
||||
unscoped list/create/mutation behavior.
|
||||
|
||||
Owner-scoped route code should use `require_user()` or equivalent policy before querying per-owner data. Current note CRUD/reorder/reminder routes do this so an auth-enabled request that reaches the route without identity returns `401` instead of falling into single-user/null-owner compatibility behavior.
|
||||
|
||||
Scheduled task actions attribute differently again. `_execute_action` (`src/task_scheduler.py:1231`) invokes the action with `owner=task.owner` read from the stored `ScheduledTask` row, so no request and no resolved principal are in flight. These trigger paths converge there: schedule, event bus, manual run (`routes/task/task_routes.py:865`), the `manage_tasks` agent tool (`src/tools/system.py:469`), webhook triggers (`routes/task/task_routes.py:1045`), which are unauthenticated by design with the token as the only credential and execute under the stored `task.owner`, and success-chained tasks (`src/task_scheduler.py:1063-1074`), which additionally require the chained target to share `task.owner` and reject cycles. Trigger-side ownership checks use the `if user and task.owner != user` shape, so a falsy caller skips them. Action bodies that reach owner-scoped storage must treat `task.owner` as the authority; route-level `require_user()` never runs on this path.
|
||||
|
||||
## API Tokens And Scoped Integrations
|
||||
|
||||
`routes/api_token_routes.py` owns token CRUD and scope normalization. Partial updates preserve existing scopes unless new scopes are supplied, write scopes imply the matching read scopes where applicable, and Cookbook scopes are part of the normalized scope set. `app.py` caches active token prefix rows and verifies bearer tokens with bcrypt. API-token requests set `request.state.current_user = "api"` plus token owner/scopes.
|
||||
|
||||
Current call sites include Codex/Claude scoped APIs, `/api/v1/chat`, webhooks, selected session routes, companion pairing, and external integrations. `/api/codex/*` and `/api/v1/chat` enforce route-local scopes; companion and selected session routes use owner attribution. `companion/pairing.py` can mint chat-scoped tokens outside normal token CRUD.
|
||||
|
||||
Admin token CRUD is cookie/admin gated. Update/delete operations check token ownership, and cache rebuild ignores active tokens whose owner no longer maps to a known auth user. Scoped route code must use the token owner and declared scopes instead of falling back to cookie-user assumptions.
|
||||
|
||||
## Internal Tool Loopback
|
||||
|
||||
Agent tools call admin-gated HTTP routes through an in-process loopback. `core.middleware.INTERNAL_TOOL_TOKEN` owns the random per-process secret. `app.py` only accepts this bypass from direct loopback clients without proxy-forwarding headers.
|
||||
|
||||
`src.tool_security` owns non-admin tool blocking. Non-admin users must not reach admin tools through agent mode, MCP tools, or loopback calls.
|
||||
|
||||
`src.tool_security.owner_is_admin_or_single_user()` treats explicit `AUTH_ENABLED=false` as intentional single-user mode even when an auth store already exists, while keeping pre-setup auth-enabled callers non-admin.
|
||||
|
||||
Current admin gates include `require_admin()` call sites across admin wipe, backup, contacts, Cookbook, diagnostics, embeddings, MCP, model, personal docs, presets, skills, uploads, vault, webhook, and companion routes. Local wrappers also exist in auth routes, shell routes, and task action policy; changes to those wrappers need the same trust-boundary review as `require_admin()`. Scheduled task action policy treats `run_local`, `run_script`, `ssh_command`, and `cookbook_serve` as admin-only action tasks across create/update/manual-run/webhook/scheduler execution.
|
||||
|
||||
`tidy_research` can remove only empty or unparseable research JSON. Because a broken file has no trustworthy owner stamp, the action checks `owner_is_admin_or_single_user()` before enumerating files; regular users and the pre-setup window cannot run that global unattributable-file sweep.
|
||||
|
||||
## Untrusted Context Policy
|
||||
|
||||
`src.prompt_security` owns the model-facing untrusted data contract:
|
||||
|
||||
- `UNTRUSTED_CONTEXT_POLICY` states the policy in system prompt text.
|
||||
- `untrusted_context_message(label, content)` wraps external content as user-role data with `metadata.trusted = False`, provenance metadata, and a default `tool_gate_untrusted` marker. Guard-like labels/content are escaped so source text cannot counterfeit the wrapper boundary.
|
||||
|
||||
Current untrusted surfaces include fetched URLs, web results, emails, memories, skills, notes, documents, active editor content, and tool output sourced from outside the server. Injecting those as trusted system instructions is a security bug.
|
||||
|
||||
`src.tool_capabilities` classifies native and MCP tools by effects and result integrity. After external/workspace-untrusted context becomes model-visible, `ToolRunSecurityContext` keeps a server-owned taint for the session turn: only explicitly low-impact tools can run immediately, while write, execute, network-egress, UI/external-side-effect, admin, destructive, unknown, and arbitrary MCP actions require exact approval. Failed tools can still arm the gate when their result carries remote or stored payload; content-free failures and server-generated blocked/approval placeholders do not.
|
||||
|
||||
`src.tool_approvals` owns opaque approvals sealed to the owner, session, origin run, exact first tool name/content, workspace, capability effects/result integrity, selected continuation tool set/query, and expiry. Document actions additionally seal document id, version, content digest, and workspace. Chat cards offer task scope, chat-session scope, or deny: both allow choices consume and execute the exact sealed first action after current-policy/freshness checks, task scope bypasses the gate only for the resumed task, and chat-session scope persists a resolved session-bound grant for later turns in that same chat. The browser submits only the opaque decision and cannot replace the sealed action, selected tools, query, composer text, or attachments. Non-chat callers retain single-action scope. A new ordinary turn or superseding action retires an unresolved approval without clearing taint.
|
||||
|
||||
## URL, Path, And Secret Policy
|
||||
|
||||
- `src/url_security.py` owns public HTTP(S) validation for integration/API-token supplied URLs. It should fail closed for private IP, loopback, invalid scheme, and unsafe redirect targets.
|
||||
- `src/url_safety.py` owns local-first outbound URL safety for model endpoints and similar local services. Loopback/LAN can be allowed by default, and private-IP blocking is an explicit caller policy. Strict `block_private=True` also rejects RFC 6598 shared/CGNAT space (`100.64.0.0/10`) explicitly because Python does not classify that range as private.
|
||||
- `core.log_safety.redact_url()` strips URL userinfo, query strings, and fragments before endpoint URLs enter logs. Model, chat/research endpoint, contact/CardDAV, and similar diagnostics should use this helper instead of logging raw admin-configured URLs.
|
||||
- `src.webhook_manager` validates webhook URLs at create and delivery time,
|
||||
rejects private/internal targets, disables redirects, and pins delivery to
|
||||
the public IP set that passed validation immediately before the request.
|
||||
- `src.integrations` owns admin-configured integration base URLs and secret
|
||||
masking. `api_call` accepts only relative paths, rejects link-local/metadata destinations through `src.url_safety`, can additionally block RFC1918/loopback/private targets with `INTEGRATION_API_BLOCK_PRIVATE_IPS=true`, and pins requests to the IP set that passed SSRF validation while preserving the intended Host/TLS identity.
|
||||
- `src.outbound_fetch` owns reusable public-URL classification, validates every redirect hop, rejects private/local resolved addresses, and pins the HTTP connection to the validated public IP while preserving original URL/SNI/Host semantics. `services.search.content` adapts that transport for extraction and caching.
|
||||
- Path-based tools, upload/document/gallery/signature/generated-image routes, embedding cache paths, and research JSON helpers must stay confined to allowed roots and owner-scoped files. Native file/code-navigation tools also apply a case-insensitive sensitive-path denylist so `grep`, `glob`, `ls`, direct reads, and writes cannot reveal `.env`, SSH/GPG material, private-key filenames, or similar secret paths.
|
||||
- Durable upload references are owner-reserved before chat/session, document,
|
||||
note, or calendar writes. Cleanup scans every current durable reference
|
||||
surface and fails closed on incomplete discovery or inconsistent upload-index
|
||||
state rather than deleting a possibly live upload.
|
||||
- File-backed SQLite startup restricts `app.db` and existing rollback/WAL/SHM
|
||||
sidecars to `0600` on POSIX after resolving the real path from the parsed
|
||||
engine URL. Windows, in-memory, and non-SQLite databases are excluded, and
|
||||
failed POSIX restriction is logged as a secret-file warning.
|
||||
- Secret-like DB columns use `EncryptedText` or `src.secret_storage`. Email passwords and Google OAuth mail tokens are encrypted manually in `EmailAccount` string columns; Google OAuth state is HMAC-signed and callback writes are owner-checked before token storage. `src.api_key_manager` keeps provider API keys encrypted in `data/api_keys.json`, writes by loading the raw encrypted dict so saving one provider does not rewrite other providers' keys as plaintext, and restricts local key-file permissions where the platform supports chmod. Vault state in `data/vault.json` is a chmod-restricted JSON secret store, not Fernet-encrypted DB storage. Do not log or return decrypted secrets except for intentional admin vault retrieval flows with audit/reason checks.
|
||||
- `.env` files are secrets-only inputs and should not be read or printed during agent work.
|
||||
|
||||
`scripts/diffusion_server.py` is a local model-serving helper with its own web surface. It defaults CORS to deny, installs a trusted-host allowlist for loopback/bind addresses, and only extends Host/CORS through explicit CLI flags.
|
||||
|
||||
`scripts/mlx_image_server.py` serves exactly the model selected when the process starts. OpenAI-compatible request `model` fields are accepted but ignored for generation and edits, so an unauthenticated caller cannot select another local directory or Hugging Face repository and drive model-specific script/bridge execution.
|
||||
|
||||
Host Docker socket access is a high-trust admin/deployment choice, not a normal container capability. Default Docker Compose does not mount `/var/run/docker.sock`; `src.host_docker_access` only reports local Docker available inside a container when `ODYSSEUS_ENABLE_HOST_DOCKER=true` and the socket exists. Remote SSH Docker/Cookbook workflows remain the safer default.
|
||||
|
||||
## Degraded And Compatibility Behavior
|
||||
|
||||
- `AUTH_ENABLED=false` skips `AuthMiddleware` and `src.auth_helpers.require_user()` returns `""` from any host. This preserves local single-user/no-login operation; it is not permission for auth-enabled logged-out callers. Storage code that adopts `storage_owner_for_request()` receives the reserved Default/Local owner; direct `get_current_user()` readers still receive `None`. Owner-scoped routes that tolerate no-login mode should call the appropriate route or storage helper so auth-enabled anonymous requests fail closed.
|
||||
- First-run setup mode redirects browser requests to `/login`, returns API `401 Setup required`, and keeps setup/status/login surfaces auth-exempt. Setup/signup/login are rate-limited; status is exempt but not rate-limited. Route helper fallbacks only tolerate unconfigured anonymous access from loopback.
|
||||
- User privilege checks distinguish legacy empty `allowed_models=[]` from explicit no-model access through `allowed_models_restricted=True`.
|
||||
- `LOCALHOST_BYPASS` in `app.py` only applies to direct loopback clients and excludes proxy/tunnel headers. Helper fallback code is weaker and should not be treated as the primary bypass boundary.
|
||||
- Legacy migrations claim null-owner SQL/JSON data for the primary admin when possible, and startup repeats a null-owner sweep hourly. Remaining null-owner rows are surface-specific compatibility data that must be deliberately included, no-oped for single-user mode, or rejected for strict ownership gates.
|
||||
- `.env` is loaded with `utf-8-sig`, so Windows BOM auth flags still parse.
|
||||
|
||||
## Current Gaps
|
||||
|
||||
- There is no shell/filesystem sandbox for admin tools.
|
||||
- Token scopes remain coarse for some surfaces.
|
||||
- `app.py` AuthMiddleware lacks direct regression coverage for bearer-token state/cache behavior, trusted-loopback proxy-header rejection, and internal-tool owner stamping.
|
||||
- Codex/Claude scoped route enforcement still needs stronger regression coverage.
|
||||
- `THREAT_MODEL.md` still has stale token-scope and `/api/v1/chat` SSRF gap text that should be reconciled with current route validation.
|
||||
- The Default/Local owner contract is canonical but only incrementally adopted; route helper `""`, chat/agent `None`, SQL/JSON null-owner compatibility, and calendar fallback owner behavior still need domain-by-domain migration decisions.
|
||||
@@ -1,186 +0,0 @@
|
||||
# Calendar, Tasks, And Notes
|
||||
|
||||
Last updated: dev@e71f8ce | 2026-08-25
|
||||
|
||||
## Scope
|
||||
|
||||
This spec covers calendar, reminders, tasks, assistant runs, and notes in:
|
||||
|
||||
- app route wiring, auth exemptions, and scheduler startup in `app.py`;
|
||||
- canonical database models in `core/database.py`, with `src/database.py` as a compatibility re-export;
|
||||
- `routes/calendar_routes.py`, `src/caldav_sync.py`, and `src/caldav_writeback.py`;
|
||||
- canonical `routes/task/task_routes.py`, compatibility shim `routes/task_routes.py`, `src/task_scheduler.py`, `src/task_endpoint.py`, `src/event_bus.py`, and `src/interactive_gate.py`;
|
||||
- shared privileged task-action policy in `src/task_action_policy.py`;
|
||||
- `routes/assistant_routes.py`;
|
||||
- canonical `routes/note/note_routes.py`, compatibility shim
|
||||
`routes/note_routes.py`, `src/builtin_actions.py`, and `src/action_intents.py`;
|
||||
- agent/tool call sites in `src/tool_index.py` and `src/tool_implementations.py`;
|
||||
- scoped Codex wrappers in `routes/codex_routes.py`;
|
||||
- database models `CalendarCal`, `CalendarEvent`, `ScheduledTask`, `TaskRun`, `Note`, and `CrewMember`;
|
||||
- direct DB CLIs `scripts/odysseus-calendar`, `scripts/odysseus-notes`, and `scripts/odysseus-tasks`;
|
||||
- frontend modules `static/js/calendar.js`, `static/js/calendar/*`, `static/js/tasks.js`, `static/js/notes.js`, and `static/js/assistant.js`;
|
||||
- tests covering calendar routes/utilities, CalDAV, recurrence, timezone handling, scheduler behavior, task webhooks, notes CLI/tool behavior, and task CLI behavior.
|
||||
|
||||
## Calendar
|
||||
|
||||
`routes/calendar_routes.py` owns `/api/calendar` behavior: config, multi-account CalDAV CRUD, connection test, sync, local calendar CRUD, event CRUD, recurrence expansion, ICS import/export, quick parse, and user timezone offset handling.
|
||||
|
||||
`src.caldav_sync` owns CalDAV fetch/sync. `src.caldav_writeback` owns pushing local changes back to remote calendars. Calendar routes request those behaviors; they do not own CalDAV protocol details.
|
||||
|
||||
Runtime behavior:
|
||||
|
||||
- local default calendars are created lazily per owner with stable UUID5 candidates. Default creation remains inside the caller's transaction so a failed event write cannot leave an orphaned calendar; SQLite serializes the absent-row check with `BEGIN IMMEDIATE`, other backends recover insert races inside a savepoint, and renamed-owner ID collisions advance through deterministic slots. List-only callers explicitly commit the lazy default.
|
||||
- route-level no-login calendar access normalizes empty owner values to `ODYSSEUS_FALLBACK_OWNER` or `owner@localhost`, so route-created calendar rows do not use the empty string as their storage owner;
|
||||
- CalDAV account config lives in per-user prefs as `caldav_accounts`, with the legacy `/api/calendar/config` route reading/upserting the first account;
|
||||
- recurring rules are expanded server-side, including compound recurrence IDs;
|
||||
- RRULE expansion is capped and marks truncated responses;
|
||||
- event datetimes preserve UTC/local metadata through `CalendarEvent.is_utc` where supported;
|
||||
- CalDAV pull uses a bounded sync window, scopes existing UID lookups to the synced calendar, stamps account ids and remote metadata on local calendars, maps Google principal URLs to event collections, preserves locally-created or writeback-pending events that are not yet remote-owned, and deletes stale in-window remote events only when remote object parsing did not fail;
|
||||
- CalDAV writeback stores `remote_href`/`remote_etag`, clears `caldav_sync_pending` only after successful remote writes, and leaves create/update/delete pending markers for retry on failure;
|
||||
- pull and writeback paths always close their `DAVClient`, including discovery,
|
||||
database, and remote-write failure paths;
|
||||
- sync direction can be pull, push, or both, and pending local writeback rows are included even before remote href metadata exists;
|
||||
- ICS import is per-owner, capped, creates fresh local IDs in the target import calendar, and preserves zero-duration events as visible imported rows rather than dropping them as empty ranges;
|
||||
- writeback is best-effort and local SQLite remains source of truth when remote writes fail.
|
||||
|
||||
Calendar credentials are encrypted at rest and are not returned to clients. CalDAV URL validation rejects unsafe schemes, credentials, fragments, localhost names, bad ports, unsafe IP literals, and hostnames resolving to disallowed addresses, with `ODYSSEUS_ALLOW_PRIVATE_CALDAV=1` as the explicit private-IP escape hatch. CalDAV sync/writeback clients disable redirects so credentials are not followed to another origin. The connection-test client keeps proxy/environment trust disabled but explicitly loads an operator `SSL_CERT_FILE` or `REQUESTS_CA_BUNDLE` when the file exists so private/self-signed deployments use the same CA trust intent as real sync.
|
||||
|
||||
## Tasks And Assistant Runs
|
||||
|
||||
`src.task_scheduler.TaskScheduler` owns scheduled task execution, next-run computation, strict single-slot execution, queued/running cleanup at startup, overdue next-run advancement, webhook-triggered tasks, notifications, run records, chained tasks, and event-triggered actions.
|
||||
|
||||
Cookbook serve scheduling crosses this domain. The Cookbook UI creates `cookbook_serve` scheduled tasks, can mirror them as Cookbook calendar events with `cookbook_event_uid`, and task deletion cleans up the linked event when present, falling back to exact-summary matching for legacy events without a stored UID. Cookbook command execution/lifecycle details stay in `cookbook-hwfit.md`.
|
||||
|
||||
`routes.task.task_routes` owns task CRUD, status, manual run/stop/cancel, pause/resume, owner-scoped run/activity history, metadata, onboarding defaults, cache clearing, parse endpoints, and webhook-token regeneration. `app.py` imports the canonical package path; `routes/task_routes.py` replaces its module entry with the canonical module for legacy import and monkeypatch compatibility. Chained-task `then_task_id` values are validated as same-owner relationships on create/update, and scheduler execution also rejects cross-owner or cyclic chains.
|
||||
|
||||
Task webhook paths are auth-exempt at the app middleware layer only for `/api/tasks/{task_id}/webhook/{token}`. The route still validates active task state plus task-specific webhook token before dispatch.
|
||||
|
||||
Task runtime behavior:
|
||||
|
||||
- task runs move through queued/running/success/error/skipped/aborted states;
|
||||
- scheduler/background execution can wait for `src.interactive_gate` to report a quiet foreground window, and running background work can use browser heartbeat/chat-stream activity as a cancellation/defer signal where implemented;
|
||||
- output targets include chat sessions, notifications, email, and MCP delivery paths;
|
||||
- LLM and research tasks can carry a built-in `character_id` persona prompt that the scheduler prepends at execution time;
|
||||
- task-created chat sessions can be foldered under `Tasks`, and startup migration backfills task/research folders for legacy sessions;
|
||||
- event-bus triggers persist counters and `next_run` before scheduler handoff;
|
||||
- the in-process scheduler is gated by `ODYSSEUS_INPROCESS_TASKS`, and multiple enabled app processes can double-run work.
|
||||
- action tasks with `run_local`, `run_script`, `ssh_command`, or
|
||||
`cookbook_serve` are admin-only. `routes.task_routes` enforces this on
|
||||
create/update/manual run and hides those actions from `/meta/actions` for
|
||||
non-admin owners; webhook and scheduler execution pause the task and clear
|
||||
`next_run` if an admin-only action belongs to a non-admin owner.
|
||||
- background LLM task execution uses the background workload path, and the
|
||||
scheduler can abort/cancel active in-process task runs when foreground browser
|
||||
activity appears.
|
||||
- `tidy_research` scans all persisted research files because broken JSON has no trustworthy owner stamp, so it runs only for admins or the explicit auth-disabled single-user operator and refuses regular/pre-setup callers before enumeration.
|
||||
|
||||
`routes.assistant_routes.py` owns crew/assistant settings and run-status surfaces that use the scheduler. `TaskScheduler.ensure_assistant_defaults()` currently seeds the personal assistant crew member and pinned assistant session, but no longer auto-creates Morning/Midday/Evening check-in tasks. Existing crew-linked check-in tasks are still rendered and managed when present.
|
||||
|
||||
## Notes And Reminders
|
||||
|
||||
`routes.note.note_routes` owns notes/todos/reminders, and `app.py` imports that
|
||||
canonical path. `routes.note_routes` replaces its module entry with the
|
||||
canonical module for legacy import and monkeypatch compatibility. Notes are
|
||||
SQLAlchemy `Note` rows and can include due dates, ordering, images, repeat
|
||||
state, AI classification, source/session provenance, and agent session
|
||||
linkage.
|
||||
|
||||
Notes CRUD/reorder/reminder routes resolve the acting owner through `require_user()`: auth-enabled anonymous requests fail closed before hitting owner-scoped queries, while documented no-login/single-user modes still resolve to the compatibility owner path.
|
||||
|
||||
Reminder policy:
|
||||
|
||||
- "remind me at 5pm" should become a todo/note with a due date;
|
||||
- calendar event alarm/reminder UI writes reminder Notes;
|
||||
- calendar events are for scheduled time blocks, meetings, appointments, or explicit calendar requests;
|
||||
- creating a calendar event named "Reminder" does not create notification behavior.
|
||||
|
||||
Built-in reminder/persona prompt text is mirrored server-side for reminder synthesis and scheduled task execution; frontend persona selectors are UI over that server-owned id map, not the authority.
|
||||
|
||||
Reminder dispatch is Note-owned:
|
||||
|
||||
- `dispatch_reminder()` owns browser, email, ntfy, generic webhook, in-app notification, optional LLM reminder text, and dedupe behavior;
|
||||
- the scheduler note scanner calls note-ping actions for backend due-note delivery with per-owner notification state, and calendar-event reminders are treated as Note-owned reminders rather than separate scheduler event pings;
|
||||
- the notes frontend has a browser-tab fallback for visible sessions;
|
||||
- calendar frontend reminder UI stores reminder records as Notes, not calendar-event notification jobs.
|
||||
|
||||
Email/ntfy failures degrade into channel result fields rather than blocking every reminder path. ntfy and generic webhook reminder URLs run through outbound URL safety checks, with `REMINDER_WEBHOOK_BLOCK_PRIVATE_IPS` controlling whether private/LAN targets are allowed. ntfy notification titles are converted to ASCII with replacement and capped at 200 characters before entering HTTP headers. Reminder dedupe uses owner-scoped cache files under `data/`.
|
||||
|
||||
## Agent, Codex, And CLI Surfaces
|
||||
|
||||
`do_manage_tasks`, `do_manage_notes`, and `do_manage_calendar` own agent-side writes. `do_manage_calendar` supports batch event creation plus list range aliases (`start`, `start_time`, `start_date`, `range_start`, `from`, `dtstart`, `since`, and matching end aliases), calendar name/short-id lookup, importance/tag aliases, and reminder offsets expressed as numbers, minute/hour words, or common abbreviations such as `min`/`mins`/`hr`/`hrs`. If a model supplies a loose `query`, `date_range`, or `range` without explicit start/end datetimes, `list_events` returns an error asking the caller to resolve the range and call again instead of guessing. Event classification reads `Memory.text` for personal context before LLM classification. `src.tool_index` encodes the reminder policy that notes/todos own reminders while calendar events own time blocks.
|
||||
|
||||
Agent native tool owner handling is not uniform today. `do_manage_tasks()` filters lists only when `owner` is truthy and creates tasks with the passed owner, so `owner=None` can create legacy/null-owner tasks. For authenticated/non-empty owners, edit/delete/pause/resume/run require an exact stored owner match and reject both cross-owner and null-owner rows; `owner=None` retains single-user compatibility. `do_manage_notes()` list/query behavior distinguishes `None` from `""`, with `None` acting as broader single-user compatibility while `""` filters to empty-owner rows in some paths. `do_manage_calendar()` query helpers filter only when owner is not `None`, while calendar creation routes through the calendar fallback owner for default calendars. These are compatibility behaviors, not a cross-user sharing model.
|
||||
|
||||
Note and calendar route/tool writers owner-reserve any canonical internal upload
|
||||
references in content, checklist/color/image fields, descriptions, and
|
||||
locations before their database writes. Missing or wrong-owner uploads fail the
|
||||
write instead of creating a dangling durable reference; reservations serialize
|
||||
with upload cleanup.
|
||||
|
||||
Chat forwards browser timezone offset and IANA timezone name so natural-language note/calendar tools can anchor dates to the user clock. A valid IANA zone wins over the fixed offset for current-time/DST reasoning; invalid or absent names fall back to the offset and then server-local/UTC compatibility behavior. Chat can auto-promote note/calendar/reminder intents to agent mode.
|
||||
|
||||
Codex todo/calendar wrappers enforce bearer-token owner and `todos:*` or `calendar:*` scopes, then delegate to note/calendar behavior as the token owner. Normal calendar/task/note routes are current-user/cookie routes and should not be treated as scoped bearer-token APIs unless they explicitly use token owner/scope policy.
|
||||
|
||||
Direct DB CLIs are local compatibility tools. They bypass HTTP route behavior, CalDAV writeback, and some owner/timezone parsing policy.
|
||||
|
||||
## Event Bus
|
||||
|
||||
`src.event_bus` owns event-triggered task counters and scheduler handoff. Current emitters include chat/session/document/memory/research/email/skill paths. Ownerless events resolve to a primary configured user instead of broadcasting to every owner.
|
||||
|
||||
The current event bus is not a calendar-event emitter despite the adjacent calendar/task/reminder domain.
|
||||
|
||||
## Timezone And Date Semantics
|
||||
|
||||
- calendar events store offset-aware input as UTC/naive fields plus `is_utc`;
|
||||
- note `due_date` uses ISO-like strings interpreted through note/tool parsers;
|
||||
- chat forwards browser UTC offset into `routes.calendar_routes` request-local state for natural-language date anchoring in calendar/note tool parsing;
|
||||
- generic scheduled task clock times are stored as UTC values after local conversion;
|
||||
- assistant check-ins can use an IANA timezone on `CrewMember`, with UTC fallback.
|
||||
|
||||
Dateutil fallbacks strip timezone-aware parser results back to the naive-UTC contract before recurrence/window comparisons. Calendar agent list tools accept current range aliases implemented by `src.tool_implementations`, and equal/same-day start/end ranges are normalized to a one-day window instead of silently returning no rows.
|
||||
|
||||
Natural-language parsers prefer time-first interpretations for short reminder/event phrases where the user supplies a clock time before a date phrase.
|
||||
|
||||
Calendar frontend week-start preference is browser-local (`cal-week-start`) with Monday/Sunday controls; it is not persisted as a server preference.
|
||||
|
||||
Natural-language date parsing and timezone behavior are compatibility-sensitive and need route/tool/frontend regression coverage when changed. Request-local timezone context is ephemeral and must not be persisted as user state. A valid browser IANA timezone is authoritative over a possibly stale or wrong-sign fixed offset because it carries daylight-saving rules.
|
||||
|
||||
## Degraded And Optional Behavior
|
||||
|
||||
- CalDAV sync no-ops with shaped errors when unconfigured, invalid, offline, or missing the optional `caldav` dependency.
|
||||
- CalDAV writeback failures are non-fatal to local calendar writes and are mostly visible through logs.
|
||||
- Missing or invalid `croniter` rejects cron schedules or yields no next run.
|
||||
- Missing timezone support falls back to UTC or legacy behavior.
|
||||
- ICS import depends on `icalendar`; missing dependency can fail before route-shaped error handling today.
|
||||
- Notes reminders can still use local browser fallback when backend email/ntfy channels fail.
|
||||
- App backup import/export does not currently include calendar events, scheduled tasks, task runs, or notes; calendar ICS import/export is separate and calendar-only.
|
||||
|
||||
## Security And Provenance
|
||||
|
||||
Calendar, task, note, and assistant routes are owner-scoped for normal users. Legacy null-owner behavior is compatibility-sensitive and should not silently grant authenticated owners broad mutation rights.
|
||||
|
||||
Because auth-disabled chat owners can arrive as `None`, tool-created rows may not use the same owner value as route-created rows. Multi-user or owner-model changes must audit both route and agent paths.
|
||||
|
||||
Task creation/update/manual run/webhook/scheduler execution blocks shell-like and Cookbook serve action types for non-admin users through `src.task_action_policy`, and tool security blocks privileged task/calendar tools for non-admin use. Assistant defaults reject synthetic owners such as `api` and `internal-tool`.
|
||||
|
||||
Note routes store caller-provided `source`, `session_id`, `image_url`, and agent-session provenance. Canonical internal upload references in persisted note/calendar fields are owner-reserved before writes, and upload-backed bytes remain protected when fetched through upload routes. Arbitrary non-upload image/provenance URLs are not otherwise normalized or validated by note storage.
|
||||
|
||||
## Testing Coverage
|
||||
|
||||
Existing coverage is strongest around CalDAV URL hardening/writeback, client cleanup and operator CA handling, bidirectional/pending CalDAV sync markers, CalDAV UID calendar scoping, calendar recurrence/timezone helpers, owner-scoped calendar basics, exact-owner task-tool mutations, scheduler restart/cancel/next-run behavior, webhook auth-exemption source shape, canonical/legacy note-module identity, note-route unauthenticated fail-closed behavior, note/calendar attachment reservations, notes CLI/tool due-date behavior, calendar reminder abbreviation parsing, task CLI preview, task persona fields, and same-owner chained task validation.
|
||||
|
||||
Route-level coverage is thinner for full calendar route behavior, task CRUD/security/run controls, live webhook token dispatch, notes owner CRUD/reminder delivery, assistant defaults/run status, event-bus triggers, Codex todo/calendar scopes, and frontend panel wiring.
|
||||
|
||||
## Current Gaps
|
||||
|
||||
- CardDAV still needs URL hardening parity with CalDAV; CalDAV now resolves hostnames during validation and revalidates writeback URLs.
|
||||
- `do_manage_notes()` should match HTTP note-route owner behavior for legacy null-owner notes.
|
||||
- Auth-disabled agent tools can produce or read broader owner scopes than route handlers because they receive `owner=None`; tasks, notes, and calendar need aligned policy/tests.
|
||||
- Task webhook tests should keep exercising live route token behavior and
|
||||
admin-only action blocking, not only middleware/source strings.
|
||||
- Reminder delivery needs tests across frontend `/fire-reminder`, backend `dispatch_reminder()`, scheduler note pings, channel degradation, and dedupe.
|
||||
- Codex todo/calendar scope and owner mapping needs dedicated regression coverage.
|
||||
- Direct DB CLIs need either documented route-bypassing support status or shared helpers to avoid owner/timezone/writeback drift.
|
||||
- `scripts/odysseus-webhook` builds the live `/api/tasks/{task_id}/webhook/{token}` path with percent-encoded path segments; its direct DB token rotation/revocation behavior remains a local compatibility surface.
|
||||
- Assistant default documentation/code comments still mention check-ins that are no longer auto-seeded.
|
||||
- App backup import/export does not cover the calendar/task/note rows described by this spec.
|
||||
-154
@@ -1,154 +0,0 @@
|
||||
# Chat
|
||||
|
||||
Last updated: dev@e71f8ce | 2026-08-25
|
||||
|
||||
## Scope
|
||||
|
||||
This spec covers current chat behavior in:
|
||||
|
||||
- `routes/chat_routes.py` and `routes/chat_helpers.py`;
|
||||
- `routes/session_routes.py` and canonical `routes/history/history_routes.py`,
|
||||
with `routes/history_routes.py` as a compatibility shim;
|
||||
- `src/chat_helpers.py`;
|
||||
- `src/agent_runs.py`;
|
||||
- `src/chat_handler.py` and `src/chat_processor.py`;
|
||||
- `core/session_manager.py` and `core/models.py`;
|
||||
- `src/attachment_refs.py` and `src/upload_handler.py` for durable attachment
|
||||
references and write reservations;
|
||||
- `src/context_budget.py`, `src/context_compactor.py`, and `src/topic_analyzer.py`;
|
||||
- `src/foreground_model_routing.py`, `src/tool_approval_scopes.py`, `src/tool_approvals.py`, and `src/tool_capabilities.py`;
|
||||
- `routes/workspace_routes.py` for workspace selection support;
|
||||
- frontend modules `static/js/chat.js`, `static/js/chatStream.js`, `static/js/chatRenderer.js`, `static/js/sessions.js`, `static/js/search-chat.js`, `static/js/compare/stream.js`, `static/js/workspace.js`, `static/js/composerArrowUpRecall.js`, `static/js/streamingSegmenter.js`, `static/js/group.js`, and `static/js/notes.js`;
|
||||
- integration points with uploads, documents, compare, research, agent tools, memory, RAG, search, and model endpoints.
|
||||
|
||||
## Session Ownership
|
||||
|
||||
`core.session_manager.SessionManager` owns session persistence and message writes. `routes/session_routes.py` owns session list/create/update/archive/delete/folder/importance behavior for the sidebar. `routes.history.history_routes` owns history/topic surfaces, with `routes/history_routes.py` kept as a compatibility shim.
|
||||
|
||||
`core.models.Session` and `ChatMessage` are pure data containers. They do not own persistence; `Session.add_message()` delegates to the configured session manager when present.
|
||||
|
||||
Startup session discovery selects non-archived sessions by the existence of persisted `ChatMessage` rows rather than trusting the denormalized `Session.message_count`. It computes authoritative counts only for the bounded discovery set, then keeps full message hydration lazy.
|
||||
|
||||
## Streaming
|
||||
|
||||
`routes/chat_routes.py` owns `/api/chat`, `/api/chat_stream`, detached stream resume/stop/status, injected context, chat-message search, and rewrite routes. Streaming is the main UI path.
|
||||
|
||||
`static/js/chat.js` owns send/abort/continue UI state, the main fetch/read loop, SSE parsing, rendering dispatch, workspace form wiring, and background/resumable stream tracking. `static/js/chatStream.js` owns UI-control event handling and stream/research notification helpers. `static/js/sessions.js` polls server stream status after refresh or session switch. `static/js/composerArrowUpRecall.js` owns prompt recall from the composer when the caret is at the top of an empty input.
|
||||
|
||||
Runtime behavior:
|
||||
|
||||
- the `/api/chat*` prefix is exempt from the global request hard timeout;
|
||||
- browser chat sends `X-Tz-Offset` and an IANA timezone name; request-local helpers prefer a valid IANA zone for DST-aware current-time reasoning, then fall back to the fixed offset;
|
||||
- browser chat can send a selected workspace path; route code only resolves it for admin/single-user flows, validates it as an existing directory, and forwards it so agent file/shell tools are confined by `src.tool_execution`;
|
||||
- stream callbacks can outlive a deleted session, so persistence must fail closed instead of recreating orphan messages;
|
||||
- message metadata carries timestamps, metrics, tool events, sources, hidden
|
||||
thinking/reasoning text when providers expose it separately, context-trim
|
||||
metrics, structured attachment references, and related UI state;
|
||||
- metadata preserves requested and actual reply models and endpoints, per-round route transitions, and answering-route cost attribution; stable session ids remain available so prompt/sequence-memory and KV-cache paths can address the same conversation consistently;
|
||||
- multimodal content can be a list of content blocks for the live provider call,
|
||||
while persistence collapses raw media into readable text and stable
|
||||
attachment-reference lines;
|
||||
- agent streams forward explicit round-cap, tool-budget, repeated-tool-loop,
|
||||
and intent-without-action guard events so the frontend can distinguish a
|
||||
controlled stop from a stalled response.
|
||||
|
||||
`src.agent_runs` owns detached in-memory stream runs, replay buffers, replacement cancellation, resume subscribers, explicit stop, and terminal-buffer eviction. Closing the SSE connection does not necessarily stop generation. `static/js/chat.js` can live-resume a still-running detached stream through `/api/chat/resume/{session_id}`; rich responses reload from DB for canonical rendering. Detached runs are process-local and do not survive server restart.
|
||||
|
||||
Provider adapters live below chat in `src.llm_core`. Chat consumes normalized SSE output, fallback/error events, reasoning/tool deltas, and metrics. Foreground chat is strict to the selected route by default. Only the selected owner can opt in through `foreground_fallback_enabled` plus ordered `foreground_model_fallbacks`; the retired `default_model_fallbacks` key is ignored. Eligible pre-content availability failures can advance through at most ten owner-visible exact model candidates, while missing configuration/endpoints, provider/schema errors, clean empty completions, and post-content failures remain on the selected route and surface an error. Once a route produces substantive text/reasoning or a tool call it is pinned as the answering route.
|
||||
|
||||
Fallback candidates receive route-neutral context shaping. Only compaction performed for the answering route is persisted. Chat and agent metadata record requested/actual model and endpoint identity, round-by-round route transitions, and costs against the route that actually answered; the browser renders same-model endpoint changes as well as model changes.
|
||||
|
||||
## Context Preface
|
||||
|
||||
`routes.chat_helpers.build_chat_context()` owns the shared route pipeline: preset extraction, preprocessing, user-message persistence, incognito/no-memory/RAG/skills flags, prefetched compare search, YouTube transcript context, research-spinoff grounding, model normalization, and compaction.
|
||||
|
||||
`src.chat_processor.ChatProcessor.build_context_preface()` owns source preface construction. It can add memory, RAG, web search, URL page content, and skills index context before the model call.
|
||||
|
||||
Chat preface enhances the model's context. It must not rewrite the user message or force literal-vs-fetch interpretation before the model sees the request. See [context-building.md](context-building.md).
|
||||
|
||||
Chat-owned external context must enter the model through `untrusted_context_message()` unless a different treatment is explicitly documented. This includes memory, RAG, web search, URL fetches, prefetched search context, YouTube transcripts, research injection, and manual context injection.
|
||||
|
||||
## Modes And Handoffs
|
||||
|
||||
Chat can dispatch to normal LLM calls, agent mode, research mode, or compare-related flows. Session mode is stored on `sessions.mode`.
|
||||
|
||||
Legacy plan-mode backend plumbing still exists below chat, but `routes/chat_routes.py` currently forces browser/form `plan_mode` input off and the old visible plan window frontend module is not part of the current SPA. Treat plan-mode changes as compatibility work unless the UI contract is intentionally reintroduced.
|
||||
|
||||
Current call sites include:
|
||||
|
||||
- chat/research dispatch in `routes/chat_routes.py`;
|
||||
- agent execution in `src/agent_loop.py`;
|
||||
- deep research orchestration in `src/research_handler.py`;
|
||||
- compare entry points in canonical `routes/compare/compare_routes.py` and frontend compare modules.
|
||||
|
||||
Agent-mode tool access is gated in layers. Chat route toggles and privileges
|
||||
build a disabled-tool set; incognito and compare mode remove persistence-heavy
|
||||
or UI-breaking tools; `src.action_intents.message_needs_tools()` provides
|
||||
conservative regex auto-escalation hints; `src.agent_loop`,
|
||||
`src.tool_security`, `src.tool_execution`, and internal loopback validation
|
||||
remain server-side enforcement owners.
|
||||
|
||||
`allow_bash` and `allow_web_search` can be read from the JSON request body for browser chat posts that do not submit traditional form fields.
|
||||
|
||||
Web search tools are per-turn explicit opt-in. Either `allow_web_search=true`
|
||||
or `use_web=true` can enable `web_search`/`web_fetch`, but an explicit
|
||||
`allow_web_search=false` wins over `use_web=true` and keeps those tools
|
||||
disabled. Explicit latest-turn web-search intent can still auto-escalate into
|
||||
agent mode and narrows the available tool set toward `web_search`/`web_fetch`,
|
||||
but it no longer re-enables web tools after an explicit denial or global
|
||||
disable.
|
||||
|
||||
Guide-only/no-tools requests build an effective tool policy before preprocessing and agent dispatch. That policy suppresses tool-backed preprocessing/background extraction/research, disables schemas and MCP for the turn, and is still enforced by `src.tool_execution` if a model emits a tool call anyway.
|
||||
|
||||
When route context is trimmed without full compaction, chat emits a
|
||||
`context_trimmed` SSE event and carries before/after message/token counts into
|
||||
metrics. Provider reasoning/thinking deltas are streamed for live UI handling
|
||||
but kept out of the visible saved assistant content and stored in metadata when
|
||||
available.
|
||||
|
||||
## Attachments
|
||||
|
||||
`src.chat_handler.ChatHandler.preprocess_message()` owns owner-scoped upload-id resolution, attachment metadata, YouTube transcript/comment preprocessing, image/VL behavior, and enhanced text used by chat. `src.document_processor.build_user_content()` owns conversion of uploaded/chat-attached files into model-ready text or multimodal blocks. `src.attachment_refs` owns persisted text/reference normalization, and `SessionManager` owner-reserves attachment ids before appending or replacing durable message rows. `static/js/fileHandler.js` owns frontend pending-file state.
|
||||
|
||||
Attachment-only sends are valid. Missing or unauthorized ids are skipped during preprocessing, while a missing/wrong-owner durable reference aborts a message/history replacement before existing transcript rows are removed. Upload failures keep pending files for retry, unsupported media can degrade to text markers, optional Office/PDF/VL dependencies can emit extraction banners, Office attachments can create markdown documents when extracted server-side, and fillable-PDF auto-document failures fall back to normal PDF extraction. `chat_messages.content` and FTS do not retain provider data URLs; structured references stay in metadata for reloads. Chat does not own upload bytes or durable document storage; it requests document/upload behavior from those subsystems.
|
||||
|
||||
Frontend chat distinguishes normal resend from regenerate-from-here: normal resend appends a fresh user copy and carries upload IDs where available, while regeneration truncates from the selected point. AI-message delete prompts before removing the AI response plus preceding user turn. Desktop Enter submits; mobile Enter inserts a newline unless another platform-specific send control is used.
|
||||
|
||||
Native document tool outputs can open or refresh the document editor from
|
||||
tool-result metadata, so the UI can recover if a later `doc_update` stream event
|
||||
is missed. The chat renderer also hides raw/incomplete leaked tool JSON and
|
||||
document fences from normal transcript text.
|
||||
|
||||
When untrusted external/workspace content has entered the agent context, high-impact tool calls pause as exact approval cards instead of executing. The browser can allow the rest of the interrupted task, allow this chat session, or deny; it submits only the opaque id/decision with an empty control-plane message and does not mutate the composer. The server restores the sealed first action plus private selected tools/query, revalidates policy and document freshness, consumes the first action, and resumes without persisting a synthetic user message. Task scope ends with that resumed run. Chat scope persists the resolved card and marks later context only for that exact session; forks do not inherit it. A normal message retires an unresolved card while preserving taint.
|
||||
|
||||
## Security And Provenance
|
||||
|
||||
`/api/chat` and `/api/chat_stream` verify session ownership before loading the session. Chat privilege gates enforce allowed models and daily message caps before LLM work. Active document injection, session auth/header recovery, endpoint repair, upload-id resolution and reservation, memory/RAG retrieval, and post-response work must stay owner-scoped.
|
||||
|
||||
The scoped API-token chat surface is `/api/v1/chat`. Browser chat routes can receive bearer-auth state from middleware, but route code must not assume `"api"` is a durable owner; API-token support requires explicit scope checks and token-owner attribution.
|
||||
|
||||
Incognito disables memory, skill, and chat-history tools and skips assistant DB persistence, but current user-message persistence and later cleanup are not a strict no-write guarantee. Treat incognito changes as security-sensitive until that contract is clarified.
|
||||
|
||||
## Search Boundary
|
||||
|
||||
`GET /api/search` in `routes/chat_routes.py` is chat-message search for the UI and slash commands. Web search routes are owned by canonical `routes/search/search_routes.py`; chat and agent web context call through `src.search`, compatibility shims, and search content fetchers. Do not confuse chat-history search with external web retrieval.
|
||||
|
||||
## Degraded And Compatibility Behavior
|
||||
|
||||
- Missing ChromaDB, embeddings, memory vectors, RAG managers, or skills indexes should remove injected context or fall back to keyword/text behavior without failing chat.
|
||||
- Direct URL prefetch failures become compact untrusted context stating that the page was not read, with only transport-owned HTTP/size/rate-limit status where recognized; raw URLs, exception text, and response-controlled diagnostics are not echoed into logs or model context.
|
||||
- Sessions hydrate legacy string headers and multimodal JSON-array content, export text/HTML/Markdown after flattening non-string blocks, can lazy-load from DB when cached state is empty, and preserve old history/index delete behavior where needed.
|
||||
- Initial shell/session loading is non-blocking: the sidebar can render before a selected transcript is hydrated, and full transcript hydration is deferred until display or a model send requires it.
|
||||
- Chat repairs empty selected models and orphaned endpoint references before provider calls when possible.
|
||||
- Deleted-session stream writes fail closed.
|
||||
- Docker/native endpoint differences are owned by runtime/model setup, but chat sessions depend on the saved endpoint URLs and headers.
|
||||
- Copying a response from the UI copies the displayed answer text and omits hidden reasoning/thinking segments.
|
||||
|
||||
## Current Gaps
|
||||
|
||||
- Chat, agent, research, and compare orchestration still meet in a large route file.
|
||||
- Context preface behavior is spread across `routes/chat_helpers.py`, `src/chat_processor.py`, route injections, and agent/tool paths.
|
||||
- Detached stream lifecycle spans `routes/chat_routes.py`, `src/agent_runs.py`, `static/js/chat.js`, `static/js/sessions.js`, and non-chat callers.
|
||||
- Some frontend stream state is still global/module-level in `static/js/chat.js` and needs careful session isolation when adding background or resumable flows.
|
||||
- Chat lacks route-level SSE regression tests for `/api/chat_stream`, live resume/stop/status, mode handoff, persistence metadata, partial-save behavior, attachment/doc-update events, browser timezone offset/workspace handling, and literal URL context intent.
|
||||
- Bearer-token behavior on browser chat routes and incognito persistence need explicit contract decisions and regression coverage.
|
||||
@@ -1,79 +0,0 @@
|
||||
# Compare
|
||||
|
||||
Last updated: dev@e71f8ce | 2026-08-25
|
||||
|
||||
## Scope
|
||||
|
||||
This spec covers model A/B comparison behavior in:
|
||||
|
||||
- canonical `routes/compare/compare_routes.py`, with `routes/compare_routes.py` as a compatibility shim;
|
||||
- `routes/session_routes.py`;
|
||||
- `routes/chat_routes.py` and `routes/chat_helpers.py`;
|
||||
- `routes/model_routes.py`;
|
||||
- canonical `routes/search/search_routes.py`, with `routes/search_routes.py` as a compatibility shim;
|
||||
- `core/database.py` model `Comparison`;
|
||||
- `src/llm_core.py` and `src/endpoint_resolver.py`;
|
||||
- frontend modules under `static/js/compare/`;
|
||||
- `static/js/chat.js`, `static/js/sessions.js`, `static/js/models.js`, and `static/js/slashCommands.js`;
|
||||
- `tests/test_compare_*` and focused blind-compare redaction tests.
|
||||
|
||||
## Runtime Behavior
|
||||
|
||||
The active text compare UI creates ordinary `[CMP]` sessions through `/api/session`, then streams each pane through `/api/chat_stream` with `compare_mode=true`. Search compare is a separate branch: it can query `/api/search/query` directly and its synthesis sessions use ordinary chat streaming without `compare_mode=true`. `static/js/compare/index.js` owns compare orchestration, session creation, execution order, search-mode branching, and export actions. `static/js/compare/panes.js` owns pane add/remove/swap/reroll lifecycle. `static/js/compare/stream.js` owns pane streaming and event rendering.
|
||||
|
||||
`routes/compare/compare_routes.py` owns the `/api/compare` HTTP surface for alternate/legacy start/vote/history/delete behavior and the active `/api/compare/record` vote-summary endpoint. The top-level module is a compatibility alias. Legacy `/api/compare/start` uses neutral helper-session names and withholds model identities/mapping from the start response while blind mode is active. It does not own provider-specific payload behavior.
|
||||
|
||||
Current call sites include:
|
||||
|
||||
- `/api/session` compare session creation and cleanup in compare frontend modules;
|
||||
- `/api/chat_stream` pane execution through chat routes and detached stream infrastructure, streamed directly into panes so upstream generation stops promptly when panes are stopped;
|
||||
- `/api/models` and probe routes for model/endpoint selection;
|
||||
- search-provider compare mode through `routes/search/search_routes.py`;
|
||||
- `/api/compare/record` as a fire-and-forget backend vote summary, while active scoreboard state is localStorage-backed.
|
||||
|
||||
`Comparison` rows currently persist vote/history metadata: prompt, first model identifiers, winner, blind flag, optional N-model JSON in `blind_mapping`, vote timestamp, and owner. Response and metric columns exist in the schema but are not populated by the active compare UI flow. Compare history must be owner-scoped.
|
||||
|
||||
Frontend compare behavior is split by responsibility:
|
||||
|
||||
- `state.js` owns local compare state;
|
||||
- `selector.js`, `models.js`, and `probe.js` own endpoint/model selection and probe UI;
|
||||
- `panes.js` and `stream.js` own paired response rendering;
|
||||
- `vote.js` and `scoreboard.js` own voting and history display.
|
||||
|
||||
Compare panes can receive `ask_user` or tool-approval controls from the shared chat stream. `static/js/compare/stream.js` routes those controls into the main chat renderer/control plane, pauses pane completion/autograding while a choice is pending, and can resume the pane after the user decision; compare orchestration keeps its busy state until those continuations settle.
|
||||
|
||||
Mobile compare layout collapses multi-pane grids to a single column so panes
|
||||
remain readable on narrow screens while the desktop grid still uses the
|
||||
selected column count.
|
||||
|
||||
## Ownership Boundaries
|
||||
|
||||
Compare owns paired evaluation flow and pane state. Chat routes own the actual stream execution path for compare panes. LLM provider code owns model-call mechanics. Session/model routes own endpoint-id resolution, owner-filtered endpoint/model visibility, header copying, and deleted-endpoint failures.
|
||||
|
||||
`compare_mode` in chat strips compare-breaking tools, disables document tools for `[CMP]` sessions, skips some research clarification, and suppresses memory, skill, and webhook side effects after pane responses.
|
||||
|
||||
Compare frontend code is part of the app DOM security surface. Current stream/search rendering sanitizes probe labels and tool labels, constrains search-result links to HTTP(S), uses safe generated-image display sources, and opens compare export/image popups with opener isolation.
|
||||
|
||||
## Policy Notes
|
||||
|
||||
- Current blind compare is UI/API masking until vote/reveal, not a full confidentiality boundary. `[CMP]` session names and session-list model fields are redacted for helper sessions, and legacy `/api/compare/start` withholds model identity/mapping while blind. Client-side selected model state and privileged/local inspection can still expose identity.
|
||||
- Compare endpoint lists and secondary endpoint lookups use owner filtering so users see and resolve only shared or owned endpoints.
|
||||
- Non-admin compare session creation must use registered owner-visible endpoints; compare must not allow arbitrary raw endpoint URLs to bypass session-route endpoint policy.
|
||||
- Prefetched search, URL, RAG, and research context entering compare panes must use the untrusted-context wrapper.
|
||||
- Compare panes use chat's foreground routing contract: selected routes are strict unless that owner explicitly enabled ordered foreground fallbacks. Verify each pane still reaches its intended route and that any opt-in route transition or error is visible.
|
||||
|
||||
## Degraded And Compatibility Behavior
|
||||
|
||||
- Missing/offline endpoints are surfaced by model/session routes; chat can clear orphaned endpoint references and recover empty models when possible.
|
||||
- Compare streams inherit chat's opt-in, eligible-pre-output-only foreground fallback and provider-normalized SSE events, but compare frontend handling for errors and model/endpoint route transitions is thinner than chat's stream path.
|
||||
- Shared legacy `ModelEndpoint.owner == NULL` rows remain visible through owner filters. Legacy `Comparison.owner == NULL` rows are not treated as shared for authenticated vote/delete/history flows.
|
||||
- `/api/compare/start` and `/{comp_id}/vote` remain implemented but are not the active frontend path.
|
||||
|
||||
## Current Gaps
|
||||
|
||||
- Blind mode is not a confidentiality boundary; client/local state can still expose model identity before vote.
|
||||
- `/api/compare/start` accepts raw endpoint URLs and can diverge from `/api/session` endpoint-owner/raw-endpoint policy.
|
||||
- `src/agent_loop.py` advertises stale compare app API endpoints.
|
||||
- Compare streaming and chat streaming are separate frontend paths but share model/provider infrastructure; regressions can happen when provider event shape changes.
|
||||
- Compare frontend needs explicit fallback/error event handling parity with chat streaming.
|
||||
- Compare tests cover endpoint owner helper behavior, blind compare redaction, ask-user/tool-approval routing, and portable JS helpers, but not full active `/api/session` pane creation, frontend pane lifecycle, or complete SSE fallback/error handling.
|
||||
@@ -1,113 +0,0 @@
|
||||
# Context Building
|
||||
|
||||
Last updated: dev@e71f8ce | 2026-08-25
|
||||
|
||||
## Scope
|
||||
|
||||
This spec covers model-context construction in:
|
||||
|
||||
- `src/chat_processor.py`;
|
||||
- `src/chat_handler.py` and `src/youtube_handler.py`;
|
||||
- `routes/chat_helpers.py` and context injection in `routes/chat_routes.py`;
|
||||
- `src/agent_loop.py`;
|
||||
- `src/tool_execution.py`;
|
||||
- `src/attachment_refs.py` and uploaded-file manifest construction in
|
||||
`routes/chat_helpers.py`;
|
||||
- `src/tool_policy.py`;
|
||||
- `src/prompt_security.py`;
|
||||
- `src/tool_capabilities.py`, `src/tool_approval_scopes.py`, and `src/tool_approvals.py`;
|
||||
- transport primitives in `src/outbound_fetch.py` plus fetch/extraction adapters in `src/search/content.py` and `services/search/content.py`;
|
||||
- search orchestration in `services/search/core.py` and the compatibility wrapper in `src/search/core.py`;
|
||||
- RAG and personal docs in `src/rag_singleton.py`, `src/rag_vector.py`, `src/rag_manager.py`, and `src/personal_docs.py`;
|
||||
- research flows in `src/deep_research.py`, `src/research_handler.py`, and `services/research/research_handler.py`;
|
||||
- memory and skills in `src/memory.py` and `services/memory/*`;
|
||||
- related policy in `THREAT_MODEL.md`.
|
||||
|
||||
## Contract
|
||||
|
||||
Context-building tools gather evidence. They do not own user-intent routing.
|
||||
|
||||
Runtime rules:
|
||||
|
||||
- if external context is available, add it as compact untrusted source data;
|
||||
- if an attempted source is unavailable and relevant, represent the unavailable state explicitly with source and reason when known;
|
||||
- preserve the user's original message for the model;
|
||||
- do not use regex preprocessing to force literal-vs-fetch intent;
|
||||
- do not disable tools or force a reply style solely because preprocessing found a URL.
|
||||
|
||||
## Untrusted Data
|
||||
|
||||
`src.prompt_security` owns the untrusted wrapper:
|
||||
|
||||
- `UNTRUSTED_CONTEXT_POLICY` states global model policy;
|
||||
- `untrusted_context_message(label, content)` wraps source content as user-role data with `metadata.trusted = False`, provenance origin, and an `arm_tool_gate`/`tool_gate_untrusted` signal that defaults to arming the server-owned tool gate.
|
||||
|
||||
Current untrusted context sources include:
|
||||
|
||||
- fetched URLs and web search results;
|
||||
- webpage content passed into deep-research extraction;
|
||||
- YouTube transcripts/comments;
|
||||
- RAG/personal document chunks;
|
||||
- memories and skills;
|
||||
- notes and active editor documents;
|
||||
- emails and attachments;
|
||||
- tool output from external/user-controlled data.
|
||||
|
||||
Live multimodal provider blocks can contain data URLs, but persisted and
|
||||
tool-facing context uses stable attachment references. Tool manifests carry an
|
||||
`odysseus://attachment/<id>` URI and owner-checked read policy; local paths are
|
||||
compatibility data added only after owner and root-confinement checks. Persisted
|
||||
chat context keeps readable text/reference lines rather than reinserting raw
|
||||
media bytes into later turns or search state.
|
||||
|
||||
## URL, Search, And Tool-Derived Context
|
||||
|
||||
Chat URL prefetch and agent `web_fetch` are different paths. Chat prefetch happens before the model call; `web_fetch` is a tool the model may choose later. Both should converge on the same intent: enrich context when content is available, represent unavailable content when it is not, and let the model interpret the user request.
|
||||
|
||||
Search results and fetched pages are evidence. `web_search` should not force a page fetch unless its explicit contract says it does. Failed fetches should not crash chat or silently imply content was read. Canonical search content fetchers can extract readable text from HTML, `text/*`, Markdown, `.txt`, `.json`, and `.jsonl` responses and should return shaped error results for HTTP status failures. URL fetches validate every redirect hop and pin the outbound connection to a public IP resolved during validation, so context-building callers do not need a second DNS-rebinding guard.
|
||||
|
||||
Current behavior is not yet unified:
|
||||
|
||||
- successful chat URL prefetch is wrapped as untrusted context; failed prefetch now adds a compact untrusted statement that the page was not read, recognizes only transport-owned HTTP/size/rate-limit categories, and suppresses raw exception/response text;
|
||||
- agent `web_fetch` returns explicit URL-specific tool errors for timeout, unsupported scheme, fetch failure, or no readable text;
|
||||
- comprehensive search reports provider-chain failures, but individual page-fetch failures can be logged and omitted;
|
||||
- YouTube fetching is owned by `ChatHandler`/`youtube_handler`, while `routes.chat_helpers` only wraps the resulting transcript/comment strings.
|
||||
|
||||
`src.outbound_fetch` owns reusable synchronous public-URL classification, per-hop DNS resolution/pinning, redirect handling, and body budgets. `services/search/core.py` owns `comprehensive_web_search()` orchestration. `services.search.content` owns content extraction and adapts the shared transport; `src/search/core.py` and `src/search/content.py` preserve compatibility imports without a second implementation.
|
||||
|
||||
## Tool Result Envelope
|
||||
|
||||
`src.tool_execution` executes and formats tools. Tool output caps live in `src.constants` and are re-exported through older facades; shared native-tool truncation lives in `src.tool_utils`. `src.agent_loop._append_tool_results()` owns model re-entry: native tool calls return as provider-style `role: "tool"` messages with untrusted metadata, while fenced-tool results use the untrusted wrapper. Classification considers both the requested tool and the result payload, so remote or stored model-visible content can arm the session gate even on a failed tool status.
|
||||
|
||||
Taint is server-owned continuation state, not a model instruction. After untrusted external/workspace context, low-impact reads can continue, but high-impact, unknown, and arbitrary MCP actions become proposals that produce an exact approval card. The server seals the exact first action plus private continuation tool/query state; document actions also bind the current document version and digest. A chat decision can allow the resumed task or persist a grant for later turns in that exact chat, while non-chat callers remain single-action. Blocked/approval placeholders and content-free failures do not recursively arm the gate.
|
||||
|
||||
Context budgeting uses known model context windows when available. `src.context_budget` treats the default 6000-token value as an automatic sentinel, scales to a capped fraction of known context length for non-explicit budgets, and leaves unknown windows on conservative defaults.
|
||||
|
||||
Side-effect enforcement lives outside context building. Chat route disabled-tool policy, `src.tool_security`, `src.tool_execution`, and `do_app_api()` block unsafe tool execution; prompt wording alone is not the authority.
|
||||
|
||||
Guide-only/no-tools policy can suppress context acquisition before the model call. `src.tool_policy` feeds chat route preprocessing and agent-loop assembly so tool-backed search/research/memory/RAG/skills/local-context paths are skipped when the latest user turn explicitly forbids tools.
|
||||
|
||||
## Degraded And Optional Dependencies
|
||||
|
||||
- ChromaDB, HTTP embeddings, and FastEmbed are installed/expected in normal setups but must degrade cleanly when a service, package, or embedding backend is unavailable.
|
||||
- `src.rag_singleton.get_rag_manager()` owns RAG startup retry throttling; `src.rag_vector.VectorRAG` is the live owner-filtered path; `src.rag_manager.RAGManager` is compatibility/backward-compat behavior.
|
||||
- Memory-vector and tool-index retrieval can fall back to keyword/text behavior when vector stores or embeddings fail.
|
||||
- Docker compose and native installs use different Chroma host defaults; model endpoint loopback rewriting is owned by model/runtime specs.
|
||||
|
||||
## Current Call Sites Include
|
||||
|
||||
- `ChatProcessor.build_context_preface()` for memory, RAG, web search, URL content, and skills index;
|
||||
- `ChatHandler.preprocess_message()` and the canonical `services.youtube.youtube_handler` import path for YouTube fetch/format, then `routes/chat_helpers.py` for wrapping prefetched search/Youtube context;
|
||||
- `routes/chat_routes.py` research context injection;
|
||||
- `src.agent_loop` for active editor document, skill context, and tool-result reinsertion;
|
||||
- uploaded-file manifest/reference context for agent tools and later chat turns;
|
||||
- `src.tool_execution` for `web_search`, `web_fetch`, file, shell, MCP, and other tool outputs;
|
||||
- `src.deep_research` and research handlers for search/fetch/extract flows used by research jobs, with fetched webpage text wrapped before extraction and analyzed URLs tracked separately from source snippets.
|
||||
|
||||
## Current Gaps
|
||||
|
||||
- URL/search context result shape is not unified across chat prefetch, agent tools, and research.
|
||||
- Failed fetch representation remains inconsistent outside direct chat URL prefetch, especially in comprehensive search and research aggregation.
|
||||
- Tool/context wording is spread across schema, prompt, and retrieval surfaces.
|
||||
- Source-specific wrapping and unavailable-state behavior still needs broader focused coverage for literal URL intent, research, RAG/memory/skills, and YouTube; external tool results and approval continuation now have dedicated gate/taint regressions.
|
||||
- Compare pre-search context is computed but may not be submitted through the current compare stream form.
|
||||
@@ -1,195 +0,0 @@
|
||||
# Cookbook And Hardware Fit
|
||||
|
||||
Last updated: dev@e71f8ce | 2026-08-25
|
||||
|
||||
## Scope
|
||||
|
||||
This spec covers model setup/serving and hardware fit in:
|
||||
|
||||
- app route registration in `app.py`;
|
||||
- `routes/cookbook_routes.py`;
|
||||
- `src/cookbook_serve_lifecycle.py`;
|
||||
- `src/host_docker_access.py`;
|
||||
- Cookbook package/rebuild/shell integration in `routes/shell_routes.py`;
|
||||
- `routes/cookbook_helpers.py`;
|
||||
- `routes/hwfit_routes.py`;
|
||||
- `services/hwfit/*` and `services/hwfit/data/hf_models.json`;
|
||||
- durable Cookbook state through `routes.cookbook_helpers.COOKBOOK_STATE_FILE`;
|
||||
- helper/CLI scripts `scripts/odysseus-cookbook`, `scripts/add_hwfit_models.py`, `scripts/hf_download.py`, and `scripts/diffusion_server.py`;
|
||||
- Docker overlays `docker-compose.gpu-*.yml`, `docker/gpu.*.yml`, `docker/host-docker.yml`, `scripts/check-docker-gpu.sh`, and `scripts/check-docker-amd-gpu.sh`;
|
||||
- frontend modules `static/js/cookbook*.js`, including Cookbook running, serve, download, diagnosis, progress, and HW Fit modules;
|
||||
- tests covering Cookbook helpers, routes, CLI state, package detection, frontend progress, HW Fit services, serve profiles, Docker GPU overlays, and GPU diagnostic scripts.
|
||||
|
||||
## Current Call Sites Include
|
||||
|
||||
- Cookbook modal and state modules in `static/js/cookbook*.js`;
|
||||
- package readiness/install and rebuild flows through `routes/shell_routes.py`;
|
||||
- direct shell exec/stream integration used by Cookbook task controls;
|
||||
- model endpoint setup and serve flows;
|
||||
- hardware-fit recommendations for model choices;
|
||||
- image-model recommendations for diffusion serving;
|
||||
- APFEL/local platform dependency paths where supported;
|
||||
- Docker GPU helper scripts and compose overlays;
|
||||
- the `odysseus-cookbook` CLI using the same Cookbook state file.
|
||||
|
||||
## Cookbook Runtime
|
||||
|
||||
`routes.cookbook_routes` owns model download, setup, SSH key, cached model scan, serve, GPU state, kill-pid, state sync, Hugging Face latest lookup, vLLM recipe lookup, serve diagnosis, and task-status endpoints. `src.cookbook_serve_lifecycle` bridges scheduled `cookbook_serve` tasks into serve/stop behavior; task/calendar scheduling ownership stays in `calendar-tasks-notes.md`.
|
||||
|
||||
Access policy is split by surface:
|
||||
|
||||
- download/setup/SSH key/cache scan/serve/GPU/kill/state/task-status are admin/internal-tool surfaces;
|
||||
- `/api/cookbook/hf-latest` is authenticated-user gated;
|
||||
- HW Fit routes are authenticated read/probe routes through normal middleware, not admin-only operations;
|
||||
- bearer API tokens do not satisfy Cookbook admin gates.
|
||||
|
||||
Runtime behavior:
|
||||
|
||||
- POSIX and most remote flows run detached through tmux;
|
||||
- local Windows uses detached process/log/pid behavior under `%TEMP%\\odysseus-tmux`; Python first publishes a valid Win32 fallback PID, then Git Bash may replace it with `/proc/$$/winpid` after a ready-file handoff, so PowerShell `Stop-Tree` can terminate the actual serving shell and children instead of receiving an MSYS PID. Frontend PowerShell venv activation is quoted safely and the local Git Bash runner converts a valid `Scripts\\Activate.ps1` prefix into `source <git-bash-path>/Scripts/activate` so the selected environment actually supplies the serve binary;
|
||||
- remote Windows uses PowerShell runner scripts;
|
||||
- missing `tmux`, `docker`, or serve-engine binaries return shaped errors where possible;
|
||||
- local Docker inside the Odysseus container is available only when the Docker CLI exists, `ODYSSEUS_ENABLE_HOST_DOCKER=true`, and `/var/run/docker.sock` is actually mounted as a socket; otherwise Cookbook should show the host-Docker access hint and prefer remote SSH Docker workflows;
|
||||
- model serve auto-registers LLM or image `ModelEndpoint` rows immediately, then frontend readiness probing can repair/create fallback endpoints;
|
||||
- diffusion-server serves are registered as image endpoints;
|
||||
- MLX image serves use `scripts/mlx_image_server.py`, which pins generation/edit dispatch to the model chosen at process start and ignores OpenAI-compatible per-request model selectors;
|
||||
- vLLM recipe routes fetch and cache model recipe manifests/YAML from `vllm-project/recipes`, normalize base args/env/dependencies/tool-calling/reasoning variants, and expose compatible strategy metadata for serve setup;
|
||||
- Hugging Face download/setup paths can detect and persist encrypted HF tokens for later Cookbook/agent use;
|
||||
- local and remote model paths can contain spaces or non-ASCII characters when helper validation/quoting accepts them;
|
||||
- task status handles tmux, remote Windows logs, local Windows PID/log files, HF cache completion checks, stale browser-state download guards, pip dependency-install success sentinels, exit-code wrappers, serve diagnosis snapshots, and scheduled serve lifecycle hooks;
|
||||
- scheduled serve lifecycle stop attempts only persist `status=stopped`, clear `_scheduledStopAtMs`, and delete auto-registered endpoints for sessions whose tmux/remote stop command succeeded or were already gone; failed stop attempts are logged without marking unrelated expired serves as stopped.
|
||||
|
||||
`routes.cookbook_helpers` owns validation and command construction:
|
||||
|
||||
- repository and model IDs;
|
||||
- local directories, SSH hosts/ports, GPU selectors, and tokens;
|
||||
- shell quoting for Bash and PowerShell;
|
||||
- pip/install fallback chains;
|
||||
- safe environment prefixes;
|
||||
- serve command validation;
|
||||
- user-shell PATH bootstrap, Git-Bash drive-path conversion, preflight, and exit-code helpers.
|
||||
|
||||
Cookbook routes request shell/SSH behavior; they do not relax shell security.
|
||||
|
||||
## Shell Dependencies
|
||||
|
||||
`routes.shell_routes.py` owns Cookbook-adjacent package readiness/install, shell execution/streaming, and llama.cpp rebuild endpoints. The Cookbook UI calls these routes for dependency diagnosis, install/update actions, engine rebuilds, and tmux/reconnect/stop/kill flows. Windows uses detached log/PID wrappers where POSIX tmux is unavailable.
|
||||
|
||||
These are admin-only code-execution surfaces and should be reviewed with Cookbook changes even though they are implemented outside `routes.cookbook_routes.py`.
|
||||
|
||||
## State, Secrets, And Provenance
|
||||
|
||||
Cookbook state lives under the shared data dir through the `COOKBOOK_STATE_FILE` constant, normally `data/cookbook_state.json`. Routes and the `odysseus-cookbook` CLI use the same state path.
|
||||
|
||||
State behavior:
|
||||
|
||||
- browser-facing state masks secrets;
|
||||
- server-side `env.hfToken` is encrypted before storage;
|
||||
- task payloads strip raw HF tokens;
|
||||
- browser local storage strips HF token values;
|
||||
- state POST has anti-wipe guards for server lists;
|
||||
- state POST rejects stale `done` download state when the latest shard/cache markers still show an incomplete download;
|
||||
- recent server-side tasks are preserved against stale browser overwrites;
|
||||
- task-status validates saved shell-bound fields before SSH/tmux commands.
|
||||
|
||||
Cookbook auto-registered endpoints are currently shared/null-owner rows with no API key when created by backend serve registration. Browser fallback registration goes through the normal model-endpoint route. The desired ownership policy for Cookbook-created endpoints should remain explicit.
|
||||
|
||||
HW Fit is an MIT-licensed llmfit adaptation; attribution lives in project acknowledgments/licenses.
|
||||
|
||||
## Hardware Fit
|
||||
|
||||
`services/hwfit/hardware.py` owns hardware detection across NVIDIA, AMD, Apple Silicon, Windows, CPU, RAM, available RAM, remote SSH, container/native probe context, and cached host detections.
|
||||
|
||||
`services/hwfit/models.py`, `fit.py`, `profiles.py`, `image_models.py`, and
|
||||
`hf_discovery.py` own model catalog loading, normalization, API-backed dynamic
|
||||
catalog refresh, memory estimates, quantization labels, fit scoring, serve
|
||||
profile computation, image model ranking, and backend/format servability
|
||||
filtering.
|
||||
|
||||
`routes/hwfit_routes.py` owns the HTTP surface and manual hardware override application.
|
||||
|
||||
Runtime behavior:
|
||||
|
||||
- hardware detection uses a cache with `fresh=true` bypass;
|
||||
- probe results include scope/container visibility metadata, and containerized no-GPU/low-RAM states can return user-facing visibility warnings with rescan/manual/copy-diagnostics actions;
|
||||
- manual hardware replacement is a what-if simulator, not additive hardware;
|
||||
- manual hardware accepts `cuda`, `rocm`, `metal`, `cpu_x86`, and `cpu_arm`
|
||||
backends and must stay in lock-step with backend support in `fit.py`. Metal
|
||||
simulation marks unified memory and filters toward locally servable GGUF/MLX
|
||||
choices instead of CUDA/vLLM-only formats.
|
||||
- ignore switches can drop detected GPU/RAM before ranking;
|
||||
- homogeneous GPU grouping targets realistic multi-GPU pools;
|
||||
- image model ranking normalizes to a single-GPU fit view;
|
||||
- Metal/RDNA/backend restrictions can filter otherwise fit models.
|
||||
- Apple Silicon bandwidth estimates use chip/core-specific tables for M-series Max/Pro/Ultra variants and avoid matching non-Apple GPU names.
|
||||
- Windows and Apple/consumer-AMD paths filter toward GGUF/llama.cpp-compatible
|
||||
choices. On multi-GPU systems, fixed GGUF target quantization that cannot be
|
||||
served by the selected backend returns `no_fit` rather than `None`.
|
||||
|
||||
## Platform And Degraded Behavior
|
||||
|
||||
- Linux, Windows/PowerShell, macOS, Docker, NVIDIA, AMD, Apple Silicon, and CPU-only systems have different command paths.
|
||||
- Remote hosts are accessed through SSH helpers; Cookbook host/port/path inputs must be validated before command construction.
|
||||
- HW Fit remote host/port query values currently do not share all Cookbook route-level validation before SSH probing.
|
||||
- Missing local tools or failed installs should surface command/output/error detail where possible.
|
||||
- GPU overlays remain optional and do not break CPU-only deployments.
|
||||
- Docker GPU overlays pass host devices/env; they do not install CUDA/ROCm engines by themselves.
|
||||
- Default Docker Compose intentionally does not mount the host Docker socket. `docker/host-docker.yml` is an explicit high-trust overlay for operators who accept broad host-Docker control from inside the container.
|
||||
- NVIDIA Docker diagnostics are read-only by default, and `.env` edits/install actions require explicit flags.
|
||||
- AMD Docker diagnostics are read-only and do not mutate `.env`.
|
||||
- vLLM is rejected on unsupported Windows/macOS paths.
|
||||
- llama.cpp CPU-only and GPU fallback scripts should preserve usable CPU paths.
|
||||
- SSH probe failures, GPU driver errors, and no-GPU states should be distinguishable.
|
||||
- Remote SSH host/port validation is shared through route validators for Cookbook/HWFit paths.
|
||||
- Windows launcher/runtime Git Bash discovery includes per-user installs under `%LocalAppData%\\Programs\\Git`, and WSL/Git Bash detection shapes PATH handling for NVIDIA/remote flows.
|
||||
- macOS startup helpers start ChromaDB alongside the app path.
|
||||
- Ollama serve can auto-pick an available port, and scheduled task stop paths
|
||||
verify stop success before persisting a stopped state.
|
||||
|
||||
## Model Catalog And Latest Lookup
|
||||
|
||||
HW Fit model scoring depends on bundled `services/hwfit/data/hf_models.json`,
|
||||
bundled `services/hwfit/data/mlx_community_models.json`, runtime dynamic caches
|
||||
under `DATA_DIR/hwfit/`, catalog normalization, and assumptions about model
|
||||
formats and quantization. `scripts/add_hwfit_models.py` updates the static HF
|
||||
catalog.
|
||||
|
||||
Hugging Face latest lookup and HW Fit dynamic refresh use external Hub metadata
|
||||
and can degrade to empty, unknown-size, partial, or malformed-result behavior.
|
||||
`refresh_catalog=1` refreshes API-backed collection caches for MLX community
|
||||
and selected HF organization collections, with a 24-hour freshness guard and
|
||||
bundled JSON fallbacks when the network/cache is unavailable. HW Fit tolerates
|
||||
non-numeric `gpu_count` values from callers. Model normalization also treats
|
||||
non-string `parameter_count` and quantization fields as unknown rather than
|
||||
calling string methods and aborting the ranking pass. Catalog drift and dynamic
|
||||
latest-model metadata are separate sources of recommendation drift.
|
||||
|
||||
## Security Policy
|
||||
|
||||
Admin gates must stay in place for install, serve, kill, setup, state mutation, and shell-like actions. `/api/shell/exec` is an admin primitive used by Cookbook task control and must stay in this review boundary. Scheduled `cookbook_serve` tasks are admin-only action tasks; task create/update/manual run/webhook/scheduler execution must all reject or pause them for non-admin owners.
|
||||
|
||||
Kill-pid guardrails:
|
||||
|
||||
- admin-only;
|
||||
- PID floor;
|
||||
- signal allowlist;
|
||||
- validated remote host/port;
|
||||
- frontend confirmation for TERM/KILL cleanup.
|
||||
|
||||
Shell-bound Cookbook inputs must pass helper validation before command construction. HF tokens, Cookbook state secrets, and endpoint API keys must remain encrypted or masked and must not be written back to clients in raw form. Host Docker socket access must stay opt-in and clearly distinguished from merely having a Docker CLI in the container.
|
||||
|
||||
## Testing Coverage
|
||||
|
||||
Existing coverage is strongest for helper validation/quoting, SSH host validation, pip fallback and dependency-completion regressions, cached scan scripts, serve profile computation, scheduled serve lifecycle state persistence, hardware detection/ranking across AMD/NVIDIA/macOS/manual/container modes, MLX/Metal ranking and request-model pinning, manual backend simulation, Docker GPU compose overlays, Cookbook CLI state, package detection, Windows venv/path/task helpers, non-numeric GPU counts, non-string model catalog fields, and selected frontend progress regressions.
|
||||
|
||||
Route-level auth/security and degraded-return coverage is thinner for Cookbook admin routes, shell dependency routes, `/api/cookbook/hf-latest`, state/status edge cases, HW Fit routes, frontend JS behavior, and helper scripts such as `hf_download.py`, `add_hwfit_models.py`, and `diffusion_server.py`.
|
||||
|
||||
## Current Gaps
|
||||
|
||||
- Cookbook-created model endpoint ownership/shared/null-owner policy needs a deliberate decision.
|
||||
- `/api/shell/exec` and Cookbook package/rebuild routes need to remain cross-referenced with shell/admin specs because they are Cookbook-critical code-execution surfaces.
|
||||
- Cookbook route auth/security and degraded-return behavior need route-level tests.
|
||||
- `/api/cookbook/hf-latest` needs tests locking its user-authenticated access policy and failure behavior.
|
||||
- HW Fit routes need route-level tests around missing catalogs, manual overrides, `fit_only`, profiles, and image-model cases.
|
||||
- Dependency install/serve diagnosis remains split across Cookbook routes, shell routes, frontend diagnosis, optional binaries, and platform-specific scripts, even though longer serve-output tails are centralized through `routes/cookbook_output.py`.
|
||||
- Model catalog, quantization, backend, and Hugging Face metadata drift need ongoing maintenance.
|
||||
@@ -1,205 +0,0 @@
|
||||
# Documents, RAG, And Uploads
|
||||
|
||||
Last updated: dev@e71f8ce | 2026-08-25
|
||||
|
||||
## Scope
|
||||
|
||||
This spec covers file/document context, document storage, and vector retrieval in:
|
||||
|
||||
- `app.py` and `src/app_initializer.py` route/manager wiring;
|
||||
- `routes/upload_routes.py`, `routes/personal_routes.py`, `routes/embedding_routes.py`, canonical `routes/document/document_routes.py` and `routes/document/document_helpers.py`, plus their top-level compatibility shims;
|
||||
- chat attachment paths in `routes/chat_routes.py`, `routes/chat_helpers.py`, `src/chat_handler.py`, and `src/chat_processor.py`;
|
||||
- `core/session_manager.py`, `src/attachment_refs.py`, `src/upload_handler.py`,
|
||||
`src/upload_limits.py`, and the public reference contract in
|
||||
`docs/attachments.md`;
|
||||
- `src/document_processor.py`, `src/document_actions.py`, `src/personal_docs.py`, and `src/markitdown_runtime.py`;
|
||||
- `src/rag_singleton.py`, `src/rag_vector.py`, `src/rag_manager.py`, `src/chroma_client.py`, `src/embeddings.py`, and `src/embedding_lanes.py`;
|
||||
- PDF/form helpers in `src/pdf_runtime.py`, `src/pdf_forms.py`, and `src/pdf_form_doc.py`;
|
||||
- `services/docs/service.py`;
|
||||
- document, upload, RAG, chat, email, and admin frontend callers in `static/app.js`, `static/js/chat.js`, `static/js/chatRenderer.js`, `static/js/fileHandler.js`, `static/js/document.js`, `static/js/documentLibrary.js`, `static/js/rag.js`, `static/js/admin.js`, `static/js/emailInbox.js`, and `static/js/slashCommands.js`;
|
||||
- tests covering upload, document, attachment, PDF, RAG, Chroma, MarkItDown, and embedding behavior.
|
||||
|
||||
## Runtime Integration
|
||||
|
||||
`app.py` registers upload, personal-doc/RAG, embedding, document, diagnostics, and Codex document routes. `src.app_initializer.initialize_managers()` creates `UploadHandler` and `PersonalDocsManager`, installs the upload handler on `SessionManager` and the shared tool helper, and startup attempts to initialize the RAG singleton. App route wiring passes that same handler to session/history, document, note, and calendar writers that can persist upload references.
|
||||
|
||||
`src.rag_singleton.get_rag_manager()` returns the live `VectorRAG` instance when Chroma/embedding dependencies are reachable. Personal routes can retry the singleton and return explicit 503s when unavailable. Chat RAG uses the `PersonalDocsManager.rag_manager` captured during app initialization and can silently skip RAG if that manager is absent.
|
||||
|
||||
## Uploads And Attachments
|
||||
|
||||
`src.upload_handler.UploadHandler` owns upload IDs, safe filenames, upload metadata, owner rename rewrites, atomic `uploads.json` writes, content-type detection, and file storage under `data/uploads`. Upload IDs accept extensionless values or one sanitized alphanumeric extension.
|
||||
|
||||
Upload-index reads track the live and `.bak` files by device, inode, size, nanosecond mtime, and ctime, then verify the combined signature after parsing. This catches same-timestamp corruption/replacement and prevents stale parsed data from being cached under a newer file identity. Non-destructive reads can recover from the backup; destructive cleanup requires a valid live index and never treats an older backup as deletion authority. Lifecycle writes can synchronize the backup so intentionally removed metadata is not resurrected.
|
||||
|
||||
`src.upload_limits` owns central upload-size caps and environment overrides for chat attachments, gallery, transforms, memory import, personal uploads, email compose, STT audio, and ICS imports. Invalid configured limits fail fast at import so routes do not silently accept unsafe sizes. Docker installs `libmagic1` plus `python-magic` so `UploadHandler.detect_content_type()` can sniff bytes in the official image; native installs can fall back to extension/MIME guesses when `python-magic` is unavailable.
|
||||
|
||||
`routes/upload_routes.py` owns:
|
||||
|
||||
- `POST /api/upload`, returning uploaded file metadata;
|
||||
- reference-aware admin upload cleanup and stats;
|
||||
- `GET /api/upload/{file_id}`;
|
||||
- `GET/PUT /api/upload/{file_id}/vision` for editable OCR/vision cache;
|
||||
- thumbnail and masked owner/admin access behavior.
|
||||
|
||||
It does not currently expose a general upload list/delete route. Download/preview responses that serve uploaded content should include `X-Content-Type-Options: nosniff` where route code owns the response so browser MIME sniffing does not widen accepted upload types.
|
||||
|
||||
Readable/code-like upload handling includes common text/code extensions plus `.nix`; document processing renders recognized code-like text into fenced blocks with language metadata.
|
||||
|
||||
Chat does not own attachment extraction. Runtime flow:
|
||||
|
||||
- the frontend uploads files and submits attachment IDs;
|
||||
- `ChatHandler.preprocess_message()` resolves IDs with the session owner through `UploadHandler.resolve_upload()`, which enforces owner/admin access and no longer treats missing owner context as permission to read owned uploads;
|
||||
- vision/OCR cache and attachment metadata are prepared before model calls;
|
||||
- text-only models receive stripped multimodal blocks;
|
||||
- `src.document_processor.build_user_content()` produces model-ready text, PDF text, Office/EPUB text when MarkItDown or the DOCX fallback is available, image/multimodal blocks, truncation, and PDF/Office auto-document updates;
|
||||
- chat streams attachment, PDF-created `doc_update`, and `rag_sources` events where applicable.
|
||||
|
||||
Extensionless image and audio attachments derive their data-URI subtype from
|
||||
the detected MIME type, so `image/png` and `audio/mpeg` uploads do not become
|
||||
invalid `data:image/;base64` or `data:audio/;base64` blocks when the filename
|
||||
has no extension.
|
||||
|
||||
## Durable References And Cleanup
|
||||
|
||||
`src.attachment_refs` owns the stable `attachment_ref` shape used outside raw
|
||||
upload storage: attachment id, name, MIME type, size, and optional checksum,
|
||||
creation time, dimensions, vision text/model, and gallery id. Live provider
|
||||
calls may still receive multimodal data URLs for the current turn, but durable
|
||||
chat content is normalized to readable text plus compact reference lines.
|
||||
Structured references remain in message attachment metadata, and chat FTS
|
||||
triggers omit inline media while startup migration scrubs legacy indexed data
|
||||
URLs.
|
||||
|
||||
Agent/tool manifests expose `odysseus://attachment/<id>` with
|
||||
`read_policy: "owner_checked_upload"`. A compatibility filesystem path is
|
||||
included only after owner-aware upload resolution, upload-root confinement, and
|
||||
tool-readable-root checks; the stable contract for external tools is the URI
|
||||
and attachment id, not host layout.
|
||||
|
||||
Writers reserve referenced uploads before committing durable state. This
|
||||
includes session message append/replace and history rewrites, document
|
||||
create/update and native document edits, note route/tool create/update,
|
||||
calendar/event route/tool create/update, and attachment-bearing session
|
||||
updates. A missing or wrong-owner reference aborts before destructive
|
||||
replacement and surfaces a route conflict or tool error. Reservations serialize
|
||||
with cleanup through the upload-index lock and refresh access time.
|
||||
|
||||
Admin cleanup first scans chat content and attachment metadata, current and
|
||||
versioned documents including PDF markers, gallery filenames/hashes, note
|
||||
image/color/content/checklist fields, and calendar color/description/location
|
||||
fields. Reference discovery or index-integrity failure aborts cleanup; the
|
||||
lower-level API removes nothing without both completed id and hash snapshots.
|
||||
Only expired, unreferenced files with coherent id/path/owner/checksum/timestamp
|
||||
metadata are candidates. Matching index rows are persisted away before byte
|
||||
deletion and restored if deletion fails. This lock is process-local, so the
|
||||
documented race protection assumes the current single-worker deployment.
|
||||
|
||||
## Living Documents And PDF
|
||||
|
||||
`routes/document/document_routes.py` owns the HTTP document API: create/read/update/archive/delete, library listing, import/export, version history, tidy/AI tidy, PDF rendering/export, PDF form helpers, and email-attachment reply preparation. The top-level document route/helper modules remain compatibility aliases.
|
||||
|
||||
`static/js/documentLibrary.js` owns local library state after archive/delete actions, including total counts and language chips. Server route truth still owns durable document state.
|
||||
|
||||
`static/js/document.js` owns the browser document editor and markdown preview. Preview rendering applies code highlighting when highlight.js is present, renders Mermaid diagrams when the Mermaid runtime is available, refreshes after AI edits, and discards pending AI diffs before switching the active document.
|
||||
|
||||
Document mutations also happen through agent tools, Codex document routes, email attachment import, and scripts. HTTP and native-agent document writers owner-reserve any internal upload/PDF references before persisting new current content or versions. Native document tool outputs include metadata that the browser can use to open/update the editor if a later stream update is missed. Those callers must preserve document owner, attachment, and version semantics.
|
||||
|
||||
After external/workspace-untrusted context, a proposed document mutation is sealed into an exact approval with document id, current version, content digest, tool content, owner/session, and workspace. Approval continuation re-reads and verifies those fields before consuming the one-use authorization, so an intervening edit cannot apply a stale approved patch to new content.
|
||||
|
||||
Email draft documents are a first-class document language. Create/update paths
|
||||
detect the `To`/`Subject`/header shape, coerce language to `email`, and preserve
|
||||
protected reply/forward headers such as `In-Reply-To`, `References`,
|
||||
`X-Source-UID`, `X-Source-Folder`, attachment headers, and quoted/original
|
||||
history when model or UI edits replace the draft body. Creating a draft for the
|
||||
same source UID/folder in the same session updates the active draft instead of
|
||||
creating a duplicate.
|
||||
|
||||
`Document` rows own current content and owner. `DocumentVersion` rows own immutable snapshots. Document access should be owner-filtered, not session-id-only; the session document listing path still needs regression coverage for per-document owner filtering after the session owner check.
|
||||
|
||||
PDF runtime behavior:
|
||||
|
||||
- direct PDF import stores the upload through `UploadHandler`;
|
||||
- PDF library entries preserve metadata/preview behavior for source PDFs;
|
||||
- pypdf text extraction remains core;
|
||||
- PyMuPDF enables form detection, page rendering, page PNGs, annotation fill, render/export PDF, and form filling;
|
||||
- PDF render routes should return a shaped 503 when PyMuPDF is absent and use same-origin framing/download behavior for rendered pages;
|
||||
- imported PDFs become either plain `pdf_source` markdown or `pdf_form_source` markdown with sidecar field data;
|
||||
- PDF markers must resolve back through an upload owned by the caller;
|
||||
- signed-reply preparation uses document `source_email_*` provenance and verifies the document owner and signature owner. Source email account resolution still needs explicit owner-scoped coverage.
|
||||
|
||||
Office/EPUB attachment extraction is optional and MarkItDown-backed for `.docx`, `.pptx`, `.xlsx`, `.xls`, and `.epub`; a pure-Python DOCX fallback can extract `word/document.xml`. When a session id is present, full extraction can be saved as a markdown `Document` while the chat-inline copy remains capped.
|
||||
|
||||
## Personal Docs And RAG
|
||||
|
||||
`src.personal_docs.PersonalDocsManager` owns personal-directory indexing and keyword retrieval.
|
||||
|
||||
`src.rag_vector.VectorRAG` owns Chroma/embedding-backed indexing and owner-filtered retrieval. Chunk ids are owner-scoped so byte-identical chunks from different owners do not suppress each other. `src.rag_singleton` owns lazy initialization, retry throttling, and reset behavior.
|
||||
|
||||
`routes/personal_routes.py` owns personal-doc and direct RAG-upload routes. Directory list/index/delete routes are admin-gated, and directory indexing runs in a worker thread so traversal/extraction does not block the async event loop. Direct RAG upload is user-authenticated, requires document privilege, forwards owner into the manager wrapper, writes unique files under per-owner subdirectories of `data/personal_uploads`, and has looser file-type validation than normal uploads.
|
||||
|
||||
Current call sites include:
|
||||
|
||||
- admin RAG pages and slash commands;
|
||||
- chat RAG preface building;
|
||||
- AI interaction and MCP RAG management tools;
|
||||
- CLI scripts for document/personal indexing.
|
||||
|
||||
Some non-route tool/script paths can index ownerless or arbitrary directories and should be treated as compatibility-sensitive management surfaces.
|
||||
|
||||
## Embedding Models
|
||||
|
||||
`routes/embedding_routes.py` owns admin-gated embedding model and custom endpoint management. It validates custom endpoints with outbound URL checks, can persist and process-expose `EMBEDDING_API_KEY`, resets embedding/RAG/tool-index/Chroma state, and does not own document extraction.
|
||||
|
||||
`src.embeddings` owns HTTP embedding fallback to FastEmbed and process-level endpoint state. `src.embedding_lanes` keeps custom HTTP embedding vectors separate from FastEmbed fallback vectors with lane-specific Chroma collections, migrates legacy unsuffixed collections into empty lanes, and dedupes query results across lanes. `src.chroma_client` owns native Chroma defaults and fast reachability checks.
|
||||
|
||||
## Compatibility State
|
||||
|
||||
`src.rag_manager.RAGManager` is a backward-compat wrapper. The live owner-aware vector path is `VectorRAG`.
|
||||
|
||||
`services/docs/service.py` is a separate facade. It accepts live `VectorRAG` query rows (`document`, `similarity`, nested metadata source), retains legacy `text`/`content` and `score` fallbacks, skips non-object rows, and maps live `indexed_count`/`failed_count` plus legacy `indexed`/`failed` index summaries into its dataclasses.
|
||||
|
||||
`src.database` re-exports `core.database`; document models and migrations live in `core.database`.
|
||||
|
||||
## Optional And Degraded Behavior
|
||||
|
||||
- ChromaDB/FastEmbed are default installed dependencies, but Chroma can be offline or unreachable.
|
||||
- Native Chroma defaults to `localhost:8100`; Docker uses the `chromadb:8000` compose service and persistent Chroma storage.
|
||||
- HTTP embeddings can fall back to FastEmbed; when both lanes exist, lane separation avoids Chroma dimension conflicts.
|
||||
- MarkItDown is optional for Office/EPUB extraction; chat attachments and personal directory indexing have clear degraded behavior, while direct RAG upload does not share the same extraction path.
|
||||
- PyMuPDF is optional, unlocks PDF form/render/fill paths, and carries AGPL implications when installed.
|
||||
- PyMuPDF-dependent document routes should use the shared runtime helper/error text so missing-dependency and license policy stay visible.
|
||||
- pypdf text extraction is core and should remain available without PyMuPDF.
|
||||
|
||||
## Security And Provenance
|
||||
|
||||
Uploaded files, documents, RAG chunks, extracted attachment text, OCR/vision text, PDF marker content, and source-email metadata are untrusted external or user-provided context when sent to an LLM.
|
||||
|
||||
Concrete enforcement points include:
|
||||
|
||||
- `UploadHandler.resolve_upload()` for upload ID validation, owner/admin access, and upload-dir confinement;
|
||||
- owner-checked write reservations before durable attachment references are
|
||||
stored, sharing the upload-index lock with reference-aware cleanup;
|
||||
- PDF marker ownership checks before resolving source uploads;
|
||||
- personal-directory and personal-upload confinement helpers, including symlink/realpath checks before deleting uploaded files or removing indexed directories;
|
||||
- owner-filtered `VectorRAG.search(owner=...)`;
|
||||
- shared untrusted-context wrappers for RAG preface insertion.
|
||||
|
||||
Extracted attachment text is currently appended into the user message rather than wrapped as a separate untrusted-context message. That is current behavior and a prompt-injection hardening gap.
|
||||
|
||||
Bearer-token callers are not a scoped document/upload API surface today. Routes that treat token-authenticated users as owners need explicit scope/effective-user policy before they are considered safe token APIs.
|
||||
|
||||
## Testing Coverage
|
||||
|
||||
Existing useful coverage includes upload owner scope, upload IDs, upload atomicity, durable attachment reference normalization, message/document/note/calendar write reservations, fail-closed reference-aware cleanup, attachment budgets, `.nix` text upload handling, upload/PDF security regressions, Docker `libmagic`/`python-magic` upload detection, RAG owner fallback, Chroma fast-fail, MarkItDown runtime, PDF runtime, document-library counter updates, and selected document helper behavior.
|
||||
|
||||
Route-level coverage is thinner for document CRUD, PDF import/render/export/fill, direct RAG upload, embedding admin/security behavior, and RAG unavailable states.
|
||||
|
||||
## Current Gaps
|
||||
|
||||
- Direct RAG upload still needs clearer file-type validation and MarkItDown/PDF extraction parity decisions.
|
||||
- Document `session_id` relinking and session document listing need owner-scope regressions.
|
||||
- Chat RAG can remain degraded after startup even if personal routes later initialize the RAG singleton.
|
||||
- PyMuPDF-dependent routes do not all share the same optional-runtime helper/error behavior.
|
||||
- Signed-reply preparation needs owner-scoped source email account/signature regression coverage.
|
||||
- Document/upload routes need explicit bearer-token scope/effective-user policy.
|
||||
- User-facing document/PDF/RAG route matrices need more regression coverage for owner denial, admin gates, unavailable services, and degraded optional dependencies.
|
||||
@@ -1,209 +0,0 @@
|
||||
# Email And Contacts
|
||||
|
||||
Last updated: dev@2e2bb52 | 2026-08-16
|
||||
|
||||
## Scope
|
||||
|
||||
This spec covers mail and contacts in:
|
||||
|
||||
- app wiring in `app.py`;
|
||||
- `core.database.EmailAccount`;
|
||||
- `routes/email_routes.py`, `routes/email_helpers.py`, and `routes/email_pollers.py`;
|
||||
- email threading in `src/email_thread_parser.py`;
|
||||
- email MCP tools in `mcp_servers/email_server.py`;
|
||||
- canonical contact/CardDAV routes in `routes/contacts/contacts_routes.py`,
|
||||
with `routes/contacts_routes.py` as a compatibility shim;
|
||||
- Codex email bridge in `routes/codex_routes.py`;
|
||||
- document signed-reply flows in canonical `routes/document/document_routes.py` and document `source_email_*` fields;
|
||||
- reminder/task email senders in `routes/note_routes.py` and `src/task_scheduler.py`;
|
||||
- email/contact agent surfaces in `src/tool_implementations.py`, `src/tool_schemas.py`, `src/tool_index.py`, and `src/agent_loop.py`;
|
||||
- CLI wrappers `scripts/odysseus-mail` and `scripts/odysseus-contacts`;
|
||||
- frontend modules `static/js/emailInbox.js`, `static/js/emailLibrary.js`, `static/js/emailLibrary/*`, `static/js/emailShared.js`, `static/js/chatStream.js`, `static/js/document.js`, and `static/js/settings.js`;
|
||||
- tests under `tests/test_email_*`, `tests/test_contacts_*`, `tests/test_mail_cli_*`, `tests/test_mcp_email_*`, `tests/test_schedule_email_*`, email/contact JS tests, and email security regressions.
|
||||
|
||||
## Current Call Sites Include
|
||||
|
||||
- browser email inbox/library, compose, schedule, account, and attachment actions;
|
||||
- document-editor compose, recipient autocomplete, compose uploads, and signed-reply handoff;
|
||||
- Codex email read/draft/send routes using API-token scopes;
|
||||
- note reminder and task-output email delivery;
|
||||
- built-in email summary/reply/calendar/urgency actions;
|
||||
- scheduled email pollers and CLI one-shot pollers;
|
||||
- MCP email tools;
|
||||
- contact manager settings, compose contact autocomplete, agent contact tools, and contacts CLI.
|
||||
|
||||
## Email Accounts And Transport
|
||||
|
||||
`EmailAccount` rows own IMAP/SMTP configuration. Password fields are string columns containing encrypted ciphertext written with `src.secret_storage`; startup migrations handle legacy plaintext rows. Google OAuth account rows also carry `oauth_provider`, encrypted access/refresh tokens, token expiry, and an optional outbound `display_name`. Do not return decrypted credentials or OAuth tokens, or write them to logs.
|
||||
|
||||
Exactly one default account per owner is enforced as a serialized database transition. Startup normalizes legacy duplicate defaults and installs a unique per-owner default constraint/index; first create, delete/promotion, set-default, demo teardown, and owner rename lock the relevant owner rows and commit atomically. Multi-owner rename acquires locks in canonical order so stale concurrent writers fail closed.
|
||||
|
||||
`routes.email_helpers` owns:
|
||||
|
||||
- account owner assertions and config fallback order;
|
||||
- IMAP/SMTP connection helpers and related transport utilities;
|
||||
- Google OAuth2 state signing/verification, token refresh, and XOAUTH2 framing;
|
||||
- SMTP security modes (`ssl`, `starttls`, `none`);
|
||||
- envelope recipients and Odysseus headers;
|
||||
- attachment extraction helpers;
|
||||
- email pre-retrieval context for AI reply drafting;
|
||||
- scheduled email, summary, reply, tag, calendar extraction, urgency, and signature-boundary side databases.
|
||||
|
||||
Email config can fall back to legacy `data/settings.json` or environment variables when no scoped account is configured. Account discovery now owner-scopes the default/first-enabled fallback and can still match legacy account rows by IMAP username or from-address. That fallback remains compatibility-sensitive in multi-user contexts.
|
||||
|
||||
Email owner semantics are route-local and compatibility-sensitive:
|
||||
|
||||
- `routes.email_helpers._require_auth()` returns `""` in `AUTH_ENABLED=false` mode, rejects configured auth with no user, and only tolerates first-run anonymous loopback fallback.
|
||||
- Empty owner is treated as single-user compatibility: account-ownership assertions no-op, default/first-enabled account fallback can be global, and email cache clauses include `owner = '' OR owner IS NULL`.
|
||||
- Non-empty owners scope account/config/cache queries. Legacy ownerless account
|
||||
rows are visible to an authenticated owner only when the row's IMAP username
|
||||
or from-address matches that owner, so old unowned rows do not become global
|
||||
cross-user accounts in configured multi-user deployments.
|
||||
|
||||
`routes.email_routes` owns the HTTP mail surface:
|
||||
|
||||
- account CRUD, test, default, and masked config reads;
|
||||
- Google OAuth authorize/callback for Workspace and .edu Gmail-style accounts;
|
||||
- list, search, read, folders, and contacts;
|
||||
- folder role resolution and UID fetch/search helpers used by the route surface;
|
||||
- owner-scoped route caches and IMAP pool behavior;
|
||||
- attachments, bulk attachment ZIP downloads, and attachment-to-document flows;
|
||||
- compose upload, draft/send, `wait_for_delivery`, Sent append, and source `\Answered` marking;
|
||||
- schedule/list/delete scheduled emails;
|
||||
- pending agent-draft approval/cancel flows;
|
||||
- mark read/unread/answered, spam flags, move, archive, and delete. IMAP move/delete/archive operations use UID commands for message identity and fail safe when the requested UID no longer exists; they never reinterpret a missing UID as a sequence number, which could mutate or expunge an unrelated message.
|
||||
|
||||
Google OAuth behavior is account-owned:
|
||||
|
||||
- `/api/email/oauth/google/authorize` requires an authenticated owner, checks account ownership, HMAC-signs state with account id, owner, and nonce, and redirects to Google with mail/userinfo scopes;
|
||||
- `/api/email/oauth/google/callback` verifies signed state before token exchange, re-checks the target account owner before writing tokens, stores access/refresh tokens encrypted, stores token expiry as a timestamp, and redirects with generic success/error codes rather than raw provider errors;
|
||||
- token refresh uses `GOOGLE_OAUTH_CLIENT_ID` and `GOOGLE_OAUTH_CLIENT_SECRET`, stores refreshed access tokens encrypted, and logs only generic/account-id context on failures;
|
||||
- SMTP and IMAP use XOAUTH2 when `oauth_provider == "google"`; OAuth accounts are send-capable without an SMTP password when host and user are configured;
|
||||
- outbound mail formats the `From` header with `display_name` when present.
|
||||
- authorize/callback redirect URIs derive their scheme and host from the mounted request unless `GOOGLE_OAUTH_REDIRECT_URI` explicitly pins a value; the browser preserves the selected SMTP security mode during connect and reopens Settings after the callback.
|
||||
|
||||
MCP full-message read/reply/attachment fetches use IMAP `BODY.PEEK[]` rather than bare `RFC822`, so iCloud-style servers return the full body without marking messages seen. Poller UID handling must tolerate both bytes and string UIDs. Built-in signature-learning and daily-brief actions also use UID SEARCH/FETCH rather than sequence-number commands.
|
||||
|
||||
IMAP helpers quote mailbox names, raise the Python IMAP line cap for large messages, close sockets after connect/login failures, and preserve Gmail FETCH attributes that follow header literals so unread flag state is not lost. Browser list routes offload blocking IMAP work from async handlers; browser search runs in FastAPI's threadpool, rejects CRLF query input, tokenizes quoted phrases/terms, searches FROM/TO/CC/SUBJECT/TEXT, can search Gmail All Mail when an INBOX query should include archived or labelled messages, and supports `scope=folder` when callers intentionally want the selected folder only. The local index fallback can return indexed results when IMAP returns empty or fails.
|
||||
|
||||
## Runtime And Pollers
|
||||
|
||||
Scheduled email rows live in `data/scheduled_emails.db` and are owner-scoped. Scheduled send times are normalized before storage.
|
||||
|
||||
`routes.email_pollers` owns the scheduled-send poller and single-shot/task/CLI automation passes. Before SMTP work, each poller atomically claims a due row with a conditional `pending` to `sending` update; concurrent in-process/CLI pollers that lose the claim skip the row instead of sending a duplicate. Only the scheduled-send poller starts in-process by default when `ODYSSEUS_INPROCESS_POLLERS` allows it; Docker forwards that gate. Background email automation can also consult the foreground activity gate so auto actions do not compete with active browser/model work. Native cron/systemd can drive one-shot pollers through `scripts/odysseus-mail`.
|
||||
|
||||
Manual and scheduled summaries use the shared LLM adapter and owner-scoped cache instead of constructing provider calls locally. Scheduled summaries use background fallback policy and yield to foreground work; provider exception text is shaped before it can reach the browser.
|
||||
|
||||
Urgency delivery publishes through a serialized atomic checkpoint transaction. Generation and membership fences prevent stale scans from overwriting newer state; authoritative scans retire deleted/disabled accounts, partial failures preserve the prior checkpoint, concurrent account-scoped actions merge disjoint facts, and cancellation rolls back without publishing.
|
||||
|
||||
Transport degraded behavior:
|
||||
|
||||
- IMAP timeouts are clamped by configuration;
|
||||
- providers can use implicit SSL, STARTTLS, or plain connections;
|
||||
- poisoned IMAP sockets are reconnected around known provider failures;
|
||||
- SMTP-capable account fallback is used where supported;
|
||||
- route helpers, MCP, and CLI do not all share identical SMTP/IMAP parsing and security behavior today.
|
||||
|
||||
## Caching And Staleness
|
||||
|
||||
Email list/read behavior uses short route caches, longer read caches, capped warm prefetch, and owner/account-aware pool/cache keys. The frontend email library has its own session SWR cache, cache-buster refreshes, scheduled/search cache exclusions, and stale-row behavior when refresh fails.
|
||||
|
||||
Opening an unread message is one authoritative backend IMAP operation. The read route fetches/parses the message and applies `\Seen` over the same connection; cached bodies still await one UID STORE, read-only mailboxes serve content without claiming a mark, and STORE failure returns the body with explicit failure state rather than caching a false read. Inbox/library clients deduplicate opens, carry immutable mailbox context, and ignore late responses after account, folder, or message changes.
|
||||
|
||||
Library prewarm runs only while genuinely idle, as one bounded single-flight request for the default or last-used enabled account and initial page. Visible foreground work, panel lifecycle, account changes, or explicit reads cancel or join it so delayed duplicate IMAP work cannot escape the idle gate.
|
||||
|
||||
List/read route caches are owner/account-aware. Helper-side summary, AI-reply, tag, calendar-extraction, urgency-alert, and learned sender-signature tables carry owner columns and owner clauses. Thread-boundary rows are still keyed by message shape rather than a full owner/account/mailbox key, so they remain cross-owner audit points when identical messages appear in multiple mailboxes.
|
||||
|
||||
## Attachments And Signed Replies
|
||||
|
||||
Compose uploads live under `ODYSSEUS_MAIL_ATTACHMENTS_DIR`; missing staged files are skipped with warnings. Attachment-to-document supports PDF, DOCX, TXT, and MD. DOCX depends on `python-docx`; PDF form/open-in-doc flows can depend on optional PyMuPDF.
|
||||
|
||||
Email attachment-as-document flows stamp `Document.source_email_*` provenance. `GET /api/email/attachments-download/{uid}` builds an owner-scoped ZIP of visible non-signature attachments using safe names. `compose-from-odysseus` and `compose-from-odysseus-zip` can stage owner-visible documents and gallery images as compose uploads, preserving legacy session fallback only where the source object remains visible to the owner. `prepare-signed-reply` verifies document ownership, reconstructs reply headers, flattens/stages signed PDFs as compose uploads, and leaves final send/draft review to the compose flow.
|
||||
|
||||
Email bodies and attachments are untrusted model context.
|
||||
|
||||
## Threading And Rendering
|
||||
|
||||
`src.email_thread_parser` owns splitting plaintext/HTML email threads into quoted conversation parts. Frontend email library modules own reply-recipient logic, signature folding, local state, and rendering behavior. Bulk selections are cleared when folder/account loads, search text, search pills, or result scope changes so actions cannot carry stale UIDs into a different visible context. `static/js/emailShared.js` owns shared email UI helpers used across inbox/library surfaces.
|
||||
|
||||
Remote inbound email HTML is sanitized by frontend email-library utilities before `innerHTML` insertion. Server-side email routes sanitize composed/generated outbound HTML with an allowlist before draft/send, dropping scripts/styles and unsafe attributes. Both sides are part of the rendering invariant.
|
||||
|
||||
When the email reader is active, browser chat sends selected-message metadata. `src.tool_implementations` stores that request-local active email reference, `src.agent_loop` injects it as protected untrusted context, and `static/js/chatStream.js` handles `ui_control open_email_reply` so default reply/draft behavior opens the selected message's compose flow instead of a generic new document.
|
||||
|
||||
## MCP Email
|
||||
|
||||
`mcp_servers/email_server.py` exposes email tools for MCP/agent use. It has its own account discovery, IMAP/SMTP, attachment, cache, and send paths, but account visibility now mirrors the HTTP owner policy. The active owner comes from a hidden `_odysseus_owner` argument when the caller provides one, or from `ODYSSEUS_MCP_EMAIL_OWNER` / `ODYSSEUS_EMAIL_OWNER`. If any enabled account is owner-scoped and no current/configured owner exists, email MCP returns an owner-scope error instead of listing global accounts.
|
||||
|
||||
MCP email account filtering includes owner-owned rows and legacy ownerless rows
|
||||
whose mailbox/from-address matches the owner. Confirmation-first `send_email`
|
||||
resolves the selected account before stashing an `agent_draft`, so drafts cannot
|
||||
be staged against another owner's account. MCP-created draft documents use the
|
||||
resolved hidden/configured owner when available, with `ODYSSEUS_DOCUMENT_OWNER`
|
||||
and single-admin fallback only as document-visibility compatibility.
|
||||
|
||||
MCP email send behavior is confirmation-first by default: `send_email` and reply send paths stash a `scheduled_emails` row with `status='agent_draft'` when `agent_email_confirm` is true, and browser routes expose pending drafts for approval or cancellation. Separate MCP draft tools create Odysseus compose documents for user review without sending.
|
||||
|
||||
MCP email remains a separate local/admin trust boundary. Public and non-admin users must not see or execute email MCP tools. It still needs route-helper parity audits for attachment path containment, sanitization, transport behavior, and pending-draft result text, but global all-account behavior is no longer the current owner model.
|
||||
|
||||
## Contacts
|
||||
|
||||
`routes.contacts.contacts_routes` owns global/admin contacts and CardDAV behavior. The top-level `routes.contacts_routes` module is a compatibility shim. The canonical package supports local contacts, CardDAV config, list/search/add/update/delete, VCF/CSV import/export, and clear.
|
||||
|
||||
Contact runtime behavior:
|
||||
|
||||
- contacts routes are admin-gated;
|
||||
- local `data/contacts.json` is used when CardDAV is unconfigured;
|
||||
- import paths tolerate malformed or non-string contact bodies by skipping invalid rows instead of crashing the import;
|
||||
- configured CardDAV uses REPORT with GET fallback and a short in-memory cache;
|
||||
- configured-but-offline CardDAV can return cached reads but writes fail instead of falling back to local JSON;
|
||||
- CardDAV config reads mask the password, settings-stored passwords are encrypted with `src.secret_storage`, omitted password updates preserve the existing secret, and an explicit empty password clears it;
|
||||
- the native contacts CLI is CardDAV-oriented and does not fully match web JSON fallback behavior;
|
||||
- agent contact tools reuse helper functions in-process because the HTTP routes require browser/admin auth.
|
||||
|
||||
Contacts are global admin-only data today. There is no per-user contact sharing model unless a future spec defines one.
|
||||
|
||||
## Security Policy
|
||||
|
||||
Email HTTP access is owner-scoped, including account selection, scheduled email rows, and attachment routes. Null-owner/single-user compatibility paths are security-sensitive and must not allow cross-user mailbox access.
|
||||
|
||||
Codex email routes are the scoped bearer-token email API. They enforce `email:read`, `email:draft`, and `email:send` scopes and use token-owner attribution before borrowing email route handlers.
|
||||
|
||||
Known security policy details:
|
||||
|
||||
- decrypted email credentials stay process-local;
|
||||
- account/config reads mask passwords and expose only OAuth status fields, not access or refresh token values;
|
||||
- SMTP/IMAP security mode behavior is part of the credential contract;
|
||||
- Google OAuth state and callback owner checks are part of the account-boundary contract;
|
||||
- scheduled emails must remain owner-scoped;
|
||||
- email pre-retrieval contacts context is allowed only for admin/single-user situations;
|
||||
- MCP attachment downloads need route-level path-containment parity; current MCP paths are separate from the HTTP compose/attachment helper path.
|
||||
|
||||
CardDAV credentials and URLs are security-sensitive. CardDAV URL setup and derived href writes/deletes pass through outbound URL validation; absolute hrefs from a CardDAV server are constrained back to the configured origin before credentials are reused. CardDAV passwords in settings are encrypted and masked on read; environment-sourced legacy password values are used as supplied.
|
||||
|
||||
## Degraded Behavior
|
||||
|
||||
- IMAP/SMTP providers can be slow or inconsistent; folder resolution, pooled connections, and reconnect behavior should fail with clear errors.
|
||||
- Google OAuth requires external Google endpoints plus configured `GOOGLE_OAUTH_CLIENT_ID`/`GOOGLE_OAUTH_CLIENT_SECRET`; missing client credentials or refresh failures degrade to reconnect-required or generic OAuth error paths.
|
||||
- Scheduled email delivery depends on `scheduled_emails.db`, poller runtime, and configured SMTP.
|
||||
- Attachment handling must tolerate missing staged files, unsupported formats, and inaccessible remote messages.
|
||||
- CardDAV local fallback applies only when CardDAV is unconfigured; configured CardDAV outages are not treated as local-write mode.
|
||||
- Multi-account list/search behavior can be sequential and cache-sensitive.
|
||||
|
||||
## Testing Coverage
|
||||
|
||||
Existing coverage includes header/envelope/IMAP/SMTP behavior, serialized default accounts, Google OAuth state/callback/token-refresh/XOAUTH2/redirect/settings behavior, shared-adapter summaries, authoritative read/mark-seen and frontend dedup, idle prewarm, UID-only mutations, scheduled-email claims and urgency checkpoint transactions, MCP full-message/owner behavior, owner scope/caches/signatures, thread/sanitizer behavior, CardDAV password encryption, mail CLI behavior, contacts basics, and selected frontend/security regressions.
|
||||
|
||||
Route-level and duplicate-path coverage is still thin for email list/read/search/mutations, account CRUD/security outside the OAuth path, send/draft security, attachments, scheduled-poller failures, contacts admin/CardDAV routes, MCP account/scope behavior, CardDAV degraded mode, and executable frontend behavior.
|
||||
|
||||
## Current Gaps
|
||||
|
||||
- Owner-keyed cache policy still needs an explicit decision for thread boundaries, plus continued migration/query audits for every email side table.
|
||||
- CardDAV still needs redirect/proxy policy and broader route-level tests for URL validation, private-address blocking configuration, and same-origin href enforcement.
|
||||
- MCP email needs continued route-helper parity for attachment path containment,
|
||||
sanitization, transport behavior, and pending-draft result text.
|
||||
- Empty-owner route compatibility and ownerless email cache rows need
|
||||
end-to-end owner-boundary tests.
|
||||
- CLI send/contact paths need parity decisions for SMTP security, recipient parsing, local fallback, and normalized contact shapes.
|
||||
- Email HTTP route coverage is concentrated in scheduling/account-test helpers rather than full list/read/search/mutation/send/draft/account/attachment flows.
|
||||
- Contacts coverage lacks admin-gate, config masking, import/export, CardDAV fallback, and CardDAV write-failure tests.
|
||||
- Multi-account performance and cache staleness remain known audit areas.
|
||||
@@ -1,158 +0,0 @@
|
||||
# Frontend
|
||||
|
||||
Last updated: dev@e71f8ce | 2026-08-25
|
||||
|
||||
## Scope
|
||||
|
||||
This spec covers the current browser app in:
|
||||
|
||||
- static serving and SPA routes in `app.py`;
|
||||
- CSP/security headers in `core/middleware.py`;
|
||||
- `static/index.html`;
|
||||
- `static/login.html`;
|
||||
- `static/app.js`;
|
||||
- `static/style.css`;
|
||||
- `static/js/*.js` and `static/js/*/*.js`;
|
||||
- vendor libraries under `static/lib/*`;
|
||||
- custom fonts and static assets under `static/fonts/*`;
|
||||
- `static/sw.js` and `static/manifest.json`;
|
||||
- frontend-oriented tests in `tests/*_js.py`, `tests/*.mjs`, `tests/bombadil-spec.ts`, static DOM/CSS/source-shape tests, and app/static tests such as `tests/test_app_static_mime.py`.
|
||||
|
||||
`/backgrounds` currently targets `static/backgrounds.html`; if that route remains, the file must exist or the route should be removed.
|
||||
|
||||
`static/manifest.json` and `static/index.html` reference PWA icon files under `static/icons/`; the current 192px, 512px, and maskable icon files exist and should stay aligned with those references.
|
||||
|
||||
## Current Call Sites Include
|
||||
|
||||
- `static/index.html` script tags and modulepreloads;
|
||||
- `static/sw.js` `PRECACHE`;
|
||||
- app-owned SPA deep links for notes, calendar, cookbook, email, memory, gallery, tasks, and library;
|
||||
- `/login` and app-owned static/HTML routes;
|
||||
- `/api/activity/heartbeat` browser visibility pings used by the foreground activity gate;
|
||||
- `static/app.js` route opener/sidebar/tool-window wiring;
|
||||
- frontend JS helper tests and static HTML/CSS/source-shape regressions;
|
||||
- CDN dependencies, local vendor libraries, service worker, and PWA manifest.
|
||||
|
||||
## Runtime Shape
|
||||
|
||||
The frontend is a raw static SPA served by FastAPI. There is no Vite, React, TypeScript, bundler, or generated build output.
|
||||
|
||||
`app.py` owns:
|
||||
|
||||
- stable `.js`/`.mjs` MIME registration;
|
||||
- the `/static` mount;
|
||||
- no-cache headers for `.js`, `.css`, and `.html` static source files;
|
||||
- nonce-injected SPA/login HTML serving;
|
||||
- SPA deep-link routes.
|
||||
|
||||
`static/index.html` owns the DOM shell and script loading order. It loads browser ES modules directly. Current boot order includes nonce-bearing inline boot scripts, self-hosted highlight.js, modulepreloads, ordered module script tags, `static/app.js`, `static/js/init.js`, `static/js/a11y.js`, workspace/chat helpers, provider device-flow helpers, and service-worker registration. KaTeX and Mermaid are vendored under `static/lib` and injected only on first real math/diagram use rather than loading in the initial HTML.
|
||||
|
||||
The two first-paint Fira Code faces are preloaded so the shell does not wait for later CSS discovery. `static/js/startupShell.js` lets the visible shell initialize before session loading completes; session/transcript hydration is deferred and coordinated by `static/js/sessions.js` plus history/session routes rather than blocking first paint.
|
||||
|
||||
Exact script URL identity matters. Versioned script tags, unversioned imports, and service-worker precache entries must stay aligned. `static/sw.js` deliberately separates first-paint `PRECACHE` from lazy `PANEL_PRECACHE`; the latter currently contains the image-editor module graph so an editor never opened online can still open offline. KaTeX scripts/styles/fonts are also precached. Current service-worker coverage is not a generated full module-graph manifest, so changes still need direct verification.
|
||||
|
||||
## Security Policy
|
||||
|
||||
`core/middleware.py` owns CSP and security headers. `app.py` injects the per-request nonce into served HTML. New inline scripts or external scripts/styles/images/media must fit the CSP contract or explicitly update it.
|
||||
|
||||
`/static/*` is public/auth-exempt. Frontend privilege gates are display-only; backend routes enforce authorization.
|
||||
|
||||
XSS/DOM policy:
|
||||
|
||||
- prefer DOM construction, `textContent`, and shared escaping helpers;
|
||||
- Markdown raw HTML preservation must remain constrained through sanitizer helpers;
|
||||
- remote email `body_html` must pass through the email-library sanitizer before insertion;
|
||||
- Mermaid, code-runner iframe `srcdoc`, visual reports, remote media, and scattered `innerHTML` templates require explicit review.
|
||||
- Visual report Markdown HTML is server-rendered and should be treated as security-sensitive alongside frontend entry points and remote media.
|
||||
|
||||
Storage/secrets policy:
|
||||
|
||||
- localStorage/sessionStorage are for preferences, UI state, offline caches, and user-switch sentinels;
|
||||
- `static/js/init.js` owns user-switch storage cleanup;
|
||||
- raw API tokens, provider keys, HF tokens, and other credentials must not be persisted in browser storage unless a feature documents masking/stripping and backend storage ownership.
|
||||
|
||||
## Service Worker And PWA
|
||||
|
||||
`static/sw.js` owns PWA cache behavior:
|
||||
|
||||
- API and non-GET requests are bypassed;
|
||||
- root navigation uses stale-while-revalidate;
|
||||
- JS/CSS use network-first behavior;
|
||||
- other static assets use cache-first with background refresh;
|
||||
- `CACHE_NAME` bumps and `PRECACHE` updates must accompany cache policy or shell asset changes.
|
||||
|
||||
`static/manifest.json` owns default PWA metadata. Route-specific manifests can be generated as Blob URLs when supported. Current default icon references must match real files under `static/icons/`.
|
||||
|
||||
KaTeX and Mermaid are self-hosted and lazy-loaded through memoized, retry-after-failure promises in `static/js/markdown.js`; math placeholders preserve source until KaTeX arrives, detached PDF export renders its own container, and Mermaid fetches only when a diagram exists. Pyodide remains a jsDelivr-loaded optional runtime, so offline/PWA behavior is not fully self-contained.
|
||||
|
||||
## Module Ownership
|
||||
|
||||
Current major frontend areas include:
|
||||
|
||||
- chat, stream handling, rendering, sessions, markdown, uploads, voice recorder, TTS, and keyboard shortcuts;
|
||||
- models, provider setup, pure model-key matching helpers, model picker, presets, search, RAG, settings, and admin;
|
||||
- settings shell modules under `static/js/settings/`: registry metadata, navigation, finder search, lifecycle/docking, DOM helpers, and persisted sidebar collapse/resize behavior;
|
||||
- compare modules under `static/js/compare/`, including sanitized popup/search/image handling;
|
||||
- document editor/library in `static/js/document.js` and `static/js/documentLibrary.js`;
|
||||
- image editor integration in `static/js/galleryEditor.js` plus leaves under `static/js/editor/`;
|
||||
- gallery, email inbox/library, calendar, research panel/jobs/synapse, notes/tasks, assistant, memory/skills, Cookbook/HW Fit, workspace picker, provider device flow, composer ArrowUp recall, theme, modal/window utilities, storage, and accessibility helpers.
|
||||
|
||||
Coordinator ownership:
|
||||
|
||||
- `static/app.js` owns late orchestration, global fetch 401 redirects, sidebar/tool route wiring, and many `window.*` compatibility bridges;
|
||||
- `static/js/init.js` owns post-load cleanup, user-switch storage wipe, and cosmetic privilege gates;
|
||||
- `static/js/storage.js` owns shared key constants and safe JSON helpers;
|
||||
- feature modules own feature state where possible.
|
||||
|
||||
`static/js/appConfig.js` owns one invalidatable promise cache for `GET /api/auth/settings` and `GET /api/tools`, including one-shot login-page settings prefetch, retry after rejected fetches, and explicit invalidation after settings/tool writes. Consumers treat resolved objects as read-only. `static/js/panels.js` owns memoized first-use panel imports; its current registry contains the image editor, shares in-flight imports, and evicts failed imports so a later online retry can succeed.
|
||||
|
||||
`static/js/MODULE_SUMMARY.md` is a refreshed ownership/navigation map for the no-build frontend. The current `static/js/` tree, `static/app.js`, `static/index.html`, and executable behavior remain the authority when the summary drifts.
|
||||
|
||||
Current small frontend helper contracts include `static/js/model/matchKey.js` for longest-substring model info/pricing matches, `static/js/models.js` for in-flight `/api/models` request sharing, `static/js/providerDeviceFlow.js` for Copilot/ChatGPT Subscription device-flow polling UI, `static/js/composerArrowUpRecall.js` for prompt recall from an empty composer, `static/js/fileHandler.js` for capped pending-file state and collapsed attachment-chip display, `static/js/streamingSegmenter.js` for incremental markdown/code-fence segmentation, `static/js/emojiShortcodes.js` for shortcode replacement, `static/js/documentLibrary.js` for keeping document counters/language chips in sync after archive/delete, `static/js/keyboard-shortcuts.js` for rejecting empty or non-string persisted keybinds before combo parsing, `static/js/modalSnap.js` for reusable desktop modal edge docking, `static/js/toolWindowZOrder.js` for shared portal/window z-index allocation, and `static/js/emailShared.js` for common email UI helpers.
|
||||
|
||||
Recent browser behavior contracts include mobile chat Enter inserting newlines while desktop Enter submits; ArrowUp recall only consuming a truly empty composer with the caret at the top, not an unsent multiline prompt; queued prompts preserving mobile behavior; regenerate-from-here versus resend; AI-message delete confirmation; native document tool results opening/updating the editor; and exact tool-approval cards that expose the sealed action/effects/workspace/document identity and submit only opaque task-scope/chat-session-scope/deny decisions without writing synthetic composer text. Chat rendering hides leaked tool JSON/document fences, no longer strips the ordinary word “assistant,” and batches live-thinking DOM updates with bounded timers. Markdown editing/restoration preserves extracted code/math blocks verbatim, including replacement-string `$&` and `$$` text and triple-backtick fences. Session URL hashes are restored, minimized sidebar icon state follows per-tab visibility, detached terminal dots remain centered, and spinner animation starts only when attached.
|
||||
|
||||
The Settings finder and navigation are registry-backed, hide admin-only destinations from non-admin users, lazy-load admin panels, and keep the registry synchronized with DOM panels. Email OAuth connect preserves SMTP security and reopens the settings surface; unread message opens use one authoritative backend read/mark-seen request with stale-response guards; email-library prewarm is idle-only, single-flight, bounded to the initial page, and cancelled around visible foreground work.
|
||||
|
||||
## UI Policy
|
||||
|
||||
- New code must run as browser ES modules without a build step.
|
||||
- Reuse existing CSS variables, modal/window patterns, icon style, storage helpers, and route conventions.
|
||||
- Custom font handling includes bundled OpenDyslexic assets plus user-supplied fonts exposed through `/api/fonts/custom`; font and text-size settings must stay coordinated between settings UI, theme helpers, and CSS variables.
|
||||
- Avoid relying on stale module summaries.
|
||||
- API shape changes must update the owning JS module and tests.
|
||||
- Add behavior to large coordinators such as `static/app.js`, `static/js/chat.js`, `static/js/document.js`, or `static/js/settings.js` only when it matches their existing wiring ownership.
|
||||
|
||||
## Degraded And Platform Behavior
|
||||
|
||||
- Server no-cache applies to `.js`, `.css`, and `.html` source files, not every static asset.
|
||||
- Service-worker cache changes can affect frontend behavior even when source files revalidate.
|
||||
- Mobile behavior uses separate CSS/media/hover/safe-area/`100dvh` handling and JS layout code; check it directly.
|
||||
- Browser APIs such as service workers, Blob route manifests, Web Speech, `getUserMedia`, visual viewport, and storage can be absent or restricted.
|
||||
- Local libraries and CDN globals degrade differently; document, markdown, math, diagrams, and code runner flows should handle missing globals where possible.
|
||||
- localStorage migrations and cross-user cleanup are part of compatibility.
|
||||
|
||||
## Testing Coverage
|
||||
|
||||
Existing frontend coverage is a mix of Node-executed helper tests, `.mjs` tests, static DOM/CSS/source-shape tests, browser exploration specs, and app/static tests. Many tests are useful source-shape regressions but do not replace browser/module-graph execution.
|
||||
|
||||
Recent focused coverage includes model-key matching under Node, document-library counters, chat resend/delete/mobile Enter/ArrowUp, scoped approval continuation and compare routing, route provenance, live-thinking throttling, startup shell/history hydration, shared app-config caching/invalidation, settings registry/navigation/finder/lifecycle, lazy panel loading/offline editor precache, vendored lazy KaTeX/Mermaid rendering, email read dedup/prewarm, Markdown restoration, malformed keybinds, currency-safe inline math, notes/calendar/modal/manifest/admin-log behavior, Markdown XSS helpers, and CardDAV unchanged-password handling.
|
||||
|
||||
Missing coverage includes:
|
||||
|
||||
- SPA route/static auth and no-cache headers;
|
||||
- CSP header contents and nonce injection for `/` and `/login`;
|
||||
- service-worker API/non-GET bypass and cache strategy;
|
||||
- service-worker precache versus `index.html` script/module tags, including query strings;
|
||||
- ongoing manifest/icon reference drift;
|
||||
- module graph/load-order validation;
|
||||
- degraded vendor-library/browser API behavior, including Pyodide's remaining CDN path.
|
||||
|
||||
## Current Gaps
|
||||
|
||||
- `static/style.css` and large coordinators remain high-risk owners: `static/js/document.js`, `static/js/settings.js`, `static/js/chat.js`, and `static/app.js`.
|
||||
- There is no build-time type checking, module graph validation, script-order validation, or service-worker precache validation.
|
||||
- Frontend state is mostly module/global/localStorage driven, so cross-session and cross-user behavior needs explicit care.
|
||||
- `window.*` compatibility bridges remain widespread.
|
||||
- PWA/static-serving behavior may deserve a separate spec if service worker, manifests, route-specific icons, and cache policy keep growing.
|
||||
- A static asset/route manifest regression should verify files referenced by `index.html`, `manifest.json`, `sw.js`, and app-owned HTML routes actually exist.
|
||||
@@ -1,165 +0,0 @@
|
||||
# Gallery, Editor, And Media
|
||||
|
||||
Last updated: dev@e71f8ce | 2026-08-25
|
||||
|
||||
## Scope
|
||||
|
||||
This spec covers media surfaces in:
|
||||
|
||||
- app route registration and generated-file serving in `app.py`;
|
||||
- canonical models in `core/database.py`, with `src.database` as a compatibility import path;
|
||||
- canonical route package `routes/gallery/gallery_routes.py` and `routes/gallery/gallery_helpers.py`, with top-level `routes/gallery_routes.py` and `routes/gallery_helpers.py` compatibility shims;
|
||||
- generated-image writers in `src/ai_interaction.py` and `mcp_servers/image_gen_server.py`;
|
||||
- local MLX image compatibility server `scripts/mlx_image_server.py`;
|
||||
- image tool schemas/dispatch/implementations in `src/tool_schemas.py`, `src/tool_execution.py`, and `src/tool_implementations.py`;
|
||||
- `routes/editor_draft_routes.py`;
|
||||
- `routes/signature_routes.py` and document signature consumers in canonical `routes/document/document_routes.py`;
|
||||
- `routes/emoji_routes.py`;
|
||||
- `routes/font_routes.py`;
|
||||
- `src/generated_images.py`;
|
||||
- `src/visual_report.py` plus research image hide/unhide routes;
|
||||
- database models `GalleryImage`, `GalleryAlbum`, `EditorDraft`, and `Signature`;
|
||||
- generated files under `data/generated_images`;
|
||||
- frontend modules `static/js/gallery.js`, `static/js/galleryEditor.js`, `static/js/editor/*`, `static/js/signature.js`, `static/js/emojiPicker.js`, `static/js/chatRenderer.js`, `static/js/document.js`, `static/js/markdown.js`, and `static/js/theme.js`;
|
||||
- CLI surfaces `scripts/odysseus-gallery` and `scripts/odysseus-signature`;
|
||||
- tests covering gallery helpers/routes, generated-image serving, editor drafts, signatures, visual reports, fonts, upload limits, and image endpoint security.
|
||||
|
||||
## Current Call Sites Include
|
||||
|
||||
- gallery upload, library, album, tag, favorite, ZIP, delete, and saved-project views;
|
||||
- chat-generated image rendering/edit/delete bubbles;
|
||||
- agent `generate_image` and stale `edit_image` tool paths;
|
||||
- MCP image-generation rows/files;
|
||||
- image editor AI tools and model endpoint pickers;
|
||||
- document PDF signing with stored signatures;
|
||||
- visual-report hero/section image insertion and research hide/unhide controls;
|
||||
- emoji picker/markdown emoji SVG proxy calls;
|
||||
- theme custom-font loading;
|
||||
- local gallery/signature CLI inspection.
|
||||
|
||||
## Gallery
|
||||
|
||||
`routes.gallery.gallery_routes` owns gallery upload/import/library/editor transform behavior: upload dedupe, image/video extension handling, EXIF extraction for images, albums, favorites, tags, generated media metadata, search/filter/sort, owner filtering, ZIP downloads, soft delete, disk cleanup, and chat-history cleanup after image delete. Top-level `routes.gallery_routes` is a `sys.modules` compatibility shim to the canonical module.
|
||||
|
||||
Frontend gallery behavior includes upload progress, folder-drop album import, stale-while-revalidate cards, saved editor projects, detail actions, bulk delete/download, and cache-busted image refreshes.
|
||||
|
||||
Album assignment and gallery image detail/update endpoints enforce owner scope and fail closed when no authenticated owner is available instead of falling back to broad access.
|
||||
|
||||
Generated media provenance:
|
||||
|
||||
- generated filenames are opaque hex-like media names, not trusted content hashes;
|
||||
- upload `file_hash` is a separate metadata field;
|
||||
- generated files live under `data/generated_images`;
|
||||
- chat image generation writes files and inserts `GalleryImage` rows through `src.ai_interaction`;
|
||||
- MCP image generation can create ownerless rows/files;
|
||||
- generated-but-not-yet-imported images can have no gallery row;
|
||||
- once a gallery row exists, owner checks decide visibility where the route enforces them.
|
||||
|
||||
`app.py` owns direct `/api/generated-image/{filename}` serving through `src.generated_images.resolve_generated_image_path()`. It validates hex-like image/video filenames, rejects path escape and missing files, serves rowless generated files, checks row owner when a row exists, allows null-owner compatibility rows, and uses immutable/nosniff cache headers. Gallery replace/rotate/save/delete/ZIP paths also resolve filenames through a shared generated-image path helper so database filenames cannot escape `data/generated_images`. Replace/rotate/save-over-original flows can mutate bytes under the same filename, so frontend cache busting matters.
|
||||
|
||||
## Image Tools And Providers
|
||||
|
||||
Gallery/editor image transforms are split across:
|
||||
|
||||
- `/api/gallery/ai-upscale` and `/api/gallery/style-transfer`;
|
||||
- `/api/image/inpaint`;
|
||||
- `/api/image/harmonize`;
|
||||
- `/api/image/sharpen`;
|
||||
- `/api/image/denoise`;
|
||||
- `/api/image/upscale-local`;
|
||||
- `/api/image/remove-bg`;
|
||||
- `/api/image/enhance-face`.
|
||||
|
||||
AI image endpoints mostly require image-generation privilege in the gallery route layer. The sharpen route is explicitly auth-gated; utility routes that live outside gallery still need their own route-level gate checks rather than assuming a shared decorator. The chat image-generation session path calls `do_generate_image()` separately and has its own privilege/tool-listing behavior.
|
||||
|
||||
Provider behavior:
|
||||
|
||||
- OpenAI image edits use multipart `/images/edits`, mask conversion, size coercion, model restrictions, and source compositing where needed;
|
||||
- diffusion/self-hosted paths use JSON APIs such as inpaint, img2img, variations, harmonize, or A1111-compatible fallbacks;
|
||||
- client-supplied endpoint URLs on selected routes must pass outbound endpoint validation; DB-selected image endpoints should be resolved through owner-visible endpoint queries before decrypted headers/keys are used;
|
||||
- provider-returned image result URLs are validated with `src.url_safety.check_outbound_url()` before server-side download, with private-IP blocking controlled by image-route settings;
|
||||
- AI endpoint path suffixes are allowlisted before proxy/download use so arbitrary endpoint paths cannot be selected through gallery/editor requests;
|
||||
- editor model pickers load `/api/model-endpoints` and classify image-capable endpoints.
|
||||
|
||||
Optional dependency behavior:
|
||||
|
||||
- Pillow-backed paths are effectively core for EXIF, rotate, sharpen, and image preparation;
|
||||
- Real-ESRGAN powers denoise/upscale when installed and otherwise returns install guidance; import-time torchvision compatibility patches run before Real-ESRGAN imports;
|
||||
- remove-bg tries `rembg`, then transformers-style fallback, then an error;
|
||||
- face enhancement falls back from GFPGAN/OpenCV toward PIL behavior;
|
||||
- video uploads intentionally skip EXIF/ffprobe metadata today.
|
||||
- grounding and mask model inputs cast only `float64` tensors to `float32` before transfer to Apple's MPS backend, because MPS rejects float64; integer/other tensors and non-tensor processor values preserve their normal device-transfer behavior.
|
||||
|
||||
## Editor Drafts
|
||||
|
||||
`routes.editor_draft_routes` owns server-backed image editor project payloads. `EditorDraft` rows store title, payload JSON, thumbnail, source image, timestamps, and owner.
|
||||
|
||||
Frontend editor behavior is split across `static/js/editor/*` and `static/js/galleryEditor.js`: canvas state, layer panel, masks, history, snapping, stroke pipeline, inpaint/rembg/harmonize tools, AI tool runner, model pickers, an AI edit command box that routes natural-language edit requests into existing inpaint/remove/upscale/background/style actions where possible, import wiring, topbar controls, auto-save, resume by draft ID or source image, draft-only open, and cleanup after close. `static/js/panels.js` loads this module graph on first editor use, shares concurrent imports, retries failed loads, and `static/sw.js` keeps the lazy graph in a separate offline panel precache.
|
||||
|
||||
Draft compatibility behavior:
|
||||
|
||||
- v2 server drafts store payloads and thumbnails server-side;
|
||||
- legacy/local raw payloads can still be restored by the frontend;
|
||||
- PUT 404 can recreate a missing draft row;
|
||||
- broken image drafts can fall back to the source image;
|
||||
- final close persist is best-effort.
|
||||
|
||||
## Signatures, Emoji, Fonts
|
||||
|
||||
`routes.signature_routes` owns reusable signature/stamp rows. Signature image payloads are normalized to bounded PNG base64, encrypted at rest, and owner-filtered; SVG signature input is not preserved. Document PDF render/export paths owner-filter signature IDs before stamping.
|
||||
|
||||
`routes.emoji_routes` owns same-origin OpenMoji black SVG proxy/caching. It validates codepoint filenames, caches SVGs under `data/emoji_cache`, and returns transparent no-store SVGs for invalid, unknown, or unreachable codepoints. `static/js/emojiPicker.js` is a curated inline monochrome picker.
|
||||
|
||||
`routes.font_routes` owns deriving available custom font family names from static font files under `static/fonts/custom`.
|
||||
|
||||
## Visual Reports
|
||||
|
||||
`src.visual_report` owns generated research/report HTML image behavior: HTTPS Open Graph image filtering, hero images, section images, icon/logo filtering, hide/reroll client controls, and inline JSON escaping for scripts.
|
||||
|
||||
Research routes and handler code own hidden-image persistence. Visual reports render model/source-influenced Markdown to HTML, so raw HTML/link/image sanitization remains security-sensitive.
|
||||
|
||||
## Security Policy
|
||||
|
||||
Media routes are cookie/current-user surfaces unless they explicitly implement token owner/scope handling. Bearer-token callers that arrive as synthetic `api` users should not be treated as owner-scoped media API clients without explicit policy.
|
||||
|
||||
Known boundaries:
|
||||
|
||||
- image-generation routes require `can_generate_images`;
|
||||
- image proxy/editor endpoints currently resolve client-selected, DB-selected, or fallback image model endpoints without full owner-scoped endpoint-key policy or uniform outbound revalidation;
|
||||
- generated-file serving allows rowless files and null-owner compatibility rows;
|
||||
- uploads are byte-limited and extension-gated, with content sniffing available through `UploadHandler.detect_content_type()` when `python-magic`/`libmagic` is installed;
|
||||
- several base64 JSON editor routes accept large decoded image payloads and need route-level size discipline;
|
||||
- gallery DB filenames should be joined through shared generated-media path helpers before filesystem operations;
|
||||
- editor draft source image IDs, payloads, and thumbnails are owner-scoped by draft owner but do not fully validate source-gallery ownership or payload size;
|
||||
- emoji proxy constrains codepoint filenames and degrades invalid, unknown, or unreachable SVGs to transparent no-store placeholders, but remote SVG content still deserves security review;
|
||||
- visual report Markdown HTML/link/image output needs continued sanitization coverage.
|
||||
- `scripts/mlx_image_server.py` pins generation/edit routing to the process-start model and ignores request-selected model names, preventing unauthenticated callers from selecting a local model directory/repository whose model-specific script or bridge would execute.
|
||||
|
||||
## Degraded And Compatibility Behavior
|
||||
|
||||
- Uploaded images record display dimensions with EXIF orientation when possible; EXIF failures warn/degrade.
|
||||
- Video uploads skip EXIF and have no metadata extraction yet.
|
||||
- Missing generated files are skipped in ZIP downloads; if all are missing, the route returns no files found.
|
||||
- Soft delete commits the gallery row state before removing the disk file, so a failed DB write does not orphan a missing image row.
|
||||
- AI tagging can fail when disk files are missing.
|
||||
- Static JS/CSS/HTML assets revalidate because there is no frontend build/versioning.
|
||||
- Gallery/editor frontend state includes stale-while-revalidate and listener cleanup to avoid stale handlers.
|
||||
- `edit_image` tool schema/implementation currently appears stale against implemented `/api/image/*` and `/api/gallery/*` routes.
|
||||
|
||||
## Testing Coverage
|
||||
|
||||
Existing tests cover EXIF dimensions, owner-filter helper behavior, direct upload limits, image-generation privilege source shape, sharpen auth, gallery null-user denial, endpoint SSRF/source checks, editor draft payload validation, lazy editor loading/offline precache, MLX request-model pinning, font family derivation, visual-report helper behavior, gallery CLI previews, and selected security regressions.
|
||||
|
||||
Route-level coverage is thin for full gallery CRUD/album/tag/download/delete flows, generated-image serving, editor draft owner CRUD, signature owner CRUD, emoji proxy/cache behavior, image-tool degraded responses, optional dependency fallbacks, and frontend editor behavior.
|
||||
|
||||
## Current Gaps
|
||||
|
||||
- Owner-scoped endpoint-key resolution is needed for image proxy/editor routes.
|
||||
- Media routes need a clear API-token policy: reject token callers, or implement owner/scope handling.
|
||||
- Generated-image serving needs live route tests for invalid filenames, rowless files, owned rows, null-owner rows, MIME/cache headers, and cross-owner behavior.
|
||||
- Mutable generated filenames plus immutable cache headers need cache-busting tests for replace/save-over-original flows.
|
||||
- Base64 JSON editor payload size limits need hardening; upload content sniffing should keep native/Docker parity coverage as dependencies change.
|
||||
- MCP image generation needs an owner attribution decision or explicit admin-only documentation.
|
||||
- `edit_image` tool route mapping appears stale.
|
||||
- Emoji SVG proxy/cache and visual-report raw HTML/link sanitization need stronger tests.
|
||||
- Optional image dependency fallbacks are mostly untested.
|
||||
@@ -1,197 +0,0 @@
|
||||
# Integrations
|
||||
|
||||
Last updated: dev@e71f8ce | 2026-08-25
|
||||
|
||||
## Scope
|
||||
|
||||
This spec covers external integration surfaces in:
|
||||
|
||||
- `routes/codex_routes.py`;
|
||||
- `integrations/codex/*` and `integrations/claude/*`;
|
||||
- `routes/api_token_routes.py` and bearer-token handling in `app.py`;
|
||||
- `routes/auth_routes.py` integration CRUD/test routes;
|
||||
- `src/integrations.py` and `data/integrations.json`;
|
||||
- canonical `routes/webhook/webhook_routes.py` plus its top-level compatibility shim, and `src/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`, and `scripts/odysseus-webhook`;
|
||||
- companion/mobile pairing in `companion/routes.py` and `companion/pairing.py`;
|
||||
- provider OAuth/device-flow endpoint links in `routes/copilot_routes.py`, `routes/chatgpt_subscription_routes.py`, `routes/device_flow.py`, and `ProviderAuthSession` rows;
|
||||
- integration UI surfaces in `static/js/settings.js` and `static/js/admin.js`;
|
||||
- database models `ApiToken` and `Webhook`.
|
||||
|
||||
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/chat` synchronous external chat;
|
||||
- `/api/models` catalog reads for `chat`-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_loop` injecting 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.json` loads 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_URL` is configured.
|
||||
- `ODYSSEUS_URL` must 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_call` output 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_SCOPES` regression 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/tokens` semantics for `ody_` 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 `chat` scope 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=true` the default.
|
||||
- Admin-authored integration descriptions and `api_call` results enter the untrusted-result/gated-action pipeline, but their product-level trust presentation still needs continued review.
|
||||
- The dormant SQLAlchemy `Integration` model should be removed, migrated into use, or documented as legacy.
|
||||
@@ -1,153 +0,0 @@
|
||||
# LLM Models And Endpoints
|
||||
|
||||
Last updated: dev@e71f8ce | 2026-08-25
|
||||
|
||||
## Scope
|
||||
|
||||
This spec covers model/provider behavior in:
|
||||
|
||||
- `src/llm_core.py`;
|
||||
- `src/endpoint_resolver.py`;
|
||||
- `src/foreground_model_routing.py`;
|
||||
- `src/model_discovery.py`;
|
||||
- `src/model_context.py`;
|
||||
- `src/model_capabilities.py`;
|
||||
- `src/model_capability_readers/`;
|
||||
- `src/task_endpoint.py`;
|
||||
- `src/tls_overrides.py`;
|
||||
- `src/copilot.py`;
|
||||
- `routes/copilot_routes.py`;
|
||||
- `routes/chatgpt_subscription_routes.py` and `routes/device_flow.py`;
|
||||
- `routes/model_routes.py`;
|
||||
- `routes/session_routes.py`;
|
||||
- `routes/cookbook_routes.py`, `routes/hwfit_routes.py`, and `services/hwfit/`;
|
||||
- `src/settings.py`;
|
||||
- `core/database.py` model `ModelEndpoint`;
|
||||
- frontend modules `static/js/models.js`, `static/js/modelPicker.js`, `static/js/model/matchKey.js`, `static/js/providers.js`, `static/js/settings.js`, `static/js/admin.js`, `static/js/compare/`, and Cookbook model-serving modules;
|
||||
- chat, compare, research, STT/TTS, and utility-model call sites.
|
||||
|
||||
## Provider Calls
|
||||
|
||||
`src.llm_core` owns provider-call mechanics. It handles OpenAI-compatible calls, Ollama normalization, Anthropic payload conversion, GitHub Copilot and ChatGPT Subscription provider detection/header injection, NVIDIA provider routing, streaming, fallback calls, upstream error formatting, async/streaming host liveness caching, configured model-list cache reads, tool-call sanitization, reasoning/thinking stream routing, and provider-specific parameter rules. GitHub Copilot OAuth/device-flow orchestration lives in `routes/copilot_routes.py` and `src/copilot.py`; ChatGPT Subscription device flow uses `routes/chatgpt_subscription_routes.py`, shared device-flow helpers, and `ProviderAuthSession` rows.
|
||||
|
||||
`llm_core` owns payload shape. Route files and chat/agent code should request a call; they should not duplicate provider-specific payload quirks.
|
||||
|
||||
Kimi Code User-Agent discovery has both sync and async implementations. Async
|
||||
post and stream paths probe `/models` through their existing async client and
|
||||
await each candidate, so header negotiation does not block the event loop; both
|
||||
paths share the accepted-value cache and 403 fallback policy.
|
||||
|
||||
Provider-specific behavior is part of this layer: `LLM_CONNECT_TIMEOUT` controls the connect budget for sync and streaming calls, Kimi Code endpoints retry a small whitelisted User-Agent set on 403 and cache the accepted value, official Moonshot/Kimi Code and Anthropic Opus 4.7+ payloads omit sampling controls where required, and major-only Opus IDs such as `claude-opus-5` also omit temperature instead of falling through numeric minor-version parsing. Reasoning models omit or clamp unsupported temperature values, while self-hosted compatible endpoints keep normal parameters unless detected otherwise. Mistral structured content is normalized in async utility calls as well as stream/chat paths, and Mistral/Moonshot/Kimi reasoning content, `gpt-oss` harmony output, DeepSeek V4 thinking identifiers, and native/OpenAI-compatible Ollama thinking formats keep hidden reasoning separate from visible text. Tool names that collide with GPT-OSS built-ins are aliased on the provider boundary and mapped back before execution. Copilot request metadata remains defensive against malformed `request_flags`.
|
||||
|
||||
## Canonical Provider And Model Shape
|
||||
|
||||
`src.model_capabilities` owns canonical model family, task, modality,
|
||||
capability, limit, evidence, assertion, deterministic-control, probe-result,
|
||||
reasoning-control token, and display-query values.
|
||||
`src.model_capability_readers` owns endpoint-scoped stable identity, lightweight
|
||||
provider detection, record serialization, and normalization of already-fetched
|
||||
provider payloads. Readers do no network I/O. Model-specific observations are
|
||||
kept in `model-quirks.md`, not a runtime registry without a consumer.
|
||||
|
||||
Provider support and model support are different facts. A provider may expose
|
||||
tools, reasoning, vision, or multiple APIs while individual models differ.
|
||||
Provider-native readers describe where model evidence can appear. Current
|
||||
concrete readers cover generic OpenAI-compatible identity, OpenAI, OpenRouter,
|
||||
Google, Ollama, LM Studio, and llama.cpp. Identity-only model lists remain
|
||||
unknown.
|
||||
|
||||
Reader dispatch uses an explicit vendor first, then endpoint kind, label-bounded hostname suffix, and common local-port hints. Generic payload handling accepts `data[]`
|
||||
or `models[]` items with `id`, `name`, or `model`; it does not accept a bare
|
||||
list and never promotes capability-looking fields. Unknown fields remain in
|
||||
the in-memory raw record. See [model-capability-canonical.md](model-capability-canonical.md),
|
||||
[model-quirks.md](model-quirks.md), and the
|
||||
[provider map](model-providers/_readme.md).
|
||||
|
||||
This canonical layer is currently exercised by focused unit tests but is not
|
||||
wired into runtime discovery, endpoint resolution, model context, request
|
||||
shaping, or frontend pickers. `routes/model_routes.py` model probes continue to
|
||||
return model IDs through their existing runtime path.
|
||||
|
||||
Route-level probe helpers in `routes/model_routes.py` are the current exception: they build minimal provider-specific probe payloads using `llm_core` detection helpers. Keep probe behavior aligned with `llm_core` provider adapters. LLM provider HTTP clients and endpoint probes share `src.tls_overrides.llm_verify()`, which can add an operator-provided `LLM_CA_BUNDLE` on top of normal certificate verification without turning verification off or widening that trust to arbitrary URL fetches.
|
||||
|
||||
## Endpoint Resolution
|
||||
|
||||
`src.endpoint_resolver` owns endpoint normalization and URL construction:
|
||||
|
||||
- base URL normalization;
|
||||
- chat and model-list URL construction;
|
||||
- endpoint ID resolution;
|
||||
- chat, utility, and vision fallback candidate selection;
|
||||
- Tailscale hostname resolution where available.
|
||||
|
||||
OpenAI-compatible model-list URL construction preserves `/v1` bases and inserts `/v1/models` for bare local bases such as LM Studio `http://localhost:1234`.
|
||||
|
||||
`routes/model_routes.py` owns model endpoint CRUD, admin provider discovery/probing, visible/hidden/pinned model lists, endpoint kind and refresh policy, curated/extra model partitioning, `/api/models` catalog caching, Docker loopback rewriting, tool-support probing, provider-auth linkage, endpoint-dependent settings cleanup, and owner filtering. Endpoint dedupe allows the same base URL under different API keys and surfaces API-key fingerprints/key presence without returning secrets.
|
||||
|
||||
`routes/session_routes.py` owns binding sessions to endpoint IDs, owner-scoped header construction, raw-endpoint rejection for non-admin users, model validation, and persisted session headers. Compare panes and normal chat session creation use this path.
|
||||
|
||||
`ModelEndpoint` rows own API keys, base URLs, cached/hidden/pinned models, model type, endpoint kind, refresh mode/interval/timeout, supports-tools state, nullable owner, optional provider-auth linkage, and provider metadata. `owner = NULL` means legacy/shared; non-null rows are private to that owner, while admins can see all. Secret fields must remain encrypted and scrubbed in responses.
|
||||
|
||||
Decrypted endpoint headers can be copied into session metadata for chat use. Endpoint deletion must clear dependent settings and copied session headers.
|
||||
|
||||
## Model Discovery And Lists
|
||||
|
||||
`src.model_discovery` owns host/env/Tailscale/local-port scanning for model servers. Admin `/api/providers` and `/api/discover` use that scanner; endpoint CRUD, test, refresh, and hidden-model controls are frontend-owned by `static/js/admin.js`.
|
||||
|
||||
`/api/models` is the normal picker/catalog surface. It is auth/owner scoped, per-user/admin-flag cached briefly, can trigger background refresh, preserves offline endpoint rows, filters hidden models, and preserves pinned model IDs for UI selection. API-token callers must carry `chat` scope and a token owner before they can list models. API/proxy endpoint inventory is visible by default until an explicit `pinned_models` allow-list is saved; an explicit empty list means show none, and legacy hidden-list state is upgraded to the equivalent pins so endpoint settings, picker checkboxes, and chat agree. Proxy/API endpoints can be marked cached-first/manual so large upstream catalogs are not repeatedly probed, while explicit refresh paths use longer manual timeouts. Local endpoints get cheap reachability probes before expensive refreshes where possible, and endpoint responses can include explicit `supports_tools` state for schema-emission heuristics. Google Gemini API endpoints use the native paginated `generativelanguage.googleapis.com/v1beta/models` catalog, send API keys in `x-goog-api-key`, retain only content-generation model IDs, and default to manual refresh unless the caller explicitly chooses another mode. Probe failure returns no curated Google fallback. `static/js/models.js` and `static/js/modelPicker.js` own the sidebar/picker catalog; `static/js/model/matchKey.js` owns longest-substring model-info/pricing key matching; `static/js/settings.js` owns default, utility, vision, image, TTS, STT, and fallback selectors.
|
||||
|
||||
`src.task_endpoint` owns background-task endpoint/model resolution for task routes and scheduler callers. It resolves `task_endpoint_id`/`task_model` through the normal endpoint resolver with owner context.
|
||||
|
||||
Cookbook and HWFit own local model download, serve, ranking, and auto-registration flows. They can create LLM or image `ModelEndpoint` rows, but provider dispatch remains owned by `llm_core`/endpoint resolution.
|
||||
|
||||
## Context Length
|
||||
|
||||
`src.model_context` owns model context-length lookup/query and token estimation. Cache keys include endpoint plus model so identical model names on different endpoints do not bleed context-window data. Unknown proxy/API models can pick up real context windows from endpoint catalog metadata such as `context_length`; otherwise unknown lengths stay explicit unknowns rather than default values. Known lengths feed chat/agent token-budget scaling through `src.context_budget`. Token estimation counts assistant `tool_calls` arguments so compaction sees tool-only turns instead of underestimating them. Chat/agent context budgeting should call this layer instead of hardcoding model windows.
|
||||
|
||||
## Runtime Fallback And Routing
|
||||
|
||||
`src.foreground_model_routing` owns foreground Chat/Agent fallback policy. Selected models are strict by default. Fallback requires owner-scoped `foreground_fallback_enabled=true` and an ordered `foreground_model_fallbacks` list; the old `default_model_fallbacks` setting is retired, ignored, and not migrated into consent. Named users never inherit a legacy flat/single-user fallback choice, candidate lists are capped at ten exact owner-visible models, and caller-provided allowed-model restrictions remain authoritative.
|
||||
|
||||
Only eligible availability failures before substantive output can fall through. Default eligible statuses are 408, 425, 429, 500, 502, 503, 504, 507, 508, and 529. Missing endpoint/configuration, provider/schema/request errors, empty completions, and post-content failures do not silently change routes. A candidate commits after non-empty visible/reasoning text or a tool call; the answering route is then pinned. Foreground routing carries model and endpoint descriptors together, shapes context/compaction route-neutrally across candidates, persists only answering-route compaction, and records requested/actual/per-round route provenance plus cost attribution. Utility/background and vision fallbacks remain separate policies.
|
||||
|
||||
Model selection has three layers: endpoint resolver hidden-model and first-chat-model selection, `/api/default-chat` per-user default/fallback resolution, and frontend picker auto-selection for empty sessions.
|
||||
|
||||
Image routing uses model-name prefixes and `ModelEndpoint.model_type == "image"` to bypass text chat and generate media. Vision analysis uses configured vision models and `vision_model_fallbacks`; image and vision endpoint lifecycle changes should update chat, document processing, Cookbook, and settings UI together.
|
||||
|
||||
Provider tool calls are untrusted requests, not authorization. `supports_tools` controls schema emission only; `llm_core` normalizes provider tool-call payloads, while execution authority remains in `src.tool_execution`, `src.tool_security`, and agent-tool policy.
|
||||
|
||||
## Degraded And Platform Behavior
|
||||
|
||||
- Provider offline or probe failures should surface actionable errors without crashing the app. Async calls retry transient 429/502/503/504 responses before failing.
|
||||
- Docker deployments may need loopback URL rewriting from `127.0.0.1` to host-accessible addresses.
|
||||
- Foreground fallback selection must preserve endpoint identity, explicit owner consent, allowed-model policy, and owner scope. User/API-token LLM dispatch that can carry configured endpoint keys must pass the effective owner into resolver calls.
|
||||
- Async and streaming calls use dead-host cooldown; sync utility/vision calls do not have identical cooldown coverage.
|
||||
- llama.cpp slot-affinity routing is local-endpoint behavior only and must not be applied to cloud/provider endpoints.
|
||||
- Hidden, pinned, cached, endpoint-kind, refresh-policy, and offline model state are UI/runtime compatibility data. Pinned models may not participate in every resolver auto-pick path unless code explicitly includes them.
|
||||
- SSE/stream parsers tolerate null choice/usage/tool-call entries and null streaming tool-call arguments; provider events should degrade to empty text or shaped stream errors instead of crashing the chat loop.
|
||||
- Provider adapters carry small model-specific quirks: Opus 4.7+ and official Kimi/Moonshot code payloads omit `temperature`, Kimi/Moonshot/Mistral reasoning content is preserved separately, ChatGPT Subscription refreshes bearer credentials, native Ollama can handle multimodal content, and Ollama `/v1` responses for Qwen3/Gemma4-style thinking can suppress thinking text when requested.
|
||||
|
||||
## Security Policy
|
||||
|
||||
- Endpoint API keys are encrypted in `ModelEndpoint.api_key` and never returned by endpoint APIs; admin surfaces return key presence only.
|
||||
- Endpoint CRUD, probes, provider discovery, and most endpoint configuration are admin-cookie or internal-tool gated.
|
||||
- `/api/models` is auth/owner scoped for configured deployments; API-token access requires `chat` scope and token-owner attribution.
|
||||
- Admin-created model endpoints may target local/LAN servers. Non-admin chat session creation must use registered endpoint IDs. API-token `/api/v1/chat` requires `chat` scope and validates direct `base_url` with public-only URL checks.
|
||||
|
||||
## Current Call Sites Include
|
||||
|
||||
- chat streaming and non-streaming calls;
|
||||
- agent loop calls with optional tool schemas;
|
||||
- compare pane calls;
|
||||
- research synthesis/probe calls;
|
||||
- utility model fallbacks for summarization/extraction;
|
||||
- frontend Settings and model picker endpoint management.
|
||||
|
||||
## Current Gaps
|
||||
|
||||
- Runtime provider detection, model curation, and frontend logos are still split across `llm_core`, `model_routes`, and `providers.js`; the canonical reader package has no production consumer yet.
|
||||
- Provider-specific behavior is concentrated in `llm_core.py`, which is large and easy to regress.
|
||||
- Several runtime request builders still use model-name heuristics. They should migrate only after endpoint/provider code supplies structured identity and a real consumer contract; the canonical catalog does not add a parallel quirk matcher.
|
||||
- Endpoint identity and fallback behavior need careful review when new OAuth/subscription providers are added.
|
||||
- Owner must continue to be threaded through new utility/research/default endpoint-resolution call sites so provider keys stay isolated.
|
||||
- `/api/models` owner-scoped listing/cache behavior, shared/private endpoint dedupe, endpoint-kind refresh policy, fallback-chain owner scope, and image endpoint create/list/update lifecycle need stronger route-level regression coverage.
|
||||
@@ -1,118 +0,0 @@
|
||||
# Memory And Skills
|
||||
|
||||
Last updated: dev@2e2bb52 | 2026-08-16
|
||||
|
||||
## Scope
|
||||
|
||||
This spec covers persistent memory and user skills in:
|
||||
|
||||
- app wiring in `app.py` and `src/app_initializer.py`;
|
||||
- active legacy memory managers `src/memory.py` and `src/memory_vector.py`;
|
||||
- canonical memory routes in `routes/memory/memory_routes.py`, with `routes/memory_routes.py` as a compatibility shim;
|
||||
- chat memory/skill gating in `routes/chat_helpers.py`;
|
||||
- memory compatibility modules in `services/memory/memory.py`, `services/memory/memory_vector.py`, and `services/memory/service.py`;
|
||||
- provider abstractions in `src/memory_provider.py`;
|
||||
- LLM extraction/audit in `services/memory/memory_extractor.py`;
|
||||
- skill storage, format, import, and extraction in `services/memory/skills.py`, `services/memory/skill_format.py`, `services/memory/skill_importer.py`, and `services/memory/skill_extractor.py`;
|
||||
- skill routes in `routes/skills_routes.py`;
|
||||
- prompt/tool call sites in `src/chat_processor.py`, `src/agent_loop.py`, `src/ai_interaction.py`, `src/tool_implementations.py`, `src/tool_execution.py`, `src/tool_schemas.py`, and `src/tool_security.py`;
|
||||
- MCP and Codex surfaces in `mcp_servers/memory_server.py` and `routes/codex_routes.py`;
|
||||
- backup/admin/CLI surfaces in `routes/backup_routes.py`, canonical `routes/admin_wipe/admin_wipe_routes.py` plus its shim, `scripts/odysseus-memory`, `scripts/odysseus-skills`, and `scripts/odysseus-backup`;
|
||||
- frontend modules `static/js/memory.js` and `static/js/skills.js`;
|
||||
- tests under `tests/test_memory_*`, `tests/test_builtin_memory_consolidation.py`, `tests/test_skill_*`, and `tests/test_skills_*`.
|
||||
|
||||
## Memory Runtime
|
||||
|
||||
`src.app_initializer.initialize_managers()` creates the active `src.memory.MemoryManager` and `src.memory_vector.MemoryVectorStore` used by app startup. `routes.memory.memory_routes` imports through `services.memory` but is passed the startup manager instances; top-level `routes.memory_routes` is a `sys.modules` compatibility shim.
|
||||
|
||||
`MemoryManager` owns JSON-backed memory storage in `data/memory.json`, validation, owner fields, pinned state, use counts, and text/keyword similarity. Read-only `load_all()` remains lenient and can degrade an unreadable store to no memories. Mutating read-modify-write paths use `load_all_for_update()`, which raises `MemoryStoreUnreadable` rather than letting a corrupt or unreadable file be overwritten with an empty list. Agent/MCP/native-provider adds, extraction, backup import, and owner migration preserve that distinction; legacy `memory.txt` migration remains allowed. `MemoryVectorStore` owns semantic lookup when Chroma and embeddings are reachable.
|
||||
|
||||
Chat memory behavior:
|
||||
|
||||
- chat preferences and incognito state gate memory preface use;
|
||||
- pinned memories are loaded for the owner;
|
||||
- retrieved memories use keyword matching plus optional vector scoring;
|
||||
- inserted memory is wrapped as untrusted context;
|
||||
- memory use counts are incremented after insertion.
|
||||
|
||||
`services/memory/memory_extractor.py` owns LLM-assisted extraction, audit, and validation flows. It requests model behavior and writes through the memory manager; it does not own chat session persistence.
|
||||
|
||||
Extraction handles reasoning-model response shapes and records explicit dislike/drop preferences as `dislikes` rather than losing them to generic fact handling.
|
||||
|
||||
## Skills Runtime
|
||||
|
||||
`services/memory/skills.py` owns disk-backed skill storage under `data/skills/<category>/<name>/SKILL.md`, plus `_usage.json` usage/audit sidecars. Legacy `data/skills.json` is a read-only fallback/import source, not the current write shape.
|
||||
|
||||
`services/memory/skill_format.py` owns frontmatter/body parsing and emission. Quoted scalar parsing/emission is symmetric: JSON escapes decode once, UTF-8/non-ASCII stays intact, emitted values escape line separators safely, and invalid JSON-style escapes fall back to literal text instead of compounding backslashes on every save. `services/memory/skill_importer.py` resolves public GitHub/skills URLs, fetches bundle files with strict public-network URL safety, and chooses/imports `SKILL.md`. Import disables automatic redirects, follows at most five hops, validates and resolves each hop, then connects only to the validated IP snapshot through a pinned transport while preserving URL, Host, and TLS identity; GitHub final-host checks and file/size limits still apply. `routes/skills_routes.py` owns CRUD/search/index/import, owner filtering, skill test/audit jobs, and admin-gated built-in tool instruction overrides.
|
||||
|
||||
Skill extraction is owned by `services/memory/skill_extractor.py`. It can suggest or save skills from conversations, tries valid brace-delimited JSON candidates with `JSONDecoder.raw_decode()`, rejects ambiguous multiple top-level JSON objects instead of guessing, and saved skills remain user-editable data.
|
||||
|
||||
Agent skill behavior:
|
||||
|
||||
- matched skills are owner-scoped, confidence-gated, usage-counted, and wrapped as untrusted context;
|
||||
- `index_for()` exposes published skills plus teacher-escalation drafts gated by platform and toolsets; `active_toolsets=None` means the caller has no explicit toolset knowledge and does not hide `requires_toolsets` skills, while an explicit list applies the gate;
|
||||
- user prefs such as skills enabled, auto-approve, and max injected skills shape runtime insertion;
|
||||
- the level-0 base skill index currently calls `index_for(owner=None)`, so it is not fully owner-scoped.
|
||||
- skill tests use the configured utility model rather than the chat default and wrap user-editable skill text as untrusted context; approval continuation for a test or teacher-generated skill uses the same exact-action gate as the normal agent loop.
|
||||
|
||||
## Tools, MCP, And Backup
|
||||
|
||||
Native `manage_memory` and `manage_skills` tool paths pass owner context and use in-process policy gates. `manage_skills` requires an explicit action instead of silently defaulting a malformed call. Manual memory add can choose a category, and route-side manual add validates the source session owner before attaching session-derived memories. `mcp_servers/memory_server.py` lazy-initializes `src` managers and exposes list/add/edit/delete/search. It can scope to `ODYSSEUS_MCP_MEMORY_OWNER` or `ODYSSEUS_MEMORY_OWNER`; if the JSON store contains owner-bearing entries and no owner env is configured, it returns an owner-scope error instead of listing or mutating across owners. Ownerless stores remain ownerless compatibility mode.
|
||||
|
||||
The direct `odysseus-memory add` CLI tolerates non-object legacy/corrupt rows
|
||||
when checking whether its newly added entry is already present; it ignores
|
||||
those rows instead of calling mapping methods on them and crashing the add.
|
||||
|
||||
`/api/export` owner-filters memories and skills. `/api/import` imports skills through current disk-backed `SkillsManager` APIs, stamping missing owners to the importer and preserving supported skill metadata. Full data snapshots through `scripts/odysseus-backup` preserve on-disk skill trees, memory JSON, and caches differently from JSON import/export.
|
||||
|
||||
## Compatibility State
|
||||
|
||||
Memory and skills are partially migrated:
|
||||
|
||||
- app startup, MCP, and some tools still use `src.memory*`;
|
||||
- services memory modules remain relevant for imports/tests, with memory and vector modules re-exporting canonical `src` implementations;
|
||||
- `services/memory/service.py` is a compatibility facade around the canonical managers, but it remains ownerless and should not be assumed equivalent to route/tool owner policy;
|
||||
- skills are service-owned and disk-backed, while backup import and some compatibility paths still expect older JSON/list shapes.
|
||||
|
||||
## Degraded Vector Memory
|
||||
|
||||
Chroma is an external HTTP service. Native defaults use `localhost:8100`; Docker uses `chromadb:8000`. Embeddings prefer configured HTTP endpoints and can fall back to local FastEmbed.
|
||||
|
||||
Startup can degrade to keyword-only memory when vector initialization fails. Extraction/audit paths catch vector failures and continue with text/JSON behavior. Vector dedup is checked against the current owner before suppressing a candidate, and audit rebuilds preserve other owners' vector rows. Chat retrieval assumes a healthy startup vector store remains usable, so post-start vector failures can still break memory retrieval unless handled by the caller.
|
||||
|
||||
Admin wipe currently has a vector cleanup compatibility gap because it imports a nonexistent helper before attempting vector clearing.
|
||||
|
||||
## Policy
|
||||
|
||||
Saved memories and skills are untrusted source data when shown to the model. A stored skill may contain useful instructions, but it is still user-editable content and must be framed consistently with prompt-injection policy.
|
||||
|
||||
Owner isolation is surface-specific:
|
||||
|
||||
- HTTP memory and skills routes are expected to owner-filter normal user data;
|
||||
- native memory/skill tools are expected to pass owner context;
|
||||
- Codex exposes scoped token memory behavior separately;
|
||||
- normal memory/skills routes are cookie/current-user surfaces, not scoped token APIs;
|
||||
- MCP memory uses an environment-configured owner for owner-scoped stores, while the agent level-0 skill index currently has ownerless/global behavior;
|
||||
- vector dedup during memory extraction suppresses only same-owner or legacy-ownerless vector matches.
|
||||
|
||||
Skill test/audit flows intentionally run user-editable `SKILL.md` content as instructions inside controlled jobs. Those jobs rely on route owner checks, admin gates where applicable, and tool execution policy.
|
||||
|
||||
Skill import is admin-gated defense-in-depth, but imported URLs are still untrusted network input. Initial and redirected targets must remain public, automatic redirects stay disabled, and the connection must use only the IP set validated for that hop so DNS rebinding cannot change the destination between validation and transport.
|
||||
|
||||
User rename flows update skill frontmatter owner fields and `_usage.json` owner keys alongside memory/upload/research ownership migrations.
|
||||
|
||||
## Testing Coverage
|
||||
|
||||
Existing tests cover memory extraction/degraded vectors, owner isolation, unreadable-store mutation refusal, MCP memory shape/scope, skill owner update/delete, prompt-injection wrapping and approval continuation, utility-model selection, toolset gating, frontmatter escape round trips, skill-import redirect and DNS-rebinding/SSRF defenses, CLI non-object rows, and selected route owner checks.
|
||||
|
||||
Route-level memory CRUD/security, skills route security, MCP memory behavior, vector degraded writes, compatibility facade owner behavior, backup skill import, admin vector cleanup, and frontend endpoint wiring need broader coverage.
|
||||
|
||||
## Current Gaps
|
||||
|
||||
- `services/memory/service.py` needs an explicit owner-scope/support decision before it is treated as a public memory API.
|
||||
- The agent level-0 skill index should thread owner or be documented as an intentional local/global index.
|
||||
- MCP memory still needs a deliberate multi-user UX/config decision, but current behavior avoids cross-owner access when owner-bearing rows exist without an explicit MCP owner env.
|
||||
- Memory JSON import does not rebuild vector indexes.
|
||||
- Admin wipe vector clearing is currently ineffective.
|
||||
- Chat memory retrieval needs a graceful path for vector failures after startup.
|
||||
- Route-level memory and skills security coverage is incomplete.
|
||||
@@ -1,178 +0,0 @@
|
||||
# Canonical Provider And Model Capability Layer
|
||||
|
||||
Last updated: dev@e71f8ce | 2026-08-25
|
||||
|
||||
## Scope
|
||||
|
||||
This spec covers the implementation introduced on current `dev` in:
|
||||
|
||||
- canonical model values and query helpers in `src/model_capabilities.py`;
|
||||
- record, identity, and provider-detection helpers in
|
||||
`src/model_capability_readers/base.py`;
|
||||
- reader dispatch in `src/model_capability_readers/__init__.py`;
|
||||
- concrete readers for generic OpenAI-compatible, OpenAI, OpenRouter, Google,
|
||||
Ollama, LM Studio, and llama.cpp payloads;
|
||||
- regression coverage in `tests/test_model_capabilities.py` and
|
||||
`tests/test_model_capability_readers.py`.
|
||||
|
||||
The layer normalizes already-fetched JSON-compatible values. It performs no
|
||||
network I/O, does not shape provider requests, does not persist its output, and
|
||||
does not authorize model or tool use. No production caller currently consumes
|
||||
the canonical records outside this package; runtime integration remains later
|
||||
work.
|
||||
|
||||
There is no `src/provider_capability_schemas.py`, capability-specific
|
||||
diagnostics module, or runtime model-quirk registry on current `dev`.
|
||||
|
||||
## Layer Boundaries
|
||||
|
||||
- `src.model_capabilities` defines normalized families, tasks, modalities,
|
||||
capabilities, evidence sources/confidence, assertion states, deterministic
|
||||
controls, probe results, reasoning-control tokens, and display-surface
|
||||
queries.
|
||||
- `ModelCapability` owns family, primary task, input/output modalities,
|
||||
capability tokens, limits, source, and confidence.
|
||||
- `CapabilityAssertion` records claimed, verified, unsupported, or unknown
|
||||
status for one capability. Missing evidence is not an unsupported claim.
|
||||
- `DeterministicControl` records support evidence for controls such as
|
||||
temperature, top-p, seed, tool choice, or prompt caching. A supported
|
||||
request control is not itself a model capability.
|
||||
- `CapabilityProbeResult` is an in-memory evidence shape that converts pass,
|
||||
fail, or partial probe state into an assertion. No current runtime probe
|
||||
stores or merges these objects.
|
||||
- `CapabilityQuery` and `display_surfaces_for()` map a normalized capability
|
||||
into candidate surfaces such as chat, vision chat, image generation,
|
||||
embeddings, or reranking. They are not wired into current pickers.
|
||||
- Reader `ModelCapabilityRecord` binds a vendor/model identity to the nested
|
||||
capability object, assertions, deterministic controls, and optional raw
|
||||
provider evidence.
|
||||
|
||||
Provider transport support and per-model support are separate facts. Request
|
||||
and response adapters remain in `src.llm_core` and related provider modules.
|
||||
Model-specific observations remain in [model-quirks.md](model-quirks.md).
|
||||
|
||||
## Current Serialized Shapes
|
||||
|
||||
`ModelCapability.to_dict()` emits the nested capability shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"family": "chat",
|
||||
"primary_task": "chat.completions",
|
||||
"modalities": {
|
||||
"input": ["text", "image"],
|
||||
"output": ["text"]
|
||||
},
|
||||
"capabilities": ["tool_call", "vision"],
|
||||
"limits": {"context_tokens": 131072},
|
||||
"source": "provider_reader",
|
||||
"confidence": "provider_reported"
|
||||
}
|
||||
```
|
||||
|
||||
`ModelCapabilityRecord.to_dict()` wraps that value with `vendor`, `model_id`,
|
||||
`stable_model_id`, `display_name`, `capability_assertions`, and
|
||||
`deterministic_controls`. It does not currently emit a schema version or the
|
||||
flat `provider`/`model`/`features`/`controls` shape. Raw provider fields are
|
||||
included only when the caller passes `include_raw=True`.
|
||||
|
||||
Endpoint configuration can explicitly map `model_type=llm` to chat and
|
||||
`model_type=image` to image generation. Missing or unrecognized endpoint types
|
||||
stay unknown rather than silently becoming chat-capable in this schema layer.
|
||||
|
||||
## Identity And Reader Dispatch
|
||||
|
||||
`records_from_payload()` selects a reader from an explicit `vendor`, or from
|
||||
`detect_vendor(base_url, endpoint_kind)` when no vendor is supplied.
|
||||
|
||||
Current detection order and behavior are:
|
||||
|
||||
1. a recognized explicit endpoint kind;
|
||||
2. label-bounded hostname checks for OpenRouter, OpenAI, Anthropic, Google APIs, and Ollama Cloud;
|
||||
3. common local ports: `11434` for Ollama, `1234` for LM Studio, `8000` for vLLM, and `30000` for SGLang;
|
||||
4. generic OpenAI-compatible for any other parsed host, otherwise unknown.
|
||||
|
||||
These are normalization hints, not authorization. Hostname checks accept an exact domain or its dot-delimited subdomains after lowercasing and removing a trailing dot, so names such as `notopenai.com` do not match `openai.com`; local-port mappings remain intentionally covered by tests. Callers must not treat any result as proof of endpoint trust.
|
||||
|
||||
Implemented reader modules are `generic_openai`, `openai`, `openrouter`,
|
||||
`google`, `llamacpp`, `ollama`, and `lmstudio`. Anthropic, Hugging Face,
|
||||
SGLang, and vLLM have placeholder vendor IDs but currently dispatch through the
|
||||
generic identity-only reader. Other explicitly supplied vendor strings are
|
||||
also preserved while using that generic reader.
|
||||
|
||||
Stable model identity is scoped in this order:
|
||||
|
||||
- explicit endpoint ID;
|
||||
- a short hash of normalized base URL when an endpoint ID is absent;
|
||||
- `global` when neither endpoint identity is supplied.
|
||||
|
||||
## Generic Identity-Only Contract
|
||||
|
||||
The generic reader accepts mapping payloads containing `data[]` or `models[]`.
|
||||
Each item must itself be a mapping and provide `id`, `name`, or `model`.
|
||||
Bare-list payloads and `key`/`slug`-only items are not accepted by the current
|
||||
implementation.
|
||||
|
||||
The reader deliberately returns unknown family, modalities, capabilities, and
|
||||
controls. It preserves the raw item on the in-memory record but does not parse
|
||||
type/task fields, descriptions, ownership, supported-parameter lists,
|
||||
capability-looking booleans, or token limits.
|
||||
|
||||
## Provider-Native Readers
|
||||
|
||||
- OpenAI keeps the official Models API identity-only.
|
||||
- OpenRouter maps explicit architecture modalities, supported parameters,
|
||||
limits, voices, and default parameters into family/capability/control state.
|
||||
- Google maps the native Models resource. Embedding-only methods map to the
|
||||
embedding family; content-generation methods do not prove modality or chat
|
||||
family. Explicit thinking, limits, sampling fields, caching, and batch
|
||||
methods are retained without parsing product names.
|
||||
- Ollama treats `/api/tags` as identity-only and maps selected-model
|
||||
`/api/show` capability tokens. Context can come from structured fields or a
|
||||
parsed `num_ctx` line in the serialized `parameters` value.
|
||||
- LM Studio maps native v1 `models[]` and v0-style `data[]` fields. A plain
|
||||
OpenAI-compatible list without native type/capability fields stays unknown.
|
||||
- llama.cpp can merge `/v1/models`, `/props`, and `/slots` evidence for one
|
||||
served model. It records tool/streaming claims, explicit unsupported
|
||||
vision/audio assertions, controls, and runtime/training/size limits.
|
||||
|
||||
Readers tolerate non-object entries and unknown fields where their helpers
|
||||
permit it. They do not infer authoritative capability from model IDs or display
|
||||
names.
|
||||
|
||||
## Evidence Semantics
|
||||
|
||||
The canonical vocabulary includes admin override, endpoint configuration,
|
||||
provider reader, Cookbook/Hugging Face, maintained registries, heuristic,
|
||||
probe, and unknown sources. It also defines explicit, provider-reported,
|
||||
registry, heuristic, and unknown confidence values.
|
||||
|
||||
Those tokens make evidence representable; current `dev` does not implement a
|
||||
global precedence, merge, expiry, or conflict-resolution engine. Assertions
|
||||
generated by readers are usually `claimed`; a `CapabilityProbeResult` maps pass
|
||||
to verified, fail to unsupported, and partial to claimed at the scope carried
|
||||
by that object.
|
||||
|
||||
## Tests
|
||||
|
||||
Focused tests pin:
|
||||
|
||||
- endpoint-kind, host, and common-port vendor detection;
|
||||
- endpoint/base-URL-scoped stable IDs;
|
||||
- unknown behavior for generic and official OpenAI lists;
|
||||
- canonical normalization and display-surface matching;
|
||||
- assertion, deterministic-control, and probe-result shapes;
|
||||
- OpenRouter, Google, Ollama, LM Studio, and llama.cpp mappings;
|
||||
- negative cases that avoid name-based media/capability inference.
|
||||
|
||||
## Current Gaps
|
||||
|
||||
- Canonical records are not yet used by runtime discovery, endpoint resolution, model context, request shaping, or frontend pickers.
|
||||
- Reader output is not persisted, refreshed, merged, or expired.
|
||||
- Provider detection still uses common-port hints; consumers must not promote normalization hints into trust decisions.
|
||||
- Only seven concrete readers exist; placeholder and other providers use the
|
||||
identity-only generic reader.
|
||||
- Generic fallback does not accept bare-list or `key`/`slug`-only payloads.
|
||||
- There is no capability-specific diagnostic/logging path.
|
||||
- Runtime request builders still contain model-name heuristics outside this
|
||||
canonical layer.
|
||||
@@ -1,100 +0,0 @@
|
||||
# Provider Capability Specs
|
||||
|
||||
Last updated: dev@e71f8ce | 2026-08-25
|
||||
|
||||
## Scope
|
||||
|
||||
This directory maps serving-provider observations and current model-catalog
|
||||
normalization into the canonical layer defined by
|
||||
[model-capability-canonical.md](../model-capability-canonical.md). It records
|
||||
current Odysseus implementation evidence, merged fixes, reproducible user
|
||||
observations, and provider documentation without treating any single source as
|
||||
global model truth.
|
||||
|
||||
## General To Specific Resolution
|
||||
|
||||
Read specs in this order:
|
||||
|
||||
1. [openai-compatible.md](openai-compatible.md) for the conservative general
|
||||
identity-only reader;
|
||||
2. the serving-provider file for native endpoints, headers, request/response
|
||||
observations, and catalog fields;
|
||||
3. [model-quirks.md](../model-quirks.md) for model-specific observations.
|
||||
|
||||
Provider files document transport; runtime adapters still own it. Model quirks
|
||||
record only deviations and are not a second runtime matcher. Shared model facts
|
||||
must not be copied into every provider file. An OpenAI-compatible provider is
|
||||
not OpenAI: an explicitly supplied vendor string is preserved even when it uses
|
||||
the generic reader.
|
||||
|
||||
Current reader dispatch does not infer a provider from payload shape. It uses an explicit vendor, then endpoint kind, label-bounded hostname matches, and common local-port hints. The port hints map 11434 to Ollama, 1234 to LM Studio, 8000 to vLLM, and 30000 to SGLang. Those hints are normalization behavior, not endpoint trust.
|
||||
|
||||
## Provider Map
|
||||
|
||||
### Implemented canonical readers
|
||||
|
||||
- [openai.md](openai.md): identity-only Models API plus Chat/Responses dialects.
|
||||
- [openai-compatible.md](openai-compatible.md): generic compatible catalog and runtime dialect boundaries.
|
||||
- [openrouter.md](openrouter.md): rich architecture, modalities, parameters, and limits.
|
||||
- [google.md](google.md): native paginated Gemini Models API and GenerateContent.
|
||||
- [ollama.md](ollama.md): `/api/tags`, `/api/show`, native chat, and OpenAI compatibility.
|
||||
- [lm-studio.md](lm-studio.md): native v1 catalog/chat, explicit v0 compatibility, and OpenAI compatibility.
|
||||
- [llama-cpp.md](llama-cpp.md): `/props`, `/slots`, OpenAI/Responses/Anthropic surfaces.
|
||||
|
||||
### Placeholder identities using the generic reader
|
||||
|
||||
- [anthropic.md](anthropic.md): identity-only Models API and native Messages runtime adapter.
|
||||
- [vllm.md](vllm.md): common-port identity hint; deployment capability remains unknown.
|
||||
- [sglang.md](sglang.md): common-port identity hint; parser/config-dependent capability remains unknown.
|
||||
- [hugging-face.md](hugging-face.md): Hub observations and download/fit metadata without a canonical reader.
|
||||
|
||||
### Provider observations without a dedicated canonical reader
|
||||
|
||||
- [mistral.md](mistral.md): rich model cards, reasoning controls, and structured runtime content.
|
||||
- [github-copilot.md](github-copilot.md): account model-list observations and required runtime headers.
|
||||
- [chatgpt-subscription.md](chatgpt-subscription.md): Codex model identity and Responses event shape.
|
||||
- [cohere.md](cohere.md): native endpoint/catalog observations; not currently normalized.
|
||||
|
||||
### Other provider identity and general/identity-only observations
|
||||
|
||||
- [moonshot-kimi.md](moonshot-kimi.md)
|
||||
- [deepseek.md](deepseek.md)
|
||||
- [groq.md](groq.md)
|
||||
- [nvidia-nim.md](nvidia-nim.md)
|
||||
- [cerebras.md](cerebras.md)
|
||||
- [together.md](together.md)
|
||||
- [fireworks.md](fireworks.md)
|
||||
- [xai.md](xai.md)
|
||||
- [zai.md](zai.md)
|
||||
- [opencode.md](opencode.md)
|
||||
- [perplexity.md](perplexity.md)
|
||||
- [github-models.md](github-models.md)
|
||||
- [venice.md](venice.md)
|
||||
- [azure-openai.md](azure-openai.md)
|
||||
- [bedrock.md](bedrock.md)
|
||||
- [cloudflare-workers-ai.md](cloudflare-workers-ai.md)
|
||||
- [atlas-cloud.md](atlas-cloud.md)
|
||||
- [siliconflow.md](siliconflow.md)
|
||||
- [minimax.md](minimax.md)
|
||||
|
||||
### Other local/proxy serving identities
|
||||
|
||||
- [local-compatible-engines.md](local-compatible-engines.md): MLX LM, TGI,
|
||||
LMDeploy, LiteLLM, and unknown compatible deployments.
|
||||
|
||||
## Provider Spec Template
|
||||
|
||||
Each provider file records:
|
||||
|
||||
- provider identity and API dialects;
|
||||
- latest observed native catalog endpoint/envelope and capability-bearing fields;
|
||||
- whether current source has a dedicated reader or only generic fallback;
|
||||
- observed request, tool, text, reasoning, and control paths owned by runtime
|
||||
adapters rather than the catalog reader;
|
||||
- what remains per-model/unknown;
|
||||
- Odysseus evidence and regressions;
|
||||
- fallback/safety behavior and current gaps.
|
||||
|
||||
Marketing capability lists and curated picker lists may guide research but do
|
||||
not automatically become model claims. Provider-returned false values can be
|
||||
negative evidence only at the same provider/endpoint/model scope.
|
||||
@@ -1,39 +0,0 @@
|
||||
# Anthropic Provider Shape
|
||||
|
||||
Last updated: dev@e71f8ce | 2026-08-25
|
||||
|
||||
## Scope
|
||||
|
||||
Canonical placeholder vendor ID `anthropic`; Anthropic Messages runtime
|
||||
adapter in `src/llm_core.py`. There is no dedicated Anthropic capability-reader
|
||||
module; explicit/auto-detected Anthropic payloads use the generic identity-only
|
||||
reader.
|
||||
|
||||
## Catalog Shape
|
||||
|
||||
`GET /v1/models` returns `data[]` model resources with `id`, `type: model`,
|
||||
`display_name`, and `created_at`, plus pagination metadata. These fields prove
|
||||
identity/availability only. Do not assume all listed Claude models share
|
||||
vision, tools, reasoning, sampling, or context limits.
|
||||
|
||||
## Request And Response Shape
|
||||
|
||||
Native Messages uses a top-level `system`, alternating `messages`, content
|
||||
blocks, `tools[].input_schema`, `tool_use` assistant blocks, and `tool_result`
|
||||
user blocks. Text, thinking, signatures, server-tool blocks, and tool calls are
|
||||
typed content rather than OpenAI roles/fields. Preserve block IDs/signatures
|
||||
needed for continuation.
|
||||
|
||||
Sampling and thinking support can be version/model specific. The Opus 4.7+ sampling omission is a model-scoped runtime observation, not an Anthropic-wide rule. Runtime version parsing accepts explicit major/minor IDs and later major-only IDs such as `claude-opus-5`, treats a missing minor as `.0`, caps both components so date stamps cannot be misread as versions, and keeps legacy Claude 3 Opus sampling intact. Anthropic-compatible proxies are Anthropic dialect only when configured or their exact payload/endpoint shape proves it (#3110).
|
||||
|
||||
## Fallback And Safety
|
||||
|
||||
Runtime and canonical reader detection use label-bounded Anthropic host matching or an explicit endpoint kind. A provider using Anthropic Messages through another host must be explicit. Identity-only model cards remain unknown.
|
||||
|
||||
## Current Gaps
|
||||
|
||||
- The public model list does not provide per-model canonical capability data.
|
||||
- There is no dedicated Anthropic canonical reader; only `id`, `name`, or
|
||||
`model` identity survives generic normalization.
|
||||
- Runtime model-version parsing needs structured identity before a later
|
||||
consumer can centralize sampling exceptions without another name matcher.
|
||||
@@ -1,21 +0,0 @@
|
||||
# Atlas Cloud Provider Shape
|
||||
|
||||
Last updated: dev@28d27ee | 2026-07-17
|
||||
|
||||
## Scope
|
||||
|
||||
Canonical provider ID `atlas_cloud`; OpenAI-compatible provider proposed in
|
||||
#5566 with live `/v1/models` observations for current Qwen/DeepSeek offerings.
|
||||
|
||||
## Shape
|
||||
|
||||
Treat the observed list as identity-only. Even capability-looking item fields
|
||||
remain raw until an Atlas-specific discriminating shape intentionally maps
|
||||
them. The model IDs observed by a PR demonstrate availability at that time,
|
||||
not permanent capability or a reason to hardcode family-name behavior.
|
||||
|
||||
## Fallback And Current Gaps
|
||||
|
||||
Exact Atlas Cloud host or explicit kind preserves identity; otherwise use the
|
||||
inventory fallback. The provider work is open/unmerged and has no independently
|
||||
versioned rich catalog schema, so evidence remains provisional.
|
||||
@@ -1,26 +0,0 @@
|
||||
# Azure OpenAI Provider Shape
|
||||
|
||||
Last updated: dev@28d27ee | 2026-07-17
|
||||
|
||||
## Scope
|
||||
|
||||
Canonical provider ID `azure_openai`; Azure deployment-scoped OpenAI dialects;
|
||||
custom endpoints use explicit configuration.
|
||||
|
||||
## Shape
|
||||
|
||||
Azure commonly identifies deployments rather than globally stable model IDs.
|
||||
Preserve endpoint, deployment ID, API version, and underlying model/version as
|
||||
separate structured identity when returned. A standard OpenAI-compatible model
|
||||
list is identity-only until an Azure-specific reader intentionally maps its
|
||||
deployment fields.
|
||||
|
||||
Request paths and authentication can be deployment/API-version specific; do
|
||||
not blindly append public OpenAI paths or copy provider quirks. Capability and
|
||||
limits are deployment scoped.
|
||||
|
||||
## Fallback And Current Gaps
|
||||
|
||||
Known `*.openai.azure.com` hosts select Azure OpenAI; other Azure gateways need
|
||||
explicit kind. Odysseus lacks a native Azure deployment catalog reader and
|
||||
structured API-version persistence in the canonical record.
|
||||
@@ -1,23 +0,0 @@
|
||||
# AWS Bedrock Provider Shape
|
||||
|
||||
Last updated: dev@28d27ee | 2026-07-17
|
||||
|
||||
## Scope
|
||||
|
||||
Canonical provider ID `bedrock`; UI/provider mapping currently recognizes AWS
|
||||
Bedrock, but the canonical layer has no native Bedrock runtime reader.
|
||||
|
||||
## Shape
|
||||
|
||||
Bedrock is not generally an OpenAI-compatible host: model IDs, inference
|
||||
profiles, request/response unions, signing, and per-family payloads differ.
|
||||
Only an explicitly configured OpenAI/Anthropic-compatible gateway may use those
|
||||
dialects. Native Bedrock capability must come from a versioned Bedrock model
|
||||
catalog plus exact foundation-model/inference-profile identity.
|
||||
|
||||
## Fallback And Current Gaps
|
||||
|
||||
Do not classify all `amazonaws.com` hosts as Bedrock; use explicit kind or a
|
||||
future region-aware exact host/path shape. General fallback is safe only behind
|
||||
an explicitly compatible gateway. Native signing, catalogs, and family payload
|
||||
mappings remain unimplemented.
|
||||
@@ -1,23 +0,0 @@
|
||||
# Cerebras Provider Shape
|
||||
|
||||
Last updated: dev@28d27ee | 2026-07-17
|
||||
|
||||
## Scope
|
||||
|
||||
Canonical provider ID `cerebras`; OpenAI-compatible cloud transport; runtime
|
||||
provider detection and cache-affinity safeguards in `src/llm_core.py`.
|
||||
|
||||
## Shape And Observations
|
||||
|
||||
Model lists use the general identity-only inventory reader. Cerebras rejects
|
||||
llama.cpp-only `session_id` and `cache_prompt` fields (#4640), so cloud identity
|
||||
must suppress local slot-affinity extensions. Current regressions pin this
|
||||
provider boundary.
|
||||
|
||||
Tool, reasoning, structured output, and limits remain per model. Do not promote
|
||||
them from the fact that the API accepts OpenAI Chat.
|
||||
|
||||
## Fallback And Current Gaps
|
||||
|
||||
Exact `*.cerebras.ai` selects provider identity. Compatible proxies require
|
||||
explicit configuration. No rich per-model Cerebras catalog reader is present.
|
||||
@@ -1,47 +0,0 @@
|
||||
# ChatGPT Subscription Provider Shape
|
||||
|
||||
Last updated: dev@e57f60b | 2026-07-20
|
||||
|
||||
## Scope
|
||||
|
||||
Canonical provider ID `chatgpt_subscription`; Codex Responses transport;
|
||||
auth and runtime code in `src/chatgpt_subscription.py`,
|
||||
`routes/chatgpt_subscription_routes.py`, and `src/llm_core.py`.
|
||||
There is no dedicated ChatGPT Subscription canonical reader on current `dev`.
|
||||
|
||||
## Catalog Shape
|
||||
|
||||
The account-scoped Codex models endpoint returns root `models[]`; `slug` is the
|
||||
request identity and `visibility`/`priority` control availability/order. These
|
||||
fields do not prove tools, reasoning, vision, or context. Null/malformed model
|
||||
lists fail soft rather than crashing discovery (#5280/#5281).
|
||||
|
||||
The canonical generic reader does not accept `slug`-only items, so this runtime
|
||||
catalog is not currently normalized into `ModelCapabilityRecord` values.
|
||||
|
||||
## Request And Response Shape
|
||||
|
||||
Transport uses a ChatGPT backend Responses endpoint, `input` items, flattened
|
||||
function tools, streamed function-call argument events, exact `call_id`, and
|
||||
`function_call_output` continuation. Parallel calls and encrypted reasoning
|
||||
continuity require preserving typed output/history rather than coercing all
|
||||
roles to text. This shape is supported by the existing adapter and the focused
|
||||
tool-calling follow-up evidence in #5490; unmerged observations remain claimed
|
||||
until integrated/reproduced.
|
||||
|
||||
OAuth/device credentials and refresh are provider-session behavior. Expired
|
||||
credentials should return an actionable reconnect error, not generic model
|
||||
failure.
|
||||
|
||||
## Fallback And Safety
|
||||
|
||||
Only the explicit internal base/ChatGPT host selects this provider. Never send
|
||||
subscription credentials to a custom OpenAI-compatible URL. Catalog slugs stay
|
||||
identity-only unless account-scoped fields or probes supply capability.
|
||||
|
||||
## Current Gaps
|
||||
|
||||
- Comprehensive Responses tool/reasoning parity is still evolving.
|
||||
- Account model slugs are not consumed by the canonical reader package.
|
||||
- The account catalog does not currently provide a complete canonical
|
||||
capability card for every slug.
|
||||
@@ -1,21 +0,0 @@
|
||||
# Cloudflare Workers AI Provider Shape
|
||||
|
||||
Last updated: dev@28d27ee | 2026-07-17
|
||||
|
||||
## Scope
|
||||
|
||||
Canonical provider ID `cloudflare_workers_ai`; OpenAI-compatible Workers AI
|
||||
endpoint observations in #5175; explicit provider configuration required.
|
||||
|
||||
## Shape
|
||||
|
||||
Cloudflare account/path identity is part of the endpoint. Use the general
|
||||
OpenAI-compatible inventory reader for returned model cards, preserving full
|
||||
model IDs but no capability fields.
|
||||
Do not identify the provider from broad `api.cloudflare.com` alone or infer
|
||||
capability from Workers AI catalog prose.
|
||||
|
||||
## Fallback And Current Gaps
|
||||
|
||||
Provider identity must be explicit until a narrow account/AI path matcher is
|
||||
implemented. There is no rich normalized capability catalog reader.
|
||||
@@ -1,56 +0,0 @@
|
||||
# Cohere Provider Shape
|
||||
|
||||
Last updated: dev@e57f60b | 2026-07-20
|
||||
|
||||
## Scope
|
||||
|
||||
Documented provider identity `cohere`; native Chat v2 plus the OpenAI
|
||||
Compatibility API. Current `dev` has no dedicated Cohere capability reader or
|
||||
direct Cohere request adapter; compatible endpoints use the general runtime
|
||||
path when explicitly configured.
|
||||
|
||||
## Catalog Shape
|
||||
|
||||
`GET /v1/models` returns a paginated `models[]` envelope. Each model can carry
|
||||
`name`, `endpoints`, `default_endpoints`, `context_length`, `features`, and
|
||||
`sampling_defaults`; the root can carry `next_page_token`.
|
||||
|
||||
These are candidate fields for a future dedicated reader:
|
||||
|
||||
- a single canonical family from `endpoints`: `chat`/`generate`, `embed`,
|
||||
`rerank`, or `classify`;
|
||||
- `context_length` to the endpoint/model context limit;
|
||||
- known sampling-default keys to deterministic controls.
|
||||
|
||||
Current canonical normalization does not map them. When the generic reader is
|
||||
explicitly selected with vendor `cohere`, it preserves only item identity plus
|
||||
the raw item; family, context, features, and sampling controls stay unknown.
|
||||
|
||||
## Request And Response Shape
|
||||
|
||||
Native `POST /v2/chat` uses `messages`, structured content blocks, tools,
|
||||
`response_format`, sampling fields, and an optional structured `thinking`
|
||||
object. Text lives in `message.content[type=text].text`; reasoning-capable
|
||||
models use `message.content[type=thinking].thinking`. Streaming uses typed
|
||||
events rather than one generic text delta.
|
||||
|
||||
The OpenAI compatibility base is `/compatibility/v1`. Its current chat subset
|
||||
includes tools, structured output, sampling, and `reasoning_effort`, but model
|
||||
support remains per-model. In the compatibility dialect only `none` and `high`
|
||||
currently map to native thinking off/on; do not assume low/medium support.
|
||||
|
||||
## Fallback And Safety
|
||||
|
||||
No Cohere host or payload-shape detection exists in the canonical reader
|
||||
registry. The caller must supply provider/endpoint configuration. Marketing
|
||||
pages and provider-wide endpoint features do not grant every listed model
|
||||
tools, vision, or reasoning.
|
||||
|
||||
## Evidence And Gaps
|
||||
|
||||
- Official List/Get Models resources define the catalog fields.
|
||||
- Official Chat v2, Reasoning, and Compatibility API resources define the
|
||||
transport and thinking controls.
|
||||
- Odysseus has no direct Cohere request adapter, canonical reader, or sanitized
|
||||
canonical fixtures yet; both normalization and runtime integration remain
|
||||
follow-up work.
|
||||
@@ -1,30 +0,0 @@
|
||||
# DeepSeek Provider Shape
|
||||
|
||||
Last updated: dev@2e2bb52 | 2026-08-16
|
||||
|
||||
## Scope
|
||||
|
||||
Canonical provider ID `deepseek`; official cloud OpenAI-compatible API;
|
||||
curation/detection in `routes/model_routes.py` and runtime reasoning handling in
|
||||
`src/llm_core.py`.
|
||||
|
||||
## Shape And Observations
|
||||
|
||||
Use the general model-list inventory shape; capability-looking fields remain
|
||||
unknown until a DeepSeek-native reader maps them. Cloud response history can use
|
||||
`reasoning_content`; preserve it structurally for reasoning turns and tool
|
||||
continuation (#968, #3152). `deepseek-chat`, reasoning models, distilled local
|
||||
variants, and future V4 models do not share one capability record.
|
||||
|
||||
Cloud endpoint evidence can support tools while a local DeepSeek-R1 deployment
|
||||
may not have a working tool parser. Existing tool-support tests intentionally
|
||||
separate official host from local engine/model-name heuristics.
|
||||
|
||||
Current runtime thinking-pattern detection includes DeepSeek V4 identifiers so their structured reasoning channel is handled like the other supported DeepSeek reasoning families. This name-level compatibility rule is not canonical capability evidence and does not make every V4-labelled local deployment tool-capable.
|
||||
|
||||
## Fallback And Current Gaps
|
||||
|
||||
Exact `*.deepseek.com` selects provider identity; self-hosted checkpoints use
|
||||
Ollama/vLLM/SGLang/llama.cpp identity. Curated model IDs and pricing/context
|
||||
tables are compatibility data, not authoritative capability. A rich official
|
||||
model-card reader is still absent.
|
||||
@@ -1,22 +0,0 @@
|
||||
# Fireworks AI Provider Shape
|
||||
|
||||
Last updated: dev@28d27ee | 2026-07-17
|
||||
|
||||
## Scope
|
||||
|
||||
Canonical provider ID `fireworks`; OpenAI-compatible cloud transport with path
|
||||
prefixes such as `/inference/v1`; curation and URL handling in
|
||||
`routes/model_routes.py` and `src/endpoint_resolver.py`.
|
||||
|
||||
## Shape
|
||||
|
||||
Use the general identity-only inventory reader. Fireworks IDs can contain
|
||||
account/model paths; preserve the full request ID and endpoint scope. Item
|
||||
modalities, supported parameters, task/type, and limits require a
|
||||
Fireworks-native mapped shape before promotion.
|
||||
|
||||
## Fallback And Current Gaps
|
||||
|
||||
Exact `*.fireworks.ai` preserves provider identity and its configured path
|
||||
prefix. Do not normalize account-qualified IDs by taking the last path segment.
|
||||
No verified rich Fireworks capability catalog is currently mapped.
|
||||
@@ -1,46 +0,0 @@
|
||||
# GitHub Copilot Provider Shape
|
||||
|
||||
Last updated: dev@e57f60b | 2026-07-20
|
||||
|
||||
## Scope
|
||||
|
||||
Canonical provider ID `copilot`; OpenAI-compatible chat with Copilot headers
|
||||
and OAuth; runtime adapter `src/copilot.py` and routes in
|
||||
`routes/copilot_routes.py`. There is no dedicated Copilot canonical reader on
|
||||
current `dev`.
|
||||
|
||||
## Catalog Shape
|
||||
|
||||
The observed Copilot `/models` response uses `data[]` entries with:
|
||||
|
||||
- `id`;
|
||||
- `model_picker_enabled`;
|
||||
- `capabilities.supports.tool_calls` and `.vision`;
|
||||
- optional limit/family metadata.
|
||||
|
||||
Runtime model discovery uses picker state for availability. The canonical
|
||||
reader package does not map the nested support fields; an explicitly supplied
|
||||
`copilot` vendor currently uses generic identity-only normalization, and
|
||||
`model_picker_enabled` does not become canonical capability.
|
||||
|
||||
## Request And Response Shape
|
||||
|
||||
Chat is OpenAI-compatible but requires Copilot/GitHub API version, editor/plugin
|
||||
identity, intent, integration, and initiator headers; image requests add the
|
||||
vision request flag. Header derivation must tolerate malformed message entries.
|
||||
OAuth token exchange and access policies are provider authentication, not model
|
||||
capability.
|
||||
|
||||
## Fallback And Safety
|
||||
|
||||
Use exact GitHub Copilot host or explicit kind, including the constrained
|
||||
enterprise `copilot-api.*.ghe.com` form. Do not treat arbitrary `ghe.com` hosts
|
||||
as Copilot. Official model availability tables are useful registry context but
|
||||
do not replace the account-scoped catalog response.
|
||||
|
||||
## Current Gaps
|
||||
|
||||
- The catalog shape is implementation-observed and needs ongoing fixture
|
||||
comparison with current Copilot clients.
|
||||
- Copilot catalog capability fields are not normalized by current `dev`.
|
||||
- Account/plan/policy availability must remain endpoint-user scoped.
|
||||
@@ -1,21 +0,0 @@
|
||||
# GitHub Models Provider Shape
|
||||
|
||||
Last updated: dev@28d27ee | 2026-07-17
|
||||
|
||||
## Scope
|
||||
|
||||
Canonical provider ID `github_models`; OpenAI-compatible GitHub Models/Azure
|
||||
inference endpoint observed in #2995; distinct from GitHub Copilot.
|
||||
|
||||
## Shape
|
||||
|
||||
Use general identity-only inventory. Deployment IDs and account access
|
||||
can differ from upstream model IDs. Do not copy Copilot picker metadata,
|
||||
headers, plan rules, or capabilities into GitHub Models; they are separate
|
||||
providers despite shared GitHub branding.
|
||||
|
||||
## Fallback And Current Gaps
|
||||
|
||||
The known `models.inference.ai.azure.com` host selects GitHub Models. Other
|
||||
Azure deployment hosts require explicit provider configuration. No rich
|
||||
account-scoped capability catalog is currently mapped.
|
||||
@@ -1,54 +0,0 @@
|
||||
# Google Gemini Provider Shape
|
||||
|
||||
Last updated: dev@e71f8ce | 2026-08-25
|
||||
|
||||
## Scope
|
||||
|
||||
Canonical provider ID `google`; native GenerateContent plus optional Google
|
||||
OpenAI-compatible chat; readers `google.py` and
|
||||
`google_ai_studio_mapping.py`; catalog/probe ownership in
|
||||
`routes/model_routes.py`.
|
||||
|
||||
## Catalog Shape
|
||||
|
||||
Use the native paginated `GET /v1beta/models` endpoint, including
|
||||
`nextPageToken`, with `x-goog-api-key` when configured. `models[]` can contain:
|
||||
|
||||
- `name`, `baseModelId`, `version`, and `displayName`;
|
||||
- `inputTokenLimit` and `outputTokenLimit`;
|
||||
- `supportedGenerationMethods`;
|
||||
- `thinking`, `temperature`, `maxTemperature`, `topP`, and `topK`.
|
||||
|
||||
Embedding-only methods map to embedding. Generation methods prove a native
|
||||
method, not chat/image/video/audio modality, so those records remain unknown
|
||||
unless stronger structured evidence exists. `thinking: true` and explicit
|
||||
sampling fields map to a reasoning claim and controls. Model IDs such as
|
||||
Imagen, Veo, or TTS names are not parsed.
|
||||
|
||||
## Request And Response Shape
|
||||
|
||||
Native generation uses `contents`, `systemInstruction`,
|
||||
`generationConfig`, `tools[].functionDeclarations`, and
|
||||
`models/{model}:generateContent|streamGenerateContent`. Responses use
|
||||
`candidates[].content.parts[]` for `text`, `functionCall`, `functionResponse`,
|
||||
`thought`, and `thoughtSignature`; token accounting is in `usageMetadata`.
|
||||
Native Google tool/thought continuity must not be flattened through an
|
||||
OpenAI-only history shape.
|
||||
|
||||
## Fallback And Safety
|
||||
|
||||
Prefer native model metadata even when chat is configured through Google's
|
||||
OpenAI compatibility URL. Pagination parameters must remain stable between
|
||||
pages. The route probe activates only for the exact
|
||||
`generativelanguage.googleapis.com` hostname, filters the picker list to
|
||||
content-generation methods, returns no curated fallback after probe failure,
|
||||
and defaults those endpoints to manual catalog refresh unless explicitly
|
||||
overridden. The canonical Google reader is not yet called by that probe.
|
||||
Unknown methods and fields stay raw; unrecognized prediction models remain
|
||||
unknown.
|
||||
|
||||
## Current Gaps
|
||||
|
||||
- The Models resource does not expose full modalities for every Google media
|
||||
family.
|
||||
- Native Gemini request/response support is not yet the only runtime path.
|
||||
@@ -1,24 +0,0 @@
|
||||
# Groq Provider Shape
|
||||
|
||||
Last updated: dev@28d27ee | 2026-07-17
|
||||
|
||||
## Scope
|
||||
|
||||
Canonical provider ID `groq`; OpenAI-compatible cloud transport; detection and
|
||||
request behavior in `src/llm_core.py`.
|
||||
|
||||
## Shape
|
||||
|
||||
Model discovery falls back to the general `data[].id` identity shape. Richer
|
||||
fields require a Groq-native mapped shape even when the payload happens to
|
||||
supply modalities, supported parameters, or limits. Groq transport may accept OpenAI-style tools and streaming extensions,
|
||||
but support remains per model and account.
|
||||
|
||||
Runtime currently exempts Groq/OpenRouter from some parameter stripping paths;
|
||||
that is transport compatibility, not a provider-wide model capability claim.
|
||||
|
||||
## Fallback And Current Gaps
|
||||
|
||||
Exact `*.groq.com` preserves Groq identity. Do not infer Llama/Gemma model
|
||||
capabilities from IDs. There is no canonical rich Groq model-card reader or
|
||||
freshness policy yet.
|
||||
@@ -1,41 +0,0 @@
|
||||
# Hugging Face Provider And Registry Shape
|
||||
|
||||
Last updated: dev@e57f60b | 2026-07-20
|
||||
|
||||
## Scope
|
||||
|
||||
Canonical placeholder vendor ID `huggingface`; download/fit metadata in
|
||||
`services/hwfit/`; OpenAI-compatible inference providers/TGI handled as their
|
||||
serving dialect. There is no dedicated Hugging Face canonical reader on
|
||||
current `dev`.
|
||||
|
||||
## Hub Model Shape
|
||||
|
||||
Hub model info can provide `modelId`/`id`, `pipeline_tag`, `tags`, `config`, and
|
||||
card metadata. Current canonical normalization does not map `pipeline_tag`,
|
||||
`config.model_type`, or Hub task/modality fields. An explicitly selected
|
||||
Hugging Face vendor uses generic identity-only normalization.
|
||||
|
||||
This source is `cookbook_hf`/registry confidence, not live endpoint truth.
|
||||
Free-form tags, README/card prose, repository names, and architecture names do
|
||||
not automatically claim capability. A serving engine can load a model with
|
||||
missing projection, different template, or disabled parser.
|
||||
|
||||
## Serving Shape
|
||||
|
||||
Hugging Face routed inference and TGI can expose OpenAI-compatible endpoints;
|
||||
their model list may be identity-only. Keep Hub identity separate from the
|
||||
serving endpoint and merge only when exact revision/model identity is known.
|
||||
|
||||
## Fallback And Safety
|
||||
|
||||
Hub metadata can fill a scoped registry record after provider payload fields
|
||||
and probes, but must not overwrite fresh endpoint-negative evidence. Treat
|
||||
remote code, model cards, and repository files as untrusted content.
|
||||
|
||||
## Current Gaps
|
||||
|
||||
- Revision/digest linkage between downloads, Hub records, and serving
|
||||
endpoints is incomplete.
|
||||
- Hub task/family metadata is not consumed by the canonical reader package.
|
||||
- Pipeline tags can be missing or overly broad; unknown stays unknown.
|
||||
@@ -1,47 +0,0 @@
|
||||
# llama.cpp Provider Shape
|
||||
|
||||
Last updated: dev@e57f60b | 2026-07-20
|
||||
|
||||
## Scope
|
||||
|
||||
Canonical provider ID `llamacpp`; OpenAI Chat/Responses and Anthropic Messages
|
||||
compatibility plus native server metadata; reader
|
||||
`src/model_capability_readers/llamacpp.py`.
|
||||
|
||||
## Metadata Shapes
|
||||
|
||||
`/v1/models` provides served identity and can include server model entries;
|
||||
native `/props` is authoritative for the running model/server combination:
|
||||
|
||||
- `model_alias`/`model_path`;
|
||||
- `default_generation_settings.n_ctx` and sampling `params`;
|
||||
- `total_slots` and optional `/slots[].n_ctx` fallback;
|
||||
- `chat_template_caps` for tools/system role;
|
||||
- `modalities.vision|audio`;
|
||||
- current server/build state.
|
||||
|
||||
Capability depends on weights, projection/model assets, chat template, parser,
|
||||
and launch flags. It is endpoint evidence, not a checkpoint-name claim.
|
||||
`/props` and `/v1/models` can be merged only for the same served identity.
|
||||
|
||||
## Request And Response Shape
|
||||
|
||||
llama-server supports several OpenAI-compatible tasks and native extensions.
|
||||
Do not infer embeddings/rerank/chat solely from the OpenAI model card; use an
|
||||
explicit server model capability field or endpoint configuration. Tool and
|
||||
reasoning correctness can depend on selected chat template and parser.
|
||||
|
||||
## Fallback And Safety
|
||||
|
||||
The registry selects llama.cpp through an explicit vendor or endpoint kind; it
|
||||
does not auto-detect `/props` from payload shape. Port 8000 currently maps to
|
||||
the vLLM placeholder, while 8080 falls through to generic OpenAI-compatible.
|
||||
llama.cpp-only `session_id` and `cache_prompt` affinity fields must remain local
|
||||
endpoint behavior and never leak to strict cloud providers (#4640 and current
|
||||
affinity tests).
|
||||
|
||||
## Current Gaps
|
||||
|
||||
- Multi-model routing requires per-served-model `/props` association.
|
||||
- Parser/template configuration is not yet fully represented in canonical
|
||||
endpoint metadata.
|
||||
@@ -1,45 +0,0 @@
|
||||
# LM Studio Provider Shape
|
||||
|
||||
Last updated: dev@e57f60b | 2026-07-20
|
||||
|
||||
## Scope
|
||||
|
||||
Canonical provider ID `lmstudio`; native LM Studio v1 plus OpenAI Chat and
|
||||
Responses compatibility; reader `src/model_capability_readers/lmstudio.py`.
|
||||
|
||||
## Catalog Shapes
|
||||
|
||||
Preferred shape is `GET /api/v1/models` with root `models[]`. Current fields
|
||||
include `key`, `type` (`llm` or `embedding`), display/publisher data,
|
||||
`architecture`, quantization/format/size, `max_context_length`,
|
||||
`loaded_instances[].config.context_length`, and a capability object containing
|
||||
`vision`, `trained_for_tool_use`, and reasoning options/defaults.
|
||||
|
||||
Compatibility shape `GET /api/v0/models` uses `data[]` with `id`, `type`
|
||||
(`llm`, `vlm`, or embeddings), `arch`, `compatibility_type`, state, and
|
||||
context metadata. It is an explicit older shape, not a loose fallback.
|
||||
OpenAI `/v1/models` is identity-only when native endpoints are unavailable.
|
||||
|
||||
Loaded-instance context is the effective runtime context; maximum context is a
|
||||
separate limit. Model type maps family, explicit capability booleans map
|
||||
vision/tools/reasoning, and architecture is provider-reported model family.
|
||||
|
||||
## Request And Response Shape
|
||||
|
||||
Native v1 chat is `/api/v1/chat` and can expose stateful/MCP-oriented output;
|
||||
LM Studio also supports OpenAI Chat and Responses compatibility. Keep dialect
|
||||
selection explicit because tool/MCP features differ between native and
|
||||
compatible paths.
|
||||
|
||||
## Fallback And Safety
|
||||
|
||||
Current reader detection identifies port 1234 as LM Studio. Prefer pathless
|
||||
native `/api/v1/models` discovery where configured (#1122, #3615), then v0,
|
||||
then general identity. The port mapping is a normalization hint, not endpoint
|
||||
trust. An error object from an unsupported native route is not a model list.
|
||||
|
||||
## Current Gaps
|
||||
|
||||
- Runtime discovery does not yet persist native capability records.
|
||||
- LM Studio API capabilities continue to evolve; each new native version needs
|
||||
an explicit shape fixture before promotion.
|
||||
@@ -1,37 +0,0 @@
|
||||
# Other Local And Proxy Compatible Engines
|
||||
|
||||
Last updated: dev@28d27ee | 2026-07-17
|
||||
|
||||
## Scope
|
||||
|
||||
Canonical explicit identities `mlx_lm`, `text_generation_inference`,
|
||||
`lmdeploy`, and `litellm`, plus unknown OpenAI-compatible deployments not
|
||||
covered by the native Ollama, LM Studio, llama.cpp, vLLM, or SGLang specs.
|
||||
|
||||
## Shape
|
||||
|
||||
Use explicit endpoint kind when known; otherwise use only the general model
|
||||
list envelopes for inventory identity. Capability-looking structural fields
|
||||
remain raw. Local host and port do not distinguish these engines.
|
||||
MLX/Cookbook launch recipes, TGI task configuration, LMDeploy
|
||||
adapters, and LiteLLM upstream routing can all change capability independently
|
||||
of the model ID.
|
||||
|
||||
Proxy model aliases are endpoint scoped. A proxy may return richer fields, but
|
||||
unknown keys remain raw until a versioned shape is added. Provider-specific
|
||||
headers/extensions must not be applied based on a port or upstream model name.
|
||||
|
||||
## Fallback And Safety
|
||||
|
||||
Discovery can probe cheap native identity endpoints when available, but
|
||||
capability probes execute only explicit bounded test contracts. Never read
|
||||
broad server/environment dumps as ordinary model metadata. Unknown compatible
|
||||
servers should still list identities and make conservative text calls where
|
||||
explicitly configured, without appearing on capability-gated surfaces.
|
||||
|
||||
## Current Gaps
|
||||
|
||||
- These engines need individual safe metadata fixtures before they can graduate
|
||||
from general fallback.
|
||||
- Gateway upstream identity and effective downstream model capability are not
|
||||
yet represented as a chain.
|
||||
@@ -1,48 +0,0 @@
|
||||
# MiniMax Provider Shape
|
||||
|
||||
Last updated: dev@28d27ee | 2026-07-17
|
||||
|
||||
## Scope
|
||||
|
||||
Canonical provider ID `minimax`; international host `api.minimax.io`, China
|
||||
host `api.minimaxi.com`; current OpenAI-compatible and recommended
|
||||
Anthropic-compatible text transports. Odysseus contains MiniMax-oriented tool
|
||||
output handling and local-serving guidance but no dedicated catalog reader.
|
||||
|
||||
## Catalog Shape
|
||||
|
||||
Current `GET /v1/models` is an OpenAI-compatible identity list:
|
||||
`object: list`, `data[]`, and model cards containing `id`, `object: model`,
|
||||
`created`, and `owned_by: minimax`. The `owned_by` discriminator identifies the
|
||||
provider shape, but the card exposes no per-model capability or modality
|
||||
fields. Keep these records unknown and preserve raw identity metadata.
|
||||
|
||||
Do not backfill current model capabilities, token limits, or modalities from
|
||||
the platform overview into this list response. Those tables are useful scoped
|
||||
registry evidence only after model/version identity and freshness are carried
|
||||
explicitly.
|
||||
|
||||
## Request And Response Shape
|
||||
|
||||
- OpenAI compatibility uses `/v1/chat/completions` and structured
|
||||
`reasoning_content` alongside normal message content.
|
||||
- Anthropic compatibility uses `/anthropic/v1/messages`; the current M2.7
|
||||
family supports typed thinking blocks and interleaved thinking, making this
|
||||
the preferred reasoning/tool-continuation transport in provider guidance.
|
||||
- Native audio, image, video, music, and file endpoints are separate product
|
||||
shapes. They must not be inferred from presence in the text model list.
|
||||
|
||||
## Local Deployments
|
||||
|
||||
The current provider guide documents vLLM, SGLang, and MLX deployment. Those
|
||||
instances retain serving-engine identity and configuration-derived capability;
|
||||
the checkpoint name alone does not turn a vLLM/SGLang card into the hosted
|
||||
MiniMax provider shape.
|
||||
|
||||
## Fallback And Current Gaps
|
||||
|
||||
Exact MiniMax hosts or the discriminating `owned_by: minimax` model-list shape
|
||||
select provider identity. Unknown compatible proxies retain the general shape.
|
||||
The identity list does not safely distinguish M2 reasoning behavior from
|
||||
speech/image/video/music products, so exact model quirks remain documentation
|
||||
until structured model-version evidence reaches runtime request builders.
|
||||
@@ -1,46 +0,0 @@
|
||||
# Mistral Provider Shape
|
||||
|
||||
Last updated: dev@2e2bb52 | 2026-08-16
|
||||
|
||||
## Scope
|
||||
|
||||
Canonical provider ID `mistral`; OpenAI-compatible chat with Mistral response
|
||||
extensions and runtime handling in `src/llm_core.py`. There is no dedicated
|
||||
Mistral canonical reader on current `dev`.
|
||||
|
||||
## Catalog Shape
|
||||
|
||||
`GET /v1/models` returns `data[]` cards with `id`, `root`, aliases,
|
||||
`max_context_length`, and `capabilities` booleans including
|
||||
`completion_chat`, `completion_fim`, `function_calling`, `vision`,
|
||||
`classification`, and lifecycle/fine-tuning fields. These are candidate fields
|
||||
for a future dedicated reader:
|
||||
|
||||
- chat/FIM or classification family;
|
||||
- vision input;
|
||||
- function calling;
|
||||
- explicitly reported reasoning/structured output when present;
|
||||
- context limit and root family.
|
||||
|
||||
Fine-tuning availability and archived status are not inference capabilities.
|
||||
The current generic reader retains identity/raw data only and does not map any
|
||||
of these fields. Different Mistral models retain independent identities.
|
||||
|
||||
## Request And Response Shape
|
||||
|
||||
Reasoning-capable models accept graded `reasoning_effort`. Mistral can return `content` as typed blocks: a `thinking` block containing text fragments plus a normal `text` block. Runtime normalizes those blocks for async utility calls as well as chat/stream paths, keeping reasoning and visible text separate instead of stringifying the list or scanning text tags (#4698, #5882).
|
||||
|
||||
## Fallback And Safety
|
||||
|
||||
Runtime `llm_core` detects label-bounded Mistral hosts for request/response
|
||||
handling. The canonical registry has no Mistral host or rich-payload detector;
|
||||
an explicitly supplied `mistral` vendor falls back to generic identity. A
|
||||
Mistral model served through another engine uses that serving engine's dialect.
|
||||
|
||||
## Current Gaps
|
||||
|
||||
- Catalog reasoning fields vary across model-card generations; absent remains
|
||||
unknown.
|
||||
- Mistral catalog capability fields are not normalized by current `dev`.
|
||||
- Runtime thinking-family selection still uses names and should migrate to
|
||||
structured root/capability identity.
|
||||
@@ -1,30 +0,0 @@
|
||||
# Moonshot And Kimi Provider Shape
|
||||
|
||||
Last updated: dev@28d27ee | 2026-07-17
|
||||
|
||||
## Scope
|
||||
|
||||
Provider IDs `moonshot` for official Moonshot API and `kimi_code` for the Kimi
|
||||
Code surface; OpenAI-compatible transport with provider-specific headers and
|
||||
model-specific behavior in `src/llm_core.py`.
|
||||
|
||||
## Shape And Observations
|
||||
|
||||
Model lists use the general OpenAI-compatible identity shape unless a richer
|
||||
account response is returned. Official Kimi K2.5/K2.6 fixes temperature by
|
||||
thinking mode, so Odysseus omits `temperature` rather than sending an invalid
|
||||
value (#3960). Thinking tool-call continuation requires preservation of
|
||||
assistant `reasoning_content` (#3118). Kimi Code negotiates a small exact
|
||||
User-Agent set on 403 and caches the accepted value; this is provider transport,
|
||||
not model capability.
|
||||
|
||||
Reports distinguish K2.5/K2.6 multimodality from older K2 variants (#2522).
|
||||
Promote those claims only through exact structured model IDs/families, not a
|
||||
`kimi` name match.
|
||||
|
||||
## Fallback And Current Gaps
|
||||
|
||||
Keep Moonshot and Kimi Code identities distinct even when both use OpenAI Chat.
|
||||
Self-hosted Kimi checkpoints inherit their serving engine shape, not official
|
||||
Moonshot sampling rules. The provider catalog does not yet yield a complete
|
||||
canonical capability card.
|
||||
@@ -1,28 +0,0 @@
|
||||
# NVIDIA NIM Provider Shape
|
||||
|
||||
Last updated: dev@28d27ee | 2026-07-17
|
||||
|
||||
## Scope
|
||||
|
||||
Canonical provider ID `nvidia`; OpenAI-compatible NVIDIA/NIM endpoints; current
|
||||
provider detection, catalog routing, and reasoning stream handling in
|
||||
`src/llm_core.py`, `routes/model_routes.py`, and tests.
|
||||
|
||||
## Shape And Observations
|
||||
|
||||
Model lists use the general identity-only shape; capability-looking fields
|
||||
require a provider-native mapped shape.
|
||||
NIM/vLLM-style responses have emitted structured `reasoning` while older paths
|
||||
used `reasoning_content`; Odysseus routes either to the reasoning channel
|
||||
(#602). This response compatibility does not claim that every NIM model
|
||||
reasons.
|
||||
|
||||
NVIDIA endpoints can host many unrelated model families with different tools,
|
||||
vision, context, and parser support. Keep endpoint/model stable identity and
|
||||
prefer provider fields or probes.
|
||||
|
||||
## Fallback And Current Gaps
|
||||
|
||||
Exact NVIDIA host preserves provider identity; private NIM installations need
|
||||
explicit endpoint kind because a local port/hostname is not distinctive. No
|
||||
safe normalized native NIM capability endpoint is currently consumed.
|
||||
@@ -1,52 +0,0 @@
|
||||
# Ollama Provider Shape
|
||||
|
||||
Last updated: dev@e71f8ce | 2026-08-25
|
||||
|
||||
## Scope
|
||||
|
||||
Canonical provider ID `ollama`; native Ollama chat/generate plus OpenAI
|
||||
compatibility; reader `src/model_capability_readers/ollama.py`; discovery and
|
||||
runtime code in `routes/model_routes.py` and `src/llm_core.py`.
|
||||
|
||||
## Catalog And Detail Shapes
|
||||
|
||||
Use two native steps:
|
||||
|
||||
1. `GET /api/tags` returns `models[]` identity (`name`/`model`, digest,
|
||||
`details.family|families`, format, parameter size, quantization). Tags do not
|
||||
claim capabilities.
|
||||
2. `POST /api/show` for a selected model returns explicit `capabilities[]`,
|
||||
`details`, and `model_info`. Map completion/chat, embedding, vision, tools,
|
||||
and thinking/reasoning tokens. Map context from exact `context_length` or
|
||||
native `<architecture>.context_length` fields.
|
||||
|
||||
The reader does not parse model names or architecture names. It does parse a
|
||||
two-column serialized `parameters` value and can take `num_ctx` from it before
|
||||
falling back to exact or suffix `*.context_length` keys in structured mappings.
|
||||
The parameters text is used only for that keyed limit lookup, not capability
|
||||
inference.
|
||||
|
||||
## Request And Response Shape
|
||||
|
||||
Native chat uses `/api/chat`, `messages`, optional OpenAI-shaped tool
|
||||
definitions, `format`, `options`, and model-dependent `think`. Responses use
|
||||
`message.content`, `message.thinking`, and `message.tool_calls`. Generate uses
|
||||
top-level `response` and `thinking`. OpenAI compatibility is a separate dialect
|
||||
and can change control names independently.
|
||||
|
||||
Manual Ollama endpoints registered against the OpenAI-compatible `/v1` surface default to text/prompted tools unless the operator explicitly enables `supports_tools`; model naming alone does not opt that dialect into native function schemas.
|
||||
|
||||
Thinking control is model-specific: most documented reasoning families accept
|
||||
a native bool, while GPT-OSS accepts low/medium/high and cannot be fully
|
||||
disabled. A reported Ollama 0.20.6 Qwen3.5 OpenAI-compat path requires
|
||||
`reasoning_effort: none` rather than `think: false` (#5503); keep it versioned
|
||||
and low-confidence until corroborated.
|
||||
|
||||
## Fallback And Safety
|
||||
|
||||
Current reader detection identifies port 11434 as Ollama, in addition to an explicit endpoint kind or an exact/label-bounded `ollama.com` hostname. This is a normalization hint, not endpoint trust or capability evidence. Names that contain `vision`, `embed`, or `qwen` are not capability evidence (#3743, #4487).
|
||||
|
||||
## Current Gaps
|
||||
|
||||
- List discovery needs an orchestrated `/api/show` detail step per model.
|
||||
- Runtime OpenAI-compat thinking suppression still contains name heuristics.
|
||||
@@ -1,57 +0,0 @@
|
||||
# General OpenAI-Compatible Inventory Fallback
|
||||
|
||||
Last updated: dev@2e2bb52 | 2026-08-16
|
||||
|
||||
## Scope
|
||||
|
||||
Canonical compatibility identity `generic_openai`; identity-only reader
|
||||
`src/model_capability_readers/generic_openai.py`; shared envelope and identity
|
||||
helpers in `src/model_capability_readers/base.py`.
|
||||
|
||||
This is not a universal OpenAI-compatible capability schema. Transport request
|
||||
and response behavior remains in `src.llm_core` and provider adapters.
|
||||
|
||||
## Accepted Inventory Shape
|
||||
|
||||
- `{"data": [...]}`;
|
||||
- `{"models": [...]}`.
|
||||
|
||||
Within an item, the reader recovers identity from `id`, `name`, or `model`.
|
||||
Bare-list payloads and `key`/`slug`-only items are not supported. It preserves
|
||||
the raw item on the in-memory record, while `to_dict()` includes it only when
|
||||
the caller explicitly requests `include_raw=True`. Capability remains unknown.
|
||||
|
||||
## Disabled Capability Paths
|
||||
|
||||
The generic reader does not inspect capability-looking fields, including:
|
||||
|
||||
- `type`, `model_type`, `task`, and `pipeline_tag`;
|
||||
- top-level or nested modality fields;
|
||||
- capability booleans/maps/lists;
|
||||
- `supported_parameters`;
|
||||
- context, input, output, and model-length fields.
|
||||
|
||||
Names, descriptions, ownership, pricing, and serialized text also never
|
||||
promote capability through this reader.
|
||||
|
||||
## Forward Compatibility
|
||||
|
||||
An explicitly configured but unknown provider ID is preserved when the generic
|
||||
reader is selected. That allows endpoint-scoped stable IDs to keep working
|
||||
while every family, modality, capability, limit, and control remains unknown.
|
||||
Non-object entries are skipped; null or malformed roots return no records.
|
||||
|
||||
Provider-specific headers, request extensions, and reasoning channels must be
|
||||
selected by explicit provider/endpoint adapters. They never leak through this
|
||||
fallback.
|
||||
|
||||
Compatible tool-call syntax is likewise a runtime concern rather than catalog capability. Current parsers recover selected Hermes/Qwen JSON bodies nested inside `tool_call` wrappers and require the full Qwen bare end delimiter; GPT-OSS compatibility can alias names that collide with its built-in tools and reverse that alias before local dispatch. None of those repairs grants execution authority or proves generic tool support.
|
||||
|
||||
## Current Gaps
|
||||
|
||||
- Compatible providers differ on path prefixes, null handling, tools,
|
||||
streaming usage, and strict extra-field rejection.
|
||||
- Bare-list and `key`/`slug`-only inventories need explicit normalization if a
|
||||
runtime consumer later requires them.
|
||||
- Safe request shaping still requires explicit endpoint/provider
|
||||
configuration even when identity normalization succeeds.
|
||||
@@ -1,34 +0,0 @@
|
||||
# OpenAI Provider Shape
|
||||
|
||||
Last updated: dev@e71f8ce | 2026-08-25
|
||||
|
||||
## Scope
|
||||
|
||||
Canonical provider ID `openai`; API dialects OpenAI Chat Completions and
|
||||
Responses; catalog reader `src/model_capability_readers/openai.py`.
|
||||
|
||||
## Catalog Shape
|
||||
|
||||
`GET /v1/models` returns `object: list` with `data[]` model cards containing
|
||||
`id`, `object`, `created`, and `owned_by`. This is identity and availability
|
||||
metadata only. It does not claim vision, tools, reasoning, modality, task, or
|
||||
context length. The record remains unknown and keeps the raw fields.
|
||||
|
||||
## Request And Response Shape
|
||||
|
||||
Chat uses `messages`, `tools[].function`, `tool_choice`, and
|
||||
`choices[].message|delta`; Responses uses `input`, flattened tools, output
|
||||
items, and typed stream events. OpenAI may support a parameter at the platform
|
||||
level while individual models differ. A later model registry or probe must
|
||||
scope that fact before it becomes canonical model capability.
|
||||
|
||||
## Fallback And Safety
|
||||
|
||||
An explicit endpoint kind selects this provider. Automatic reader detection accepts exact `openai.com` or a dot-delimited subdomain after normalizing case/trailing dots; it is a normalization hint rather than a trust boundary. Do not parse model IDs or ownership labels. If a proxy returns richer fields while explicitly configured as OpenAI, the reader preserves them as raw evidence but keeps capability unknown.
|
||||
|
||||
## Current Gaps
|
||||
|
||||
- OpenAI's Models API does not publish the per-model capability shape needed
|
||||
for automatic canonical classification.
|
||||
- Runtime model-specific sampling/reasoning behavior still needs a maintained
|
||||
structured registry or endpoint probes.
|
||||
@@ -1,21 +0,0 @@
|
||||
# OpenCode Provider Shape
|
||||
|
||||
Last updated: dev@2e2bb52 | 2026-08-16
|
||||
|
||||
## Scope
|
||||
|
||||
Canonical provider identity `opencode` with Zen/Go endpoint variants; OpenAI-compatible transport and webhook presets in `src/llm_core.py` and canonical `routes/webhook/webhook_routes.py`, with the top-level route module retained as a compatibility shim.
|
||||
|
||||
## Shape
|
||||
|
||||
Keep Zen and Go path identity in endpoint metadata even though the canonical
|
||||
provider family is OpenCode. Model discovery uses general identity-only
|
||||
fallback. Path/version, account policy, and model selection can differ between
|
||||
variants; do not flatten them into OpenAI.
|
||||
|
||||
## Fallback And Current Gaps
|
||||
|
||||
Exact `*.opencode.ai` plus configured `/zen` or `/zen/go` selects this family.
|
||||
No provider-specific rich capability catalog is mapped, and runtime still has
|
||||
separate variant labels that should eventually become structured endpoint
|
||||
metadata.
|
||||
@@ -1,41 +0,0 @@
|
||||
# OpenRouter Provider Shape
|
||||
|
||||
Last updated: dev@e71f8ce | 2026-08-25
|
||||
|
||||
## Scope
|
||||
|
||||
Canonical provider ID `openrouter`; OpenAI-compatible chat dialect; rich reader
|
||||
`src/model_capability_readers/openrouter.py`.
|
||||
|
||||
## Catalog Shape
|
||||
|
||||
`GET /api/v1/models` returns `data[]`. Canonical fields are:
|
||||
|
||||
- `id` (falling back to `name`) and display `name`;
|
||||
- `architecture.input_modalities`, `architecture.output_modalities`, and
|
||||
compatibility `architecture.modality`;
|
||||
- `context_length` and `top_provider.max_completion_tokens`;
|
||||
- `supported_parameters`, `default_parameters`, `supported_voices`, and
|
||||
`per_request_limits`.
|
||||
|
||||
Modalities determine family and vision/file/audio/image/video behavior.
|
||||
Recognized supported parameters claim tools, JSON/structured output,
|
||||
reasoning, and web search. Sampling/default parameters become controls, not
|
||||
capabilities. Descriptions, pricing, author slugs, and tokenizer names do not.
|
||||
|
||||
## Provider Versus Routed Endpoint
|
||||
|
||||
OpenRouter normalizes requests while routing a model to one of several
|
||||
underlying providers. The catalog model record is OpenRouter-scoped. Do not
|
||||
copy a direct-provider quirk to OpenRouter unless its normalized API and exact
|
||||
model/endpoint evidence require it. `top_provider` limits describe the current
|
||||
route class, not a permanent global model limit.
|
||||
|
||||
## Fallback And Safety
|
||||
|
||||
The reader receives OpenRouter through explicit selection or an exact/label-bounded `openrouter.ai` hostname hint. Future fields remain raw. If modalities are absent, it falls back to an identity-only OpenRouter record and does not parse the model slug; supported-parameter controls are not retained on that fallback path.
|
||||
|
||||
## Current Gaps
|
||||
|
||||
- Per-upstream endpoint differences can still invalidate an aggregate claim.
|
||||
- Catalog values change frequently and need freshness/expiry when persisted.
|
||||
@@ -1,20 +0,0 @@
|
||||
# Perplexity Provider Shape
|
||||
|
||||
Last updated: dev@28d27ee | 2026-07-17
|
||||
|
||||
## Scope
|
||||
|
||||
Canonical provider ID `perplexity`; OpenAI-compatible cloud endpoint recognized
|
||||
by current UI/provider host maps and agent cloud-host safeguards (#3015).
|
||||
|
||||
## Shape
|
||||
|
||||
Use general identity-only inventory mapping. Perplexity products may perform
|
||||
search, but `web_search` becomes a canonical model capability only when an
|
||||
exact model card, maintained registry, or probe reports it. Provider identity
|
||||
alone and product descriptions are insufficient.
|
||||
|
||||
## Fallback And Current Gaps
|
||||
|
||||
Exact `*.perplexity.ai` preserves provider identity. No rich per-model catalog
|
||||
or search-control mapping is currently consumed.
|
||||
@@ -1,47 +0,0 @@
|
||||
# SGLang Provider Shape
|
||||
|
||||
Last updated: dev@e57f60b | 2026-07-20
|
||||
|
||||
## Scope
|
||||
|
||||
Canonical provider ID `sglang`; OpenAI Chat/Responses plus native generation;
|
||||
Cookbook launch behavior in `routes/cookbook_routes.py` and serving UI modules.
|
||||
There is no dedicated SGLang canonical reader on current `dev`.
|
||||
|
||||
## Metadata Shapes
|
||||
|
||||
Preferred native `GET /model_info` (legacy `/get_model_info`) returns:
|
||||
|
||||
- `model_path` and `tokenizer_path`;
|
||||
- `is_generation`;
|
||||
- `has_image_understanding` and `has_audio_understanding`;
|
||||
- `model_type`, `architectures`, `weight_version`;
|
||||
- `preferred_sampling_params`.
|
||||
|
||||
These are provider observations for a future dedicated reader. Current generic
|
||||
normalization does not map `is_generation`, modality booleans, sampling keys,
|
||||
or `max_model_len`.
|
||||
|
||||
`GET /v1/models` returns served IDs with `owned_by: sglang`, `root`, and
|
||||
`max_model_len`; it supplies identity/context but not parser capability.
|
||||
|
||||
## Runtime Capability
|
||||
|
||||
Tools and reasoning depend on explicit `--tool-call-parser` and
|
||||
`--reasoning-parser`; multimodality and context can also be launch-configured.
|
||||
Cookbook recipes for Qwen, DeepSeek, GLM, Kimi, MiniMax, StepFun, and other
|
||||
families are deployment observations, not universal model-name rules. Persist
|
||||
the selected parser/config as endpoint evidence before canonical promotion.
|
||||
|
||||
## Fallback And Safety
|
||||
|
||||
Current reader detection identifies port 30000 as SGLang, or accepts an
|
||||
explicit endpoint kind, then dispatches to the generic identity-only reader.
|
||||
It does not infer SGLang from `/model_info` payload shape. Avoid normal
|
||||
discovery through the broad admin `/server_info` dump.
|
||||
|
||||
## Current Gaps
|
||||
|
||||
- Endpoint records do not yet store parser/task configuration canonically.
|
||||
- Non-generation task classification needs explicit serving metadata.
|
||||
- No dedicated reader maps SGLang metadata today.
|
||||
@@ -1,21 +0,0 @@
|
||||
# SiliconFlow Provider Shape
|
||||
|
||||
Last updated: dev@28d27ee | 2026-07-17
|
||||
|
||||
## Scope
|
||||
|
||||
Canonical provider ID `siliconflow`; global/CN OpenAI-compatible provider
|
||||
proposed in #5562.
|
||||
|
||||
## Shape
|
||||
|
||||
Use the general `/v1/models` identity-only inventory reader for both regional
|
||||
surfaces. Region/base URL and API key remain endpoint identity. A regional
|
||||
provider-native schema is required before any item fields are promoted; model
|
||||
tokens in returned IDs or PR examples are never capability evidence.
|
||||
|
||||
## Fallback And Current Gaps
|
||||
|
||||
Exact SiliconFlow hosts or explicit kind preserve provider identity. The open
|
||||
provider work has no confirmed rich capability card; regional path/host details
|
||||
and current payload fixtures need revalidation before runtime integration.
|
||||
@@ -1,27 +0,0 @@
|
||||
# Together AI Provider Shape
|
||||
|
||||
Last updated: dev@e57f60b | 2026-07-20
|
||||
|
||||
## Scope
|
||||
|
||||
Canonical provider ID `together`; OpenAI-compatible cloud transport; curated
|
||||
models and discovery compatibility in `routes/model_routes.py`.
|
||||
|
||||
## Shape And Observations
|
||||
|
||||
Together has returned both standard `data[]` and bare model-card lists. The
|
||||
current generic reader accepts the standard envelope when the caller supplies
|
||||
the Together vendor, but it does not accept a bare root list. It keeps
|
||||
identity/provider scope and promotes no capability fields. Task, modality,
|
||||
parameter, and limit data needs a dedicated Together reader before it becomes
|
||||
canonical; model names and the curated picker list are not capability evidence.
|
||||
|
||||
Together can serve many upstream families. Direct-provider quirks do not
|
||||
automatically apply because Together may normalize requests and responses.
|
||||
|
||||
## Fallback And Current Gaps
|
||||
|
||||
Both `*.together.xyz` and `*.together.ai` identify the provider. Malformed/null
|
||||
lists fail soft. A provider-specific rich capability schema has not been
|
||||
confirmed, so general fallback remains intentional. Bare-list catalogs require
|
||||
route-specific preprocessing or a future reader update.
|
||||
@@ -1,19 +0,0 @@
|
||||
# Venice Provider Shape
|
||||
|
||||
Last updated: dev@28d27ee | 2026-07-17
|
||||
|
||||
## Scope
|
||||
|
||||
Canonical provider ID `venice`; paid OpenAI-compatible cloud API represented in
|
||||
webhook presets and cloud/self-hosted classification tests.
|
||||
|
||||
## Shape
|
||||
|
||||
Use general identity-only inventory mapping. Treat `api.venice.ai` as a remote API
|
||||
for routing/security, while keeping model capability per returned model. Do not
|
||||
infer privacy, tools, reasoning, or context from provider marketing or names.
|
||||
|
||||
## Fallback And Current Gaps
|
||||
|
||||
Exact `*.venice.ai` preserves provider identity. No verified rich model-card
|
||||
schema is currently mapped.
|
||||
@@ -1,44 +0,0 @@
|
||||
# vLLM Provider Shape
|
||||
|
||||
Last updated: dev@e57f60b | 2026-07-20
|
||||
|
||||
## Scope
|
||||
|
||||
Canonical placeholder provider ID `vllm`; OpenAI Chat and Responses serving;
|
||||
generic identity-only inventory normalization. There is no dedicated vLLM
|
||||
reader or model-card detector on current `dev`.
|
||||
|
||||
## Catalog Shape
|
||||
|
||||
Current `GET /v1/models` returns `object: list`, `data[]` model cards with
|
||||
`id`, `object`, `owned_by: vllm`, `root`, `parent`, `max_model_len`, and
|
||||
`permission[]`. The generic reader retains only identity/raw data and does not
|
||||
inspect `owned_by`, `root`, `parent`, `max_model_len`, or `permission`. The card
|
||||
does not prove chat template, tools,
|
||||
reasoning parser, vision assets, embeddings, transcription, or rerank.
|
||||
|
||||
LoRA cards can use a different `id`, root path, and parent. Keep each served ID
|
||||
endpoint scoped and do not merge it globally with the base checkpoint.
|
||||
|
||||
## Runtime Capability
|
||||
|
||||
vLLM's supported API surface is broad, but actual behavior depends on the
|
||||
loaded model task, chat template, multimodal assets, tool-call parser,
|
||||
reasoning parser, structured-output configuration, and launch flags. Current
|
||||
Odysseus reasoning regressions cover structured `reasoning`, legacy
|
||||
`reasoning_content`, and compatible fields (#602). These response channels are
|
||||
transport evidence, not a claim that every vLLM model reasons.
|
||||
|
||||
## Fallback And Safety
|
||||
|
||||
Current reader detection identifies port 8000 as vLLM, or accepts an explicit
|
||||
endpoint kind, then dispatches to the generic identity-only reader. It does not
|
||||
infer vLLM from the model-card payload. Do not consume `/server_info`
|
||||
environment/config dumps for normal discovery because they can be large and
|
||||
operationally sensitive.
|
||||
|
||||
## Current Gaps
|
||||
|
||||
- A small safe native capability endpoint is not part of the canonical probe.
|
||||
- Deployment parser/template flags are not persisted with endpoint capability.
|
||||
- No dedicated reader maps vLLM model-card fields today.
|
||||
@@ -1,21 +0,0 @@
|
||||
# xAI Provider Shape
|
||||
|
||||
Last updated: dev@28d27ee | 2026-07-17
|
||||
|
||||
## Scope
|
||||
|
||||
Canonical provider ID `xai`; OpenAI-compatible xAI cloud transport; provider
|
||||
labels/curation in `src/llm_core.py` and `routes/model_routes.py`.
|
||||
|
||||
## Shape
|
||||
|
||||
Model discovery uses general identity-only inventory. Reasoning effort, tools,
|
||||
image input, or other Grok behavior must be
|
||||
scoped per returned model/registry/probe. The provider's broad API feature set
|
||||
does not grant every listed model every capability.
|
||||
|
||||
## Fallback And Current Gaps
|
||||
|
||||
Exact `*.x.ai` selects xAI. Preserve provider identity through OpenAI-compatible
|
||||
fallback and reject lookalikes. A current rich model catalog schema and
|
||||
structured version registry are not yet mapped.
|
||||
@@ -1,23 +0,0 @@
|
||||
# Z.AI Provider Shape
|
||||
|
||||
Last updated: dev@28d27ee | 2026-07-17
|
||||
|
||||
## Scope
|
||||
|
||||
Canonical provider ID `zai`; Z.AI/GLM OpenAI-compatible endpoints including
|
||||
coding-plan variants; curated discovery in `routes/model_routes.py` and prior
|
||||
vision/reasoning fixes such as #664.
|
||||
|
||||
## Shape And Observations
|
||||
|
||||
Use general identity-only inventory mapping. Some working coding-plan models may be
|
||||
absent from `/models`, so pinned/curated IDs are availability compatibility,
|
||||
not capability truth. GLM reasoning controls have appeared as structured
|
||||
objects or serving-template kwargs depending on direct cloud versus local
|
||||
engine (#3031). Keep those scopes separate.
|
||||
|
||||
## Fallback And Current Gaps
|
||||
|
||||
Exact `*.z.ai` or explicit endpoint kind preserves Z.AI identity. Never infer
|
||||
vision/reasoning/tool support from `glm` in a name. A rich official model-card
|
||||
reader and direct-versus-coding-plan schema split are still missing.
|
||||
@@ -1,90 +0,0 @@
|
||||
# Model Behavior Observations
|
||||
|
||||
Last updated: dev@2e2bb52 | 2026-08-16
|
||||
|
||||
## Scope
|
||||
|
||||
This file records model- or provider+model-specific behavior observed in
|
||||
Odysseus code, tests, Issues, PRs, commits, and provider documentation. It is a
|
||||
compact evidence map, not a runtime matcher. General canonical rules belong in
|
||||
[model-capability-canonical.md](model-capability-canonical.md); provider-wide
|
||||
transport belongs in [the provider map](model-providers/_readme.md).
|
||||
|
||||
The canonical capability layer intentionally has no
|
||||
`src/model_behavior_quirks.py`.
|
||||
Adding a registry before runtime call sites carry structured provider, model,
|
||||
version, and dialect identity would create another model-name matching layer.
|
||||
|
||||
## General Observation Template
|
||||
|
||||
Record only the fields supported by the evidence:
|
||||
|
||||
- provider and endpoint/dialect scope;
|
||||
- exact provider-returned model ID or family;
|
||||
- structured model/provider version when available;
|
||||
- capability or request/response behavior observed;
|
||||
- exact native request field/value and response field when relevant;
|
||||
- source, confidence, status, and reproduction date;
|
||||
- whether the behavior is already implemented in runtime code.
|
||||
|
||||
If exact structured identity is unavailable, keep the observation here and in
|
||||
its current tested runtime location. Do not promote it through substring,
|
||||
regex, prose, or serialized-prompt parsing in the canonical layer.
|
||||
|
||||
## Model-Specific Observation Map
|
||||
|
||||
| Observation | Scope | Behavior | Evidence/status |
|
||||
| --- | --- | --- | --- |
|
||||
| Moonshot Kimi K2.5/K2.6 fixed temperature | official Moonshot, K2.5/K2.6, OpenAI Chat | omit `temperature`; thinking mode owns its fixed value | #3960, `f5d3e509`; implemented in current runtime |
|
||||
| Moonshot reasoning tool history | same provider/models/dialect | preserve assistant `reasoning_content` across tool continuation | #3118, `2e6fff22`; implemented |
|
||||
| Claude Opus 4.7+ sampling omission | Anthropic Messages, Opus 4.7+ and major-only later IDs such as `claude-opus-5` | omit `temperature`, `top_p`, and `top_k` where the runtime rule applies | #3117, `4f48cfa9`, #5761; implemented through current runtime identity logic |
|
||||
| Mistral structured reasoning | reasoning-capable Mistral model through native/compatible response shape | use graded effort where accepted; keep typed thinking separate from text | #4698, `bd9149f7`, provider docs; partly implemented |
|
||||
| Ollama native reasoning control | selected reasoning model/deployment | native `think`; reasoning in `message.thinking`/`thinking` | #3031 and provider docs; deployment scoped |
|
||||
| Ollama native `gpt-oss` reasoning level | `gpt-oss` served through Ollama native | `think` accepts low/medium/high and does not represent off | provider docs; deployment scoped |
|
||||
| Ollama compatibility disable observation | Ollama 0.20.6+, observed Qwen3.5 compatibility path | `reasoning_effort: none` was reported to disable reasoning | #5503; unmerged/low confidence until reproduced |
|
||||
|
||||
Issue and commit references are evidence identifiers, not runtime dependencies.
|
||||
Open or unmerged observations remain provisional until reproduced or supported
|
||||
by current provider documentation.
|
||||
|
||||
## Other Model-Level Observations
|
||||
|
||||
- Kimi K2.5/K2.6 multimodality differs from older K2 variants (#2522). Promote
|
||||
only from an exact provider card or scoped registry, never the `kimi` token.
|
||||
- Google product names suggest media tasks to humans, but its Models resource
|
||||
does not publish complete modalities. Keep those modalities unknown without
|
||||
stronger model-scoped evidence.
|
||||
- Ollama `/api/tags` names can omit vision markers (#3743, #4487). Use selected
|
||||
model `/api/show.capabilities`, not its name.
|
||||
- Local reasoning controls vary by serving template/config: message/system
|
||||
directives, `chat_template_kwargs.enable_thinking`, native booleans,
|
||||
structured objects, budgets, and effort levels were all observed (#3031).
|
||||
These are endpoint/deployment facts, not universal checkpoint properties.
|
||||
- DeepSeek, vLLM/NIM, Mistral, Moonshot, Ollama, and harmony-style servers use
|
||||
different structured reasoning channels. Provider/dialect evidence chooses
|
||||
the channel; generic response-text scanning is not capability discovery.
|
||||
- Current runtime recognizes DeepSeek V4 identifiers in its thinking-model patterns; that is request/response handling evidence, not proof that every V4-named endpoint exposes identical capabilities.
|
||||
- GPT-OSS deployments can reserve native tool names. Runtime aliases colliding Odysseus tool names at the provider boundary and reverses the alias before local execution; this is dialect compatibility, not extra tool authorization.
|
||||
- Cohere native and compatibility transports expose different thinking
|
||||
controls/channels. The Cohere model list does not itself prove reasoning.
|
||||
- MiniMax M2.7 exposes different thinking channels through Anthropic and
|
||||
OpenAI-compatible transports. Its current model list is identity-only.
|
||||
- Gemma/Phi/Qwen vision behavior has changed across serving engines (#1430,
|
||||
#1704, #1478). Native engine metadata or a verified endpoint probe outranks
|
||||
a model-family name list.
|
||||
|
||||
## Promotion Gate
|
||||
|
||||
Before an observation becomes canonical runtime behavior, a consumer must
|
||||
already have the necessary structured identity and tests must cover both its
|
||||
positive scope and a neighboring negative scope. Request control and response
|
||||
visibility remain separate: hiding reasoning text is not the same as disabling
|
||||
reasoning at the provider (#2905).
|
||||
|
||||
## Current Gaps
|
||||
|
||||
- Runtime still contains model-name helpers for several implemented behaviors;
|
||||
this spec records them but the canonical catalog does not duplicate them.
|
||||
- Hosted aliases and provider behavior can change; there is no durable
|
||||
observation expiry/revalidation layer yet.
|
||||
- Detail/probe-only model facts cannot safely be populated from list discovery.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user