Compare commits

...
Author SHA1 Message Date
dependabot[bot] f8e10d3051 chore(deps): bump the actions group across 1 directory with 13 updates
Bumps the actions group with 13 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [actions/setup-python](https://github.com/actions/setup-python) | `7.0.0` | `7.1.0` |
| [actions/setup-node](https://github.com/actions/setup-node) | `7.0.0` | `7.1.0` |
| [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) | `10.2.0` | `10.3.3` |
| [github/codeql-action/init](https://github.com/github/codeql-action) | `4.37.7` | `4.39.1` |
| [github/codeql-action/analyze](https://github.com/github/codeql-action) | `4.37.7` | `4.39.1` |
| [hadolint/hadolint-action](https://github.com/hadolint/hadolint-action) | `3.4.0` | `3.5.0` |
| [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) | `4.3.0` | `4.4.1` |
| [docker/build-push-action](https://github.com/docker/build-push-action) | `7.3.0` | `7.4.0` |
| [github/codeql-action/upload-sarif](https://github.com/github/codeql-action) | `4.37.7` | `4.39.1` |
| [actions/deploy-pages](https://github.com/actions/deploy-pages) | `5.0.0` | `5.0.1` |
| [docker/login-action](https://github.com/docker/login-action) | `4.6.0` | `4.7.0` |
| [actions/upload-artifact](https://github.com/actions/upload-artifact) | `7.0.1` | `7.0.2` |
| [actions/download-artifact](https://github.com/actions/download-artifact) | `8.0.1` | `8.0.2` |



Updates `actions/setup-python` from 7.0.0 to 7.1.0
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/5fda3b95a4ea91299a34e894583c3862153e4b97...6d4fd22b20465ccc0f7354476588e8df7c9ca0e0)

Updates `actions/setup-node` from 7.0.0 to 7.1.0
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/820762786026740c76f36085b0efc47a31fe5020...949feb2413d6458794dcd2491c4babbbce0c15c1)

Updates `astral-sh/setup-uv` from 10.2.0 to 10.3.3
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/c18668ad3cf93ea998bef934396af7bb5c839dc7...68559e99cc807c1eb7e9238fd43a15bba460eec2)

Updates `github/codeql-action/init` from 4.37.7 to 4.39.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd...9b3254085494b55a0f7c4c2b3d2d52cc712ce370)

Updates `github/codeql-action/analyze` from 4.37.7 to 4.39.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd...9b3254085494b55a0f7c4c2b3d2d52cc712ce370)

Updates `hadolint/hadolint-action` from 3.4.0 to 3.5.0
- [Release notes](https://github.com/hadolint/hadolint-action/releases)
- [Commits](https://github.com/hadolint/hadolint-action/compare/2a66e89f53d0771bb131a7fa31f3136336094aa6...06be81baf89a55ffd0e24b8f04a4185738dd3387)

Updates `docker/setup-buildx-action` from 4.3.0 to 4.4.1
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/37fe631027851001ddb9b187196cc803df7f5f0e...f87e5991a6d7451dcb8d9637bfbc97413f497069)

Updates `docker/build-push-action` from 7.3.0 to 7.4.0
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/53b7df96c91f9c12dcc8a07bcb9ccacbed38856a...c3c9e263c25d99ce0380d002d59b67737d91b0dc)

Updates `github/codeql-action/upload-sarif` from 4.37.7 to 4.39.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd...9b3254085494b55a0f7c4c2b3d2d52cc712ce370)

Updates `actions/deploy-pages` from 5.0.0 to 5.0.1
- [Release notes](https://github.com/actions/deploy-pages/releases)
- [Commits](https://github.com/actions/deploy-pages/compare/cd2ce8fcbc39b97be8ca5fce6e763baed58fa128...368f82528645a54fb793d4d04e342629a3f51346)

Updates `docker/login-action` from 4.6.0 to 4.7.0
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/dbcb813823bdd20940b903addbd779551569679f...81c6f93d4bc991e510b342a00b419b3bb4efc5c6)

Updates `actions/upload-artifact` from 7.0.1 to 7.0.2
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/043fb46d1a93c77aae656e7c1c64a875d1fc6a0a...cf430e030ddbb5b0abf93d22962f4752f3646cd9)

Updates `actions/download-artifact` from 8.0.1 to 8.0.2
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c...9000827ccba6bdab643e8b6fd33ac0654aef8333)

---
updated-dependencies:
- dependency-name: actions/deploy-pages
  dependency-version: 5.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: actions/download-artifact
  dependency-version: 8.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: actions/setup-node
  dependency-version: 7.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: actions/setup-python
  dependency-version: 7.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: astral-sh/setup-uv
  dependency-version: 10.3.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: docker/build-push-action
  dependency-version: 7.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: docker/login-action
  dependency-version: 4.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: docker/setup-buildx-action
  dependency-version: 4.4.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.39.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: github/codeql-action/init
  dependency-version: 4.39.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: github/codeql-action/upload-sarif
  dependency-version: 4.39.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: hadolint/hadolint-action
  dependency-version: 3.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-11-02 23:16:11 +00:00
Alexandre Teixeira 2790708b19 feat(benchmarks): add offline evidence integrity auditor (#7030)
* feat(benchmarks): add offline C5 evidence auditor

* fix(benchmarks): prevent sensitive data in audit logs

* fix(benchmarks): prevent Markdown report injection

* fix(benchmarks): prevent Markdown inline injection

* fix(benchmarks): secure GFM report rendering

* ci(benchmarks): gate rendered GFM security regressions
2026-11-02 23:07:56 +00:00
4126bea113 fix(gallery): drop the second scrollbar on the photo detail view (#6597)
* fix(gallery): drop the second scrollbar on the photo detail view

With a photo open on desktop, the modal body, the images container and the
detail view are each height: 100%, so the container fills the body exactly.
The container's inline 2px top margin (grid-view spacing) then overflows the
body by 2px, and the body shows its own scrollbar next to the inspector's.
Zero that margin while the detail view is open.

Bumps the shared stylesheet/app cache token and the service worker cache
name, since a stylesheet changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(static): leave admin.js's cache tag unchanged

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cache): keep upstream's service worker cache name format

dev's editor tests require CACHE_NAME to match
odysseus-v<N>-pr40-editor-precache, so keep the version bump and use
that suffix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Alexandre Teixeira <general@alexandreteixeira.dev>
2026-11-02 15:44:29 +00:00
DracieVajkoandMatej f7f6abf82b feat(settings): add a tool approval mode with an explicit off (YOLO) option (#6304)
* feat: add optional tool approval mode

Admin-configurable tool_approval_mode setting (ask/auto) for the
post-external-context tool approval gate.

- ask (default): existing approval flow, gate stays on
- auto: bypasses the approval gate for all runs - same effect as
  ODYSSEUS_TOOL_APPROVAL_GATE=0. Documented honestly in the settings
  row copy and in THREAT_MODEL.md, per review on #6304.
- value validated server-side ({ask, auto}, anything else -> 400)
- no new env var: the setting wires to the existing gate control
- tests: setting validation + _tool_approval_gate_bypassed behavior

* feat: surface deployment gate state on Tool approval row

Follow-up to StressTestor's re-review: when ODYSSEUS_TOOL_APPROVAL_GATE is
set to a falsy value, the Tool approval row still read 'Always ask' even though
the gate was off, misleading admins the other way.

GET /api/auth/settings now returns tool_approval_gate_env_disabled (derived
from TOOL_APPROVAL_GATE_ENABLED) for admins, and the settings row renders a
red notice naming the env var and explaining that the gate is off for every
run regardless of the stored setting. Non-admin/scrubbed responses do not
expose the deployment state. Test covers both env values and the scrub path.

* refactor: rename tool approval mode auto to off

The gate-off value is now "off" ("Off (YOLO)" in the UI) so the auto-approve name stays available for the future judged mode.

---------

Co-authored-by: Matej <matej@local>
2026-11-01 12:22:56 -07:00
Alexandre Teixeira 2a413814c5 fix(chat): honor effective RAG policy in context building (#7025) 2026-11-01 16:14:51 +00:00
Tarık KöşkerandAlexandre Teixeira 1de3c72140 fix(effects): open the effect journal in binary mode on Windows (#6811)
Raw os.open() is text-mode on Windows unless O_BINARY is set, so each
\n written by the journal became \r\n. The log tracks byte offsets,
so the next append read the extra byte as a corrupt tail and every tool
call after the first in a turn failed with 'Effect claim could not be
persisted durably'.

OR O_BINARY (0 on POSIX) into the journal and launch-reservation opens.

Co-authored-by: Alexandre Teixeira <general@alexandreteixeira.dev>
2026-11-01 16:12:42 +00:00
27 changed files with 5051 additions and 84 deletions
+22 -5
View File
@@ -76,7 +76,7 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
persist-credentials: false persist-credentials: false
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
with: with:
python-version: "3.11" python-version: "3.11"
# Byte-compile sources — catches syntax errors without installing deps. # Byte-compile sources — catches syntax errors without installing deps.
@@ -91,7 +91,7 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
persist-credentials: false persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with: with:
node-version: "22.23.4" node-version: "22.23.4"
- run: npm ci --ignore-scripts --no-audit --no-fund - run: npm ci --ignore-scripts --no-audit --no-fund
@@ -151,7 +151,7 @@ jobs:
echo "docs_only=false" >> "$GITHUB_OUTPUT" echo "docs_only=false" >> "$GITHUB_OUTPUT"
fi fi
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
if: steps.docs-check.outputs.docs_only != 'true' if: steps.docs-check.outputs.docs_only != 'true'
with: with:
python-version: "3.11" python-version: "3.11"
@@ -168,7 +168,7 @@ jobs:
# uv resolves the same versions as pip but installs ~5x faster, and its # uv resolves the same versions as pip but installs ~5x faster, and its
# cache keeps each shard from re-downloading the whole requirement set. # cache keeps each shard from re-downloading the whole requirement set.
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 - uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
if: steps.docs-check.outputs.docs_only != 'true' if: steps.docs-check.outputs.docs_only != 'true'
with: with:
version: "0.13.5" version: "0.13.5"
@@ -181,7 +181,7 @@ jobs:
uv pip install --system -r requirements.txt uv pip install --system -r requirements.txt
uv pip install --system "playwright==1.62.0" uv pip install --system "playwright==1.62.0"
if: steps.docs-check.outputs.docs_only != 'true' if: steps.docs-check.outputs.docs_only != 'true'
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
if: steps.docs-check.outputs.docs_only != 'true' if: steps.docs-check.outputs.docs_only != 'true'
with: with:
node-version: "22.23.4" node-version: "22.23.4"
@@ -264,3 +264,20 @@ jobs:
ODYSSEUS_TEST_PYTHON=python ODYSSEUS_LAYOUT_BROWSER=chromium npx --no-install playwright test \ ODYSSEUS_TEST_PYTHON=python ODYSSEUS_LAYOUT_BROWSER=chromium npx --no-install playwright test \
--config tests/e2e/ios-layout.config.cjs \ --config tests/e2e/ios-layout.config.cjs \
--reporter=line --reporter=line
benchmark-gfm-security:
name: Benchmark GFM rendering security
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
with:
python-version: "3.11"
# Test-only renderer, pinned independently of application requirements.
- name: Install GitHub Flavored Markdown renderer
run: |
python -m pip install --disable-pip-version-check --only-binary=:all: "cmarkgfm==2025.10.22"
- name: Verify rendered benchmark report security
run: python tests/test_audit_c5_benchmark_evidence.py --gfm
+2 -2
View File
@@ -31,11 +31,11 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7 uses: github/codeql-action/init@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
build-mode: none build-mode: none
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7 uses: github/codeql-action/analyze@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
with: with:
category: "/language:${{ matrix.language }}" category: "/language:${{ matrix.language }}"
+1 -1
View File
@@ -42,7 +42,7 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Lint Dockerfile - name: Lint Dockerfile
uses: hadolint/hadolint-action@2a66e89f53d0771bb131a7fa31f3136336094aa6 # v3.4.0 uses: hadolint/hadolint-action@06be81baf89a55ffd0e24b8f04a4185738dd3387 # v3.5.0
with: with:
dockerfile: Dockerfile dockerfile: Dockerfile
# DL3008: pinning apt package versions is impractical on a -slim base # DL3008: pinning apt package versions is impractical on a -slim base
+5 -5
View File
@@ -61,14 +61,14 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Set up Buildx - name: Set up Buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
with: with:
driver: docker driver: docker
# Build without pushing so a broken Dockerfile is caught here, and the # Build without pushing so a broken Dockerfile is caught here, and the
# exact image we ship is what gets scanned. # exact image we ship is what gets scanned.
- name: Build image - name: Build image
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with: with:
context: . context: .
push: false push: false
@@ -107,12 +107,12 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Set up Buildx - name: Set up Buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
with: with:
driver: docker driver: docker
- name: Build image - name: Build image
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with: with:
context: . context: .
push: false push: false
@@ -133,7 +133,7 @@ jobs:
TRIVY_DB_REPOSITORY: ghcr.io/aquasecurity/trivy-db:2 TRIVY_DB_REPOSITORY: ghcr.io/aquasecurity/trivy-db:2
- name: Upload Trivy results - name: Upload Trivy results
uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7 uses: github/codeql-action/upload-sarif@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
with: with:
sarif_file: trivy-results.sarif sarif_file: trivy-results.sarif
category: trivy-image category: trivy-image
+1 -1
View File
@@ -63,7 +63,7 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Set up Python - name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
with: with:
python-version: '3.12' python-version: '3.12'
+1 -1
View File
@@ -47,4 +47,4 @@ jobs:
steps: steps:
- name: Deploy to GitHub Pages - name: Deploy to GitHub Pages
id: deployment id: deployment
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1
+7 -7
View File
@@ -55,16 +55,16 @@ jobs:
shell: bash shell: bash
run: echo "IMAGE_NAME=${GITHUB_REPOSITORY,,}" >> "$GITHUB_ENV" run: echo "IMAGE_NAME=${GITHUB_REPOSITORY,,}" >> "$GITHUB_ENV"
- name: Set up Buildx - name: Set up Buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- name: Log in to GHCR - name: Log in to GHCR
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.REGISTRY }}
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push by digest - name: Build and push by digest
id: build id: build
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with: with:
context: . context: .
pull: true pull: true
@@ -78,7 +78,7 @@ jobs:
digest="${{ steps.build.outputs.digest }}" digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}" touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest - name: Upload digest
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with: with:
name: digest-${{ matrix.arch }} name: digest-${{ matrix.arch }}
path: /tmp/digests/* path: /tmp/digests/*
@@ -107,15 +107,15 @@ jobs:
echo "version=$v" >> "$GITHUB_OUTPUT" echo "version=$v" >> "$GITHUB_OUTPUT"
echo "short=${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT" echo "short=${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
- name: Download digests - name: Download digests
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with: with:
path: /tmp/digests path: /tmp/digests
pattern: digest-* pattern: digest-*
merge-multiple: true merge-multiple: true
- name: Set up Buildx - name: Set up Buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- name: Log in to GHCR - name: Log in to GHCR
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.REGISTRY }}
username: ${{ github.actor }} username: ${{ github.actor }}
+2 -2
View File
@@ -62,9 +62,9 @@ jobs:
env: env:
SEARXNG_TEST_IMAGE: ${{ steps.image.outputs.image }} SEARXNG_TEST_IMAGE: ${{ steps.image.outputs.image }}
run: docker pull "$SEARXNG_TEST_IMAGE" run: docker pull "$SEARXNG_TEST_IMAGE"
- uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 - uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- name: Check unmodified upstream trust and actual entrypoint offline - name: Check unmodified upstream trust and actual entrypoint offline
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with: with:
context: . context: .
file: docker/searxng/Dockerfile file: docker/searxng/Dockerfile
+1 -1
View File
@@ -66,7 +66,7 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Set up Python - name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
with: with:
python-version: '3.12' python-version: '3.12'
+1 -1
View File
@@ -64,7 +64,7 @@ External content that reaches the LLM is treated as untrusted via `src/prompt_se
`src/tool_capabilities.py` carries a second layer: once untrusted content has entered a run, `ToolRunSecurityContext.decision_for()` blocks tools that execute code, mutate state, or cause external side effects until the user authorises the action separately. `src/tool_capabilities.py` carries a second layer: once untrusted content has entered a run, `ToolRunSecurityContext.decision_for()` blocks tools that execute code, mutate state, or cause external side effects until the user authorises the action separately.
It is on unless `ODYSSEUS_TOOL_APPROVAL_GATE` is set to a falsy value (`0`/`false`/`no`/`off`). Request authority narrows which tool families a turn may use, but within an admitted family it does not bind the exact action: a request to read email admits `send_email` and `delete_email`, and agent processes inherit the host network. Until those are covered by their own boundaries, this gate is what stops an injected instruction from reaching them. Turning it off lets a successful injection reach `bash`, `host_shell`, `send_email` and `delete_email` without a separate confirmation. It is on unless `ODYSSEUS_TOOL_APPROVAL_GATE` is set to a falsy value (`0`/`false`/`no`/`off`). The admin-only `tool_approval_mode` setting (Settings → Agent Tools, `off` = "Off (YOLO)") is the same knob exposed in the UI: with `off`, every run sets `approval_gate_bypassed` and this gate stops asking, exactly as if the env var were off. Request authority narrows which tool families a turn may use, but within an admitted family it does not bind the exact action: a request to read email admits `send_email` and `delete_email`, and agent processes inherit the host network. Until those are covered by their own boundaries, this gate is what stops an injected instruction from reaching them. Turning it off — by either knob — lets a successful injection reach `bash`, `host_shell`, `send_email` and `delete_email` without a separate confirmation.
Three exemptions apply even when the gate is on, all deliberate: Three exemptions apply even when the gate is on, all deliberate:
+18
View File
@@ -18,6 +18,7 @@ from src.constants import DEEP_RESEARCH_DIR, MEMORY_FILE, PASSWORD_MIN_LENGTH, S
from src.auth_helpers import _auth_disabled from src.auth_helpers import _auth_disabled
from src.rate_limiter import RateLimiter from src.rate_limiter import RateLimiter
from src.settings_scrub import scrub_settings from src.settings_scrub import scrub_settings
from src import tool_capabilities
from src.settings import ( from src.settings import (
load_settings as _load_settings, load_settings as _load_settings,
save_settings as _save_settings, save_settings as _save_settings,
@@ -753,6 +754,11 @@ def setup_auth_routes(auth_manager: AuthManager) -> APIRouter:
user = _get_current_user(request) user = _get_current_user(request)
settings = without_retired_settings(_load_settings()) settings = without_retired_settings(_load_settings())
if _auth_disabled() or (user and auth_manager.is_admin(user)): if _auth_disabled() or (user and auth_manager.is_admin(user)):
# Deployment-level truth for the Tool approval row: a falsy
# ODYSSEUS_TOOL_APPROVAL_GATE disables the approval gate for every
# run regardless of the stored tool_approval_mode, so surface it to
# admins instead of letting the row mislead them the other way.
settings["tool_approval_gate_env_disabled"] = not tool_capabilities.TOOL_APPROVAL_GATE_ENABLED
return settings return settings
return scrub_settings(settings) return scrub_settings(settings)
@@ -769,6 +775,9 @@ def setup_auth_routes(auth_manager: AuthManager) -> APIRouter:
"agent_max_tool_calls": (0, 1000), # 0 = unlimited "agent_max_tool_calls": (0, 1000), # 0 = unlimited
"auto_compact_threshold_percent": (50, 95), "auto_compact_threshold_percent": (50, 95),
} }
_VALID_VALUES = {
"tool_approval_mode": {"ask", "off"},
}
for key in DEFAULT_SETTINGS: for key in DEFAULT_SETTINGS:
if key in RETIRED_SETTING_KEYS: if key in RETIRED_SETTING_KEYS:
continue continue
@@ -782,6 +791,15 @@ def setup_auth_routes(auth_manager: AuthManager) -> APIRouter:
except (TypeError, ValueError): except (TypeError, ValueError):
raise HTTPException(400, f"{key} must be an integer") raise HTTPException(400, f"{key} must be an integer")
val = max(lo, min(val, hi)) val = max(lo, min(val, hi))
if key in _VALID_VALUES:
allowed = _VALID_VALUES[key]
if not isinstance(val, str) or val not in allowed:
raise HTTPException(
400,
f"{key} must be one of {sorted(allowed)}",
)
current[key] = val current[key] = val
_save_settings(current) _save_settings(current)
return without_retired_settings(current) return without_retired_settings(current)
+1 -2
View File
@@ -1276,6 +1276,7 @@ async def build_chat_context(
message=_ctx_msg, message=_ctx_msg,
session=sess, session=sess,
use_web=use_web_val and not skip_web, use_web=use_web_val and not skip_web,
use_rag=use_rag_val,
use_memory=mem_enabled, use_memory=mem_enabled,
time_filter=time_filter, time_filter=time_filter,
preset_system_prompt=preset.system_prompt, preset_system_prompt=preset.system_prompt,
@@ -1286,8 +1287,6 @@ async def build_chat_context(
incognito=incognito, incognito=incognito,
use_skills=skills_enabled, use_skills=skills_enabled,
) )
if use_rag is not None or is_research_spinoff or casual_low_signal:
_preface_kwargs["use_rag"] = use_rag_val
preface, rag_sources, web_sources = chat_processor.build_context_preface(**_preface_kwargs) preface, rag_sources, web_sources = chat_processor.build_context_preface(**_preface_kwargs)
# Capture used memories immediately # Capture used memories immediately
File diff suppressed because it is too large Load Diff
+16 -4
View File
@@ -14,12 +14,12 @@ import csv
import difflib import difflib
import html import html
import json import json
import logging
import os import os
import re import re
import shlex import shlex
import shutil import shutil
import time import time
import logging
import hashlib import hashlib
from src.web_recovery import WebRecoveryBudget from src.web_recovery import WebRecoveryBudget
from itertools import count from itertools import count
@@ -16994,6 +16994,20 @@ def _detect_runaway_call(call_freq, threshold=15):
sig = next((s for s, n in call_freq.items() if n >= threshold), None) sig = next((s for s, n in call_freq.items() if n >= threshold), None)
return sig.split(":", 1)[0] if sig else None return sig.split(":", 1)[0] if sig else None
def _tool_approval_gate_bypassed(
exact_approval: Optional[ExactToolApproval] = None,
) -> bool:
"""Return whether the post-external-context tool approval gate is bypassed.
``tool_approval_mode=off`` (admin setting, Settings → Agent Tools) bypasses
the gate on every run, the same effect as ``ODYSSEUS_TOOL_APPROVAL_GATE=0``.
A chat-session grant bypasses it only within the scope of its own task.
"""
return (
get_setting("tool_approval_mode", "ask") == "off"
or bool(exact_approval and exact_approval.allow_remaining_actions)
)
def _tool_result_signature(tool_result_records: list[dict]) -> str: def _tool_result_signature(tool_result_records: list[dict]) -> str:
"""Return a bounded fingerprint of the observable result of a tool batch. """Return a bounded fingerprint of the observable result of a tool batch.
@@ -21437,9 +21451,7 @@ async def stream_agent_loop(
) )
or messages_contain_external_untrusted_context(messages) or messages_contain_external_untrusted_context(messages)
), ),
approval_gate_bypassed=bool( approval_gate_bypassed=_tool_approval_gate_bypassed(exact_approval),
exact_approval and exact_approval.allow_remaining_actions
),
delegated_credential=bool(delegated_credential), delegated_credential=bool(delegated_credential),
) )
_has_tui_host_bridge = _tui_host_bridge_is_usable(client_runtime_context) _has_tui_host_bridge = _tui_host_bridge_is_usable(client_runtime_context)
+6 -5
View File
@@ -47,6 +47,7 @@ EFFECTS_DIR = os.path.join(DATA_DIR, "effects")
_RUN_ID = re.compile(r"[a-f0-9]{32}") _RUN_ID = re.compile(r"[a-f0-9]{32}")
_TYPES = {"claim": EffectClaim, "outcome": EffectOutcome, "observation": Observation} _TYPES = {"claim": EffectClaim, "outcome": EffectOutcome, "observation": Observation}
_VERSION = 1 _VERSION = 1
_O_BINARY = getattr(os, "O_BINARY", 0) # Windows: raw os.open() is text-mode without it
def _fsync_directory(directory: str | os.PathLike) -> None: def _fsync_directory(directory: str | os.PathLike) -> None:
@@ -243,7 +244,7 @@ class EffectLog:
"""Hold the file exclusively with every durable record merged.""" """Hold the file exclusively with every durable record merged."""
assert self.path is not None assert self.path is not None
_ensure_directory(self.path.parent) _ensure_directory(self.path.parent)
flags = os.O_RDWR | os.O_APPEND | os.O_CREAT | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_CLOEXEC", 0) flags = os.O_RDWR | os.O_APPEND | os.O_CREAT | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_CLOEXEC", 0) | _O_BINARY
descriptor = os.open(self.path, flags, 0o600) descriptor = os.open(self.path, flags, 0o600)
try: try:
if fcntl is not None: if fcntl is not None:
@@ -358,7 +359,7 @@ class EffectLog:
with self._lock: with self._lock:
try: try:
descriptor = os.open(self.path, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0) descriptor = os.open(self.path, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0)
| getattr(os, "O_CLOEXEC", 0)) | getattr(os, "O_CLOEXEC", 0) | _O_BINARY)
except FileNotFoundError: except FileNotFoundError:
return return
except OSError: except OSError:
@@ -394,7 +395,7 @@ class EffectLog:
log = cls(run_id, directory=directory) log = cls(run_id, directory=directory)
assert log.path is not None assert log.path is not None
try: try:
descriptor = os.open(log.path, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_CLOEXEC", 0)) descriptor = os.open(log.path, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_CLOEXEC", 0) | _O_BINARY)
except FileNotFoundError: except FileNotFoundError:
return log return log
except OSError as error: except OSError as error:
@@ -460,7 +461,7 @@ class EffectLog:
temporary = target.with_suffix(".tmp") temporary = target.with_suffix(".tmp")
data = json.dumps({"run_id": self.run_id, "effect_id": effect_id}, sort_keys=True).encode() data = json.dumps({"run_id": self.run_id, "effect_id": effect_id}, sort_keys=True).encode()
_ensure_directory(target.parent) _ensure_directory(target.parent)
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_CLOEXEC", 0) flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_CLOEXEC", 0) | _O_BINARY
descriptor = os.open(temporary, flags, 0o600) descriptor = os.open(temporary, flags, 0o600)
try: try:
view = memoryview(data) view = memoryview(data)
@@ -478,7 +479,7 @@ class EffectLog:
return None return None
base = Path(directory) if directory is not None else effects_dir() base = Path(directory) if directory is not None else effects_dir()
try: try:
descriptor = os.open(base / f"launch-{generation}.json", os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0)) descriptor = os.open(base / f"launch-{generation}.json", os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0) | _O_BINARY)
with os.fdopen(descriptor, "rb") as stream: with os.fdopen(descriptor, "rb") as stream:
if os.fstat(stream.fileno()).st_nlink != 1: if os.fstat(stream.fileno()).st_nlink != 1:
return None return None
+1
View File
@@ -120,6 +120,7 @@ DEFAULT_SETTINGS = {
# Tune via Settings or by editing data/settings.json. # Tune via Settings or by editing data/settings.json.
"research_run_timeout_seconds": 1800, "research_run_timeout_seconds": 1800,
"agent_max_tool_calls": 0, "agent_max_tool_calls": 0,
"tool_approval_mode": "ask",
"agent_max_rounds": 20, # per-message agent step cap (clamped 1..200) "agent_max_rounds": 20, # per-message agent step cap (clamped 1..200)
# Soft input-token budget for the agent loop. The DEFAULT value (6000) is the # Soft input-token budget for the agent loop. The DEFAULT value (6000) is the
# "auto" sentinel: it means "scale the budget to the model's context window" # "auto" sentinel: it means "scale the budget to the model's context window"
+7
View File
@@ -339,6 +339,13 @@ mark.doc-find-mark {
height: 100%; height: 100%;
min-height: 0; min-height: 0;
} }
/* The container now fills the body exactly, so its inline 2px top margin
(spacing for the grid view) would push it 2px past the body and add a
second, outer scrollbar beside the inspector's own. !important is needed
to beat the inline style. */
.gallery-images-container:has(> #gallery-detail[style*="flex"]) {
margin-top: 0 !important;
}
} }
.gallery-detail-image:hover .gallery-detail-rotate { opacity: 0.85; } .gallery-detail-image:hover .gallery-detail-rotate { opacity: 0.85; }
.gallery-detail-nav-disabled { .gallery-detail-nav-disabled {
+39 -23
View File
@@ -247,32 +247,32 @@
real request is discarded and the font fetched a second time. --> real request is discarded and the font fetched a second time. -->
<link rel="preload" as="font" type="font/woff2" crossorigin href="/static/fonts/FiraCode-Regular.woff2"> <link rel="preload" as="font" type="font/woff2" crossorigin href="/static/fonts/FiraCode-Regular.woff2">
<link rel="preload" as="font" type="font/woff2" crossorigin href="/static/fonts/FiraCode-SemiBold.woff2"> <link rel="preload" as="font" type="font/woff2" crossorigin href="/static/fonts/FiraCode-SemiBold.woff2">
<link rel="stylesheet" href="/static/css/00-tokens.css?v=20260929csssplit"> <link rel="stylesheet" href="/static/css/00-tokens.css?v=20261007detailscroll1">
<link rel="stylesheet" href="/static/css/01-agent-chat.css?v=20260929csssplit"> <link rel="stylesheet" href="/static/css/01-agent-chat.css?v=20261007detailscroll1">
<link rel="stylesheet" href="/static/css/02-compare.css?v=20260929csssplit"> <link rel="stylesheet" href="/static/css/02-compare.css?v=20261007detailscroll1">
<link rel="stylesheet" href="/static/css/03-agent-chat.css?v=20260929csssplit"> <link rel="stylesheet" href="/static/css/03-agent-chat.css?v=20261007detailscroll1">
<link rel="stylesheet" href="/static/css/04-memory.css?v=20260929csssplit"> <link rel="stylesheet" href="/static/css/04-memory.css?v=20261007detailscroll1">
<link rel="stylesheet" href="/static/css/05-documents.css?v=20260929csssplit"> <link rel="stylesheet" href="/static/css/05-documents.css?v=20261007detailscroll1">
<link rel="stylesheet" href="/static/css/06-admin-settings.css?v=20260929csssplit"> <link rel="stylesheet" href="/static/css/06-admin-settings.css?v=20261007detailscroll1">
<link rel="stylesheet" href="/static/css/07-documents.css?v=20260929csssplit"> <link rel="stylesheet" href="/static/css/07-documents.css?v=20261007detailscroll1">
<link rel="stylesheet" href="/static/css/08-skills.css?v=20260929csssplit"> <link rel="stylesheet" href="/static/css/08-skills.css?v=20261007detailscroll1">
<link rel="stylesheet" href="/static/css/09-gallery.css?v=20260929csssplit"> <link rel="stylesheet" href="/static/css/09-gallery.css?v=20261007detailscroll1">
<link rel="stylesheet" href="/static/css/10-cookbook.css?v=20260929csssplit"> <link rel="stylesheet" href="/static/css/10-cookbook.css?v=20261007detailscroll1">
<link rel="stylesheet" href="/static/css/11-tasks.css?v=20260929csssplit"> <link rel="stylesheet" href="/static/css/11-tasks.css?v=20261007detailscroll1">
<link rel="stylesheet" href="/static/css/12-gallery.css?v=20260929csssplit"> <link rel="stylesheet" href="/static/css/12-gallery.css?v=20261007detailscroll1">
<link rel="stylesheet" href="/static/css/13-image-editor.css?v=20260929csssplit"> <link rel="stylesheet" href="/static/css/13-image-editor.css?v=20261007detailscroll1">
<link rel="stylesheet" href="/static/css/14-email.css?v=20260929csssplit"> <link rel="stylesheet" href="/static/css/14-email.css?v=20261007detailscroll1">
<link rel="stylesheet" href="/static/css/15-notes.css?v=20260929csssplit"> <link rel="stylesheet" href="/static/css/15-notes.css?v=20261007detailscroll1">
<link rel="stylesheet" href="/static/css/16-calendar.css?v=20260929csssplit"> <link rel="stylesheet" href="/static/css/16-calendar.css?v=20261007detailscroll1">
<link rel="stylesheet" href="/static/css/17-research.css?v=20260929csssplit"> <link rel="stylesheet" href="/static/css/17-research.css?v=20261007detailscroll1">
<!-- Panel stylesheets split out of style.css for maintainability. They are <!-- Panel stylesheets split out of style.css for maintainability. They are
eager and their order is load-bearing: the cascade is the concatenation of eager and their order is load-bearing: the cascade is the concatenation of
these files in this order, so they must stay in this order here and in the these files in this order, so they must stay in this order here and in the
PRECACHE list in sw.js. None of them is lazy-loaded. --> PRECACHE list in sw.js. None of them is lazy-loaded. -->
<link rel="stylesheet" href="/static/css/documents-gallery-editor.css?v=20260929csssplit"> <link rel="stylesheet" href="/static/css/documents-gallery-editor.css?v=20261007detailscroll1">
<link rel="stylesheet" href="/static/css/email-calendar-notes-tasks.css?v=20260929csssplit"> <link rel="stylesheet" href="/static/css/email-calendar-notes-tasks.css?v=20261007detailscroll1">
<link rel="stylesheet" href="/static/css/cookbook-research-memory-settings.css?v=20260929csssplit"> <link rel="stylesheet" href="/static/css/cookbook-research-memory-settings.css?v=20261007detailscroll1">
<link rel="modulepreload" href="/static/app.js?v=20260929csssplit"> <link rel="modulepreload" href="/static/app.js?v=20261007detailscroll1">
<link rel="modulepreload" href="/static/js/chat.js?v=20260917toolttft1"> <link rel="modulepreload" href="/static/js/chat.js?v=20260917toolttft1">
<link rel="modulepreload" href="/static/js/ui.js?v=20260916largetoolscroll1"> <link rel="modulepreload" href="/static/js/ui.js?v=20260916largetoolscroll1">
<link rel="modulepreload" href="/static/js/sessions.js"> <link rel="modulepreload" href="/static/js/sessions.js">
@@ -2463,6 +2463,22 @@
<label class="settings-label">Max steps per message</label> <label class="settings-label">Max steps per message</label>
<input id="set-agentMaxRounds" type="text" inputmode="numeric" placeholder="20" class="settings-select" style="width:120px;"> <input id="set-agentMaxRounds" type="text" inputmode="numeric" placeholder="20" class="settings-select" style="width:120px;">
</div> </div>
<div class="settings-row">
<label class="settings-label">Tool approval</label>
<select id="set-toolApprovalMode" class="settings-select">
<option value="ask">Always ask</option>
<option value="off">Off (YOLO)</option>
</select>
</div>
<div class="admin-toggle-sub" style="margin-top:4px;">
Auto-approve turns off the tool approval gate (same control as the
ODYSSEUS_TOOL_APPROVAL_GATE env var). Bash, host_shell, send_email and
delete_email then run without a confirmation even after the agent has
seen untrusted external content (email, web pages, documents), so a
prompt-injected instruction can reach them directly.
</div>
<div id="set-toolApprovalEnvNote" class="admin-toggle-sub" style="margin-top:4px;display:none;color:var(--red);"></div>
<div id="set-agentMsg" style="font-size:11px;color:color-mix(in srgb, var(--fg) 45%, transparent);"></div> <div id="set-agentMsg" style="font-size:11px;color:color-mix(in srgb, var(--fg) 45%, transparent);"></div>
</div> </div>
</div> </div>
@@ -2693,7 +2709,7 @@
<script type="module" src="/static/js/censor.js"></script> <script type="module" src="/static/js/censor.js"></script>
<script type="module" src="/static/js/settings.js?v=20260912writingstyle3"></script> <script type="module" src="/static/js/settings.js?v=20260912writingstyle3"></script>
<script type="module" src="/static/js/assistant.js?v=20260912firefoxjscleanup1"></script> <script type="module" src="/static/js/assistant.js?v=20260912firefoxjscleanup1"></script>
<script type="module" src="/static/app.js?v=20260929csssplit"></script> <!-- app.js must be LAST --> <script type="module" src="/static/app.js?v=20261007detailscroll1"></script> <!-- app.js must be LAST -->
<script type="module" src="/static/js/init.js?v=20260829chatstyle12"></script> <script type="module" src="/static/js/init.js?v=20260829chatstyle12"></script>
<script type="module" src="/static/js/a11y.js"></script> <script type="module" src="/static/js/a11y.js"></script>
<script nonce="{{CSP_NONCE}}">if('serviceWorker' in navigator){navigator.serviceWorker.register('/static/sw.js?v=20260916autoscroll1').catch(()=>{});}</script> <script nonce="{{CSP_NONCE}}">if('serviceWorker' in navigator){navigator.serviceWorker.register('/static/sw.js?v=20260916autoscroll1').catch(()=>{});}</script>
+21
View File
@@ -10,6 +10,7 @@ export async function initAgentSettings() {
var toolsInput = el('set-agentMaxTools'); var toolsInput = el('set-agentMaxTools');
var roundsInput = el('set-agentMaxRounds'); var roundsInput = el('set-agentMaxRounds');
var supInput = el('set-agentSupervisorLadder'); var supInput = el('set-agentSupervisorLadder');
var approvalModeInput = el('set-toolApprovalMode');
var msg = el('set-agentMsg'); var msg = el('set-agentMsg');
if (!toolsInput) return; if (!toolsInput) return;
@@ -19,6 +20,21 @@ export async function initAgentSettings() {
if (settings.agent_max_tool_calls) toolsInput.value = settings.agent_max_tool_calls; if (settings.agent_max_tool_calls) toolsInput.value = settings.agent_max_tool_calls;
if (roundsInput && settings.agent_max_rounds) roundsInput.value = settings.agent_max_rounds; if (roundsInput && settings.agent_max_rounds) roundsInput.value = settings.agent_max_rounds;
if (supInput) supInput.checked = !!settings.agent_supervisor_ladder; if (supInput) supInput.checked = !!settings.agent_supervisor_ladder;
if (approvalModeInput) {
approvalModeInput.value =
settings.tool_approval_mode === 'off' ? 'off' : 'ask';
}
var approvalEnvNote = el('set-toolApprovalEnvNote');
if (approvalEnvNote) {
if (settings.tool_approval_gate_env_disabled) {
approvalEnvNote.textContent =
'Forced by deployment: ODYSSEUS_TOOL_APPROVAL_GATE is set to a falsy value on this server, so the approval gate is off for every run regardless of this setting.';
approvalEnvNote.style.display = 'block';
} else {
approvalEnvNote.textContent = '';
approvalEnvNote.style.display = 'none';
}
}
} catch (e) {} } catch (e) {}
// Clamp + coerce a raw input to an int in [lo, hi]; falls back to `dflt` // Clamp + coerce a raw input to an int in [lo, hi]; falls back to `dflt`
@@ -37,6 +53,10 @@ export async function initAgentSettings() {
var payload = { agent_max_tool_calls: tools }; var payload = { agent_max_tool_calls: tools };
if (rounds != null) payload.agent_max_rounds = rounds; if (rounds != null) payload.agent_max_rounds = rounds;
if (supInput) payload.agent_supervisor_ladder = !!supInput.checked; if (supInput) payload.agent_supervisor_ladder = !!supInput.checked;
if (approvalModeInput) {
payload.tool_approval_mode =
approvalModeInput.value === 'off' ? 'off' : 'ask';
}
try { try {
await _postSettings(payload); await _postSettings(payload);
msg.textContent = (tools > 0 ? 'Limit: ' + tools + ' tool calls' : 'Unlimited tool calls') + msg.textContent = (tools > 0 ? 'Limit: ' + tools + ' tool calls' : 'Unlimited tool calls') +
@@ -49,6 +69,7 @@ export async function initAgentSettings() {
toolsInput.addEventListener('change', save); toolsInput.addEventListener('change', save);
if (roundsInput) roundsInput.addEventListener('change', save); if (roundsInput) roundsInput.addEventListener('change', save);
if (supInput) supInput.addEventListener('change', save); if (supInput) supInput.addEventListener('change', save);
if (approvalModeInput) approvalModeInput.addEventListener('change', save);
var cur = parseInt(toolsInput.value, 10) || 0; var cur = parseInt(toolsInput.value, 10) || 0;
var curR = roundsInput ? (parseInt(roundsInput.value, 10) || 20) : null; var curR = roundsInput ? (parseInt(roundsInput.value, 10) || 20) : null;
msg.textContent = (cur > 0 ? 'Limit: ' + cur + ' tool calls' : 'Unlimited tool calls') + msg.textContent = (cur > 0 ? 'Limit: ' + cur + ' tool calls' : 'Unlimited tool calls') +
+23 -23
View File
@@ -7,7 +7,7 @@
// - Other static assets (images/fonts/libs): cache-first with bg refresh. // - Other static assets (images/fonts/libs): cache-first with bg refresh.
// - API / non-GET: never cached. // - API / non-GET: never cached.
// Bump CACHE_NAME whenever the precache list or SW logic changes. // Bump CACHE_NAME whenever the precache list or SW logic changes.
const CACHE_NAME = 'odysseus-v1134-pr40-editor-precache'; const CACHE_NAME = 'odysseus-v1135-pr40-editor-precache';
// KaTeX resolves these from its own stylesheet, so caching the CSS without them // KaTeX resolves these from its own stylesheet, so caching the CSS without them
// gives offline math fallback glyphs instead of proper typesetting. // gives offline math fallback glyphs instead of proper typesetting.
@@ -40,28 +40,28 @@ const KATEX_FONTS = [
const PRECACHE = [ const PRECACHE = [
'/static/js/compare/sse.js', '/static/js/compare/sse.js',
'/', '/',
'/static/css/00-tokens.css?v=20260929csssplit', '/static/css/00-tokens.css?v=20261007detailscroll1',
'/static/css/01-agent-chat.css?v=20260929csssplit', '/static/css/01-agent-chat.css?v=20261007detailscroll1',
'/static/css/02-compare.css?v=20260929csssplit', '/static/css/02-compare.css?v=20261007detailscroll1',
'/static/css/03-agent-chat.css?v=20260929csssplit', '/static/css/03-agent-chat.css?v=20261007detailscroll1',
'/static/css/04-memory.css?v=20260929csssplit', '/static/css/04-memory.css?v=20261007detailscroll1',
'/static/css/05-documents.css?v=20260929csssplit', '/static/css/05-documents.css?v=20261007detailscroll1',
'/static/css/06-admin-settings.css?v=20260929csssplit', '/static/css/06-admin-settings.css?v=20261007detailscroll1',
'/static/css/07-documents.css?v=20260929csssplit', '/static/css/07-documents.css?v=20261007detailscroll1',
'/static/css/08-skills.css?v=20260929csssplit', '/static/css/08-skills.css?v=20261007detailscroll1',
'/static/css/09-gallery.css?v=20260929csssplit', '/static/css/09-gallery.css?v=20261007detailscroll1',
'/static/css/10-cookbook.css?v=20260929csssplit', '/static/css/10-cookbook.css?v=20261007detailscroll1',
'/static/css/11-tasks.css?v=20260929csssplit', '/static/css/11-tasks.css?v=20261007detailscroll1',
'/static/css/12-gallery.css?v=20260929csssplit', '/static/css/12-gallery.css?v=20261007detailscroll1',
'/static/css/13-image-editor.css?v=20260929csssplit', '/static/css/13-image-editor.css?v=20261007detailscroll1',
'/static/css/14-email.css?v=20260929csssplit', '/static/css/14-email.css?v=20261007detailscroll1',
'/static/css/15-notes.css?v=20260929csssplit', '/static/css/15-notes.css?v=20261007detailscroll1',
'/static/css/16-calendar.css?v=20260929csssplit', '/static/css/16-calendar.css?v=20261007detailscroll1',
'/static/css/17-research.css?v=20260929csssplit', '/static/css/17-research.css?v=20261007detailscroll1',
'/static/css/documents-gallery-editor.css?v=20260929csssplit', '/static/css/documents-gallery-editor.css?v=20261007detailscroll1',
'/static/css/email-calendar-notes-tasks.css?v=20260929csssplit', '/static/css/email-calendar-notes-tasks.css?v=20261007detailscroll1',
'/static/css/cookbook-research-memory-settings.css?v=20260929csssplit', '/static/css/cookbook-research-memory-settings.css?v=20261007detailscroll1',
'/static/app.js?v=20260929csssplit', '/static/app.js?v=20261007detailscroll1',
'/static/js/storage.js', '/static/js/storage.js',
'/static/js/appConfig.js', '/static/js/appConfig.js',
'/static/js/ui.js?v=20260916largetoolscroll1', '/static/js/ui.js?v=20260916largetoolscroll1',
File diff suppressed because it is too large Load Diff
+100
View File
@@ -0,0 +1,100 @@
"""Effective RAG policy reaches the real search and context consumer."""
from types import SimpleNamespace
from unittest.mock import Mock
import pytest
from src.chat_processor import ChatProcessor
from tests.test_kv_cache_invalidation_2927 import _build_context_harness
pytestmark = pytest.mark.area_routes
@pytest.mark.parametrize('mode', ['chat', 'agent', 'compare'])
@pytest.mark.parametrize('policy', [
'normal', 'incognito', 'preprocessing_disabled', 'research_spinoff', 'low_signal',
])
@pytest.mark.parametrize('use_rag', [None, 'true', 'false', True, False])
async def test_rag_policy_preserves_context_channels(monkeypatch, mode, policy, use_rag):
"""Exercise effective policy with real preface assembly and synthetic I/O.
Nonempty results prove permitted retrieval still injects context. Independent
memory, skills and web spies ensure a RAG disable cannot disable other channels.
"""
from routes import chat_helpers
from src import chat_processor, llm_core, user_time
sess, request, handler, _ = _build_context_harness(monkeypatch, chat_helpers, history=[])
monkeypatch.setattr(chat_helpers, 'load_prefs_for_user', lambda user: {
'memory_enabled': True, 'skills_enabled': True})
monkeypatch.setattr(user_time, 'current_datetime_context_message', lambda: {
'role': 'user', 'content': 'Synthetic current date context'})
incognito = policy == 'incognito'
preprocessing = policy != 'preprocessing_disabled'
if policy == 'research_spinoff':
sess.history = [{'role': 'system', 'metadata': {'research_spinoff_from': 'fixture-report'}}]
message = 'hi' if policy == 'low_signal' else 'Find the retention policy for archived project documents'
search = Mock(return_value=[{
'document': 'Synthetic retention document',
'metadata': {'filename': 'fixture.txt'}, 'similarity': 0.99,
}])
memory = Mock(return_value=[{
'id': 'fixture-memory', 'text': 'User identity synthetic memory',
'category': 'identity', 'pinned': True,
}])
skills = Mock(return_value=[{'name': 'fixture_skill', 'description': 'Synthetic skill'}])
web = Mock(return_value=('Synthetic web context', [{'url': 'https://example.invalid/fixture'}]))
query = Mock(return_value='retention policy')
monkeypatch.setattr(llm_core, 'llm_call', query)
monkeypatch.setattr(chat_processor, 'comprehensive_web_search', web)
processor = ChatProcessor(
memory_manager=SimpleNamespace(load=memory),
personal_docs_manager=SimpleNamespace(rag_manager=SimpleNamespace(search=search)),
skills_manager=SimpleNamespace(index_for=skills),
)
received = []
real_preface = processor.build_context_preface
def capture(**kwargs):
received.append(dict(kwargs))
return real_preface(**kwargs)
monkeypatch.setattr(processor, 'build_context_preface', capture)
ctx = await chat_helpers.build_chat_context(
sess=sess, request=request, chat_handler=handler, chat_processor=processor,
message=message, session_id='c5-policy', use_rag=use_rag,
incognito=incognito, allow_tool_preprocessing=preprocessing,
persist_user_message=False, agent_mode=mode == 'agent',
compare_mode=mode == 'compare', use_web='true',
search_context='Synthetic prefetched context' if mode == 'compare' else None,
)
rag_allowed = policy == 'normal' and str(use_rag).lower() != 'false'
assert len(received) == 1
assert search.call_count == int(rag_allowed)
assert received[0]['use_rag'] is rag_allowed
if rag_allowed:
search.assert_called_once_with(message, k=5, owner='tester')
assert ctx.rag_sources == [{
'filename': 'fixture.txt', 'snippet': 'Synthetic retention document', 'similarity': 0.99,
}]
else:
assert ctx.rag_sources == []
text = '\n'.join(m.get('content', '') for m in ctx.messages)
assert ('Synthetic retention document' in text) is rag_allowed
memory_allowed = policy == 'normal'
assert received[0]['use_memory'] is memory_allowed
assert memory.call_count == int(memory_allowed)
assert ('User identity synthetic memory' in text) is memory_allowed
assert bool(ctx.used_memories) is memory_allowed
skills_allowed = policy in {'normal', 'research_spinoff'}
assert received[0]['use_skills'] is skills_allowed
assert skills.call_count == int(skills_allowed and mode == 'agent')
assert ('fixture_skill' in text) is (skills_allowed and mode == 'agent')
web_allowed = preprocessing and policy != 'low_signal' and mode != 'compare'
assert received[0]['use_web'] is web_allowed
assert web.call_count == query.call_count == int(web_allowed)
assert ('Synthetic web context' in text) is web_allowed
assert bool(ctx.web_sources) is web_allowed
assert ('Synthetic prefetched context' in text) is (
mode == 'compare' and preprocessing and policy != 'low_signal')
+10
View File
@@ -344,3 +344,13 @@ def test_hardlinked_effect_state_is_control_plane_without_scanning_the_store(tmp
elsewhere.write_text("y") elsewhere.write_text("y")
os.link(elsewhere, workspace / "pnpm-style.txt") os.link(elsewhere, workspace / "pnpm-style.txt")
assert resources._control_plane_path(str(workspace / "pnpm-style.txt")) is True assert resources._control_plane_path(str(workspace / "pnpm-style.txt")) is True
def test_consecutive_claims_write_lf_only_records(tmp_path, target):
# Raw os.open() is text-mode on Windows unless O_BINARY is set; the \r\n it
# wrote made the next append read a stray empty line as a corrupt tail.
log = EffectLog(RUN, directory=tmp_path / "fx")
claim(log, target, "e1", "act-1")
claim(log, target, "e2", "act-2")
assert b"\r" not in log.path.read_bytes()
assert len(records(log.path)) == 2
+14
View File
@@ -162,6 +162,20 @@ def test_gallery_body_uses_available_window_height_without_grid_crop():
assert "max-height: 60vh;" not in grid_rule assert "max-height: 60vh;" not in grid_rule
def test_gallery_detail_container_drops_grid_margin_so_body_does_not_scroll():
# With a photo open on desktop, the modal body, the images container and
# the detail view are each `height: 100%`. The container's inline 2px top
# margin (grid-view spacing) would then overflow the body by 2px, which
# shows a second, outer scrollbar next to the inspector's own.
js = _read("static/js/gallery.js")
css = app_css()
assert 'id="gallery-images-container" style="margin-top:2px"' in js
rule = '.gallery-images-container:has(> #gallery-detail[style*="flex"]) {'
idx = css.index(rule)
assert "margin-top: 0 !important;" in css[idx:idx + 120]
def test_gallery_photo_search_is_not_vertically_offset(): def test_gallery_photo_search_is_not_vertically_offset():
css = app_css() css = app_css()
+60
View File
@@ -3,6 +3,7 @@
import asyncio import asyncio
import json import json
from types import SimpleNamespace from types import SimpleNamespace
from fastapi import HTTPException
import pytest import pytest
@@ -117,3 +118,62 @@ def test_manage_settings_tombstones_legacy_fallback_key(monkeypatch):
assert save_calls == [] assert save_calls == []
assert store["default_model_fallbacks"] == LEGACY_VALUE assert store["default_model_fallbacks"] == LEGACY_VALUE
@pytest.mark.asyncio
async def test_tool_approval_mode_accepts_only_supported_values(monkeypatch):
store = dict(settings_mod.DEFAULT_SETTINGS)
monkeypatch.setattr(auth_routes, "migrate_from_settings", lambda: None)
monkeypatch.setattr(auth_routes, "_load_settings", lambda: dict(store))
def save_settings(updated):
store.clear()
store.update(updated)
monkeypatch.setattr(auth_routes, "_save_settings", save_settings)
router = auth_routes.setup_auth_routes(_AuthManager())
set_settings = _route(router, "/api/auth/settings", "POST")
response = await set_settings(
_Request({"tool_approval_mode": "off"}, admin=True)
)
assert response["tool_approval_mode"] == "off"
assert store["tool_approval_mode"] == "off"
response = await set_settings(
_Request({"tool_approval_mode": "ask"}, admin=True)
)
assert response["tool_approval_mode"] == "ask"
assert store["tool_approval_mode"] == "ask"
with pytest.raises(HTTPException) as exc_info:
await set_settings(
_Request({"tool_approval_mode": "banana"}, admin=True)
)
assert exc_info.value.status_code == 400
@pytest.mark.asyncio
async def test_settings_expose_gate_env_kill_switch_to_admins_only(monkeypatch):
store = dict(settings_mod.DEFAULT_SETTINGS)
monkeypatch.setattr(auth_routes, "migrate_from_settings", lambda: None)
monkeypatch.setattr(auth_routes, "_load_settings", lambda: dict(store))
router = auth_routes.setup_auth_routes(_AuthManager())
get_settings = _route(router, "/api/auth/settings", "GET")
monkeypatch.setattr(auth_routes.tool_capabilities, "TOOL_APPROVAL_GATE_ENABLED", True)
response = await get_settings(_Request(admin=True))
assert response["tool_approval_gate_env_disabled"] is False
monkeypatch.setattr(auth_routes.tool_capabilities, "TOOL_APPROVAL_GATE_ENABLED", False)
response = await get_settings(_Request(admin=True))
assert response["tool_approval_gate_env_disabled"] is True
# The stored setting itself is untouched by the env kill-switch.
assert response["tool_approval_mode"] == "ask"
# Unauthenticated/scrubbed callers must not learn the deployment gate state.
response = await get_settings(_Request())
assert "tool_approval_gate_env_disabled" not in response
+1 -1
View File
@@ -340,7 +340,7 @@ def test_route_context_agent_frontend_and_cache_bust_wire_the_contract():
assert "_without_latest_matching_user_message(" not in helpers assert "_without_latest_matching_user_message(" not in helpers
assert "selected_tools=approval_selected_tools" in agent assert "selected_tools=approval_selected_tools" in agent
assert "continuation_query=_retrieval_query or _last_user" in agent assert "continuation_query=_retrieval_query or _last_user" in agent
assert "approval_gate_bypassed=bool(" in agent assert "approval_gate_bypassed=_tool_approval_gate_bypassed(" in agent
assert "['approve', 'approve_task', 'deny']" in frontend assert "['approve', 'approve_task', 'deny']" in frontend
assert "input.value = label" not in frontend assert "input.value = label" not in frontend
assert "const msg = approvalForSend ? '' : el('message').value;" in frontend assert "const msg = approvalForSend ? '' : el('message').value;" in frontend
+34
View File
@@ -500,3 +500,37 @@ async def test_dispatcher_revalidates_sealed_workspace(monkeypatch, tmp_path):
assert result["blocked"] is True assert result["blocked"] is True
assert result["policy"] == "exact_tool_approval" assert result["policy"] == "exact_tool_approval"
def test_tool_approval_mode_defaults_to_ask(monkeypatch):
import src.agent_loop as agent_loop
monkeypatch.setattr(
agent_loop,
"get_setting",
lambda key, default=None: (
"ask" if key == "tool_approval_mode" else default
),
)
assert agent_loop._tool_approval_gate_bypassed() is False
def test_tool_approval_gate_respects_setting(monkeypatch):
import src.agent_loop as agent_loop
monkeypatch.setattr(
agent_loop,
"get_setting",
lambda key, default=None: (
"ask" if key == "tool_approval_mode" else default
),
)
assert agent_loop._tool_approval_gate_bypassed() is False
monkeypatch.setattr(
agent_loop,
"get_setting",
lambda key, default=None: (
"off" if key == "tool_approval_mode" else default
),
)
assert agent_loop._tool_approval_gate_bypassed() is True