mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-11-03 03:32:25 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8e10d3051 | ||
|
|
2790708b19 | ||
|
|
4126bea113 | ||
|
|
f7f6abf82b | ||
|
|
2a413814c5 | ||
|
|
1de3c72140 |
@@ -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
|
||||||
|
|||||||
@@ -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 }}"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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,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)
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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')
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user