You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Plan 034, task T095, slice acceptance (AT-R1, the HTTP half), phase 3. The plan is specs/034-opendox-standalone-operation/tasks.md at opensoft/openxFactory main80217a92, § "Acceptance: AT-R1". It realizes FR-011's HTTP half.
Rulings:5961364221 item 2 (T095 is drafted now and does not go READY before T063 lands); 5850003126 (R1Q10 (a), R1Q12 (a), R1Q13 (a) with (c), R1Q15 (b), R1Q16 (iii) and (iv)).
State: DRAFT. It lands after T089 and T104 (openDox-code#84), per the holder. The holder posts READY and the landers merge; this writer does neither.
On this PR, acceptance is EXPECTED red; validate stays green
Most of the stack the harness drives has landed. main390e2c28 (2026-10-03) carries:
Still a draft: #84 (T104, the console token via the opened URL).
This head reads the console token the way T104 delivers it (section 6 below). At this branch (main plus the harness), the acceptance job therefore fails by design, at [a.capabilities carries no console token], until T104 lands (quoted below). Against a local integration that includes #84, the same harness passes. The validate job runs the whole suite as before, and is green.
Making acceptance a required check is a ruleset change for the repository's owner (docs/branch-protection.md). This PR does not touch the ruleset.
What changes (four files, nothing else)
file
change
acceptance/at_r1_http.py
NEW. The harness. Standard library only. Not a pytest module: it sits outside tests/ and tests_runtime/, so testpaths never collects it and #1144's F9.1 is unchanged (FR-006's "no exclusion" holds).
.github/workflows/validate.yml
A new acceptance job, appended. It has no services, runs no pytest and installs nothing itself: one step, python3 acceptance/at_r1_http.py. The validate job and its triple are untouched.
tests_runtime/test_deploy_shape.py
Required, because test_the_workflow_declares_the_required_job_and_its_steps pinned sorted(jobs) == ["validate"]. It now admits ["acceptance", "validate"]. A new case holds acceptance to no service or container, no pytest run, no pip install step, and one harness step outside testpaths. And test_every_install_of_this_package_reads_one_dependency_lock now also requires that the harness passes the lock to pip.
tests/test_at_r1_http_harness.py
NEW, from Copilot's review at b440d12b. It tests the harness's DECISIONS in process, as tests/test_smoke_signals.py tests the browser half's oracle. It loads the harness from its file, against an in-process loopback server, so nothing is installed or started. 140 cases: the lexer; import specifiers against routes; a module-graph cycle; a dynamic refusal against a static import of the same path; the media types of served modules and stylesheets; modules and stylesheets from outside the plane; bare module specifiers; nineteen catalog shapes; and T104's delivery: the opener's location and privacy (a FIFO included, which is never read); its refresh as a browser follows it; its forward, which must open the console page on a host the plane answers on, with no encoded copy of the token outside the fragment; its token as the page accepts it; its record; the token absent from /capabilities; the stop's exit status; and the after-stop checks. 64 mutants of the harness's decisions are each killed by their case (below).
The two test changes add 141 cases to SELECTED and PASSED, and none skips. The floors permit a rise, and no floor is edited, as with every phase-2 landing since T037. CI's validate at f29b4ddd printed triple: selected=3764 passed=3753 skipped=11 failures=0 errors=0.
Overlap, checked before editing: gh pr diff --name-only on every open openDox-code PR (#60, #61, #62, #63, #64, #65, #67, #69, #71, #72, #73, #74). None touches .github/workflows/validate.yml, tests_runtime/test_deploy_shape.py or acceptance/. Re-checked at 2026-10-03T16:1xZ over every open PR: #76 (T082) and #82 (T100) now edit validate.yml, but only the validate job's triple pins (around line 257). This PR only appends a job after the last line of the file (@@ -299,3 +299,40 @@), so the two never touch the same lines. No open PR touches the other three files.
The documented command, and how the harness runs it
The command the root's README.md documents is the standalone install and its one start: pip install "opendox[local]", then opendox generate-and-open --local …. The flag selects the local mode explicitly (13.4 as this batch amends it), and the local extra carries the bundled server that mode starts (13.1 as this batch amends it). After the install, that start is the single command requirement 10's second scenario has a user run. The entry point is still the code leg's console script, and no Makefile target is added, for the reason above. Carried out by T070 and T076.
The harness holds both lines as constants (DOCUMENTED_INSTALL, DOCUMENTED_START, the … being U+2026 after one space). Compared mechanically with T076's README (opensoft/openDox#17 at 7eef03b4):
The start line. The harness runs opendox generate-and-open --local --repo-root <repo> --repository fixture --no-open --port <free port>. That is quickstart.md § 3's filled form, with --port given a port found free at run time and checked free on 127.0.0.1 and ::1 just before the start. Before it starts anything, it asserts that its argv begins with exactly the tokens of DOCUMENTED_START before the … (start.documented-command). The … is the verb's own arguments, as the README says, and every flag above is one the README names. opendox is the fresh venv's console script, resolved on the child's PATH and asserted to lie inside that venv; argv[0] stays the literal opendox.
The install line. No release of opendox is published (PyPI answers 404 for opendox; the release-channel question is with Brett). So the harness installs the same distribution with the same extra from the checkout: pip install "<checkout>[local]", from a copy of this checkout's tracked files. That is exactly the substitution the README makes for its first line (pip install "./code[local]", run from the root). It installs through constraints-cpython312-linux.txt where the lock's own interpreter runs it (CPython 3.12 on Linux, as in the job). The lock pins versions and adds no package, so the install is still opendox[local] and nothing else (AT-R1 step 2).
What the harness asserts, in order (AT-R1 steps 1-4, and the route answers behind 5-8)
Each assertion has an id. The run stops at the first that fails and prints AT-R1 HTTP half: FAIL [<id>]: <why>, then exits 1. --keep-going reports every failure instead, and still exits 1. A harness that breaks before a verdict exits 2, never 0.
Install. A fresh venv, then opendox[local] (R1Q16 (iii)). Then install declares the local extra, read from the installed distribution's metadata.
The clean machine, ASSERTED (clean.*), in a fresh OPENDOX_STATE_DIR created empty for the run:
none of openxdox, ideation_dashboard, doc_health and corpus_adapter_openxfactory is importable in the venv (python -I, from an empty directory);
omp is not on the PATH, nor is the product's own doxbench_bridge.HARNESS_COMMAND;
no identity broker (openprofiler-broker) is on the PATH, and no issuer setting is set;
nothing listens on 127.0.0.1:5432 or [::1]:5432;
no PostgreSQL Unix socket answers in a shared directory. The harness reads every listening .s.PGSQL.* socket the kernel lists in /proc/net/unix, wherever it lies, plus the distribution sockets as a fallback. One in a directory other users can traverse, as libpq's defaults (/tmp, /var/run/postgresql) are, must not answer. One in a private 0700 directory (another install's own) is noted;
no DSN or PG* setting reaches a child;
neither repository holds a binding at the product's own doxbench_binding.DEFAULT_BINDINGS_RELPATH;
no bundled-server process exists before the start.
Every child runs with GIT_*, XF_*, OPENDOX_*, PG*, DATABASE_URL and PYTHONPATH dropped, a fresh HOME, and TMPDIR inside the scratch directory.
Two plain repositories, each a fresh git init (spec.md AT-R1 step 3): (a) tests/fixtures/plain-documents, and (b) quickstart.md § 2's three notes, byte for byte, with no front matter. Each gets git config user.name/user.email, which the served actor is read from.
The documented start, once per repository (start.*): the port is free, the argv matches, the console script lies in the venv, the server is ready within 120 s, and the process that answered is still the one it launched.
Fetches.
/ must be text/html with an <html> element.
/snapshot.json must be a JSON object, non-empty, and neutral per F5.3: none of #1144's fourteen declared words (5.5's falsifier list, verbatim) in any string value.
The grouping station, named by /capabilities' own display facet (fields.grouping.field), must be filled, so a grouping tile can open the chat pane (R1Q13 (a) with (c); this fails, and does not skip).
/capabilities must give install.mode == "local", and must carry noconsole_token. A standalone plane hands its token to no loopback caller (T104, RULED openxFactory#656 5963851934, adversarial review 2's M5).
The console token, as the user's browser is handed it (T104). The start prints console <file URL>, the PATH of a private opener, and never the token. The harness reads that opener itself, with the standard library, and imports nothing from the product (opendox.console_access.read_private_copy is the product's own reader, and it is not used). The checks:
the start printed the opener (console opener printed);
it is <OPENDOX_STATE_DIR>/console/<port>.html, outside the served repository;
it is private: this user's regular file, mode exactly 0600, one link, in a directory no one else can enter. It is opened with O_NOFOLLOW | O_NONBLOCK and read only if fstat calls it a regular file, so a FIFO can never hang the run;
it has exactly one meta-refresh that a browser follows (the HTML standard's declarative refresh steps: a delay that does not start with a digit or . aborts it). It goes to this plane's console page (/ or /index.html), on a loopback host the launched plane answers on (served_loopback_hosts probes 127.0.0.1 and ::1), with no backslash, whitespace or user information. #console_token=<token> is in the FRAGMENT, and never in the query or the path, raw or in any percent-decoding;
that token is one the page accepts ([A-Za-z0-9_-]{16,512}, T104's takeDeliveredConsoleToken);
its JSON record (<script type="application/json" id="opendox-console">, kind opendox-console-access v1) describes that same forward: its port, its token, its opened_url, and its page_url.
No failure message quotes the token.
The model catalog./workbench/model-catalog must refuse a caller with no token (4xx). With the opener's token in X-XF-Console-Token, it must answer 200 in the envelope the chat rail adopts (views/doxbench-chat-model.js, adoptCatalog):
schema_version is any JSON number equal to 1 (so 1, 1.0 and 1e0), never true and never "1";
kind is workbench-model-catalog;
models is an array, with no entry whose available is true (16.4).
The harness also asserts that the served bundle names both this route and this kind, so neither constant can drift from the bundle.
Every route the panes can request answers, and none answers 5xx or drops the connection (derivation below).
Stop and look. The harness sends SIGTERM to the entry point alone, as kill would, never to its process group, and waits up to 90 s. Then:
the entry point must exit 0 (stop exits 0), as tests_runtime/test_bundled_postgres.py requires of the same stop;
it scans /proc for any live process whose executable lies in the venv's pixeltable_pgserver package, or whose command line names the fresh state directory, and there must be none (R1Q16 (iv)). That is checked at the operating system, not by the product's report;
the opener must be gone (stop removes the console opener);
the token must appear nowhere in either output stream of the whole serve (console token never printed).
Cleanup runs however the run ends: it stops any server it started and kills any bundled-server process the scan finds, but only AFTER the verdict is taken, so a leftover still fails the run. Then it removes the scratch and state directories.
How the route list is derived (from the served JS, not by hand)
derive_bundle reads the bundle from the running server:
It fetches /, then follows the page's <script type="module"> and stylesheet links.
In every module, a small JS lexer strips comments and regular-expression literals and yields each string literal with the code before it. That code tells an import … from "x", an export … from "x" or an import "x" from an ordinary string.
It follows every static specifier, every literal dynamic import("x"), and every view-binding module /capabilities declares, transitively. Each module is fetched from the server under test once, and judged once per way it is imported. A static import must answer 200, even of a path some other module imports dynamically, because a failed one is a module-load pageerror, which AT-R1 step 8 forbids and no declaration can excuse. A dynamic import may be refused, but not with a 5xx. Standalone, /views/intent-feed.js answers 404 (10.2a: not owed), and views/intent-binding.js degrades. A module the server DOES serve, statically or dynamically imported, must be served as JavaScript ([bundle.module-type <path>]: a JavaScript MIME type essence, as tests_runtime/test_served_bundle.py's JAVASCRIPT_TYPES lists them). A linked stylesheet must be text/css ([bundle.sheet-type <path>]). Both comparisons ignore parameters such as charset, and case. A BARE module specifier (import "child.js"), which a browser with no import map refuses, is [bundle.bare <specifier>]. A module or stylesheet from OUTSIDE the plane (https://…, //host/…, data:) is [bundle.external <url>], and is never fetched from loopback by its path: a clean machine with only openDox installed cannot be assumed to reach it.
In every module of that graph, every string literal that is a same-origin path (/ or ./ followed by a letter, with no whitespace, not a .js/.css path) is a route the bundle can request. At the integration below that is 38 modules and 18 routes.
A static read cannot tell a load-time request from an on-click one. So the harness requests ALL of them, a superset of the wheel's, the lens's and the chat rail's load-time reads; T096's browser run observes the load-time set itself. The nine /actions/… routes get a GET too. A GET there finds no handler (each answers 404), so it never executes an action.
Two kinds of route take values the panes fill from the snapshot, and the harness fills them the same way:
A prefix ending in / (/source/). The wheel, the viewer and the workbench's source loader complete it with a document path, so it is also requested once per document the snapshot lists, both plain and keyed by the workbench's key (/source/fixture%40main/<doc>).
The thread read (/workbench/thread). It is also requested with the exact query the chat rail sends when it opens on a document (views/staging-workbench.js:2925, loadThread, called at views/doxbench-chat.js:1276-1281):
repository and ref come from the workbench's key, which with no snapshot index is the snapshot's repository at main (app.js, sourceKeyFor);
tile_kind is cluster (a grouping tile's kind, views/wheel-model.js: "clusters -> "cluster"");
tile_id is the first grouping tile, and document is its first member.
The query-less form alone would stop at the 400 parameter check, before the reach that crashes.
Every request carries the console token the opener delivered, as the doxBench transports do, so a guarded read answers from its handler and not from the console check.
Falsifier: the harness itself
At this branch, main90ac7033 plus the harness (what this PR's own acceptance job runs): FAIL, named
== r32-r8-on-pr-tree rc=1 held=32
AT-R1 HTTP half: FAIL [a.capabilities carries no console token]: /capabilities publishes `console_token` to any loopback caller; a standalone plane delivers it only through the 0600 opener file (T104, adversarial review 2's M5)
1 failed, 32 held
The expected red moved as the stack landed:
at main047bb4fa and 66ff7257, it was [install declares the local extra] (r4, r12);
Against a LOCAL integration of the stack (never pushed): PASS with T104, and without it, FAIL where T104 is missing
Both integrations live in this writer's own clone, with no upstream. The live heads were read at 2026-10-03T21:xxZ, after #77 (T084) and #80 (T103) landed:
tree
what it is
b0a8ec88, the integration WITHOUT T104
maine49b17c3 + #80b9025b6c, #80's landed head. Its tree is exactly main390e2c28's (git diff is empty)
#84 at c979747a conflicts with main390e2c28 itself in 14 files, as a history effect of #80's squash landing: the same content merges clean through #80's landed head. That main merge is #84's writer's.
The harness at 27479495, and then at this PR's head 33841d4a, whose change since only tightens the bundle walk and the opener's read:
== r40-2747-on-live8-t104-failfast rc=0 held=302
AT-R1 HTTP half: PASS (302 assertions held)
== r41-2747-on-live8-t104-keepgoing rc=0 held=302
AT-R1 HTTP half: PASS (302 assertions held)
== r42-2747-on-live8-keepgoing rc=1 held=276
== r43-r13-on-live8-t104-failfast rc=0 held=302
AT-R1 HTTP half: PASS (302 assertions held)
== r44-r13-on-live8-t104-keepgoing rc=0 held=302
AT-R1 HTTP half: PASS (302 assertions held)
== r45-r13-on-main390e-keepgoing rc=1 held=276
AT-R1 HTTP half: FAIL [a.capabilities carries no console token]: /capabilities publishes `console_token` to any loopback caller; a standalone plane delivers it only through the 0600 opener file (T104, adversarial review 2's M5)
also FAIL [a.console opener printed]: the start printed no `console <file URL>` line naming the opener, so a user has no way to open the console page
also FAIL [a.catalog answers]: /workbench/model-catalog with no console token to present (step 6 found none) answers HTTP 403: ...console_required...
also FAIL [b.capabilities carries no console token]: ...
also FAIL [b.console opener printed]: ...
also FAIL [b.catalog answers]: ...
6 failed, 276 held
r42 printed the same six failures as r45. r43 to r45 ran the working tree that 64dc06f5 committed; cmp is identical, and the merge of main after it changed no harness file. The same verdicts held at every earlier integration:
The pushed harness from before the token change (4dcb4221) stops at the integration with T104's first local commit, [a.capabilities console_token] (r16), which is the reason for this change.
Verdict at the integration with T104: AT-R1's HTTP half passes, with 302 assertions held in both passes. That covers:
the install, the clean machine and both repositories;
the documented start: ready, with the process the harness launched;
/ as HTML;
the neutral, non-empty snapshot with a grouping tile: repository (a) has 8 documents and 2 clusters; repository (b), with no front matter, has 3 documents and 2 clusters;
install.mode == local, and no console_token on /capabilities;
the opener: printed, at <OPENDOX_STATE_DIR>/console/<port>.html, private, forwarding with the token in its fragment, and with a record that agrees;
the catalog: it refuses a caller with no token, and with the token answers in the envelope the chat rail adopts, with no available entry;
38 modules, none bare and none from outside the plane, every one served as JavaScript and every static import 200, /views/intent-feed.js refused as 10.2a's not-owed dynamic import, and /styles.css served as text/css;
all 18 derived routes and every parameterized form below 500;
the stop: exit status 0 within the bound, no bundled PostgreSQL process left, the opener removed, and the token in neither output stream.
Pass (a)'s route answers at d05c266e (r44). Every answer is the same as at 16d1053b, a945ef01, 365e4e9b and 887e72f5 before it, by a diff of the runs' GET lines:
Before T084: two product gaps, both T084's, both removed by #77
The same harness against the integration without #77 (e7187f06: main + #69f66e5f82, #721b0c3a63, #7371d24af6, #7183213eb2, #749061b22a, #65c0a97648) held 198 and failed 4. The 4 were these two gaps, each hit in both repositories:
== r6-integ-keepgoing rc=1 held=198
AT-R1 HTTP half: FAIL [a.route /project-register.json]: GET /project-register.json answers no answer (RemoteDisconnected: ...)
also FAIL [a.route /workbench/thread?repository=fixture&ref=main&tile_kind=cluster&tile_id=grouping-compost-corner&document=grouping-compost-corner.md]: ... no answer (RemoteDisconnected: ...)
also FAIL [b.route /project-register.json]: ... no answer (RemoteDisconnected: ...)
also FAIL [b.route /workbench/thread?repository=fixture&ref=main&tile_kind=cluster&tile_id=budget-roadmap&document=budget.md]: ... no answer (RemoteDisconnected: ...)
4 failed, 198 held
GET /project-register.json dropped the connection.src/opendox/serve_project.py:271 (at main047bb4fa) did from openxdox.gate_console import DEFAULT_RECORDS_DIR (and :272, from openxdox.kickoff import …), so it raised ModuleNotFoundError: No module named 'openxdox'. The page requests this route on every load (app.js:1004 → views/repo-selector.js:138-139). It is batch L's named crash site (5920216845). Removed by T084, 4.3: the last deferred reaches through declared seams; consumer_reach retired (plan 034) #77. The handler now reads column_seams.gate and column_seams.kickoff. openDox's default discovers no register, so the route answers the existing structured 404 "no project register", and the picker hides.
The chat rail's thread read dropped the connection.src/opendox/serve_workbench.py:560 at the pre-T084 integration (:548 at main) did from openxdox import doxbench_scope, which is reached once the five query fields are present. The rail requests it when it opens on a document (views/staging-workbench.js:2925). It was not among batch L's three named crash sites; the holder relayed it to T084's writer. Removed by T084, 4.3: the last deferred reaches through declared seams; consumer_reach retired (plan 034) #77. The handler now uses openDox's own scope type. With no live session on that scope, it answers 403 with the no-live-session absence body.
No from openxdox import is left in either file at d05c266e, now that #77 has landed on main, and the runs with #77 found no new gap.
The assertions bite: eleven product mutants, four workflow mutants and sixty-four decision mutants, all killed
Product mutants, each run through the full harness: m1, m2, m4 and m5 at b4fc637f, and m3 again at b440d12b. The only harness change since b4fc637f is main()'s handling of an error while preparing. m1 and m2 were merged from main when #69 was at fedfa75d and #72 at 20032d02. m3 to m5 are working-tree edits of the integration as it was then, 8e4203d8 (before T084):
[a.capabilities install.mode == local]: "the served install block is None"
m3, the server is never stopped
bundle.BundledServer.stop() returns at once, and no parent-death signal
[a.stop leaves no bundled PostgreSQL process]: "still running after the entry point stopped: pid 599656: …/pixeltable_pgserver/pginstall/bin/postgres -D …" (and the harness's cleanup then removed it)
m4, a governance word in the fixture
title: A slow leak at the rain barrel, ratified
[a.snapshot neutral (F5.3)]: "openxFactory's vocabulary leaked into the neutral snapshot: ['ratified']"
m5, a missing module
src/opendox/web/views/lens-model.js deleted
[a.bundle.module /views/lens-model.js]: "imported statically by /app.js, answers HTTP 404"
T104's delivery, each a working-tree edit of the integration WITH #84, a945ef01, run through the full harness at d53a7378 (r33):
mutant
edit
killed at
m6, a readable opener
console_access.PRIVATE_MODE = 0o644
[a.console opener is private]: "… has mode 644, not 600"
m7, the token in the query
opened_url joins with ? instead of #
[a.console opener forwards with the token in its fragment]: "the opener's forward carries the token in its QUERY …"
m8, the opener outlives the server
remove_private_copy returns at once
[a.stop removes the console opener]: "… is still there after the server stopped …"
m9, the token back on /capabilities
build_server publishes it on every plane
[a.capabilities carries no console token]
m10, the token printed
generate-and-open prints token <token>
[a.console token never printed]
m11, an unguarded catalog
the catalog's console check removed
[a.catalog refuses a caller without the console token]: "… answers HTTP 200 …"
The harness's own decisions, each mutated in acceptance/at_r1_http.py and run against tests/test_at_r1_http_harness.py, every one killed:
at d53a7378 (evidence/decision_mutants_d53a7378.py in this writer's work directory), 25 of 25: each of T104's checks (12), the media types (5), the opener's record (6), and the catalog's number (2);
at 5636eb8d, 6 more: the browser-divergence check, the user-information check, the guarded split of the forward, the unquoted message, the guarded split of the printed location, and the record's redaction;
at 486e426e, 7 more: the refresh delay, the optional url keyword, the quote truncation, and the token shape (no check, no upper bound, search for fullmatch, and a lower bound of 1);
at 142d1352, 3 more: no console-page check, an empty path refused, and / not a console page;
at 27479495, 8 more: the origin discarded, the external module and stylesheet checks each dropped, the served-host check, localhost never served, no decoding, one decoding round, and any stop status;
at 64dc06f5, 5 more: a bare specifier resolved, a bare one not judged, any /-containing specifier read as relative, a blocking open of the opener, and no regular-file check (killed by a FIFO that a gone writer filled with a page). Five earlier mutants (static-after-dynamic, the catalog's shapes and its envelope) were killed at f0e0ffe1 and 4dcb4221. One proposed mutant, admitting a string schema_version, is equivalent, because no string equals 1 in Python.
Workflow mutants, against the three affected cases of tests_runtime/test_deploy_shape.py:
Restored: 3 passed. Locally, tests_runtime/test_deploy_shape.py and tests/test_triple_pin.py give 115 passed.
How the harness is run, and where the bundled server's state lives
The harness takes no path and no port (only --keep and --keep-going; see the SonarCloud round below). It installs the checkout its own file is in. Its scratch space and its OPENDOX_STATE_DIR are fresh tempfile directories, so TMPDIR chooses where they go. To measure the integration and each mutant, this writer copied the harness, untracked, into that tree's acceptance/ and ran it there.
In CI, in the runner's temporary directory (/tmp/odx-XXXXXXXX), which is sticky.
The socket path is capped. The bundled server's socket is <OPENDOX_STATE_DIR>/postgres/run/.s.PGSQL.5432, and Linux takes at most 107 bytes (runtime/config.py, UNIX_SOCKET_PATH_MAX). So the harness's state directory is a short odx-XXXXXXXX directly under TMPDIR. A run whose socket could not fit is refused with exit 2, naming TMPDIR, before anything is installed.
The local runs above used TMPDIR=~/.local/state/opendox-t095. This writer's work directory resolves through a world-writable, non-sticky mount, and the product rightly refuses a state directory below it: "… is writable by every user and is not sticky (mode 777) …". That directory was empty after every run, and no bundled PostgreSQL started by these runs was left running.
Review rounds
SonarCloud (advisory; the gate failed at 1c064bb3 on Security Rating D, and passes at every head since bbdeb9ec). Fixed in bbdeb9ec:
pythonsecurity:S8703, S8705, S8707: the --port, --scratch, --state-base and --checkout options flowed into a socket, the server's argv and the filesystem. They are gone.
python:S5443: the /tmp/.s.PGSQL.5432 probe is replaced by the distribution sockets.
python:S3776: the lexer is a class, and the long functions are split one per step.
python:S9073: the acceptance job's two assertions are split.
Copilot, fourteen rounds. Every finding was real and was fixed with evidence: each fixed head reports a named FAIL, and the reviewed head reproduces the defect. Every thread is resolved.
At 1c064bb3, fixed in 32ef3e8c:
r4170450448: a module refused as a dynamic import hid a later static import of the same path.
r4170450491: a malformed 200 catalog raised a harness error instead of a named failure.
At bbdeb9ec: r4170537382, the same static-after-dynamic case. Already covered by 32ef3e8c, and answered there.
At 32ef3e8c, fixed in b4fc637f:
r4170537350: a server on /tmp's socket alone, with TCP off, passed the fixed socket list.
r4170567257: a non-list documents value passed as "non-empty".
A "previously missed" item: a grouping value with no tile holding an id and a member document passed, and silently dropped the rail's thread read.
At b4fc637f, "Needs a closer look" with no new finding. It noted that the PR needs integrated verification with the dependent product changes: that is the integration above, now including T084. Its "previously missed" item, fixed in b440d12b: an OSError in prepare() escaped main() with exit 1. This head returns 2; at b4fc637f, an ENOSPC from tempfile.mkdtemp escaped.
At b440d12b, "Needs a closer look" with no findings. Its "previously missed" item, fixed in f0e0ffe1: the module-graph verdict had no collected regression test, and on this branch the acceptance job stops before derive_bundle. The new tests/test_at_r1_http_harness.py above answers it.
At f0e0ffe1, "Needs a closer look" with no findings. Its "previously missed" item, fixed in 4dcb4221: a 200 catalog of {"models": []} passed, though adoptCatalog adopts only schema_version === 1 and kind === "workbench-model-catalog", and shows any other catalog as unreadable. The envelope is now a named check, and the served bundle must name the kind. Seven refused envelopes are tested, and relaxing the int check fails the true case.
At 4dcb4221, "Needs a closer look" with no findings.
At 1c0ff975, fixed in f29b4ddd: r4173473346, the envelope check refused "schema_version": 1.0 and 1e0, which JavaScript's === 1 adopts. It now admits any JSON number equal to 1, and never true or "1".
At f29b4ddd ("Changes recommended"), fixed in de8b2274:
r4173769822: a module served 200 as text/plain or text/html passed;
r4173769844: so did a stylesheet.
Both now require their type.
At d53a7378 ("Changes recommended"), fixed in 5636eb8d:
r4173842763: a forward holding a backslash or user information passed, though a browser opens another host;
r4173842794: a refused destination's message quoted a URL that could hold the token;
r4173842805 and r4173842811: an unparseable printed location or refresh URL raised a harness error instead of a named failure.
At 5636eb8d ("Changes recommended"), fixed in 486e426e:
r4173894317: a refresh whose delay a browser aborts (invalid;url=, -1;url=) still yielded a token;
r4173894352: a token that takeDeliveredConsoleToken discards (short) passed.
At 486e426e, "Needs a closer look" with no findings. Its "previously missed" item, fixed in 142d1352: with an agreeing record, a forward to /missing.html or /snapshot.json passed. The forward must now open / or /index.html.
At 142d1352 ("Changes recommended"), fixed in 27479495:
r4174355680: a percent-encoded copy of the token in the query passed;
previously missed: a forward to [::1] passed, though the plane listens on 127.0.0.1 alone;
previously missed: an import from https://… was fetched as its local path and passed;
previously missed: any stop exit status passed.
At 27479495 ("Changes recommended"), fixed in 64dc06f5:
r4174411680: a bare specifier (import "child.js") passed when /child.js was served;
previously missed: a FIFO opener hung the harness under --keep-going.
Not done here
No ruleset change: making acceptance required is the owner's.
No fix to any other writer's PR. The two gaps above are reported.
No README edit: the openDox root's README is T076's (openDox#17).
Add an isolated AT-R1 HTTP acceptance check that validates standalone openDox operation and secure console access in CI.
New Features:
Add an AT-R1 HTTP acceptance harness that installs openDox in an isolated environment, exercises standalone operation on clean plain repositories, validates served routes and console-token delivery, and verifies clean shutdown.
Add a dedicated CI acceptance job that runs the harness without database services or pytest.
Enhancements:
Expand deployment-shape checks to enforce the acceptance job’s isolation, single harness command, test-path exclusion, and dependency-lock usage.
Add in-process tests covering the harness’s bundle traversal, route validation, catalog checks, console-token security, and shutdown decisions.
CI:
Run the standalone HTTP acceptance checks in their own 20-minute GitHub Actions job alongside the existing validation suite.
Tests:
Add comprehensive unit coverage for acceptance-harness decisions and malformed or insecure server responses.
…n 034)
acceptance/at_r1_http.py is release 1's acceptance test, the HTTP half
(FR-011; spec.md section AT-R1, steps 1-4 and the route answers behind
steps 5-8). It is not a pytest module and sits outside tests/ and
tests_runtime/, so testpaths and F9.1 are unchanged. It:
- installs openDox alone into a fresh venv as opendox[local] (R1Q16 (iii)),
from a copy of this checkout's tracked files, through the dependency lock;
- asserts the clean machine in a fresh, short OPENDOX_STATE_DIR: the four
siblings not importable, no omp, no identity broker, no database on the
default port or socket and no DSN, and no model binding;
- copies both plain repositories into fresh git inits with a git identity;
- runs the documented opendox generate-and-open --local ... (T007 batch H's
10.3 addendum, as the openDox root README documents it) on loopback,
checking its argv against that line;
- fetches /, /snapshot.json (non-empty, neutral per F5.3, a grouping tile)
and /capabilities (install.mode == local);
- fetches the model catalog with the console token: no available entry;
- fetches every route the served bundle names, derived from the running
server's module graph, and none may answer 5xx or drop the connection;
- stops the entry point with SIGTERM and asserts no bundled PostgreSQL
process is left (R1Q16 (iv)).
It exits 1 on the first failed assertion, naming it, and 2 on a harness
error.
.github/workflows/validate.yml gains the acceptance job, which has no
database service, because the harness asserts a clean machine. It runs
no pytest and installs nothing itself. Making it a required check is a
ruleset change for the repository's owner.
tests_runtime/test_deploy_shape.py admits that one other job, holds it to
no service, no pytest and no pip step, and extends the one-lock rule to
the harness's install.
Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Introduces a dedicated, intentionally database-free CI acceptance job backed by a comprehensive standard-library AT-R1 HTTP harness, while extending workflow-shape tests to enforce its isolation, execution contract, and dependency-lock usage; the existing validate suite remains unchanged.
Sequence diagram for the AT-R1 HTTP acceptance harness
sequenceDiagram
participant CI as acceptance job
participant H as at_r1_http.py
participant V as Fresh venv
participant O as opendox
participant R as Plain Git repository
participant S as HTTP server
CI->>H: Run python3 acceptance/at_r1_http.py
H->>V: Create venv and install checkout[local] with lock
H->>H: Assert clean machine and no database service
H->>R: Create fresh repository and commit documents
H->>O: Start opendox generate-and-open --local
O-->>S: Serve repository
H->>S: Fetch /, /snapshot.json, /capabilities
H->>S: Fetch model catalog and derived bundle routes
H->>O: Send SIGTERM
H->>H: Assert no bundled PostgreSQL process remains
alt Assertion fails
H-->>CI: Exit 1 with named failure
else All assertions hold
H-->>CI: Exit 0
end
Loading
Flow diagram for AT-R1 harness assertions
flowchart TD
START["Start harness"] --> INSTALL["Install opendox[local] in fresh venv"]
INSTALL --> CLEAN["Assert clean machine: no sibling tools, broker, DB, or bindings"]
CLEAN --> REPOS["Create two plain Git repositories"]
REPOS --> STARTAPP["Run documented local start"]
STARTAPP --> CORE["Check HTML, neutral snapshot, grouping station, and local mode"]
CORE --> CATALOG["Check model catalog has no available model"]
CATALOG --> DERIVE["Derive module graph and request bundle routes"]
DERIVE --> STOP["SIGTERM entry point and scan for leftover bundled PostgreSQL"]
STOP --> RESULT{Assertions pass?}
RESULT -->|Yes| PASS["PASS"]
RESULT -->|No| FAIL["FAIL with assertion id"]
Loading
File-Level Changes
Change
Details
Files
Add a standalone AT-R1 HTTP acceptance harness that installs and exercises openDox on a clean Linux environment.
Build a tracked checkout into a fresh virtual environment and install the local extra through the CPython 3.12 dependency lock.
Assert isolation from sibling packages, external harnesses, identity services, database services, inherited environment settings, and model bindings.
Create two plain Git repositories and run the documented local server command against each.
Validate HTML, snapshot neutrality and grouping data, local install capabilities, no-model catalog behavior, derived frontend module and route availability, and clean shutdown without bundled PostgreSQL leftovers.
Provide fail-fast or keep-going diagnostics, Linux/process checks, cleanup, and explicit exit codes.
acceptance/at_r1_http.py
Run the HTTP acceptance harness in a dedicated CI job isolated from the database-backed validation job.
Add an Ubuntu Python 3.12 acceptance job with checkout and setup steps.
Run only the standard-library harness command, with no services, container, pytest invocation, or job-level package installation.
Leave the existing validate job and its required-check behavior unchanged.
.github/workflows/validate.yml
Pin the new workflow shape and dependency-lock usage in deployment-shape tests.
Allow exactly validate and acceptance workflow jobs.
Verify acceptance has no database service or container, does not run pytest or pip install, and invokes the harness once outside pytest testpaths.
Verify the harness installation references and passes the CPython 3.12 Linux constraints file.
tests_runtime/test_deploy_shape.py
Tips and commands
Interacting with Sourcery
Trigger a new review: Comment @sourcery-ai review on the pull request.
Continue discussions: Reply directly to Sourcery's review comments.
Generate a GitHub issue from a review comment: Ask Sourcery to create an
issue from a review comment by replying to it. You can also reply to a
review comment with @sourcery-ai issue to create an issue from it.
Generate a pull request title: Write @sourcery-ai anywhere in the pull
request title to generate a title at any time. You can also comment @sourcery-ai title on the pull request to (re-)generate the title at any time.
Generate a pull request summary: Write @sourcery-ai summary anywhere in
the pull request body to generate a PR summary at any time exactly where you
want it. You can also comment @sourcery-ai summary on the pull request to
(re-)generate the summary at any time.
Generate reviewer's guide: Comment @sourcery-ai guide on the pull
request to (re-)generate the reviewer's guide at any time.
Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
pull request to resolve all Sourcery comments. Useful if you've already
addressed all the comments and don't want to see them anymore.
Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
request to dismiss all existing Sourcery reviews. Especially useful if you
want to start fresh with a new review - don't forget to comment @sourcery-ai review to trigger a new review!
Adds the HTTP half of standalone openDox acceptance testing, separate from the existing validation suite. Acceptance is intentionally expected to fail until dependent features land.
Changes:
Adds a clean-install harness covering HTTP responses and shutdown.
Runs acceptance in a separate CI job without database services.
Extends workflow-shape tests to enforce isolation and dependency-lock usage.
File
Description
tests_runtime/test_deploy_shape.py
Enforces acceptance-job configuration and lock usage.
… (SonarCloud)
SonarCloud's quality gate on openDox-code#75 failed on Security Rating D
for four findings in acceptance/at_r1_http.py, and flagged five
cognitive-complexity smells and one composite assertion.
- pythonsecurity:S8703, S8705, S8707: --port, --scratch, --state-base and
--checkout flowed into a socket, the server's argv and the filesystem.
The harness now takes no path and no port: it installs the checkout its
own file is in (copy it into another tree to measure that tree), its
scratch and state directories are fresh tempfile directories (TMPDIR
chooses where), and the port is a free one found at run time. Only
--keep and --keep-going remain.
- python:S5443: the /tmp/.s.PGSQL.5432 probe is gone. The clean-machine
check probes the distribution sockets (/var/run/postgresql and
/run/postgresql) beside TCP 5432 on both stacks; the bundled server
never listens in either place.
- python:S3776: js_strings becomes the JsStrings lexer class, and
serve_one, main and derive_bundle are split into one function per step.
Behaviour is unchanged: the same 89 and 198 assertions hold at the
local integration, the same falsifier fires at main, and the same five
product mutants are killed.
- python:S9073: the acceptance job's service and container assertions
are separate in tests_runtime/test_deploy_shape.py.
Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…usal, and a malformed catalog is a named failure (Copilot)
Copilot's review of openDox-code#75 at 1c064bb:
- r4170450448: a module first reached by a dynamic import and refused
was marked seen, so a later STATIC import of the same path was never
judged, and a page that cannot load passed. derive_bundle now fetches
and scans each path once but judges every (path, static) pair, so the
static import must still answer 200. Proved over a crafted bundle
(app.js imports missing.js dynamically, child.js statically): the
reviewed head reports no failure; this head reports
[t.bundle.module /missing.js].
- r4170450491: a 200 catalog of [], null or {"models": 1} raised inside
the harness (exit 2) instead of failing a named check. Every nested
read of the product's JSON (the catalog, /capabilities' install, display
and views blocks, the snapshot's documents and groups) now goes through
as_object / as_list, so each such answer fails [catalog is a catalog]
or its own check, with no exception.
The local integration's verdict is unchanged: 89 held fail-fast, and
198 held with 4 failed under --keep-going, all at T084's two crash sites.
Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Accepts invalid grouping without required document
acceptance/at_r1_http.py:1038
This truthiness check accepts both a non-list grouping value and a list such as [{"id":"topic","document_edges":[]}]. In either case, thread_query() returns None, so requests_for() silently omits the parameterized thread read. The query-less request stops at parameter validation and cannot expose the handler failure this harness is intended to catch. Require a grouping tile with an id and a member document before accepting this check.
… of documents, a grouping tile with a member (Copilot)
Copilot's review of openDox-code#75 at 32ef3e8:
- r4170537350: a server listening only on /tmp's socket, with TCP off,
passed the clean-machine check's fixed list of directories. The check
now reads every listening `.s.PGSQL.*` socket from /proc/net/unix,
wherever it lies, and requires that none answers in a directory other
users can traverse, as every libpq default can (/tmp,
/var/run/postgresql). A socket in a private (0700) directory is another
install's own, reachable only through an explicit setting that no child
inherits, so it is noted, not failed. No directory is hardcoded, so
SonarCloud's S5443 stays clear. Proved with a listening fake socket: a
shared directory fails [clean.database socket ...] here and passed at
32ef3e8; a 0700 directory is noted.
- r4170567257: a truthy non-list documents value passed [snapshot
non-empty]; it must now be a non-empty list.
- previously missed: [snapshot fills the grouping station] accepted a
grouping value with no tile holding an id and a member document, which
silently dropped the rail's parameterized thread read. It now requires
exactly what that read is built from.
The local integration's verdict is unchanged: 89 held fail-fast, and
198 held with 4 failed under --keep-going, all at T084's two crash sites.
Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Handle preparation errors with documented ERROR status 2
acceptance/at_r1_http.py:1283
Filesystem errors from prepare() bypass this handler. For example, tempfile.mkdtemp() raising OSError(ENOSPC) escapes main() and exits with status 1, instead of the documented harness ERROR and status 2. This makes an infrastructure failure look like a product acceptance failure. Catch unexpected preparation exceptions and report status 2, as the later execution block does.
…t 2 (Copilot)
Copilot's review of openDox-code#75 at b4fc637 ("previously missed"):
an OSError from prepare(), such as ENOSPC under tempfile.mkdtemp,
escaped main() and exited 1, so an infrastructure failure read as a
product acceptance failure. main() now catches any unexpected exception
from prepare() and reports `AT-R1 HTTP half: ERROR` with exit 2, as the
execution block already does. Proved with mkdtemp patched to raise
ENOSPC: this head returns 2, while at b4fc637 the OSError escapes
main().
At the local integration with T084 (openDox-code#77 at b1db196) merged
in, the harness now PASSES: 202 assertions held, fail-fast and
--keep-going alike.
Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The reason will be displayed to describe this comment to others. Learn more.
Copilot review overview
🔵 Needs a closer look
The substantial harness lacks automated graph-verdict regression coverage and a passing end-to-end result against the complete feature stack.
Review effort: Balanced Findings: None
Previously missed (1)
In code that hasn't changed since last review
Add behavioral regression tests for module-graph verdict
acceptance/at_r1_http.py:661
The module-graph verdict has no automated behavioral regression tests. The new deployment tests inspect configuration and source text, while this branch's acceptance run stops at the missing local extra before reaching derive_bundle. Add collected, in-process tests for traversal cycles, route extraction, and the already-fixed case where a dynamic 404 precedes a static import of the same URL. Mocking get would exercise these decisions without installing or starting the product, while keeping the harness itself outside pytest collection. tests/test_smoke_signals.py provides a similar pattern for testing acceptance decision logic.
…s tests (Copilot)
Copilot's review of openDox-code#75 at b440d12 ("previously missed"):
the module-graph verdict had no collected regression test, and on this
branch the acceptance job stops at the missing `local` extra, before
derive_bundle ever runs.
tests/test_at_r1_http_harness.py loads acceptance/at_r1_http.py from its
file and tests its decisions in process, as tests/test_smoke_signals.py
tests the browser half's oracle. An in-process loopback server stands in
for the product (the pattern test_model_provider_broker.py uses), so
nothing is installed or started. 13 cases:
- the lexer skips comments and regular expressions;
- an import specifier is told from a route literal, and a route resolves
against the page;
- the module graph terminates on a cycle and counts each module once;
- a dynamic refusal does not hide a static import of the same path
(r4170450448), and a dynamic refusal alone is not a failure (10.2a);
- a malformed 200 catalog is a named failure, never an exception
(r4170450491), in seven shapes;
- the documented start is the README line less its ellipsis.
Four mutants of the harness, each killed by its case: a path judged once
by path alone, every `/` read as a division, the catalog read without a
shape check, and a stylesheet path taken for a route. The harness stays
outside testpaths, so F9.1 is unchanged. tests/: 2450 passed, 11
skipped, locally.
Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The reason will be displayed to describe this comment to others. Learn more.
Copilot review overview
🔵 Needs a closer look
The catalog check accepts envelopes the chat rail rejects, allowing a false acceptance result.
Review effort: Balanced Findings: None
Previously missed (1)
In code that hasn't changed since last review
HTTP check accepts catalogs with invalid envelope metadata
acceptance/at_r1_http.py:1106
An HTTP 200 response containing {"models": []} passes every catalog check, even if kind and schema_version are missing or incorrect. However, adoptCatalog requires schema_version === 1 and kind === "workbench-model-catalog" (src/opendox/web/views/doxbench-chat-model.js:228–235); the chat rail treats a rejected envelope as unreadable. This lets the HTTP acceptance check pass a catalog the UI cannot use. Require both envelope fields in this named check, update the successful catalog fixtures to use valid envelopes, and add missing/wrong kind and version cases.
…dopts (Copilot)
Copilot's review of openDox-code#75 at f0e0ffe ("previously missed"):
a 200 catalog of {"models": []} passed every catalog check, though the
chat rail's adoptCatalog (views/doxbench-chat-model.js) adopts only
schema_version === 1 and kind === "workbench-model-catalog", and shows
any other catalog as unreadable, never as its no-model state.
- A named check, [catalog envelope is the one the chat rail adopts],
requires schema_version to be the integer 1 (as JavaScript's === 1,
so no true and no "1") and kind == "workbench-model-catalog".
- So that constant cannot drift from the bundle, the module-graph walk
also collects every string literal, and [bundle names the catalog kind]
requires the served bundle to name it, as [bundle names the catalog
route] already does for the route.
- tests/test_at_r1_http_harness.py: the passing catalogs carry a valid
envelope, and seven envelopes adoptCatalog refuses are added: missing
kind, missing version, version 2, true, "1", and a different kind.
Relaxing the int check fails the `true` case.
At the local integration with T084 (#77 at b1db196), the product's real
catalog passes: 206 assertions held, PASS.
Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…page read them (plan 034)
Copilot review of openDox-code#75 at 5636eb8:
- r4173894317: the refresh delay was ignored, so `content="invalid;url=…"`
or `"-1;url=…"` yielded a token, though a browser aborts that refresh
and leaves the user on the opener. `refresh_target` now follows the
HTML standard's shared declarative refresh steps for the forms a refresh
takes. The delay must start with an ASCII digit or `.`. A `,` or `;`
separator and the `url =` keyword, in any case and spacing, are
optional. A quoted URL ends at its closing quote. A refresh with no URL
part opens no console.
- r4173894352: any non-empty fragment value was taken as the token, but
T104's `takeDeliveredConsoleToken` (web/views/notebook.js) discards a
value that is not `[A-Za-z0-9_-]{16,512}`. The harness could then
authenticate where the user's page cannot. The forward's token must now
match that shape exactly (CONSOLE_TOKEN_SHAPE), and the message does
not quote the value.
The tests add 14 cases. Four refresh forms a browser follows: comma and
spaces, a fractional delay, no `url` keyword, and a quote that truncates.
Four delays that abort the refresh: a word, a negative, an empty delay,
and a bare `url=`. Four tokens the page discards: too short, too long,
dots, and an escaped `+`. Two tokens at the page's bounds, 16 and 512
characters. Seven mutants are each killed.
At the local integration with T104 (#84cb89954), the real opener passes
(r37: PASS, 302 assertions held).
Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The destination check validates the authority but not the page path. With a matching JSON record, both /missing.html#console_token=… and /snapshot.json#console_token=… pass every opener assertion. Later checks request / and the catalog independently, so acceptance can pass while the user's browser opens a missing page or JSON instead of the console. Require / or /index.html, and add regression cases where both the forward and record name a non-console path.
Copilot review of openDox-code#75 at 486e426, previously missed: the
destination check validated the authority but not the path. With a
record that agreed, `/missing.html#console_token=…` or
`/snapshot.json#console_token=…` passed every opener assertion, while the
user's browser would open a missing page or JSON instead of the console.
The forward's path must now be one of CONSOLE_PAGES, `/` or the
`/index.html` that T104's entry points open. An empty path is `/`, as a
browser reads it. The message does not quote the path.
The tests add 5 cases: `/missing.html`, `/snapshot.json` and
`//index.html` are refused; `/` and an empty path are accepted. Three
mutants are each killed: no path check, an empty path refused, and `/`
not a console page.
At the local integration with T104 (#84cb89954), the real opener passes
(r38: PASS, 302 assertions held).
Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Reject IPv6 opener targets when launch binds only to IPv4
acceptance/at_r1_http.py:220
launch() uses the default IPv4-only bind (127.0.0.1), but this allows an opener targeting [::1]:<port>. With an agreeing record, that opener passes delivery validation even though it cannot reach the launched server. Later catalog and route probes use IPv4 directly, so they do not catch the broken destination. Restrict opener hosts to those supported by this launch and add an IPv6-forward regression case.
Preserve origins and reject unsupported external dependencies
acceptance/at_r1_http.py:677
Discarding the origin makes external dependencies look local. For example, a static import of https://unreachable.invalid/app.js resolves here to /app.js. The walker can reuse the successful local response and pass, although the browser cannot load the external module. Script roots and stylesheets use the same resolver. Preserve the origin during resolution and report a named acceptance failure for unsupported external dependencies rather than fetching their paths from loopback. Add regression cases for external imports and page links.
Require zero shutdown status for a passing lifecycle test
acceptance/at_r1_http.py:1641
Any exit status passes this check, including 1 or -SIGTERM. If shutdown fails after removing the opener and stopping PostgreSQL, the harness can still report PASS. The existing lifecycle test requires status zero (tests_runtime/test_bundled_postgres.py:440). Require rc == 0 here and add a decision test for a nonzero shutdown status.
… a failed stop are named failures (plan 034)
Copilot review of openDox-code#75 at 142d135:
- r4174355680: the duplicate-token check searched the raw query, so
`?extra=<percent-encoded token>#console_token=<token>` passed, though
the request line still carries a recoverable copy. The path and query
are now also searched in every percent-decoding (`_decodings`, with
`+` read as a space and not, until nothing new appears).
- previously missed: a forward to `[::1]` passed, though the launched
plane listens on 127.0.0.1 alone. The forward's host must now be one
the plane answers on. `served_loopback_hosts` probes 127.0.0.1 and ::1
at the server's port, and adds `localhost` where one answers.
- previously missed: `_resolve` discarded the origin, so a static import
of `https://unreachable.invalid/app.js` was fetched as `/app.js` from
loopback and passed. It now keeps any URL that leaves the plane whole.
A module or stylesheet from outside the plane is a named failure,
`[bundle.external <url>]`, and is never fetched from loopback.
- previously missed: any exit status passed the stop. It must now be 0,
as tests_runtime/test_bundled_postgres.py requires of the same stop:
`[stop exits 0]`.
The tests add 19 cases: an encoded query, a double-encoded query and an
encoded path; an unserved `[::1]`; a served `[::1]`; the host probe;
three imports from outside the plane (static https, dynamic
protocol-relative, a data: URL); page links from outside the plane;
and four stop statuses (0, 1, -15, timeout). Eight mutants are each
killed.
At the local integration with T104 (#84cb89954), the real plane passes
(r39: PASS, 302 assertions held, `[a.stop exits 0]` and `[b.stop exits 0]`
among them).
Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With --keep-going, a FIFO opener fails the privacy check but is still passed to this blocking os.open. If it has no writer, the harness hangs instead of reporting its verdict or reaching cleanup. A bounded probe confirms the hang after the non-regular-file rejection. Open nonblocking and check that the opened descriptor is a regular file before reading; add a FIFO regression case.
…ures (plan 034)
Copilot review of openDox-code#75 at 2747949:
- r4174411680: a bare specifier such as `import "child.js"` resolved to
/child.js and passed when that file was served. A browser with no
import map refuses it: the HTML standard's "resolve a module
specifier" accepts only a URL, or a specifier that starts with `/`,
`./` or `../`. A bare specifier, static or dynamic, is now
`[bundle.bare <specifier>]`, and is never fetched by its path
(`_specifier`, BARE_PREFIX).
- previously missed: under --keep-going, a FIFO at the opener's path
failed the privacy check but was still opened with a blocking
os.open. With no writer, that hung the harness before its verdict
and its cleanup. `_read_without_following` now opens with O_NONBLOCK
and reads only what fstat calls a regular file.
The tests add 10 cases:
- four bare specifiers (static, dynamic, export-from and a package
name), each with the file served;
- three relative specifiers that still resolve (./, ../ and /);
- a FIFO opener, which must fail as not private and not delivering,
within a bounded wait;
- a FIFO that a gone writer has filled with a page, which must be
refused and not read.
Five mutants are each killed: a bare specifier resolved, a bare one
not judged, any slash read as relative, a blocking open, and no
regular-file check.
At the local integration with T104 (#84c979747, on a tree identical
to main 390e2c2), the real plane passes (r43 fail-fast and r44
--keep-going: PASS, 302 assertions held). Without T104, main's own tree
fails in 6 checks, all where T104 is missing (r45).
Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Brings in #77 (T084) and #80 (T103). No file this branch edits changed
on main; the merge is clean.
Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ment names its path (plan 034)
Copilot review of openDox-code#75 at 33841d4:
- r4174621486: `Server.said()` put the tail of the entry point's stdout
and stderr into start and route failures. If the entry point printed
its console token and then failed, the harness published that token
in its own log before the after-stop check could catch it. A
diagnostic now names where the output is (both files), says why it
is not echoed, and points at --keep. `printed()` still reads all of
it for the after-stop check.
- r4174621535: `documents: [{}]` passed `snapshot non-empty`, while
`requests_for` dropped every entry without a path and so skipped
every document's source reads. New check, [snapshot documents each
name a path]: every entry must be an object with a non-empty string
`path`.
The tests add 7 cases: six document lists (named; an empty object; an
empty path; a numeric path; a string entry; one of two pathless) and a
diagnostic that must name both files and quote no token, while
printed() still holds the token. Four mutants are each killed: the
output echoed, the path check dropped, an empty path admitted, and a
path of any type admitted.
At the local integration with T104 (#84c979747, on a tree identical
to main 390e2c2), the real plane passes (r46 fail-fast and r47
--keep-going: PASS, 304 assertions held, the two new ones among them).
Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…, and no line quotes a token (plan 034)
Copilot review of openDox-code#75 at 4809b3d:
- r4174671390: a refresh inside `<template>` or `<noscript>` was counted
as the opener's forward, though a browser that runs scripts never acts
on it. `_RefreshContents` now reads only LIVE elements: nothing inside
a template, a noscript or a raw-text or escapable raw-text element
(title, style, script, textarea, xmp, iframe, noembed, noframes,
plaintext), and, failing closed, select and frameset. The first of a
repeated attribute is read, `http-equiv` is `refresh` exactly, and
`/>` closes only a void element.
- r4174671426: the privacy check stopped at `console/`. The opener's whole
path is now judged by T104's own rules for that tree: the state
directory is this user's and no one else can write it; every directory
above it, as written and as resolved, is this user's or root's, sticky
where others can write it; every symbolic link on the way is this
user's or root's. `prepare()` refuses a TMPDIR that fails them (exit 2),
so the verdict judges what the product made.
Carried from r4174621486 to every diagnostic: `Verdict` redacts every
console token the run has seen (the opener's forwards and record, and a
token `/capabilities` publishes) from every id, reason, note and the
report; the printed opener path is quoted only where it is the expected
one; a record nested past the JSON parser's depth is a named failure.
Tests: 68 new cases (215 harness cases; 326 with deploy-shape). Decision
mutants lv1-lv9, tr1-tr8, rd1-rd9: 25 of 26 killed at the named cases;
lv2 survived the first textarea case (this Python already parses textarea
as text) and is answered by the noscript and iframe cases.
At the local integration with T104 (#8460bace0, which merges main
390e2c2), the real plane passes (r48 fail-fast: PASS, 304 assertions
held).
Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… by name nor by value, and console/ is 0700 exactly (plan 034)
T007 batch N (openxFactory bdd0f586; RULED 5963851934) amends T095: "As
quickstart § 3 does, it asserts that the raw `/capabilities` payload
carries the console token neither by name, at any depth, nor by value".
- By name: `[capabilities carries no console token]` (the id CI's
by-design failure already names) now fails where `console_token` is
anywhere in the RAW payload, as § 3 checks it, or in any key of the
parsed one, however deep or however escaped. It read a top-level key
only.
- By value: `[capabilities carries the opener's token nowhere]`, run
once the opener has handed the harness the token: the token is
nowhere in the raw payload, nor in any key or string value of the
parsed one, under whatever name. Neither is quoted.
- The opener's directory is 0700 exactly, as § 3 checks it (it was "no
bits for others"). The catalog header is sent in process, so the token
is on no command line, as § 3 asks.
Tests: 15 new cases (230 harness cases; 341 with deploy-shape).
Mutants rp1-rp6 and dm1 are each killed at the named cases. Product
mutant m12 (#84d4b9943 with the token under `session_hint`) fails
`[a.capabilities carries the opener's token nowhere]` (r51), and prints
no token.
Runs: with T104 (#84d4b9943, which merges main 0116293), r49
--keep-going: PASS, 306 assertions held. Without it (main 0116293), r50
--keep-going: the known 6 failures, all where T104 is missing.
Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Brings in #81 (T102). No file this branch edits changed on main; the
merge is clean.
Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Plan 034, task T095, slice
acceptance(AT-R1, the HTTP half), phase 3. The plan isspecs/034-opendox-standalone-operation/tasks.mdat opensoft/openxFactorymain80217a92, § "Acceptance: AT-R1". It realizes FR-011's HTTP half.Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
5961416730.5961364221item 2 (T095 is drafted now and does not go READY before T063 lands);5850003126(R1Q10 (a), R1Q12 (a), R1Q13 (a) with (c), R1Q15 (b), R1Q16 (iii) and (iv)).On this PR,
acceptanceis EXPECTED red;validatestays greenMost of the stack the harness drives has landed.
main390e2c28(2026-10-03) carries:localextra);Still a draft: #84 (T104, the console token via the opened URL).
This head reads the console token the way T104 delivers it (section 6 below). At this branch (
mainplus the harness), theacceptancejob therefore fails by design, at[a.capabilities carries no console token], until T104 lands (quoted below). Against a local integration that includes #84, the same harness passes. Thevalidatejob runs the whole suite as before, and is green.Making
acceptancea required check is a ruleset change for the repository's owner (docs/branch-protection.md). This PR does not touch the ruleset.What changes (four files, nothing else)
acceptance/at_r1_http.pytests/andtests_runtime/, sotestpathsnever collects it and #1144's F9.1 is unchanged (FR-006's "no exclusion" holds)..github/workflows/validate.ymlacceptancejob, appended. It has no services, runs no pytest and installs nothing itself: one step,python3 acceptance/at_r1_http.py. Thevalidatejob and its triple are untouched.tests_runtime/test_deploy_shape.pytest_the_workflow_declares_the_required_job_and_its_stepspinnedsorted(jobs) == ["validate"]. It now admits["acceptance", "validate"]. A new case holdsacceptanceto no service or container, no pytest run, nopip installstep, and one harness step outsidetestpaths. Andtest_every_install_of_this_package_reads_one_dependency_locknow also requires that the harness passes the lock to pip.tests/test_at_r1_http_harness.pyb440d12b. It tests the harness's DECISIONS in process, astests/test_smoke_signals.pytests the browser half's oracle. It loads the harness from its file, against an in-process loopback server, so nothing is installed or started. 140 cases: the lexer; import specifiers against routes; a module-graph cycle; a dynamic refusal against a static import of the same path; the media types of served modules and stylesheets; modules and stylesheets from outside the plane; bare module specifiers; nineteen catalog shapes; and T104's delivery: the opener's location and privacy (a FIFO included, which is never read); its refresh as a browser follows it; its forward, which must open the console page on a host the plane answers on, with no encoded copy of the token outside the fragment; its token as the page accepts it; its record; the token absent from/capabilities; the stop's exit status; and the after-stop checks. 64 mutants of the harness's decisions are each killed by their case (below).The two test changes add 141 cases to SELECTED and PASSED, and none skips. The floors permit a rise, and no floor is edited, as with every phase-2 landing since T037. CI's
validateatf29b4dddprintedtriple: selected=3764 passed=3753 skipped=11 failures=0 errors=0.Overlap, checked before editing:
gh pr diff --name-onlyon every open openDox-code PR (#60, #61, #62, #63, #64, #65, #67, #69, #71, #72, #73, #74). None touches.github/workflows/validate.yml,tests_runtime/test_deploy_shape.pyoracceptance/. Re-checked at 2026-10-03T16:1xZ over every open PR: #76 (T082) and #82 (T100) now editvalidate.yml, but only thevalidatejob's triple pins (around line 257). This PR only appends a job after the last line of the file (@@ -299,3 +299,40 @@), so the two never touch the same lines. No open PR touches the other three files.The documented command, and how the harness runs it
T007 batch H's 10.3 addendum (
openspec/changes/add-neutral-product-standalone-operability/tasks.md, 10.3, "AMENDED — T007 Batch H (5850003126; Ruled R1Q15 (b), R1Q16 (iii))"), verbatim:The harness holds both lines as constants (
DOCUMENTED_INSTALL,DOCUMENTED_START, the…being U+2026 after one space). Compared mechanically with T076's README (opensoft/openDox#17 at7eef03b4):opendox generate-and-open --local --repo-root <repo> --repository fixture --no-open --port <free port>. That is quickstart.md § 3's filled form, with--portgiven a port found free at run time and checked free on 127.0.0.1 and ::1 just before the start. Before it starts anything, it asserts that its argv begins with exactly the tokens ofDOCUMENTED_STARTbefore the…(start.documented-command). The…is the verb's own arguments, as the README says, and every flag above is one the README names.opendoxis the fresh venv's console script, resolved on the child's PATH and asserted to lie inside that venv; argv[0] stays the literalopendox.opendoxis published (PyPI answers 404 foropendox; the release-channel question is with Brett). So the harness installs the same distribution with the same extra from the checkout:pip install "<checkout>[local]", from a copy of this checkout's tracked files. That is exactly the substitution the README makes for its first line (pip install "./code[local]", run from the root). It installs throughconstraints-cpython312-linux.txtwhere the lock's own interpreter runs it (CPython 3.12 on Linux, as in the job). The lock pins versions and adds no package, so the install is stillopendox[local]and nothing else (AT-R1 step 2).What the harness asserts, in order (AT-R1 steps 1-4, and the route answers behind 5-8)
Each assertion has an id. The run stops at the first that fails and prints
AT-R1 HTTP half: FAIL [<id>]: <why>, then exits 1.--keep-goingreports every failure instead, and still exits 1. A harness that breaks before a verdict exits 2, never 0.Install. A fresh venv, then
opendox[local](R1Q16 (iii)). Theninstall declares the local extra, read from the installed distribution's metadata.The clean machine, ASSERTED (
clean.*), in a freshOPENDOX_STATE_DIRcreated empty for the run:openxdox,ideation_dashboard,doc_healthandcorpus_adapter_openxfactoryis importable in the venv (python -I, from an empty directory);ompis not on the PATH, nor is the product's owndoxbench_bridge.HARNESS_COMMAND;openprofiler-broker) is on the PATH, and no issuer setting is set;.s.PGSQL.*socket the kernel lists in/proc/net/unix, wherever it lies, plus the distribution sockets as a fallback. One in a directory other users can traverse, as libpq's defaults (/tmp,/var/run/postgresql) are, must not answer. One in a private 0700 directory (another install's own) is noted;PG*setting reaches a child;doxbench_binding.DEFAULT_BINDINGS_RELPATH;Every child runs with
GIT_*,XF_*,OPENDOX_*,PG*,DATABASE_URLandPYTHONPATHdropped, a freshHOME, andTMPDIRinside the scratch directory.Two plain repositories, each a fresh
git init(spec.md AT-R1 step 3): (a)tests/fixtures/plain-documents, and (b) quickstart.md § 2's three notes, byte for byte, with no front matter. Each getsgit config user.name/user.email, which the served actor is read from.The documented start, once per repository (
start.*): the port is free, the argv matches, the console script lies in the venv, the server is ready within 120 s, and the process that answered is still the one it launched.Fetches.
/must betext/htmlwith an<html>element./snapshot.jsonmust be a JSON object, non-empty, and neutral per F5.3: none of #1144's fourteen declared words (5.5's falsifier list, verbatim) in any string value./capabilities' own display facet (fields.grouping.field), must be filled, so a grouping tile can open the chat pane (R1Q13 (a) with (c); this fails, and does not skip)./capabilitiesmust giveinstall.mode == "local", and must carry noconsole_token. A standalone plane hands its token to no loopback caller (T104, RULED openxFactory#6565963851934, adversarial review 2's M5).The console token, as the user's browser is handed it (T104). The start prints
console <file URL>, the PATH of a private opener, and never the token. The harness reads that opener itself, with the standard library, and imports nothing from the product (opendox.console_access.read_private_copyis the product's own reader, and it is not used). The checks:console opener printed);<OPENDOX_STATE_DIR>/console/<port>.html, outside the served repository;O_NOFOLLOW | O_NONBLOCKand read only iffstatcalls it a regular file, so a FIFO can never hang the run;.aborts it). It goes to this plane's console page (/or/index.html), on a loopback host the launched plane answers on (served_loopback_hostsprobes 127.0.0.1 and ::1), with no backslash, whitespace or user information.#console_token=<token>is in the FRAGMENT, and never in the query or the path, raw or in any percent-decoding;[A-Za-z0-9_-]{16,512}, T104'stakeDeliveredConsoleToken);<script type="application/json" id="opendox-console">, kindopendox-console-accessv1) describes that same forward: its port, its token, itsopened_url, and itspage_url.No failure message quotes the token.
The model catalog.
/workbench/model-catalogmust refuse a caller with no token (4xx). With the opener's token inX-XF-Console-Token, it must answer 200 in the envelope the chat rail adopts (views/doxbench-chat-model.js,adoptCatalog):schema_versionis any JSON number equal to 1 (so1,1.0and1e0), nevertrueand never"1";kindisworkbench-model-catalog;modelsis an array, with no entry whoseavailableis true (16.4).The harness also asserts that the served bundle names both this route and this kind, so neither constant can drift from the bundle.
Every route the panes can request answers, and none answers 5xx or drops the connection (derivation below).
Stop and look. The harness sends SIGTERM to the entry point alone, as
killwould, never to its process group, and waits up to 90 s. Then:stop exits 0), astests_runtime/test_bundled_postgres.pyrequires of the same stop;/procfor any live process whose executable lies in the venv'spixeltable_pgserverpackage, or whose command line names the fresh state directory, and there must be none (R1Q16 (iv)). That is checked at the operating system, not by the product's report;stop removes the console opener);console token never printed).Cleanup runs however the run ends: it stops any server it started and kills any bundled-server process the scan finds, but only AFTER the verdict is taken, so a leftover still fails the run. Then it removes the scratch and state directories.
How the route list is derived (from the served JS, not by hand)
derive_bundlereads the bundle from the running server:/, then follows the page's<script type="module">and stylesheet links.import … from "x", anexport … from "x"or animport "x"from an ordinary string.import("x"), and every view-binding module/capabilitiesdeclares, transitively. Each module is fetched from the server under test once, and judged once per way it is imported. A static import must answer 200, even of a path some other module imports dynamically, because a failed one is a module-loadpageerror, which AT-R1 step 8 forbids and no declaration can excuse. A dynamic import may be refused, but not with a 5xx. Standalone,/views/intent-feed.jsanswers 404 (10.2a: not owed), andviews/intent-binding.jsdegrades. A module the server DOES serve, statically or dynamically imported, must be served as JavaScript ([bundle.module-type <path>]: a JavaScript MIME type essence, astests_runtime/test_served_bundle.py'sJAVASCRIPT_TYPESlists them). A linked stylesheet must betext/css([bundle.sheet-type <path>]). Both comparisons ignore parameters such ascharset, and case. A BARE module specifier (import "child.js"), which a browser with no import map refuses, is[bundle.bare <specifier>]. A module or stylesheet from OUTSIDE the plane (https://…,//host/…,data:) is[bundle.external <url>], and is never fetched from loopback by its path: a clean machine with only openDox installed cannot be assumed to reach it./or./followed by a letter, with no whitespace, not a.js/.csspath) is a route the bundle can request. At the integration below that is 38 modules and 18 routes.A static read cannot tell a load-time request from an on-click one. So the harness requests ALL of them, a superset of the wheel's, the lens's and the chat rail's load-time reads; T096's browser run observes the load-time set itself. The nine
/actions/…routes get a GET too. A GET there finds no handler (each answers 404), so it never executes an action.Two kinds of route take values the panes fill from the snapshot, and the harness fills them the same way:
A prefix ending in
/(/source/). The wheel, the viewer and the workbench's source loader complete it with a document path, so it is also requested once per document the snapshot lists, both plain and keyed by the workbench's key (/source/fixture%40main/<doc>).The thread read (
/workbench/thread). It is also requested with the exact query the chat rail sends when it opens on a document (views/staging-workbench.js:2925,loadThread, called atviews/doxbench-chat.js:1276-1281):repositoryandrefcome from the workbench's key, which with no snapshot index is the snapshot's repository atmain(app.js,sourceKeyFor);tile_kindiscluster(a grouping tile's kind,views/wheel-model.js: "clusters -> "cluster"");tile_idis the first grouping tile, anddocumentis its first member.The query-less form alone would stop at the 400 parameter check, before the reach that crashes.
Every request carries the console token the opener delivered, as the doxBench transports do, so a guarded read answers from its handler and not from the console check.
Falsifier: the harness itself
At this branch,
main90ac7033plus the harness (what this PR's ownacceptancejob runs): FAIL, namedThe expected red moved as the stack landed:
main047bb4faand66ff7257, it was[install declares the local extra](r4,r12);main8e377823), it was[a.capabilities install.mode == local]. This PR's job printed that atf29b4ddd(run 37133775447, job 111233985686);Against a LOCAL integration of the stack (never pushed): PASS with T104, and without it, FAIL where T104 is missing
Both integrations live in this writer's own clone, with no upstream. The live heads were read at 2026-10-03T21:xxZ, after #77 (T084) and #80 (T103) landed:
b0a8ec88, the integration WITHOUT T104maine49b17c3+ #80b9025b6c, #80's landed head. Its tree is exactlymain390e2c28's (git diffis empty)d05c266e, the integration WITH T104b0a8ec88+ #84c979747a(git merge --no-ff, clean)#84 at
c979747aconflicts withmain390e2c28itself in 14 files, as a history effect of #80's squash landing: the same content merges clean through #80's landed head. Thatmainmerge is #84's writer's.The harness at
27479495, and then at this PR's head33841d4a, whose change since only tightens the bundle walk and the opener's read:r42printed the same six failures asr45.r43tor45ran the working tree that64dc06f5committed;cmpis identical, and the merge ofmainafter it changed no harness file. The same verdicts held at every earlier integration:16d1053b, with DRAFT (phase 3, after T063): T104, the console token travels in the opened URL, not /capabilities (plan 034) #84cb899546(r34tor39);a945ef01, with DRAFT (phase 3, after T063): T104, the console token travels in the opened URL, not /capabilities (plan 034) #846db50b03(r29tor31).The pushed harness from before the token change (
4dcb4221) stops at the integration with T104's first local commit,[a.capabilities console_token](r16), which is the reason for this change.Verdict at the integration with T104: AT-R1's HTTP half passes, with 302 assertions held in both passes. That covers:
/as HTML;install.mode == local, and noconsole_tokenon/capabilities;<OPENDOX_STATE_DIR>/console/<port>.html, private, forwarding with the token in its fragment, and with a record that agrees;/views/intent-feed.jsrefused as 10.2a's not-owed dynamic import, and/styles.cssserved astext/css;Pass (a)'s route answers at
d05c266e(r44). Every answer is the same as at16d1053b,a945ef01,365e4e9band887e72f5before it, by adiffof the runs'GETlines:Before T084: two product gaps, both T084's, both removed by #77
The same harness against the integration without #77 (
e7187f06: main + #69f66e5f82, #721b0c3a63, #7371d24af6, #7183213eb2, #749061b22a, #65c0a97648) held 198 and failed 4. The 4 were these two gaps, each hit in both repositories:GET /project-register.jsondropped the connection.src/opendox/serve_project.py:271(atmain047bb4fa) didfrom openxdox.gate_console import DEFAULT_RECORDS_DIR(and:272,from openxdox.kickoff import …), so it raisedModuleNotFoundError: No module named 'openxdox'. The page requests this route on every load (app.js:1004→views/repo-selector.js:138-139). It is batch L's named crash site (5920216845).Removed by T084, 4.3: the last deferred reaches through declared seams; consumer_reach retired (plan 034) #77. The handler now reads
column_seams.gateandcolumn_seams.kickoff. openDox's default discovers no register, so the route answers the existing structured404 "no project register", and the picker hides.src/opendox/serve_workbench.py:560at the pre-T084 integration (:548atmain) didfrom openxdox import doxbench_scope, which is reached once the five query fields are present. The rail requests it when it opens on a document (views/staging-workbench.js:2925). It was not among batch L's three named crash sites; the holder relayed it to T084's writer.Removed by T084, 4.3: the last deferred reaches through declared seams; consumer_reach retired (plan 034) #77. The handler now uses openDox's own scope type. With no live session on that scope, it answers
403with the no-live-session absence body.No
from openxdoximport is left in either file atd05c266e, now that #77 has landed onmain, and the runs with #77 found no new gap.The assertions bite: eleven product mutants, four workflow mutants and sixty-four decision mutants, all killed
Product mutants, each run through the full harness: m1, m2, m4 and m5 at
b4fc637f, and m3 again atb440d12b. The only harness change sinceb4fc637fismain()'s handling of an error while preparing. m1 and m2 were merged frommainwhen #69 was atfedfa75dand #72 at20032d02. m3 to m5 are working-tree edits of the integration as it was then,8e4203d8(before T084):25f1f6af)[a.catalog offers no available entry]: "no model is configured, yet the catalog offers ['omp-local']"f3957515)[a.capabilities install.mode == local]: "the served install block is None"bundle.BundledServer.stop()returns at once, and no parent-death signal[a.stop leaves no bundled PostgreSQL process]: "still running after the entry point stopped: pid 599656: …/pixeltable_pgserver/pginstall/bin/postgres -D …" (and the harness's cleanup then removed it)title: A slow leak at the rain barrel, ratified[a.snapshot neutral (F5.3)]: "openxFactory's vocabulary leaked into the neutral snapshot: ['ratified']"src/opendox/web/views/lens-model.jsdeleted[a.bundle.module /views/lens-model.js]: "imported statically by /app.js, answers HTTP 404"T104's delivery, each a working-tree edit of the integration WITH #84,
a945ef01, run through the full harness atd53a7378(r33):console_access.PRIVATE_MODE = 0o644[a.console opener is private]: "… has mode 644, not 600"opened_urljoins with?instead of#[a.console opener forwards with the token in its fragment]: "the opener's forward carries the token in its QUERY …"remove_private_copyreturns at once[a.stop removes the console opener]: "… is still there after the server stopped …"/capabilitiesbuild_serverpublishes it on every plane[a.capabilities carries no console token]generate-and-openprintstoken <token>[a.console token never printed][a.catalog refuses a caller without the console token]: "… answers HTTP 200 …"The harness's own decisions, each mutated in
acceptance/at_r1_http.pyand run againsttests/test_at_r1_http_harness.py, every one killed:d53a7378(evidence/decision_mutants_d53a7378.pyin this writer's work directory), 25 of 25: each of T104's checks (12), the media types (5), the opener's record (6), and the catalog's number (2);5636eb8d, 6 more: the browser-divergence check, the user-information check, the guarded split of the forward, the unquoted message, the guarded split of the printed location, and the record's redaction;486e426e, 7 more: the refresh delay, the optionalurlkeyword, the quote truncation, and the token shape (no check, no upper bound,searchforfullmatch, and a lower bound of 1);142d1352, 3 more: no console-page check, an empty path refused, and/not a console page;27479495, 8 more: the origin discarded, the external module and stylesheet checks each dropped, the served-host check,localhostnever served, no decoding, one decoding round, and any stop status;64dc06f5, 5 more: a bare specifier resolved, a bare one not judged, any/-containing specifier read as relative, a blocking open of the opener, and no regular-file check (killed by a FIFO that a gone writer filled with a page). Five earlier mutants (static-after-dynamic, the catalog's shapes and its envelope) were killed atf0e0ffe1and4dcb4221. One proposed mutant, admitting a stringschema_version, is equivalent, because no string equals 1 in Python.Workflow mutants, against the three affected cases of
tests_runtime/test_deploy_shape.py:acceptancegains apostgresservicetest_the_acceptance_job_runs_the_harness_on_a_clean_machine-c <lock>test_every_install_of_this_package_reads_one_dependency_lockacceptancechains&& python -m pytest -qtest_the_acceptance_job_runs_the_harness_on_a_clean_machineacceptancegains its ownpip install .stepRestored:
3 passed. Locally,tests_runtime/test_deploy_shape.pyandtests/test_triple_pin.pygive115 passed.How the harness is run, and where the bundled server's state lives
The harness takes no path and no port (only
--keepand--keep-going; see the SonarCloud round below). It installs the checkout its own file is in. Its scratch space and itsOPENDOX_STATE_DIRare freshtempfiledirectories, soTMPDIRchooses where they go. To measure the integration and each mutant, this writer copied the harness, untracked, into that tree'sacceptance/and ran it there./tmp/odx-XXXXXXXX), which is sticky.<OPENDOX_STATE_DIR>/postgres/run/.s.PGSQL.5432, and Linux takes at most 107 bytes (runtime/config.py,UNIX_SOCKET_PATH_MAX). So the harness's state directory is a shortodx-XXXXXXXXdirectly underTMPDIR. A run whose socket could not fit is refused with exit 2, namingTMPDIR, before anything is installed.TMPDIR=~/.local/state/opendox-t095. This writer's work directory resolves through a world-writable, non-sticky mount, and the product rightly refuses a state directory below it: "… is writable by every user and is not sticky (mode 777) …". That directory was empty after every run, and no bundled PostgreSQL started by these runs was left running.Review rounds
1c064bb3on Security Rating D, and passes at every head sincebbdeb9ec). Fixed inbbdeb9ec:pythonsecurity:S8703,S8705,S8707: the--port,--scratch,--state-baseand--checkoutoptions flowed into a socket, the server's argv and the filesystem. They are gone.python:S5443: the/tmp/.s.PGSQL.5432probe is replaced by the distribution sockets.python:S3776: the lexer is a class, and the long functions are split one per step.python:S9073: the acceptance job's two assertions are split.At
1c064bb3, fixed in32ef3e8c:r4170450448: a module refused as a dynamic import hid a later static import of the same path.r4170450491: a malformed 200 catalog raised a harness error instead of a named failure.At
bbdeb9ec:r4170537382, the same static-after-dynamic case. Already covered by32ef3e8c, and answered there.At
32ef3e8c, fixed inb4fc637f:r4170537350: a server on/tmp's socket alone, with TCP off, passed the fixed socket list.r4170567257: a non-listdocumentsvalue passed as "non-empty".At
b4fc637f, "Needs a closer look" with no new finding. It noted that the PR needs integrated verification with the dependent product changes: that is the integration above, now including T084. Its "previously missed" item, fixed inb440d12b: anOSErrorinprepare()escapedmain()with exit 1. This head returns 2; atb4fc637f, anENOSPCfromtempfile.mkdtempescaped.At
b440d12b, "Needs a closer look" with no findings. Its "previously missed" item, fixed inf0e0ffe1: the module-graph verdict had no collected regression test, and on this branch the acceptance job stops beforederive_bundle. The newtests/test_at_r1_http_harness.pyabove answers it.At
f0e0ffe1, "Needs a closer look" with no findings. Its "previously missed" item, fixed in4dcb4221: a 200 catalog of{"models": []}passed, thoughadoptCatalogadopts onlyschema_version === 1andkind === "workbench-model-catalog", and shows any other catalog as unreadable. The envelope is now a named check, and the served bundle must name the kind. Seven refused envelopes are tested, and relaxing the int check fails thetruecase.At
4dcb4221, "Needs a closer look" with no findings.At
1c0ff975, fixed inf29b4ddd:r4173473346, the envelope check refused"schema_version": 1.0and1e0, which JavaScript's=== 1adopts. It now admits any JSON number equal to 1, and nevertrueor"1".At
f29b4ddd("Changes recommended"), fixed inde8b2274:r4173769822: a module served 200 astext/plainortext/htmlpassed;r4173769844: so did a stylesheet.Both now require their type.
At
d53a7378("Changes recommended"), fixed in5636eb8d:r4173842763: a forward holding a backslash or user information passed, though a browser opens another host;r4173842794: a refused destination's message quoted a URL that could hold the token;r4173842805andr4173842811: an unparseable printed location or refresh URL raised a harness error instead of a named failure.At
5636eb8d("Changes recommended"), fixed in486e426e:r4173894317: a refresh whose delay a browser aborts (invalid;url=,-1;url=) still yielded a token;r4173894352: a token thattakeDeliveredConsoleTokendiscards (short) passed.At
486e426e, "Needs a closer look" with no findings. Its "previously missed" item, fixed in142d1352: with an agreeing record, a forward to/missing.htmlor/snapshot.jsonpassed. The forward must now open/or/index.html.At
142d1352("Changes recommended"), fixed in27479495:r4174355680: a percent-encoded copy of the token in the query passed;[::1]passed, though the plane listens on 127.0.0.1 alone;https://…was fetched as its local path and passed;At
27479495("Changes recommended"), fixed in64dc06f5:r4174411680: a bare specifier (import "child.js") passed when/child.jswas served;--keep-going.Not done here
acceptancerequired is the owner's.🤖 Generated with Claude Code
Summary by Sourcery
Add an isolated AT-R1 HTTP acceptance check that validates standalone openDox operation and secure console access in CI.
New Features:
Enhancements:
CI:
Tests: