* refactor(notes): decompose Notes into feature-owned modules
* fix(notes): avoid HTML interpolation in reminder picker
* docs(notes): drop checkpoint archives, session logs and publication drafts
The N0-N6 tarballs and raw pytest/node logs are not read by any script or
test, and the logs carry absolute home-directory paths. Once squashed into
dev they would stay in history for good. notes-pr.md and notes-issue.md
were the drafts used to open #6712 and #6713, not documentation.
notes-validation.md keeps the counts, the checkpoint hashes and
implementation.sha256 as the record; local interpreter paths and the local
Docker image name are replaced with portable placeholders.
* fix(notes): keep the state store raw and stop masonry re-observing after close
- state: writes built from views (filter, spread, splice) stored nested
read-only views in the store, so structuredClone on a stored note threw
DataCloneError. Store writes now unwrap nested views.
- layout: a save that resolves after closePanel re-renders the leaving pane
and re-created the masonry ResizeObserver that teardown had disposed.
Skip creating it once Notes is closed.
- index.js: drop 30 imported or declared names that were never used and
group the imports at the top. Import order is unchanged.
- Revert unrelated punctuation edits in .gitattributes and keep the header
comment and docstring first in compare/vote.js and
test_notes_dom_xss_helpers.py.
Both fixes have a node test that fails on the previous head.
* test(notes): make script rejection assertion case-insensitive
Address CodeQL js/bad-tag-filter alert #1150 in test-only assertion and refresh Notes evidence manifest checksum.
---------
Co-authored-by: Léo <leograndcontact@gmail.com>
* fix(docker): mount the ChromaDB volume where the image actually persists
chromadb/chroma runs `chroma run /config.yaml`, and that config is one line:
persist_path: "/data". The image declares no VOLUME, so everything the store
writes lives in the container's writable layer. The chromadb-data volume was
mounted at /chroma/chroma — where pre-1.0 images kept the store — so it caught
nothing and the entire vector store was destroyed on every container recreate.
Nothing errors when that happens. The collection survives with a count of 0, so
retrieval simply returns nothing from then on, which reads as "no match".
docs/backup-restore.md tells users to archive the chromadb-data volume, which
means the documented backup was empty too.
Fixed in all three compose files. The volume was never written to, so there is
nothing to migrate.
* fix(tests,docs): harden volume-path test helper and document upgrade migration
tests/test_chromadb_volume_path.py
- _chromadb_volumes() now normalises long-form dict volume entries
(source/target/type map) to 'source:target' strings, preventing
AttributeError crashes if a future Compose change uses that syntax.
- test_chromadb_does_not_use_the_pre_1_0_path now uses split(':')[1]
comparison instead of endswith(), so mount-option suffixes (:z, :ro)
cannot mask a wrong legacy target path.
- Three new unit tests validate the helper against both YAML syntaxes
and the mount-option edge case explicitly.
docs/chromadb-volume-migration.md (new)
- Step-by-step guide for users upgrading from pre-#6166 Docker installs
whose ChromaDB data accumulated in the container's writable layer.
- Covers: detecting the situation, backing up via 'docker cp', seeding
the named volume before recreating the container, verification, and
rollback. Validated with disposable Docker volumes.
- Distinguishes fresh installs (no action needed) from existing
installs with live writable-layer data (action needed before recreate).
docs/backup-restore.md
- Extended ChromaDB caveat block to cross-reference the new migration
guide for users upgrading from pre-#6166.
Closes the P1 documentation gap and P2/P3 test-robustness findings
from the PR #6166 adversarial review.
* fix(docs,tests): eliminate ChromaDB migration data-loss risks and enforce safe cutover
- Require service quiescence before cold database copy to prevent torn SQLite pages
- Mandate container preservation via immutable Docker image commit before recreation
- Enforce positive empty check on destination volume prior to seeding
- Add staging container validation on an isolated loopback port before Compose cutover
- Provide verified rollback procedures prohibiting return to legacy /chroma/chroma mount
- Add regression test coverage for all migration safety invariants
---------
Co-authored-by: PR #6166 Reviewer <reviewer@odysseus-review.local>
* fix(security): deny generated-image access when ownership verification fails
The except Exception block in serve_generated_image catches all
exceptions during ownership verification and falls through to serve
the file anyway. If the database is down or corrupted, the ownership
check is bypassed entirely — any authenticated user can download any
generated image by guessing the content-hash filename.
Raise HTTP 503 (Service Unavailable) instead of falling through,
so the endpoint fails closed when verification is impossible. This
matches the defense-in-depth principle: deny access on verification
errors rather than granting it.
* fix(security): enforce fail-closed ownership and secure caching for generated images
- Fail closed on missing gallery records, null/empty owners, and DB errors
- Support both cookie session and bearer API token owners via effective_user
- Add in-memory registry for freshly generated images to preserve access
- Eliminate file existence oracle by verifying ownership before disk probing
- Update Cache-Control to private, no-cache, must-revalidate
- Pass owner attribution through tool execution and image MCP server
- Add comprehensive hermetic regression tests for ownership gates and caching
* fix(security): sanitize tool owner arguments and drop unneeded MCP env fallback
- Remove redundant and unsafe ODYSSEUS_MCP_IMAGE_OWNER fallback in image_gen_server.py to fix 4 tests in test_env_reference.py.
- Sanitize caller-supplied tool arguments across all MCP and email tool dispatch paths in src/tool_execution.py: strip untrusted _odysseus_* keys from arguments.
- Only inject _odysseus_owner when verified trusted server context is present, preventing untrusted/anonymous users from spoofing ownership claims.
- Sanitize empty/whitespace owner strings in image_gen_server.py to None.
- Add comprehensive regression tests in test_generated_image_ownership.py for argument sanitization, owner injection, fallback propagation, and server sanitization.
---------
Co-authored-by: isharak7m <192635824+isharak7m@users.noreply.github.com>
Co-authored-by: PR #6166 Reviewer <reviewer@odysseus-review.local>
* fix(auth): honor AUTH_ENABLED=false in admin-gated app-config routes
With auth explicitly disabled (reverse-proxy deployments) there is no
session user, so the inline admin checks in auth_routes.py returned 403
unconditionally and settings changes were silently lost. The central
require_admin in core/middleware.py already allows this case; the inline
checks did not.
- add _require_admin_user helper mirroring require_admin semantics and
use it in the app-config routes (settings, features, integrations)
- return unscrubbed settings on GET /settings when auth is disabled
(the caller is the operator)
- settings.js: check res.ok in saveDefault so a failed save no longer
reports 'Saved'
- add regression tests
User-management routes keep requiring a real admin identity; they act on
the user store, which is not meaningful with auth disabled.
Fixes#6028
* fix(settings): verify response.ok on save and expand auth regression tests
- Ensure all settings save handlers in settings.js check response.ok before displaying success notifications, preventing false 'Saved' messages on server rejection.
- Add DOM and JS unit tests verifying response.ok checks and error handling across settings handlers.
- Expand tests/test_admin_routes_auth_disabled.py with comprehensive authorization test coverage for AUTH_ENABLED=false and AUTH_ENABLED=true across settings, features, integrations, secret scrubbing, and user management.
---------
Co-authored-by: Alexandre Teixeira <111787685+alteixeira20@users.noreply.github.com>
Co-authored-by: PR #6029 Reviewer <reviewer@odysseus-review.local>
* fix(history): reject implicit and coerced truncation targets
* fix(history): reject falsy non-string message IDs; drop redundant keep_count guard
The or-chain used to resolve before_msg_id / message_id evaluated
falsy non-strings (False, 0) to '' before the isinstance(…, str) check
was reached, so they silently bypassed type validation. Replace the
chain with explicit presence-aware logic that falls through on None or
empty string (preserving alias semantics) but rejects any other
non-string value with HTTP 400.
Also remove the now-tautological 'if keep_count >= 0:' guard, which
is always True after the negativity check introduced in the parent
commit. The DB block is unconditional and indented accordingly.
Tests: add {'before_msg_id': False}, {'before_msg_id': 0},
{'message_id': 0} to the parametrised rejection suite.
36 passed, 0 failed (Python 3.14.4 / pytest 9.1.1).
---------
Co-authored-by: Alexandre Teixeira <111787685+alteixeira20@users.noreply.github.com>
Skills tagged remote-only in frontmatter are excluded from the injected
skill index when the active model's endpoint is local (loopback/private/
tailscale per is_local_endpoint). Big on-demand reference lookups
overwhelm small local contexts; local models never see the skill, while
remote models and API/UI listings are unaffected.
- SkillsManager.index_for gains optional keyword-only local_model flag
(None = don't gate, same contract as active_toolsets=None)
- _build_system_prompt/_build_base_prompt thread endpoint_url from
_build_route_request_state; unknown endpoint fails open
- tests: cover hidden-on-local, visible-on-remote/unknown, untagged
skills never gated
Refs: discussed for the local-model overwhelm problem
Co-authored-by: Alexandre Teixeira <111787685+alteixeira20@users.noreply.github.com>
With Auto-approve OFF the agent loop asked get_relevant_skills for
min_confidence=2.0, meant as 'published only'. The gate applies the
confidence check to published skills too, and confidence is 0-1, so no
user skill was ever injected, published or not. The explicit-activation
path already treats OFF as status == published.
Add a published_only flag to get_relevant_skills and pass it from both
callers that used the sentinel (relevance injection and the skill
preflight). Auto-approve ON is unchanged: a skill still needs a passing
audit and the confidence threshold.
Fixes#6634
* feat(settings): opt-in reasoning effort for utility model calls
Add utility_reasoning_effort (empty = provider default) and let utility
calls inherit a chat session's effort only on that session's matching
endpoint+model route. Explicit caller effort wins; otherwise the utility
setting wins over session inheritance on utility-chain routes. Resolve
the chain and setting per user and validate levels against model evidence.
No-session work uses the setting only, with no chat-effort crossover.
- src/utility_effort.py owns effort_for_call and candidate_effort_factory
- remove global effort fill from llm_call/llm_call_async; utility call
sites pass effort explicitly, so unrelated calls on the same route do
not pick it up
- wire auto-title, memory/persona/skill extraction, memory suggest/import/
audit, compaction, auto-sort, and per-owner background task candidates
- wire no-session email writing-style, summary, and translation calls,
plus skills evaluation, necessity/retrieval audits, and improvement
- deliberately exclude email replies because thinking_mode='off'
- add the optional candidate_request_factory to llm_call_async_with_fallback
- add sync llm_call reasoning_effort and include it in the cache key
- show the Utility Model effort selector only for advertised levels
Fixes#6569
* fix(utility): exclude email replies and support inherited effort in settings
Pass reasoning_effort='' to task_llm_call_async for scheduled reply drafting to honor the email reply exclusion. In AI settings, resolve the effective Default Chat endpoint and model when Utility is set to 'Same as chat' so supported reasoning effort levels can be selected.
* feat(utility): support same-as-chat effort and session model fallback in compaction
In AI settings, show 'Same as chat' as the default effort option when Utility Model is inherited, and support an explicit 'Provider default' (off) override. Pass the active session fallback in context compaction callers so compaction shares the live chat session's model and reasoning effort for maximum prompt/KV cache affinity.
* fix(settings): clarify chat effort inheritance and keep option visible
Label inherited effort as 'Same as chat (when supported)' and mention
cache affinity and latency speedups in the help text. Ensure the effort
control remains visible when the utility model is set to 'Same as chat'
even if the default chat model lacks advertised reasoning levels, and
populate candidate levels across configured endpoints so the user can
configure effort or keep provider default.
* feat(settings): per-fallback reasoning effort for utility model
Each utility fallback row gets an Effort select, shown only when the
selected model advertises reasoning levels. The choice is stored as
reasoning_effort on the utility_model_fallbacks entry and sent only to
that candidate; an empty value means provider default.
* fix(utility): transmit reasoning effort in non-streaming calls
---------
Co-authored-by: Alexandre Teixeira <111787685+alteixeira20@users.noreply.github.com>
ToolApprovalStore.create() sealed a process identity only from the request
authority's launch scopes, which exist only for granted bash/python. For an
ungranted call the sealing failed silently, so the approval was created
without a process identity and the executor always refused it ("Approved
action has no sealed process/job identity"). When the authority is not
inherited and has no launch scope for the backend, seal one in the
request's single workspace root at approval time, as filesystem approvals
already do. Inherited authorities still cannot widen.
The skill test run and audit arm left their `async for` over
stream_agent_loop early when an approval was needed, without closing the
generator. asyncio closed it later from another task, so its ContextVar
resets raised "Token ... was created in a different Context". Close the
stream with aclosing, like the scheduler and teacher escalation do.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat: add Notes and Todos as agent chat context
- Send selected Note/Todo IDs from the Notes UI
- Load selected notes with owner scoping in chat routes
- Format note content and checklist items for the model
- Inject selected notes as untrusted chat context
- Add a selected-context indicator beside Shell Access
- Keep selected context associated with the active chat session
* Improved selected notes context handling and discouragin redundant note lookup and permission requests
* Fix selected notes UI when switching chats
* fix(notes): harden selected context lifecycle and validation
---------
Co-authored-by: Alexandre Teixeira <111787685+alteixeira20@users.noreply.github.com>
The #6503 merge added _control_plane_path() to _is_denied_tool_path(),
which grep, glob and ls call per enumerated entry. Without a snapshot
argument every call rebuilt _control_plane_snapshot() and stat'ed every
protected state file again. A 5,000-file grep went from 0.75 s to ~7 s,
and around 20k files it hits the grep timeout.
Thread an optional snapshot through _is_denied_tool_path() and
_can_traverse_tool_path(), and build it once per scan, the way
process_resources already does for its launch-boundary walk. Root
resolution and bound-resource resolution still observe fresh state.
The compact preview reduces every pre-execution tool error to a fixed
allowlist so exception text cannot reach the client. Messages our own
validators build per call are not on that list, so the model got "The
tool call could not be validated..." instead of the pdf_extract path
hint, the email uid provenance error, the artifact-Python target
message, or which schema argument was wrong. That is the default path
for the tool-profile models, Qwen3.5-9B included.
Each validator site now records its message in a per-call
validator_error before raising, and only that text is returned
verbatim. A schema failure gets a hint rebuilt from the offered schema
and the call's own arguments (missing property, expected type, allowed
values), never from the caught ValidationError. Everything else,
including all execution failures, still goes through
_public_preview_tool_error.
test_allows_user_content_the_app_hands_to_the_model is parametrized on
UPLOAD_DIR and friends, which sit under the per-worker data dir. pytest
used those paths as test ids, so every xdist worker collected different
names and `pytest -n 4` (documented in tests/README.md) aborted at
collection. CI shards with --shard and never saw it.
Each of the four pytest shards spends ~84 s on setup before pytest
starts, and repeats all of it: pip resolves and installs ~108 packages
(~24 s, even with the pip cache hit), Playwright downloads Chromium and
the headless shell with no cache (~11 s), and three separate
apt-get update calls run.
- Install requirements with uv (astral-sh/setup-uv, pinned) and cache
its downloads keyed on requirements.txt. uv resolves the same 108
pins as pip for this requirements.txt.
- Cache ~/.cache/ms-playwright keyed on package-lock.json so a hit
skips the browser downloads; --with-deps still installs system libs.
- Install FFmpeg and bubblewrap in one apt pass. The containment step
keeps its bwrap probes unchanged.
- Add --durations=25 so the slowest tests per shard are visible; the
shard planner only weights the slow marker, and shard times vary
from 154 s to 296 s within one run.
website/configuration-reference.md pinned every ODYSSEUS_* read to a
`path:line`, and test_env_reference fails when the committed page differs from
the generator output. So any change that adds or removes a line above an env
read in any scanned file has to regenerate the page, even when no variable
changed. Since the page landed on 2026-09-30, 24 of the 34 commits touching it
changed nothing but line numbers, and all three open PRs that touch it today do
the same - which also makes them conflict with each other on that file.
The Read in column now names the file, and "+N more" counts other files rather
than other read sites. The page still goes stale, and the test still fails,
when a variable is added, removed, moves to another file, or changes default.
check_notes still reports path:line on stderr, where it is useful and never
committed.
Request authority admits whole tool families from the user's request, so a
request to read email also admits send_email, delete_email and bulk_email,
and agent processes inherit the host network. With the gate defaulting to
off, an instruction injected through an email or a fetched page reaches those
tools with no other check; dev refuses them today.
Default the gate on, keep ODYSSEUS_TOOL_APPROVAL_GATE=0 as the opt-out, and
pin the production default with a test that imports the module in a fresh
interpreter. Four routing tests written for the opt-out posture now set it
explicitly.
Use Map-backed cost ledgers so externally derived session and run identifiers never cross ordinary object prototype semantics. Preserve the existing JSON storage format and extend browser and isolated ledger regressions for replay, overflow, legacy data, and reserved keys.
Require caller-supplied model endpoints to resolve through enabled owner-visible registrations, harden session path encoding, and remove the SVG title HTML parsing sink.
Regenerate website/configuration-reference.md with
scripts/generate_env_reference.py. The negative-web correction in
96e82562 inserted five lines in src/agent_loop.py ahead of the
ODYSSEUS_MAX_VISUAL_EVIDENCE_IMAGES and _FRAMES reads, so their recorded
locations move from 15329/15361 to 15334/15366. No variable, default or
description changed.
Document opt-in local workers and the per-process runtime ownership model.
Record the two parallel-only failures found and fixed in test
infrastructure. Record the measured results on the final code: serial
oracle 496.3s, -n 2 267.6s (1.85x), and two green -n 4 runs at 155.6s mean
(3.19x), all with identical skip and xfail sets and no leaked processes,
listeners, state, or runtime roots. Recommend -n 4 locally and explain
why -n auto was not run. The full serial run remains the release oracle.