Commit Graph
2310 Commits
Author SHA1 Message Date
Alexandre Teixeira eaddc03729 Merge pull request #26 from o3LL/fix/tmp-realpath-test-bugs
fix(tests): resolve temp paths consistently on macOS
2026-09-30 18:21:04 +01:00
Alexandre Teixeira bfae249120 Merge pull request #28 from o3LL/fix/cookbook-stop-procfs-guard
fix(cookbook): skip the pid sweep when the host has no procfs
2026-09-30 18:07:07 +01:00
Alexandre Teixeira 054df080af Merge pull request #32 from o3LL/fix/6215-mcp-args-validation
fix(mcp): reject malformed Args on Add MCP Server instead of defaulting to []
2026-09-30 17:18:51 +01:00
Alexandre Teixeira 11ecf46abb Merge pull request #30 from o3LL/fix/6228-tailscale-empty-lookup-cache
fix(discovery): cache a successful but empty Tailscale lookup
2026-09-30 17:05:16 +01:00
Léo 9738405310 fix(tests): bind the docker-socket fixtures somewhere sun_path fits
Four more tests in the same family as the /tmp ones this change already
fixes, and they hide for the same reason: the failure depends on how
long $TMPDIR happens to be.

tests/test_shell_routes.py::TestHostDockerAccess (three) and
tests/test_cookbook_docker_access.py::test_container_opt_in_with_unix_
socket_is_allowed each bind an AF_UNIX socket at tmp_path/"docker.sock".
macOS gives sun_path 104 bytes including the terminator. pytest roots
tmp_path at $TMPDIR, which on a stock Mac is a 49-character
/var/folders/<2>/<30>/T/; add pytest-of-<user>/pytest-<n>/ and the
test's own name and the bind path is 115 bytes before the filename.

    OSError: AF_UNIX path too long

Linux allows 108 and roots $TMPDIR at /tmp, so CI never sees it. Under a
shortened $TMPDIR the path lands at exactly 103 and passes — until
pytest's run counter reaches two digits and it becomes 104. That is why
the ledger's counts did not include these: they were measured somewhere
the path fit.

Adds tests/helpers/unix_sockets.bound_unix_socket, which binds under a
short directory and asserts the length before it tries, so the next
socket fixture fails with a sentence rather than an errno. Records the
trap in KNOWN_FAILURES.md along with the instruction to re-measure with
the default $TMPDIR.
2026-09-30 17:35:06 +02:00
Léo 5f18767528 fix(mcp): show the route's rejection reason on the Integrations form too
The route now answers 400 with a message naming the expected shape.
admin.js was taught to print `data.detail`; the Unified Integrations
form in settings.js still printed `Failed (400)` and dropped it.

That gap is exactly where the new validation bites. The client-side
JSON.parse guard added alongside it catches unparseable input, so the
only values that reach the route's 400 are ones that parse but are not
a list — `"npx"`, `{}`, `null` — and for those the status code alone
tells the user nothing about what is wrong with what they typed.

Adds source-level coverage for both forms; the PR changed two JS files
with no test on either.
2026-09-30 17:15:52 +02:00
Léo 01b8ac5fea fix(mcp): reject malformed Args on Add MCP Server instead of defaulting to []
`add_server` wrapped `json.loads(args)` in a bare `except` that fell back to
`[]`, so an Args value that is not JSON — a bare path, which is what the form's
placeholder invites people to type — registered the server and spawned the
stdio subprocess with an empty argv. Nothing surfaced the loss: the POST
returned 200 and the row persisted with `"args": []`.

The route now returns 400 for an unparseable value, and also for valid JSON of
the wrong shape: `args=5` reached `StdioServerParameters(args=5)` and raised an
unhandled TypeError in the error formatter's `" ".join(...)`.

Both form clients mirror the guard instead of leaving the user to read a 400
they cannot see. `settings.js` stops silently defaulting a bad Args value, and
`admin.js` gains the same client-side parse check plus a `res.ok` branch so a
server-side rejection is not reported as a connection failure.

Ported from public `dev` (`9d5c0319`, #6215 upstream, fixing #6211), with its
test. The `admin.js` hunks are inert on `lab` — `initMcpForm` early-returns
because that form's markup is not in this build — and are carried anyway to
keep the two lines from diverging further.
2026-09-30 12:54:09 +02:00
Léo f8269a829f fix(discovery): cache a successful but empty Tailscale lookup
The host cache was gated on the list being non-empty, so "queried fine, no
eligible peers" looked exactly like a cold cache and every caller paid for
another `tailscale status --json` — a subprocess with a 5s timeout.

Gate on the timestamp instead. Failures still leave the timestamp unset, so a
missing binary, a non-zero exit or unparseable output stays retryable rather
than being cached for the full TTL.

Ported from public `dev` (`affaee1e`, #6228 upstream), with its test.
2026-09-30 12:14:03 +02:00
Léo 30ef7652c0 fix(cookbook): skip the pid sweep when the host has no procfs
`_cookbook_kill_session` kills the tmux session, then sweeps /proc for
model servers that survived the SIGHUP. The sweep had no guard, so on
macOS and Windows `os.listdir("/proc")` raised FileNotFoundError after
the kill had already succeeded. The function's outer except turned that
into `{"error": "...No such file or directory: '/proc'", "exit_code": 1}`
and skipped the state write that marks the session stopped — the agent
is told a stop failed that actually worked.

Guard the sweep with a procfs check, the way `_scan_running_model_processes`
already does a few hundred lines up. The root and the check now live in
`core/platform_compat`, which is where OS differences belong and which
makes both branches patchable from a test on either kind of host.

Adds a class-level guard test: the third instance of this defect, and two
of the three were found by reading source rather than by a test.
2026-09-30 12:13:04 +02:00
Léo 32d9dbc267 fix(tests): resolve temp paths consistently on macOS
Three of the six recorded failures were the same test bug: an unresolved
/tmp path compared against a resolved /private/tmp one. macOS makes /tmp a
symlink, so a fixture built with tempfile.mkdtemp(dir="/tmp") and a code path
that resolves what it reports disagree about a file both found correctly.

test_code_nav_tools builds its fixture unresolved and compares it against the
reported path. One realpath fixes both of its failures.

test_glob_confined_e2e is the same cause through a longer route: it mixed
os.path.realpath(ws) with an unresolved secret directory, so relpath emitted
"../../../../tmp/<absolute path>" and the assertion that the absolute path was
absent from the output matched it as a substring. Resolving the secret
directory puts both sides in one tree and the relative path stays short.

macOS full suite goes from 6 failures to 3. The remaining three are an ffmpeg
build without a WebP encoder, a socket test that needs a fast connection
refusal, and the rich-text colour test that is still unexplained.

The ledger is updated in the same change so it does not describe failures that
no longer happen.
2026-09-30 10:56:58 +02:00
Léo b5d1505582 docs(tests): record the known full-suite failures
specs/testing-devops.md lists "no canonical full-suite known-failing/flaky
ledger" as a gap. Without one a first local run is uninterpretable: you cannot
tell a regression from a platform artifact, so you either chase a non-bug or
ignore a real one.

Six failures on macOS against lab@c499c01b, each with its cause and a verdict
rather than a blanket "environmental":

- three compare an unresolved /tmp path against a resolved /private/tmp one.
  Those are test bugs and the file says so.
- one asserts ffmpeg exit 0 for a .webp still, which is a build option Homebrew
  does not always carry. Needs a skip or a PNG fallback.
- one opens real sockets and needs a fast connection refusal. Environmental.
- one Playwright colour-contrast test had been written off as a flake. It is
  not: three consecutive runs failed identically at ~31s. Recorded as
  unexplained and possibly a real defect, because calling it noise is what
  stopped anyone looking.

Also documents the prerequisites, since most surprise failures are a missing
npm ci rather than anything here, and the CHROMADB_PORT precaution: the client
reaches Chroma over HTTP regardless of the data directory, so a test run can
attach to a store holding real data.
2026-09-29 18:00:12 +02:00
Alexandre Teixeira 6105702901 Merge pull request #16 from o3LL/lane/decomposition-and-test-isolation
refactor(lane): decompose style.css, pin it with a computed-style harness, and isolate worktree runs
2026-09-29 14:54:36 +01:00
Alexandre Teixeira 65c35c8211 Merge pull request #15 from o3LL/fix/test-static-port-isolation
test(harness): bind the static test server to an ephemeral port
2026-09-29 14:54:18 +01:00
Alexandre Teixeira 956671a731 Merge pull request #14 from o3LL/fix/proc-guard-private-browser
fix(browser): skip the Chrome sweep when the host has no procfs
2026-09-29 14:54:00 +01:00
Alexandre Teixeira aafd8a98eb Merge pull request #13 from o3LL/v1/css-split-3
refactor(css): move cookbook, research, memory and settings styles to their own file
2026-09-29 14:53:44 +01:00
Léo 4318974903 test(css): read the static origin at call time, not at import
The snapshot harness read ODYSSEUS_TEST_STATIC_ORIGIN into a module constant.
That env var is published by the session static-server fixture, which runs
after collection has already imported the module, so the constant always held
the 7011 fallback and the capture connected to a port nothing was listening on:

    route.fetch: connect ECONNREFUSED 127.0.0.1:7011

Neither branch is wrong on its own. The harness was written while the fixture
still bound a fixed 7011, and the ephemeral-port change removed that port. The
two only disagree once they are in the same tree, which is what this integration
branch is for.

With this, the baseline recorded before the stylesheet split is reproduced
exactly after it, so the split is confirmed to preserve computed styles rather
than only argued to.
2026-09-29 10:38:06 +02:00
Léo 822f7d80e4 Merge branch 'feat/odysseus-dev-worktree-boot' into lane/decomposition-and-test-isolation 2026-09-29 10:36:18 +02:00
Léo ac5e1a600d Merge branch 'fix/test-static-port-isolation' into lane/decomposition-and-test-isolation 2026-09-29 10:36:18 +02:00
Léo f8e9d00256 Merge branch 'fix/proc-guard-private-browser' into lane/decomposition-and-test-isolation 2026-09-29 10:36:18 +02:00
Léo 16c0fe0975 Merge remote-tracking branch 'fork/v1/css-split-3' into lane/decomposition-and-test-isolation 2026-09-29 10:36:18 +02:00
Léo 255f7c46c1 feat(scripts): add odysseus-dev, a per-worktree isolated boot
start-macos.sh is the single-instance launcher and adopts whatever is
already listening: an open ChromaDB port is a resource it reuses. With
one checkout that is right. With several, it means a scratch worktree
silently attaching to another checkout's vector store, and the script
reports it as a success.

odysseus-dev is the sibling that owns isolation instead. Ports are
derived from the worktree path, so two checkouts never collide and one
checkout always gets the same URL. A ChromaDB this worktree did not
start is refused, never adopted — we start our own or fall closed to
keyword mode and say which. The data dir, database and browser-MCP
cache live under .odysseus-dev/, leaving data/ to a normal launch. A
checkout wired into launchd or systemd will not boot at all, and the
ports the project already means something by (7000, 7011, 7860, 8100)
are refused even when asked for explicitly.

Readiness is /api/ready rather than a TCP accept: the port accepting
connections says nothing about the database or a writable data dir.
That endpoint is not auth-exempt, so the tool owns a dev admin account,
hands it to setup.py and prints it.

--from-pr N fetches pull/N/head into its own worktree and boots it,
borrowing a venv so a PR is a few seconds rather than a pip install.

start-macos.sh is untouched: it is what the LaunchAgent runs.
2026-09-25 17:35:47 +02:00
Léo 1863de33c2 test(css): pin computed styles against a committed baseline
static/style.css is 51,425 lines in one file. Hundreds of selectors are
declared more than once and !important is used throughout, so the rendered
result is a function of source order. Extracting a block into its own file
changes that order, and nothing in the suite would notice - which makes a
51k-line split unfalsifiable and "looks fine to me" the only available
evidence.

This moves no CSS. It captures getComputedStyle over a fixed inventory of
676 elements across three pages, four viewports, both themes and the three
density modes - 16,224 element snapshots - hashes them, and compares against
tests/css_snapshot/baseline.json. A capture takes about 21 seconds.

The bench page synthesises one element per selector from an evidence-driven
list: every selector declared more than once in style.css that can be
expressed as a static compound chain, plus a curated set per feature area.
Redeclared selectors are the ones a reorder can flip. The bench loads
whatever stylesheets index.html ships, so it keeps measuring the real set
once the file is split. tests/test_css_computed_style_snapshot.py also carries
a self-test that swaps two conflicting .attach-strip declarations and asserts
the digest moves, so the harness cannot silently stop watching.

The second half is the asset-manifest check specs/frontend.md asks for,
scoped to stylesheets: every stylesheet referenced by shipped HTML and by the
sw.js precache exists, and index.html and sw.js agree on the ?v= string. They
hardcode it independently today, so a split that updates one and not the
other ships an offline cache nobody notices until a plane.
2026-09-25 11:32:46 +02:00
Léo 9b2185f1cc test(harness): bind the static test server to an ephemeral port
The session-scoped autouse fixture bound 127.0.0.1:7011 and raised when the
port was taken. Because it is autouse, that raise errored every collected
test rather than the browser ones: a second worktree running its own suite
produced 10,612 errors, none of them about the code under test. 7011 is also
the application's own default port, so the suite could not run while a local
instance was up.

Bind port 0 instead and publish the resulting origin as
ODYSSEUS_TEST_STATIC_ORIGIN. The browser tests shell out to node, which
inherits the environment, so the snippets read process.env rather than
hardcoding a port. ODYSSEUS_TEST_STATIC_PORT still pins one when something
outside pytest has to reach the server; that is the only path that can now
fail to bind, and it fails with a message that says so.

Two concurrent full runs from one checkout now both pass. Only the docx export
snippet is an rf-string, so it is the only one whose JS braces needed doubling.
2026-09-25 10:45:58 +02:00
Léo 8b85e11fb4 fix(browser): skip the Chrome sweep when the host has no procfs
_terminate_owned_chrome() walked Path("/proc") unconditionally, so on macOS
and Windows iterdir() raised FileNotFoundError out of private-browser session
shutdown. src/tools/cookbook.py already guards the same kind of scan with
os.path.isdir("/proc").

The sweep only reclaims Chrome trees that agent-browser reparented, so it is
an optimisation rather than a correctness requirement: degrade to a no-op
rather than failing the whole shutdown path.

_PROC_ROOT is a module attribute so both branches are testable on either kind
of host. The procfs-present path had no coverage at all before this.
2026-09-25 10:30:49 +02:00
Alexandre Teixeira 0e07d9a675 Merge pull request #10 from o3LL/fix/review-20260923-bound-outbound-and-draft-limits
fix(security): harden scholarly lookups and editor draft limits
2026-09-24 12:22:04 +01:00
Alexandre Teixeira 11ad4e2739 fix(url-safety): resolve NAT64 well-known prefix to IPv4 target
The R09 scholarly hardening routes every hop through check_outbound_url with
block_private=True. On a DNS64/NAT64 network, an IPv4-only host can resolve
through the RFC 6052 Well-Known Prefix. For example, export.arxiv.org resolved
to 64:ff9b::924b:5b2a in the reproduced environment.

CPython classifies that outer IPv6 prefix as reserved, so the URL guard rejected
the request before examining the effective IPv4 destination.

Decode addresses in exactly 64:ff9b::/96 to their embedded IPv4 destination and
evaluate that destination under the strict outbound policy.

The translated target is always checked with private-address blocking enabled.
This prevents the NAT64 prefix from becoming a path to loopback, private,
shared/CGNAT, link-local, multicast, unspecified, or other non-global IPv4
space.

Network-specific translation prefixes are not decoded. In particular,
64:ff9b:1::/48 remains subject to the existing IPv6 policy.

Coverage includes:

- public IPv4 destinations embedded through the RFC 6052 prefix
- loopback, link-local, private, CGNAT, multicast, unspecified, benchmark, and
  TEST-NET rejection
- network-specific NAT64 prefixes remaining undecoded
- deterministic scholarly provider tests without live DNS dependence
- redirect query parameter isolation
- relative redirect resolution
- HTTP error fallback behavior
- editor draft GET and DELETE behavior remaining unaffected

R09, R11, and R01 were independently audited and otherwise left unchanged.
2026-09-23 15:32:04 +01:00
Léo 3807cb22e7 refactor(css): move cookbook, research, memory and settings styles to their own file
Last step of splitting static/style.css by panel. This moves the cookbook,
deep research, memory and settings rules that can move into
static/css/cookbook-research-memory-settings.css.

After the three steps style.css still holds the shell and every rule whose
move would change the cascade, which on this branch is most of the file.
Eager ordered links, verbatim moves, no rendering change.
2026-09-23 16:12:53 +02:00
Léo acefcbec47 refactor(css): move email, calendar, notes and task styles to their own file
Second step of splitting static/style.css by panel. This moves the email,
calendar, notes and tasks rules that can move into
static/css/email-calendar-notes-tasks.css.

Same shape as the first step: eager ordered links, verbatim moves, no
rendering change. The header comment in each split file lists the load order
and the earlier file's header is updated so all of them agree.
2026-09-23 16:12:52 +02:00
Léo e3eb7137b7 refactor(css): move document, gallery and image-editor styles to their own file
static/style.css is 51,425 lines and holds every panel's styles, so two
people working on unrelated panels still edit the same file. This is the
first of three steps that give each panel group its own stylesheet. It moves
the document library, image gallery and image editor rules that can move
into static/css/documents-gallery-editor.css.

The new file is loaded eagerly from index.html immediately after style.css,
so the cascade is the concatenation of the two in that order, which is the
order those rules already had. Nothing is deferred and nothing renders
differently.

Rules moved verbatim. A rule stays in style.css when its selector group
covers more than one panel, when the class is shared design language used
from modules outside the panel, or when moving it would flip which of two
equally specific rules wins on an element the markup puts both classes on.
That last rule is what keeps this change small: most panel-prefixed rules on
this branch compete with a shared component rule somewhere later in the file.

Nineteen tests read static/style.css directly and go red the moment a rule
they assert on moves, which says nothing about the page. They now go through
tests/helpers/stylesheets.app_css(), which concatenates the stylesheets in
the order index.html loads them. The helper also exposes the <link> markup
for tests that build a synthetic page through Playwright.
2026-09-23 16:12:51 +02:00
Alexandre Teixeira 0784cd2fed fix(review): harden scholarly redirects, editor draft limits, and threat model
- Enforce outbound URL policy on all hops via bounded manual redirects in scholarly lookups
- Guard declared Content-Length in EditorDraftRoute before request body parsing
- Keep scholarly lookup timeouts and budgets as internal constants rather than surface env vars
- Narrow TUI threat-model description around demonstrable host shell bridge behavior
- Add behavioral regression tests for redirect security and pre-parsing body size guards
2026-09-23 12:53:06 +01:00
Alexandre Teixeira c90a81dbdc Merge commit '14afd3afb6274ff986733ddff992001e19c80e29' into fix/pr10-merge-ready 2026-09-23 12:29:27 +01:00
Alexandre Teixeira f577934777 fix(ci): restore green lab baseline before runtime W1 (#9)
Validated locally and on GitHub Actions before integration into lab.
2026-09-23 10:16:54 +01:00
Léo 1b75fdb438 test: pin the 2026-09-23 review fixes
Covers both behaviours end to end: a refused outbound URL and an exhausted
budget must skip the request entirely rather than fall through to httpx, the
remaining budget caps each hop's timeout, and an over-ceiling Content-Length
returns 413 without the body being parsed.

Verified against the pre-fix tree: the User-Agent was hardcoded, the endpoints
were literals, check_outbound_url was absent, both hops carried independent
12.0s timeouts, and an oversized declared body returned 422 after parsing
rather than 413 before it.
2026-09-23 11:09:48 +02:00
Léo 130b49f75d docs(security): document the tool approval gate and its default
The post-external-context gate is off unless ODYSSEUS_TOOL_APPROVAL_GATE is
set, but THREAT_MODEL.md described only the untrusted-context wrapper. A reader
auditing the prompt-injection posture would reasonably assume the gate was
active.

State the default, what it blocks when enabled, the two deliberate exemptions,
and note in Known Gaps that the compensating control for the missing shell
sandbox is off by default.
2026-09-23 11:09:48 +02:00
Léo 3412c4212e fix(editor-drafts): refuse oversized drafts before parsing the body
The 256 MiB ceiling was only enforced inside _dump_payload, which runs after
FastAPI has parsed the request and after json.dumps has re-serialised it. By
that point the payload has been materialised several times over, so the limit
rejected an allocation it had already paid for.

Check Content-Length first, on both write routes. A request that omits or lies
about the header still reaches the exact byte count, which remains
authoritative.
2026-09-23 11:09:48 +02:00
Léo edc94a244e fix(search): bound and police the scholarly metadata lookups
The arXiv and OpenAlex title resolvers called two hardcoded endpoints with a
hand-written "Odysseus/0.20" User-Agent, no outbound-URL policy, and a full
12s timeout each. APP_VERSION was already 1.0.3, so the agent string was wrong
the moment it was written, and a scholarly query could hold a user-facing
search open for the sum of all three hops.

Move the endpoints to named constants, build the User-Agent from APP_VERSION,
run both calls through check_outbound_url (they follow redirects, so the final
host is not the one in the constant), and give the whole SearXNG -> OpenAlex ->
arXiv chain one shared wall-clock budget.

The budget is a ContextVar rather than a parameter so the existing test doubles
for _openalex_title_results and _arxiv_title_results keep working unchanged.
2026-09-23 11:09:48 +02:00
Alexandre Teixeira 771d48f147 ci: install FFmpeg for media integration tests 2026-09-23 01:08:20 +01:00
Alexandre Teixeira 3cabbb9bca test(agent): restore exact turn-policy regressions 2026-09-23 00:14:30 +01:00
Alexandre Teixeira c0b71a5cef fix(tools): constrain Python environment namespace mount 2026-09-23 00:10:50 +01:00
Alexandre Teixeira 0012baa17c ci(trivy): free build cache before image scan 2026-09-22 23:27:29 +01:00
Alexandre Teixeira c0a1cecc00 test(baseline): align UI and model profile expectations 2026-09-22 23:27:17 +01:00
Alexandre Teixeira 9bc1cdcab0 test(harness): isolate imports and stabilize browser and DNS fixtures 2026-09-22 23:27:08 +01:00
Alexandre Teixeira 4520b4f6f0 fix(ui): restore rich text controls and accessible research map 2026-09-22 23:26:43 +01:00
Alexandre Teixeira b9cccb93ac fix(tools): expose active Python environment in workspace namespace 2026-09-22 23:26:28 +01:00
Alexandre Teixeira 79a55fac38 fix(agent): preserve focused turn contracts and verified completion 2026-09-22 23:26:16 +01:00
Alexandre Teixeira 128102ef26 test(runtime): isolate tool execution module state 2026-09-22 18:17:42 +01:00
Alexandre Teixeira 5568d7d631 fix(sw): complete editor panel precache 2026-09-22 18:09:04 +01:00
Alexandre Teixeira 83088f8811 ci: provision browser dependencies for pytest 2026-09-22 18:08:16 +01:00
Alexandre Teixeira ebcc6c524f test(database): restore model endpoint isolation 2026-09-22 18:07:09 +01:00
Alexandre Teixeira dbba4978ee test(harness): support cache-busted frontend module imports 2026-09-22 18:06:18 +01:00