Skip to content

T080, 16.3: the credential stays a reference, and a raw key is refused (plan 034) - #63

Merged
brettheap merged 30 commits into
mainfrom
build/034-p3b-t080-raw-key-refusal
Oct 3, 2026
Merged

brettheap merged 30 commits into
mainfrom
build/034-p3b-t080-raw-key-refusal

Conversation

@brettheap

@brettheap brettheap commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Lane: openxfactory-4 (openXfactory-4-openDox_extraction)

Arc: neutral-product-standalone-operability

Plan 034, phase-3 slice P3-B, task T080. The plan was read at openxFactory main e369cb25, from specs/034-opendox-standalone-operation/tasks.md, and read again at main 2140f5a7 and, after T063 landed, at main a883bbf6 (see the box below). The slice is claimed on openxFactory#656, comment 5875729625.

This was drafted ahead, and T063 has now landed. Brett's phase-3 word, as the holder recorded it (2026-09-28, ~18:00Z), was "Only the independent ones (Recommended)". Every phase-3 task comes after T063, which landed as openxFactory#1218 → a883bbf6 and closes phase 2. T078 landed as #61 → 8a98e317 and T079 as #62 → 2fc714d2, so this PR may go READY. Its READY line is the holder's to post.

Important

Dependencies: After: T079, T007 (batches H and K). Both batches have landed, and so have T063 and T079 (#62).

  • Batch H, openxFactory#1206 → f99a2097: 16.3's addendum for the built-in resolver and the auth kind none (R1Q17 (b), R1Q18 (a)). This PR carries it out.
  • Batch K, openxFactory#1210 → 39f19145: the dated note in requirement 17's body in #1144's spec delta, with a pointer after 16.3's batch H addendum (5916000030, item 1). See Ruled below.
  • Read at openxFactory main 2140f5a7, and again at a883bbf6, where it is unchanged. Plan 034's T080 entry records the loopback rule, batch K and openDox-code#64, and its After: line names both batches. F16.1's block is unchanged.

One PR per task, stacked. T078 (#61) and T079 (#62) have landed, and this PR is based on main. #64, the broker hardening, is based on this branch, so retarget #64 to main before this branch is deleted, because a merge with --delete-branch closes the PR stacked on it.

What changes

A key inside the endpoint URL is refused when the binding is declared. Two checks ask. One is the product's own detector, runtime/local_git_adapter.carries_a_credential, imported where it is asked, as authoring.py does. The other is a raw key's shape anywhere in the URL, its path and fragment included (the adversarial review's M5, below). Both run before the scheme check, and every refusal of an endpoint is a fixed sentence, the scheme's too (M3), so no refusal repeats the URL. The key's refusal is ENDPOINT_CARRIES_A_CREDENTIAL, because the URL it refuses carries the key, and through the console the refusal can reach a browser. A key in an extra field is refused as an unknown key, as it always was.

An endpoint longer than the product's URL bound is refused before the detector is asked. The bound is runtime/config.MAX_REMOTE_URL_CHARS (2048), which the repository act applies to a remote for the same reason: the detector is quadratic in a parameter name's length. It is read when the binding is declared, and its refusal, ENDPOINT_TOO_LONG, repeats nothing of the endpoint.

A reference is never a raw key (the adversarial review's M2, below). A reference is held to the same length bound (CREDENTIAL_REF_TOO_LONG), and then refused if the detector flags it or it has a raw key's shape (CREDENTIAL_REF_IS_A_RAW_KEY). Both sentences are fixed. A reference a broker hands back at intake meets the same rule.

Each record now has ONE resolver, and credential_source() names it:

the record its resolver broker_argv credential_ref
any other reference the broker it names, as before required required
env:NAME or keyring:SERVICE/USERNAME the built-in resolver (R1Q17 (b)) not needed; one given is refused the reference
auth kind none (R1Q18 (a)) nothing: no credential is presented forbidden forbidden

The built-in resolver is doxbench_provider.resolve_credential_reference, inside doxbench_provider.py only.

  • It reads the reference at call time, once per request, and keeps nothing. There is no mint, no ledger event, and no credential on the port between turns, so a rotated key is the one the next request presents. The resolved value travels as Authorization: Bearer <value> and nowhere else.
  • The resolved value must be non-empty printable ASCII with no whitespace, which is what a bearer credential is by its grammar. A value that is unset, or is anything else, refuses with the fixed DIAG_REFERENCE_UNRESOLVED, before any provider is contacted. A keyring that cannot be read refuses with DIAG_KEYRING_UNAVAILABLE. That refusal is raised after the backend's error has been handled, so it has no cause and no context: neither the backend's words nor its frames travel with it. FIXED_DIAGNOSTICS goes from eight to eleven: these two, and DIAG_PROVIDER_REDIRECTED below.
  • A 401 without a broker is DIAG_PROVIDER_REFUSED with no retry. The 2026-08-26 ruling's re-mint is for a minted token, and a second read of a reference names the same value.

A credential the built-in resolver reads travels only by a private route: https://, or http:// to 127.0.0.1, ::1 or localhost. This is Brett's ruling of 2026-09-28, openxFactory#656 comment 5880893901, which batch K's note to #1144's requirement 17 records (see Ruled below).

  • doxbench_binding.is_a_private_route is the one predicate. It matches the endpoint as written, case-blind: https://, or http:// followed by exactly one of LOOPBACK_HOSTS (the IPv6 literal bracketed), an optional port, and then /, ?, # or the end.
  • It does not use a URL parser's reading of the host, because the parser and the HTTP client disagree. For http://evil.example\@localhost/, urllib.parse reads the host as localhost, while the HTTP client reads the whole authority.
  • The record refuses a built-in reference on any other route when it is declared, whether through the constructor, a stored record or the operator door. The refusal is one fixed sentence, ENDPOINT_NOT_PRIVATE, and it comes before any resolution, because no binding exists to resolve.
  • The resolver asks the same predicate before its first read. No declared binding reaches that check, so a binding-shaped object that does gets an AssertionError with nothing read, as a broker's reference already does.
  • The broker path's route is unchanged, as the ruling says. So is none's, since it presents no credential. The one change this PR makes to the broker path is L4's mapping in the shared transport (below).

A request that carries a built-in credential keeps it on that route. This came out of Copilot's later rounds, below.

  • It follows no redirect. urllib's default opener re-sends the credential header to any Location, whatever its host or scheme. The redirect is declined, and the turn refuses with the fixed DIAG_PROVIDER_REDIRECTED.
  • Over plain http:// it uses no proxy, because a proxy would carry a loopback request off this host. An https:// request may still use one, since a proxy reaches it only by CONNECT and the credential stays inside TLS.
  • No frame a refusal keeps holds the raw credential. The credential travels as _PresentedCredential, whose repr says nothing, on both paths. A refusal of such a request is raised with no cause and no context, so no traceback reaches urllib's own frames, whose locals hold the headers.

none joins AUTH_KINDS after api_key and oauth, so F16.1's AUTH_KINDS[0] is unchanged. Its requests carry no authorization header.

Around them:

  • The record parses a reference's form once, for both sides (built_in_reference_parts), and reads nothing a reference names. Both forms parse to one shape, BuiltInReference(form, name, user).
  • The env: and keyring: forms are reserved for the built-in resolver. So a broker whose intake answer names a reference in one of them is refused as malformed (DIAG_BROKER_MALFORMED), which both entry points already catch.
    • Otherwise the record would be declared again with two resolvers.
    • The console's intake route builds that record outside the handler that catches a refused binding (serve_workbench.py, which is left alone), so there the refusal would have gone uncaught.
  • A stored record may leave out the fields its own resolver forbids or does not need.
  • Each read-back and each removal states the custody sentence that is true of its resolver.
  • The operator door takes --auth-kind none, an omitted --credential-ref, and an empty broker invocation.
  • set-credential refuses a binding no broker answers, without reading its standard input.
  • broker_operation_argv treats such a binding as a programming error, since its empty base invocation would run the subcommand as a program.
  • http.client.HTTPException joins the errors the transport maps. A response http.client cannot read, or a path it cannot send, lands on DIAG_PROVIDER_UNREACHABLE (the adversarial review's L4, below). The transport is shared, so this changes the broker path's failure too: a broker's turn now lands on that sentence where it escaped before. Raising it afresh there, with no token in a kept frame, is Broker-path credential hardening (follows T080): a minted token keeps a built-in credential's rules (plan 034) #64's (Copilot at 44582f8f).
  • A keyring package that fails as it is imported, with anything other than an ImportError, refuses with DIAG_KEYRING_UNAVAILABLE, raised with no context, as a failed read already does (Copilot at 82ec9a20).

Files: src/opendox/doxbench_binding.py, src/opendox/doxbench_provider.py, src/opendox/cli_model_binding.py and tests/test_model_provider_broker.py, all inside P3-B's row. One docstring outside the row, doxbench_intake.auth_kind_disclosure's, now says which kinds the console flow offers (see Review remarks). pyproject.toml is untouched.

Realizes: 16.3. Ruled: R1Q22 (a), 5817152735; R1Q17 (b) and R1Q18 (a), 5850003126; and the loopback rule, 5880893901, with batch K's note to requirement 17, 5916000030, item 1 (see Ruled below).

Readings Brett let stand

Brett's word, relayed by the holder on 2026-09-28: the readings below stand.

  1. A broker_argv given beside a built-in reference is refused. This is the plan's fail-closed reading (analyze round 2, V2-21), which openxFactory#656 comment 5851950767 records as standing, not overruled. That such a record needs no broker at all follows from R1Q17 (b). One test holds both halves: test_a_built_in_reference_needs_no_broker_and_refuses_one_beside_it.
  2. Where the plan is silent, this PR chose:
    • the reference syntax: env: plus a portable variable name, and keyring:SERVICE/USERNAME, split at the last / so a service may contain one;
    • the OS keyring through the keyring package, imported at call time and not declared as a dependency. Without it, a keyring reference refuses with the fixed sentence. pyproject.toml is outside this slice, and T072 owns its local extra, so declaring it is a holder decision;
    • the resolved value presented as a bearer credential for either kind that takes one. Refreshing an OAuth grant stays a broker's job.

The falsifier: F16.1's three refusals, and tests of the resolver and of none

This is F16.1's record block, extracted verbatim from #1144's tasks.md at openxFactory e369cb25, and unchanged at main 2140f5a7 after batches H and K and at main a883bbf6 after T063 (sha256 8af3001b5f5fb885…), with the control record and the three raw keys:

rec = {f: "stand-in" for f in b.BINDING_FIELDS}
rec.update(auth_kind=b.AUTH_KINDS[0], dialect="openai-chat-v1", endpoint="http://127.0.0.1:9/v1/chat/completions")
if "broker_argv" in rec:
    rec["broker_argv"] = ["stand-in-broker"]
b.ModelProviderBinding.from_record(rec)               # the control: a clean record is accepted
for bad in (dict(rec, endpoint="https://user:sk-stand-in@api.example.invalid/v1"),
            dict(rec, endpoint="https://api.example.invalid/v1?api_key=sk-stand-in"),
            dict(rec, api_key="sk-stand-in")):
    ...
print("dialect and model declared; a raw key is refused in a field and in the URL")

At T079's head b04a3a95 it stops with FAIL: a raw key was accepted in ['endpoint']. At this head 05cb1c70, as at 4948e6dd, the whole block passes:

dialect and model declared; a raw key is refused in a field and in the URL

The named tests are in tests/test_model_provider_broker.py:

  • test_f16_1_a_raw_key_is_refused_in_a_field_and_in_the_url (the block above, as a test) and test_f16_1_the_first_auth_kind_still_takes_a_credential;
  • the URL detector over eight keyed shapes, with none repeated in its refusal; four clean endpoints accepted; extra fields refused;
  • the URL bound: an endpoint past it refused without the detector being asked, and nothing of it repeated; one at it accepted; the number read from runtime/config at declaration;
  • the private route (32 cases; each declaration case runs with an env: and a keyring: reference):
    • accepted: https://, 127.0.0.1, [::1] (IPv6 loopback), localhost, LOCALHOST and a bare http://localhost;
    • refused, by the constructor and from a stored record: another host, localhost.evil.com, 127.0.0.1.evil.com, localhost only in the path, 127.0.0.2, [0:0:0:0:0:0:0:1], localhost., localhost%2eevil.com (the HTTP client decodes it) and 0.0.0.0;
    • mixed-case schemes, read by the predicate: HTTP:// to another host and Http://localhost.evil.com are not private, while hTTp://127.0.0.1, HTTP://[::1], HTTPS:// and hTtPs:// are. Also not private: the backslash case, a leading space and another scheme;
    • the resolver reads nothing, from the environment or the keyring, for four routes that are not private, one of them in mixed case. It does read for IPv6 loopback;
    • the operator door refuses an env: binding over http:// to another host and stores nothing, while a none binding to that host is declared. A broker binding and a none binding keep their route;
  • the transport (9 cases, over real sockets unless noted):
    • a 301, 302, 303, 307 or 308 declined, with the second server hearing nothing;
    • a stand-in proxy hearing nothing;
    • three walks of every frame a refusal keeps, through its causes and contexts, finding no local that holds the secret. One is a real refused socket, one is an unpresentable value, and one, with a stand-in opener, is the broker path's provider-call frame;
  • none: forbids both fields, round-trips, may leave both out, sends no authorization header, spawns no broker (over a real loopback socket too), and refuses a 401 without a retry;
  • the resolver: env: read at call time and rotated between turns; production reading os.environ; eleven unusable values refused before any request, and every printable ASCII character but the space presented unchanged; a value outside latin-1 refused over a real socket with nothing chained; availability restored after a success; the keyring read per request at the last-/ split; an absent entry and three unusable keyring answers; a failing backend (fixed sentence, no cause and no context); the keyring package absent; the production import path; the bearer over a real socket; no trace of the resolved value in answers, notices, the ledger, the port's state or on disk; and the refusal mapping onto the seam's fixed model_failed;
  • the reference forms: one parse and one shape for both; fourteen malformed references refused without being repeated, two of them variable names of another script (\w is held to ASCII); a broker's answer in either built-in form refused as malformed, through two real broker scripts;
  • the resolver is in doxbench_provider.py alone: the record reads no environment and no keyring, and get_password and import keyring appear in no other module of the package;
  • the operator door: each resolver declared, two-resolver and no-reference records refused, a keyed URL refused and nothing stored, set-credential refusing a binding no broker answers.

F16.1's other blocks are later tasks'. tests/test_provider_boundary.py is T083's, and it passes here: 24 passed. The no-model block is T081's, and it still fails as #1144 records (['omp-local']), untouched here. tests/test_chat_model_configuration.py is T081's and T082's, and does not exist yet.

The repository's own suite

Local runs of the whole suite, as CI runs it (python -m pytest -q), use a PostgreSQL 16 service for tests_runtime and LANG=C.UTF-8, as on the runner:

tree passed skipped failed
main 9a490405 (T081 landed) 3262 11 0
this head 05cb1c70 3475 11 0

Along the way this round, abbb05d4 read 3361, 93660ec9 (the merge of main 2fc714d2) 3418, 82ec9a20 3419 and 44582f8f 3420. Before this round, main 047bb4fa, T079's b04a3a95 and this branch's 4948e6dd read 3044, 3083 and 3206. Before phase 2 landed, the same three read 2468 (2d116415), 2507 (053e207a) and 2630 (3f14bb96).

CI's validate at this head (run 37086697677) reads selected=3486 passed=3475 skipped=11 failures=0 errors=0, against the pins 2476, 2465 and exactly 11.

The +213 cases are in tests/test_model_provider_broker.py: 64 at the first push (e9ef9514, 2571 passed), 16 in the fix commit f92fca47, 1 in 146b5a22, 32 in the ruling's commit 4abc6d4d, 9 in the three transport commits (5167084c, 1b0fb3f4 and 286655f3), 1 in 3f14bb96, 82 in the adversarial review's commit 645280ac, 1 in 82ec9a20, 1 in 44582f8f and 6 in 47c9da9a. None skips, because the keyring is always a stand-in. Skipped stays at the pinned 11.

This round's commits, and its merges (05cb1c70)

In order:

The suite above is this head's. Before this round, 4948e6dd merged T079's b04a3a95, which carried T078's merge of main 047bb4fa (phase 2's nine landings). SonarCloud at this head: 0 issues, 0 hotspots to review, quality gate OK.

The adversarial review at 4948e6dd: M2, M3, M5 and L4

An adversarial reviewer read this PR at 4948e6dd, and the holder forwarded four confirmed findings. 645280ac fixes all four. Every new case fails at 4948e6dd, and every mutant of the new checks is killed.

  • M2 (medium): a raw key given as --credential-ref was taken as a broker's reference. It was stored in the file the module calls safe to commit, and list printed it. Each mint then put it in the broker's argv, where /proc/<pid>/cmdline shows it to every local user.
    • The record now asks two questions of every reference, inside the built-in forms too: the product's detector, and a raw key's shape (has_a_raw_key_shape, below). A reference that fails either is refused with one fixed sentence, CREDENTIAL_REF_IS_A_RAW_KEY.
    • The reference a broker hands back at intake is held to the same rule. One that breaks it is a malformed answer (DIAG_BROKER_MALFORMED), which both entry points already catch.
    • No grammar is declared for a broker's references. #1144 box 16.3 says only that a reference is never a raw key. So this is the floor the holder named: the detector, plus a shape check.
  • M3 (medium): the scheme refusal repeated the endpoint. So --endpoint <key>, ftp://<key> and " https://…?q=<key>" printed the key to the terminal, to startup's standard error (doxbench_install.py:322) and, through the console's intake route, to a browser (serve_workbench.py:1110). It is now one fixed sentence, ENDPOINT_SCHEME_REFUSED, built from ENDPOINT_SCHEMES alone. A short key that no shape rule knows is not repeated either. Since 82ec9a20 it also says nothing about hosts, because every resolver meets it and only a built-in credential is held to a private route (Copilot at abbb05d4).
  • M5 (medium): a key in the endpoint's path or fragment was accepted. The detector reads only a URL's userinfo and its parameters' names. The endpoint is now checked for the same shape, before the scheme check, and refused with ENDPOINT_CARRIES_A_CREDENTIAL. That covers a key used as a path segment, glued to a path word (bot<key>), placed in the fragment, put in a parameter with an innocent name, or given in place of the URL.
  • L4 (low): http.client.HTTPException escaped dispatch. http.client raises it when it cannot read a status line, a protocol, a header line or a body, or cannot send a path. It is no OSError, so it escaped, and its traceback kept do_open's frame, whose headers hold the key.
    • The transport now maps it to the fixed DIAG_PROVIDER_UNREACHABLE. The built-in path raises that afresh, with no cause and no context, as it does every refusal.
    • There is one case per subclass, over a real loopback server: BadStatusLine, UnknownProtocol, LineTooLong, HTTPException (more than 100 headers), IncompleteRead, and InvalidURL (a path with a space, which the record accepts).
    • Each case first checks that urllib alone raises exactly that class. Each runs for a built-in credential, for none and, since 47c9da9a, for a broker's turn with a real fake broker's mint. For a broker's turn it pins the sentence only. Broker-path credential hardening (follows T080): a minted token keeps a built-in credential's rules (plan 034) #64 raises it afresh there and pins that.

The shape, and where it stops. A key has no grammar, so the rule is a shape, and it errs toward refusing. A text has a raw key's shape if it holds any of these:

  • a run of 32 or more letters and digits that mixes upper case, lower case and digits;
  • a run of 40 or more that mixes two of the three;
  • a widely used key prefix (sk-, ghp_, xoxb-, hf_ and the rest of _KEY_PREFIXED) with 16 or more key characters after it. Those characters must mix all three classes, or two where the prefix begins a word, so bot glued to a key is still refused, while a word that only ends in a prefix, as benchmark- ends in rk-, is not;
  • a Google API key, an AWS access key id, or a JSON Web Token.

The rule passes this product's opref- references, UUIDs, model and deployment names under 32 characters, and the 32-character lowercase hex ids that gateways put in their paths and key vaults put in their secrets' versions. The tests hold both sides at each rule's edge.

It cannot see a key that uses only one class, a two-class key under 40 characters with no known prefix at the start of a word (that is the shape of a gateway's account id), or a format it does not list. Every refusal around it repeats nothing, so a key it misses still reaches no message.

A length bound on the reference, which is new and accepted. Asking the detector about a reference means running a quadratic check on a value that had no bound. Measured locally, an opref- reference of 65,536 characters took the detector 4.3 s. So a reference is held to the endpoint's bound first (MAX_REMOTE_URL_CHARS), with its own fixed sentence, CREDENTIAL_REF_TOO_LONG, and carries_a_raw_key never asks the detector about a longer text. The holder accepted the cap. Brett's "Leave unbounded (Recommended)" (openxFactory#656 comment 5916000030, item 5) is about a TIME limit on reading the operator's key. It does not reach the size of a reference field.

Not in this PR: M4, and the execution of broker_argv from a served repository's bindings file. Both belong to T100, another writer's PR stacked on #64, and neither is touched here.

Failing first. The 82 new cases were run against 4948e6dd's sources. 67 fail, and the 15 that pass are controls. Each failure is the case's own: DID NOT RAISE where a key was accepted, the scheme refusal's echo, and each http.client subclass escaping. Each later case fails without its fix: 82ec9a20's with 93660ec9's record, 44582f8f's with 82ec9a20's import, and 47c9da9a's six with 4948e6dd's transport.

30 mutants, each run against the whole broker module, and each killed:

mutant killed by
B1 no 3-class run rule 9: the base62 run, as a reference and in seven endpoint places
B2 no 2-class run-of-40 rule 2: the 40-character hex run
B3 the run of 40 widened to 41 2: the 40-character hex run
B4 the run of 40 narrowed to 32 5: the 32- and 39-character hex controls, the gateway's account id
B5 the run of 32 widened to 33 8: the 32-character base62 run
B6 the run of 32 narrowed to 31 2: the 31-character control
B7 no prefix rule 8: the four prefixed shapes
B8 a prefix always needs 3 classes 6: a prefix with a two-class tail, at a word start
B9 a prefix always needs 2 classes 2: benchmark-runner-…, a word that ends in a prefix
B10 the prefix's tail of 16 widened to 17 2: a tail of exactly 16
B11 the prefix's tail of 16 narrowed to 15 2: the 15-character tail control
B12, B13, B14 no Google, AWS or JSON Web Token format 2 each: that format
B15 digits not a class 18
B16 no bound floor in carries_a_raw_key 2: the predicate's floor, and a broker's reference past the bound
B17 carries_a_raw_key without the shape 35
B18 carries_a_raw_key without the detector 2: F16.1's keyed URLs, and the reference at the bound
B19 the endpoint asks the detector alone (4948e6dd) 14: every endpoint place, both keys
B20 no length check on the reference 1: the bound case
B21 no raw-key check on the reference 12
B22 the scheme checked before the key 7
B23 the scheme refusal repeats the endpoint (4948e6dd) 5: every scheme case
B24 a prefix at the start of the text begins no word 6
B25 the scheme refusal's host advice restored (93660ec9) 1: test_the_scheme_refusal_is_route_neutral
P1 no http.client.HTTPException in the transport's tuple 18: every subclass, for a built-in credential, none and a broker's turn
P2 a broker's reference not held to the record's rule 2: both intake cases
P3 a built-in refusal re-raised with its cause 7: the six subclasses and the refused connection, built-in
K1 a keyring import that catches only ImportError (82ec9a20) 1: test_a_keyring_package_that_fails_as_it_is_imported_refuses_the_same_way
K2 that refusal raised inside the handler 1: the same case, on its context

Review remarks, answered at 4948e6dd

Copilot's overview (review 5343397205, at e9ef9514) asked to "address the endpoint-size handling and credential encoding validation findings". It posted no inline threads. Both remarks were valid, and both were measured before they were fixed:

  1. Endpoint size. The endpoint reached the detector unbounded, from the command line or from the console's intake route. Measured locally over two runs, the detector takes about 0.002 s on a 2,048-character parameter name, about 0.1 s on 16,384 characters, and 1.4 to 2.2 s on 65,536. The bound now comes first (see What changes).

  2. Credential encoding. The resolver refused only CR, LF and NUL. Measured against urllib:

    • a character outside latin-1 failed while the header was encoded. That refusal read DIAG_PROVIDER_UNREACHABLE, which names the wrong party, and the UnicodeEncodeError chained to it held the whole header, credential included;
    • any other non-ASCII character, and an embedded space, was sent.

    Both now refuse with DIAG_REFERENCE_UNRESOLVED. With the old check restored, test_a_value_outside_latin_1_is_refused_before_any_header_is_built fails over a real socket with the provider-unreachable sentence.

SonarCloud (13 findings at e9ef9514):

  • in source: S8495 (the parser returned tuples of two lengths, now one named tuple), S6353 ([A-Za-z_]\w* under re.ASCII) and S7632 (a bare # noqa: BLE001, with the reason on the line above);
  • in tests: S5778 ×8 (one throwing call per pytest.raises block) and S9073 ×2 (composite assertions split).

All 13 are fixed in f92fca47. Its own analysis found one more, S8997 on the new real-socket test, which reset the stand-in handler's record by assignment. 68b6e412 resets it through monkeypatch. At 4abc6d4d it found S3776: the binding's __post_init__ reached a cognitive complexity of 17, where 15 is allowed. d240fd50 moves the endpoint's own checks into _require_a_declarable_endpoint and the loopback rule into _require_a_private_route. Every check keeps its order.

Five mutants of the fix, each killed by a named test:

mutant test that fails
re.ASCII dropped …malformed_built_in_reference…[env:NAMÉ]
the old CR/LF/NUL check restored eight cases, including …outside_latin_1… over the socket
no length bound test_an_endpoint_past_the_url_bound_is_refused_before_the_detector
the bound checked after the detector the same test
the bound copied as 2048 test_the_url_bound_is_the_products_own_read_when_it_is_asked

Copilot's later rounds. Each inline thread is answered with evidence and resolved.

review at verdict threads answered in
146b5a22 Copilot reported an error none
4abc6d4d Changes recommended redirects forward the credential (high); the console's kinds are not derived from AUTH_KINDS (low) 5167084c; the pin test, with evidence and no code change
d240fd50 Changes recommended the raw credential in frame locals (high) 1b0fb3f4
5167084c Needs a closer look none new
1b0fb3f4 Needs a closer look environment proxies carry a loopback request off the host 286655f3
286655f3 Changes recommended the header sends six asterisks (high): not reproduced, since the review's secret filter masks the Bearer expression evidence, three real-socket tests; no code change
286655f3, re-run Needs a closer look none; its summary named a broker's reserved-form reference and the keyring refusal's context 3f14bb96
3f14bb96 Needs a closer look none; its summary names the draft's upstream dependencies, which is the flag at the top
4948e6dd, the merge of main Needs a closer look none; its summary names the upstream dependencies and asks for a human review of the security-sensitive change
4948e6dd, asked again through the reviewer API Needs a closer look none; its summary asks for a final human review of the credential and transport changes
abbb05d4, the adversarial review's fixes and T079's merges Changes recommended the scheme refusal's advice, "an http:// one on this host", was false for a broker's binding and a none binding, which may name an http:// endpoint on another host (moderate) 82ec9a20: the sentence names the schemes alone; reply 4171022804
82ec9a20 Needs a closer look none; its summary named a keyring package whose import fails with something other than an ImportError 44582f8f
44582f8f Needs a closer look none; its summary named the shared transport's L4 mapping as a change to the broker path that the description did not state 47c9da9a, and this description
05cb1c70, the merge of main 9a490405 Needs a closer look none; see the two unposted notes below

Copilot's run at abbb05d4 (Actions run 37082414121) also stored notes it did not post. Its summary reads "Three unresolved moderate findings remain in diagnostics, malformed endpoint handling, and bearer-header construction", and its log gives each note by location only:

  • the diagnostics are the scheme refusal, doxbench_binding.py:290-293, posted as the one thread above;
  • the bearer header's construction, doxbench_provider.py:905, was dropped as a duplicate of the resolved six asterisks thread;
  • doxbench_provider.py:995-1000, where the request is built from the declared endpoint, passed the duplicate check but was not posted, and its text is not in the log. This PR's reading, an inference, is an endpoint that http.client cannot send, such as one with a space in its path. The record accepts it, and the turn refuses it with the unreachable sentence (L4's InvalidURL case). Refusing such an endpoint when it is declared would be a new rule, so it is left for the holder.

Copilot's run at 05cb1c70 (Actions run 37086706082) stored two notes and posted neither. Its summary reads "Update catalog privacy metadata for none and locally resolved credentials, and clarify the endpoint diagnostic for none." Its log gives the notes by location:

  • doxbench_provider.py:1217-1221, where a binding no broker answers takes _dispatch_without_a_broker. This is the catalog's data-handling sentence, which For the holder below records as outside this PR.
  • doxbench_binding.py:276-280, a nit: ENDPOINT_CARRIES_A_CREDENTIAL advises naming the credential by its reference in credential_ref, which a none binding may not declare. The sentence still repeats nothing, and it is left as it is.

SonarCloud at 82ec9a20 found five issues, all in this round's code. S5713: urllib.error.URLError is an OSError, so the transport's tuple named one class twice. S5778 ×4: four new cases built an argument inside their pytest.raises block. 67d00617 fixes all five, and no behaviour moves.

Before that, Copilot's run at f92fca47 posted nothing, but its log recorded three notes by location only. 146b5a22 answered two of them: a spelling note, and the console flow's kinds. Its security note later became the frame-locals thread.

Six mutants of the transport fixes, each killed:

mutant tests that fail
the redirect-declining opener not used 5: every redirect code
the redirect declined silently 5: the diagnostic
the wrapper's repr disclosing 2: the real refused socket and the broker path's frame
the chained cause kept 1: the real refused socket
the unpresentable value kept 1
the proxy bypass removed 1: the stand-in proxy answered the turn

Two mutants of 3f14bb96, each killed:

mutant test that fails
the keyring refusal raised inside the handler, keeping the backend's error as its context test_a_keyring_that_cannot_be_read_refuses_and_says_nothing_of_its_own
a broker's reference in a built-in form accepted test_a_broker_reference_in_a_built_in_form_is_malformed

Ruled: a built-in credential travels only by a private route

Brett Heap ruled on this PR's question on 2026-09-28, choosing "Refuse unless loopback (Recommended)". It is recorded at openxFactory#656 comment 5880893901:

A credential resolved by openDox's built-in resolver (env:/keyring: reference) is sent only over https://, or over http:// to 127.0.0.1, [::1] or localhost; any other http:// endpoint is refused (ENDPOINT_NOT_PRIVATE). The broker path is unchanged in this task.

Batch K records it in #1144 (openxFactory#1210 → 39f19145). Brett Heap's word for the batch is 5916000030, item 1: "Yes, amendment batch K (Recommended)".

  • The amendment is a dated note in requirement 17's body in #1144's spec delta, after its SHALL paragraph and above its scenarios. #1144's tasks.md points to it after 16.3's batch H addendum.
  • The note narrows requirement 17's and scenario 17.1's "any endpoint" in one respect only: the route a credential travels by. It covers a credential the built-in resolver resolves (5880893901) and a token a broker mints (5890601202). It rewrites no ratified text.
  • This PR carries out the built-in resolver's half. openDox-code#64, stacked on this branch, carries out the broker's.
  • F16.1 stands, as the pointer says. Its control record names a broker reference on a loopback endpoint, which the rule accepts, and its three raw-key refusals are unchanged.

Until 5880893901 was posted, this body quoted the holder's in-session relay of the same ruling.

4abc6d4d realizes it, and d240fd50 answers SonarCloud's complexity note on it. 5167084c and 286655f3 complete it on the wire, because a followed redirect or an environment proxy would have sent the credential by another route. See What changes for the rule and The falsifier for the cases.

Seven mutants of the rule, each killed by the named tests:

mutant tests that fail
the declaration check removed 10: every refused route, and the operator door
a case-sensitive test for "is this http?" 5, including HTTP:// to another host and Http://localhost.evil.com
the host matched as a prefix 6, including localhost.evil.com and 127.0.0.1.evil.com
::1 dropped from LOOPBACK_HOSTS 3, including the IPv6 declaration and the resolver's IPv6 control
the resolver's own check removed 4: every route the resolver must read nothing for
a case-sensitive match 6, including LOCALHOST and hTTp://127.0.0.1
a URL parser's reading of the host 3: the backslash case, in the predicate and in the resolver, and a leading space

Readings, for Brett to overrule if he wishes:

  • The three hosts count only as the ruling spells them. 127.0.0.2, [0:0:0:0:0:0:0:1] and localhost. are refused, which fails closed. Widening to all of 127.0.0.0/8 would change one constant.
  • The scheme check is case-sensitive and unchanged from main, so it refuses a mixed-case scheme at declaration before this rule is asked. The predicate's case-blind reading is what the resolver's own check relies on.
  • The reference forms are case-sensitive, so ENV:NAME or Keyring:… is a broker's reference, which needs its broker. Copilot's run at 5167084c left an unposted note at that check.
  • 3f14bb96 refuses a broker's intake answer in a built-in form. That is the broker's enrolment answer, not its token's route, so this PR reads the ruling's "leave the broker path as it is today" as still met. The token is presented exactly as at main.

For the holder: downstream, none of it edited here

The broker path's pre-existing gaps, left here as the ruling says, and closed by #64

Each of these is at main, and only item 3 moves here, as noted. This PR asked whether the broker path should follow the built-in path's pattern. Brett Heap answered "Yes, separate phase-3 draft (Recommended)", openxFactory#656 comment 5890601202. openDox-code#64, stacked on this branch, closes all four, and T080's scope is unchanged.

  1. The token is presented over plain http:// to any host. test_the_loopback_rule_is_the_built_in_resolvers_alone pins that scope.
  2. The default opener follows redirects, and the environment's proxies, with the token on the request.
  3. A provider-unreachable refusal chains urllib's error, and that error's frames hold the token in their locals. Measured: do_open.headers, _send_request.headers and send.data. Since 645280ac, an http.client.HTTPException on a broker's turn lands on that refusal too, where it escaped before, so this item covers it as well.
  4. The token is not checked for presentability, so one outside latin-1 fails in urllib as DIAG_PROVIDER_UNREACHABLE.

🤖 Generated with Claude Code

brettheap and others added 3 commits September 28, 2026 18:49
`openai-chat-v1` joins doxbench_binding.DIALECTS as the second member,
after `xfactory-prompt-v1`, which stays first. Its request is the
chat-completions grammar (`model`, `messages`, the assembled prompt as
one message in the user role), and its answer is read at
`choices[0].message.content`. Both are spoken by one arm in
doxbench_provider.py alone (`_DIALECT_ARMS`), beside the prompt
grammar's arm, which sends the bytes it always sent. An unknown dialect
is still refused when a binding is declared, and a test holds the arm
table's keys equal to the vocabulary.

Falsifier: F16.1's dialect assertion, `"openai-chat-v1" in b.DIALECTS`.
Ruled: R1Q22 (a), openxFactory#656 comment 5817152735. doxbench_binding.py
is a moved_verbatim row, and editing it needs no declared-edit act.

Drafted ahead of T063 under Brett's phase-3 word ("Only the independent
ones"). It does not land before T063.

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 binding record gains `model`, the model name the provider receives
as the request's model, in either dialect. It sits after `dialect`, with
the route it belongs to, so BINDING_FIELDS grows from nine to ten, and
still no field can hold a secret. `model-binding add|edit --model` sets
it, and `model-binding list` shows it.

A binding that declares no model keeps the meaning it had: the request
names the catalog handle, the binding's `id`, byte for byte. So `model`
is keyword-only and defaults to None, and a stored record may leave it
out (OPTIONAL_BINDING_FIELDS). Every construction written before the
field existed builds the binding it built, and every nine-field document
reads. The console's intake route in serve_workbench.py builds a binding
without a model, and it keeps working unedited. The catalog handle stays
the binding's id, and `model` is not an argv placeholder.

Falsifier: F16.1's field assertion, `"model" in b.BINDING_FIELDS`.
Ruled: R1Q22 (a), openxFactory#656 comment 5817152735.

Drafted ahead of T063 under Brett's phase-3 word ("Only the independent
ones"). It does not land before T063.

Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…lan 034)

A key inside the endpoint URL is refused when a binding is declared, by
the product's own detector, runtime/local_git_adapter's
carries_a_credential. It runs before the scheme check, so no refusal
repeats the URL. The refusal is one fixed sentence. A key in an extra
field is refused as an unknown key, as it always was.

Each record now has ONE resolver, and credential_source() says which:
- a BROKER, for any other reference, as before;
- the BUILT-IN RESOLVER, for `env:NAME` and `keyring:SERVICE/USERNAME`
  references (R1Q17 (b)). doxbench_provider reads the reference at call
  time, once per request, and keeps nothing: no mint, no ledger event,
  no retry on a 401. Such a record needs no broker, and a broker_argv
  given beside it is refused. That refusal is the plan's fail-closed
  reading (analyze round 2, V2-21), recorded as standing in
  openxFactory#656 comment 5851950767; no answer rules it. The OS
  keyring is read through the `keyring` package, imported at call time.
  It is not a dependency: without it, a keyring reference refuses with a
  fixed sentence. Two fixed diagnostics join (eight to ten);
- NONE, for an endpoint that takes no credential: the auth kind `none`
  (R1Q18 (a)), under which credential_ref and broker_argv are forbidden.
  It joins AUTH_KINDS after api_key and oauth, so AUTH_KINDS[0] is
  unchanged, and its requests carry no authorization header.

The record parses a reference's form, once, for both sides, and never
reads what it names. A stored record may leave out the fields its
resolver forbids or does not need. Each read-back and each removal
states the custody sentence that is true of its resolver. The operator
door takes `--auth-kind none`, an omitted `--credential-ref` and an
empty broker invocation, and `set-credential` refuses a binding no
broker answers without reading its standard input.

Falsifier: F16.1's three refusals and its control, plus tests of the
resolver and of `none`.
Ruled: R1Q22 (a), openxFactory#656 comment 5817152735; R1Q17 (b) and
R1Q18 (a), comment 5850003126.
After: T079, and T007 (batch H), which has NOT landed on openxFactory
main (e369cb25): #1144's 16.3 has no addendum yet. This is built to plan
034's text.

Drafted ahead of T063 under Brett's phase-3 word ("Only the independent
ones"). It does not land before T063.

Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Sep 28, 2026

Copy link
Copy Markdown

Reviewer's Guide

This PR makes credential references the only persisted credential representation: binding validation rejects raw keys and keyed URLs, records select exactly one broker, built-in, or no-credential resolver, and provider dispatch resolves built-in references at request time or sends no credential for none bindings. CLI behavior, custody messaging, broker safeguards, and extensive regression tests are updated accordingly.

Sequence diagram for built-in credential resolution per provider request

sequenceDiagram
    participant Client
    participant Port as BrokeredProviderPort
    participant Resolver as resolve_credential_reference
    participant EnvOrKeyring as Environment_or_OS_Keyring
    participant Provider

    Client->>Port: dispatch(prompt_envelope)
    Port->>Resolver: resolve_credential_reference(binding)
    Resolver->>EnvOrKeyring: Read referenced value at call time
    EnvOrKeyring-->>Resolver: credential value
    Resolver-->>Port: credential
    Port->>Provider: POST request with Authorization Bearer credential
    Provider-->>Port: response
    Port-->>Client: assistant response
Loading

Sequence diagram for none-auth provider dispatch

sequenceDiagram
    participant Client
    participant Port as BrokeredProviderPort
    participant Provider

    Client->>Port: dispatch(prompt_envelope)
    Port->>Provider: POST request without Authorization header
    Provider-->>Port: response
    Port-->>Client: assistant response
    Note over Port,Provider: A 401 becomes DIAG_PROVIDER_REFUSED with no retry
Loading

Flow diagram for credential resolver selection and validation

flowchart TD
    A[Declare model provider binding] --> B{Endpoint carries a credential?}
    B -- Yes --> X[Refuse with ENDPOINT_CARRIES_A_CREDENTIAL]
    B -- No --> C{auth_kind is none?}
    C -- Yes --> D{credential_ref and broker_argv absent?}
    D -- No --> Y[Refuse binding]
    D -- Yes --> N[NO_CREDENTIAL]
    C -- No --> E{credential_ref uses env: or keyring:?}
    E -- Yes --> F{broker_argv absent?}
    F -- No --> Y
    F -- Yes --> G[CREDENTIAL_FROM_BUILT_IN_RESOLVER]
    E -- No --> H{broker_argv present?}
    H -- No --> Y
    H -- Yes --> I[CREDENTIAL_FROM_BROKER]
Loading

File-Level Changes

Change Details Files
Enforce credential references and reject raw credentials at binding declaration.
  • Reuse the product URL credential detector before scheme validation with a fixed non-leaking diagnostic.
  • Reject credential-shaped extra fields and validate built-in reference syntax without reading referenced values.
  • Allow resolver-specific omissions while preserving broker requirements for ordinary references.
src/opendox/doxbench_binding.py
src/opendox/cli_model_binding.py
Add resolver selection for brokered, built-in, and credential-free bindings.
  • Add the explicit none authentication kind and enforce exactly one resolver per record.
  • Classify env: and keyring: references as built-in-resolver bindings, rejecting a broker alongside them.
  • Expose resolver-specific custody and removal notices and normalize optional stored fields.
src/opendox/doxbench_binding.py
src/opendox/cli_model_binding.py
Implement call-time built-in credential resolution and no-broker provider dispatch.
  • Resolve environment and OS keyring references per request with fixed diagnostics for unusable values, missing dependencies, and backend failures.
  • Present resolved credentials only as bearer authorization headers, with no caching, ledger entries, retries, or persistence.
  • Send none bindings without authorization, refuse 401s without retry, and prevent broker operations for non-broker bindings.
src/opendox/doxbench_provider.py
Expand coverage for credential safety, resolver behavior, none bindings, and operator workflows.
  • Test URL and field refusal, resolver parsing and rotation, keyring failure handling, secret non-disclosure, and fixed diagnostic reachability.
  • Test none request behavior, record round trips, custody messaging, CLI declaration/list/remove flows, and set-credential refusal without reading input.
  • Verify the resolver implementation remains isolated to the provider module.
tests/test_model_provider_broker.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!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Address the endpoint-size handling and credential encoding validation findings.

Review effort: Lite
Findings: None

What changed in this PR

Adds credential-reference resolution and unauthenticated model-provider bindings while rejecting raw credentials.

Changes:

  • Rejects credential-bearing URLs and unknown secret fields.
  • Supports environment/keyring references and auth_kind=none.
  • Updates CLI behavior and expands provider tests.
File Summary
tests/​test_model_provider_broker.py Tests credential refusal, resolution, unauthenticated behavior, and CLI handling.
src/​opendox/​doxbench_provider.py Resolves references and dispatches provider requests.
src/​opendox/​doxbench_binding.py Validates binding credentials and resolver combinations.
src/​opendox/​cli_model_binding.py Supports resolver-specific and brokerless bindings.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

brettheap and others added 5 commits September 28, 2026 19:10
…arCloud)

SonarCloud's analysis of openDox-code#61 flagged five of T078's new test
lines: four `pytest.raises` blocks whose envelope was built inside the
block (S5778), and one composite assertion (S9073). The envelope is now
built before each block and the assertion is split in two. No assertion
changes meaning, and the case count is unchanged.

Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot's overview of openDox-code#62 noted that doxbench_intake.py's
module docstring still called the binding a "closed nine-field record"
and the intake declaration "not a tenth field on it". T079 made the
record ten fields, so both phrases were false.

The paragraph now names each count by its tuple, as it already did for
the catalog entry, and records that the binding grew once, by `model`
(#1144 box 16.2). The declaration is "not a field on it". This is a
docstring only: no code, no test and no behaviour changes.

Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…o T080's

Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rer can carry

Two remarks in Copilot's overview of openDox-code#63, both measured here.

The endpoint reached the credential detector unbounded. The detector is
quadratic in a parameter name's length, and an endpoint arrives from the
command line or from the console's intake route. The binding now refuses
an endpoint longer than runtime/config.MAX_REMOTE_URL_CHARS before the
detector is asked. That is the product's own URL bound, which the
repository act applies to a remote for the same reason. It is read when
the binding is declared, and the refusal is a fixed sentence that
repeats nothing of the endpoint.

A resolved credential could be one no header carries. The resolver
checked only for CR, LF and NUL. Measured against urllib:
- a character outside latin-1 failed while the header was encoded, as
  DIAG_PROVIDER_UNREACHABLE, which names the wrong party, and the
  UnicodeEncodeError it chained held the whole header, credential
  included;
- any other non-ASCII character, or an embedded space, was sent.
A resolved value must now be non-empty printable ASCII with no
whitespace, or it refuses with DIAG_REFERENCE_UNRESOLVED before any
provider is contacted.

SonarCloud's findings on the same PR:
- built_in_reference_parts returns one shape for both forms, a
  BuiltInReference named tuple (S8495);
- the variable-name pattern reads [A-Za-z_]\w* under re.ASCII (S6353);
- the broad except carries a bare noqa code, with its reason on the line
  above (S7632);
- each pytest.raises block in T080's tests holds one throwing call
  (S5778), and two composite assertions are split (S9073).

Sixteen new cases: the bound (past it, at it, read from config), six
unpresentable values and a printable-ASCII control, a real-socket case
for a value outside latin-1, three unusable keyring answers, and two
non-ASCII variable names. Five mutants of the fix were each killed.

Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings September 28, 2026 19:36
brettheap and others added 2 commits September 28, 2026 19:42
…rCloud)

SonarCloud's analysis of openDox-code#63 at f92fca4 flagged one line of
the new real-socket test: it reset the stand-in handler's class-level
record by assignment (S8997). The test now takes monkeypatch, so the
record is restored when the test ends.

The test file's four new non-ASCII literals are written as \u escapes,
so the source shows which code point each case uses. The string values
are unchanged, and the case count is unchanged.

Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… pinned

With `none` in AUTH_KINDS, the console's intake flow offers a subset of
the vocabulary: api_key and oauth, the kinds a broker enrols. A `none`
binding holds no credential, so the flow, which exists to hand one to a
broker, has nothing to collect for it. The operator declares one with
`model-binding add --auth-kind none` instead.

doxbench_intake.auth_kind_disclosure's docstring now says so, and a new
test pins the relationship, in the vocabulary's order. The disclosure's
code is unchanged. The docstring sits outside P3-B's row; the PR body
flags it.

The test named "this_processs_own_environment" is renamed to
"the_serving_process_environment".

Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Brett Heap ruled on the question openDox-code#63 raised, on 2026-09-28,
choosing "Refuse unless loopback (Recommended)"; the lane's holder
relayed the ruling. A credential the built-in resolver reads (an env: or
keyring: reference) is a long-lived key, so it is sent only over
https://, or over http:// to 127.0.0.1, ::1 or localhost. Any other
http:// endpoint is refused before resolution.

- doxbench_binding.is_a_private_route is the one predicate. It matches
  the endpoint as written, case-blind, against https:// or http:// to
  one of LOOPBACK_HOSTS with an optional port. A URL parser's reading is
  not used, because it and the HTTP client read
  "http://evil.example\@localhost/" as two different hosts.
- The record refuses a built-in reference on any other route when it is
  declared, from the command line or a stored record, with one fixed
  sentence, ENDPOINT_NOT_PRIVATE, that repeats nothing of the endpoint.
- The built-in resolver asks the same predicate before it reads
  anything. No declared binding reaches that check, so what does is a
  programming error, raised as an AssertionError with nothing read, as
  the resolver already does for a broker's reference.
- The broker path is left as it is, as the ruling says: a minted token
  may still be declared over plain http:// to any host. The auth kind
  none presents no credential and keeps its route too.

32 new cases, including IPv6 loopback, the lookalike host
"localhost.evil.com" and mixed-case schemes. Seven mutants of the rule
were each killed.

Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings September 28, 2026 22:37

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Credentialed redirects may forward secrets to another host, and endpoint validation has unresolved scheme and authority issues.

Review effort: Lite
Findings: 1 High severity · 1 Low severity

Open (2)

Comment thread src/opendox/doxbench_provider.py Outdated
Comment thread src/opendox/doxbench_intake.py
…helpers (SonarCloud)

SonarCloud's analysis of openDox-code#63 at 4abc6d4 measured the
binding's __post_init__ at a cognitive complexity of 17, over the 15
allowed (S3776). The endpoint's own checks (the length bound, the key
detector and the scheme, in that order) move into
_require_a_declarable_endpoint, and the loopback rule into
_require_a_private_route. The order of every check is unchanged, and so
is every refusal. The whole suite and the seven mutants of the rule give
the same results as at 4abc6d4.

Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings September 28, 2026 22:46

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Unresolved critical credential exposure and additional endpoint, redirect, and disclosure issues remain.

Review effort: Lite
Findings: 2 High severity · 1 Low severity

Open (3)

Comment thread src/opendox/doxbench_provider.py Outdated
Copilot's review of openDox-code#63 at 4abc6d4 (high severity): the
default urllib opener follows redirects, and it re-sends every header
but the content ones to the Location. Measured with two loopback
servers: a POST answered 301, 302 or 303 reached the redirect's target
as a GET that still carried "Authorization: Bearer ...". So a private
endpoint could hand a long-lived key to any host and any scheme, which
the loopback ruling of 2026-09-28 forbids.

A request that carries a credential the built-in resolver read now uses
_open_without_redirects. Its _DeclineRedirects handler closes the
redirect's answer unread and declines, and the port answers with a new
fixed diagnostic, DIAG_PROVIDER_REDIRECTED. FIXED_DIAGNOSTICS goes from
ten to eleven. An injected opener is still used as given. The auth kind
none sends no credential, and the broker path keeps the default opener,
as the ruling leaves that path.

Five new cases over real sockets (301, 302, 303, 307 and 308). The
second server hears nothing. Two mutants were each killed: the opener
swap removed, and the redirect declined silently.

Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings September 28, 2026 22:59
brettheap and others added 2 commits October 3, 2026 01:20
S5713: urllib.error.URLError is an OSError, so listing both in the
transport's except tuple named one class twice. The tuple keeps OSError,
and its comment says URLError is caught through it. No behaviour moves.

S5778 x4: four of the adversarial review's cases built an argument
inside their pytest.raises block, so a second call there could have
raised the refusal under test. Each argument is built before the block.

Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ailed read (Copilot at 82ec9a2)

Copilot's review of openDox-code#63 at 82ec9a2: "Keyring import
failures beyond ImportError can escape without the required fixed
diagnostic." An import runs the package's own code, and a backend can
fail there as it can when it is read. The resolver already drops a
read's error of any class; the import now drops its own the same way,
and the refusal, DIAG_KEYRING_UNAVAILABLE, is raised outside the
handler, so it keeps no context. A case pins it with a RuntimeError at
import, and fails with 82ec9a2's ImportError-only catch.

Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings October 3, 2026 01:21

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

A shared HTTP exception handler alters broker-backed behavior contrary to the stated contract; scope it or update the contract and tests.

Review effort: Lite
Findings: None

brettheap and others added 2 commits October 3, 2026 01:30
Copilot's review of openDox-code#63 at 44582f8: the shared transport's
handler changes the broker path's failure, which the description said
this PR leaves alone. It does: an http.client.HTTPException on a
broker's turn escaped dispatch, and now lands on the fixed unreachable
sentence, as on the built-in and none paths. The L4 case runs a real
fake broker's mint for each subclass and pins that. Raising it afresh
on the broker path, with no token in a kept frame, is openDox-code#64's,
as the ruling leaves that path to it, and the description now says so.

Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
openDox-code main moved to 9a49040: T081 (#74), "no model configured"
is a state, shown before any turn. It changes seven files, one of them
tests/test_model_provider_broker.py, at cases this branch does not
change, and the merge has no conflict.

Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@sonarqubecloud

sonarqubecloud Bot commented Oct 3, 2026

Copy link
Copy Markdown

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Update catalog privacy metadata for none and locally resolved credentials, and clarify the endpoint diagnostic for none.

Review effort: Lite
Findings: None

@brettheap brettheap changed the title DRAFT (phase 3, after T063): T080, 16.3: the credential stays a reference, and a raw key is refused (plan 034) T080, 16.3: the credential stays a reference, and a raw key is refused (plan 034) Oct 3, 2026
@brettheap
brettheap marked this pull request as ready for review October 3, 2026 01:46
@brettheap

Copy link
Copy Markdown
Contributor Author

READY at 05cb1c7 — phase 3 is open (T063 landed, #1218 → a883bbf6); T079 (#62) landed as 2fc714d; Brett: draft phase 3 ahead, land in plan order when green. The holder checked: validate and SonarCloud green (0 issues), Copilot's latest review has no findings, 0 unresolved threads; adversarial-review M2/M3/M5/L4 folded in; dependent #64 retargeted to main and not READY. Lane: openxfactory-4 (openXfactory-4-openDox_extraction)

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @brettheap, your pull request is larger than the review limit of 150,000 diff characters

@brettheap
brettheap merged commit 1130e99 into main Oct 3, 2026
4 checks passed
@brettheap

Copy link
Copy Markdown
Contributor Author

Lane: openxfactory-4 (openXfactory-4-openDox_extraction)

LANDED — lane openxfactory-4, 2026-10-03T01:48:01Z, PR #63 → 1130e99 (opensoft/openDox-code main; plain gate)

Brett: land phase 1 / phase 2 PRs when green

brettheap added a commit that referenced this pull request Oct 3, 2026
Brings in #60, #61, #62, #63, #65, #67, #71 and #74 (T071, T078, T079,
T080, T088, T070, T085, T081). No file this branch touches 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>
brettheap added a commit that referenced this pull request Oct 3, 2026
T080 landed on openDox-code main as #63, squashed to 1130e99, after
T081 (#74), T079 (#62), T070 (#67) and T078 (#61). This branch was
stacked on T080's branch and is now based on main. So the merge takes
main whole and does not replay T080's own commits: its tree is the
three-way merge of this branch and main over their old base, T080's
4948e6d (git merge-tree --merge-base 4948e6d f8bc8ac 1130e99),
with three resolutions, and nothing else:

- The test module's imports conflicted, each side adding one (functools
  here, http.client there). Both are kept.
- T080's route-neutral case declares an http:// endpoint on another
  host for a broker's binding. Here a broker's minted token is held to
  a private route too, so the case declares it for the none binding
  alone, and its docstring says why.
- ENDPOINT_SCHEME_REFUSED's comment names both credentials that
  ENDPOINT_NOT_PRIVATE covers on this branch.

The provider merged with no conflict: _intake_reference, where this
branch reads an intake answer, holds a broker's reference to the
record's rule, and the transport maps http.client.HTTPException.

Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 3, 2026
Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 3, 2026
The adversarial review's L4 is in both PRs. T080 (#63) maps an
http.client.HTTPException in the shared transport to the unreachable
sentence, and its case runs a broker's turn and pins that sentence only.
On this branch every refusal of a request that carried a credential,
the broker's minted token included, is raised afresh by _call_provider.
So for a broker's turn the case now pins that too: no cause, no context,
and neither the key nor the token in any frame the refusal keeps.

With T080's transport (05cb1c7), where the broker path chains its
cause, the six broker cases fail on that cause. With _call_provider
re-raising every refusal with its cause, 19 cases fail.

Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 3, 2026
…ild (plan 034) (#69)

Lane: openxfactory-4 (openXfactory-4-openDox_extraction)

Arc: neutral-product-standalone-operability

Plan 034 (`specs/034-opendox-standalone-operation/tasks.md`, read at openxFactory `main` `91e4685f`), phase-3 slice **P3-I, install mode and the bundle**:

- **T072** (#1144's 13.1): the bundled PostgreSQL server, as T007 batch H's 13.1 addendum reads (openxFactory#1206 → `f99a2097`).
- **Falsifier:** F13.1's TCP-listener block, which reads the kernel's socket table at run time, and its `runtime status` block.
- **After:** T070 (openDox-code#67, landed as `66ff7257`). It was stacked on #67's branch; the holder has retargeted it to `main`, and `57b7ed8f` merges main `66ff7257`, so this diff is T072 alone.

**Ruled:**
- R1Q22 (a), `5817152735`;
- R1Q16 (i)–(iv), `5850003126`;
- the phase-3 draft-ahead widening, `5901112350`.

Claimed on openxFactory#656 in [`5901575394`](opensoft/openxFactory#656 (comment)). **Authored ahead as a DRAFT**, which did not go READY before T063 landed and the holder said so. **T063 has landed** (openxFactory#1218 → `a883bbf6`, 2026-10-02 23:37:43Z), which closes phase 2, so phase 3 is open. #60 (`7ff434d9`) and #67 (`66ff7257`) ahead of it have landed.

## What it does, by R1Q16's four parts

- **(i) The document server starts it as its own child, and reports it.**
  - `opendox generate-and-open --local` (or `OPENDOX_INSTALL_MODE=local`) starts `postgres` as a **direct child** of the process serving the document surface. It uses `subprocess.Popen` and never `pg_ctl`, which would re-parent it.
  - It prints the socket, the pid and what it migrated.
  - `runtime status` reports `database_bundle` (`data_dir`, `socket_dir`, `pid`). It reads the pid from the server's own `postmaster.pid` and believes it only while a postgres runs there.
- **(ii) Started and migrated, and nothing more.**
  - `initdb` runs once per data directory.
  - An idempotent bootstrap makes the database and the SERVED role. Its grants are the compose stack's (`init-runtime-role.sh`): CONNECT, USAGE on `public`, and DML on what the owner creates, by default privileges.
  - Then `migrations.MigrationRunner` runs as the owner, with the served role and database declared. The run narrows the ledger to SELECT for the served role and verifies its access, exactly as a hosted `runtime migrate` does.
  - `runtime migrate` under `local` migrates the bundle too.
- **(iii) It ships as the `opendox[local]` extra**, which is `opendox[runtime]` plus `pixeltable-pgserver>=0.6.0` (RULED, openxFactory#656 `5916000030` item 2). **The `test` extra joins it**, so F9.1's `.[test]` install still runs every case.
- **(iv) It stops with the entry point.**
  - SIGTERM is read as the Ctrl-C the serve loop already stops on, followed by a PostgreSQL fast shutdown (then an immediate one, then SIGKILL, each bounded).
  - The backstop is `PR_SET_PDEATHSIG` on Linux, so a SIGKILLed entry point still takes its server with it.
  - The server runs in its own session, so a terminal's Ctrl-C reaches the entry point, and the stop happens in order.

### 13.1's fixed identity

- **The data and socket directories live under `OPENDOX_STATE_DIR`**, at `<state>/postgres/data` and `<state>/postgres/run`.
  - The setting is new. It defaults to `$XDG_STATE_HOME/opendox`, else `~/.local/state/opendox`, and must be absolute, because the server's process and a `runtime status` run from elsewhere must derive the same socket.
  - A state directory too long for the kernel's `sun_path` is refused, naming the setting.
- **No TCP listener:** `listen_addresses` is empty, and the socket directory is narrowed to 0700.
- **Peer authentication** (RULED, openxFactory#656 `5916000030` item 3, *"Peer auth + accept (Recommended)"*):
  - `initdb` runs with `--auth-local=peer --auth-host=reject`.
  - Before every launch the bundle rewrites `pg_hba.conf` and `pg_ident.conf` (atomically, mode 0600). `pg_hba.conf` holds one rule, `local all all peer map=opendox`, plus `host … reject` for IPv4 and IPv6. `pg_ident.conf` maps the running OS user, and nobody else, to `opendox` and `opendox_runtime`.
  - The kernel reports the connecting uid, so the DSNs carry no password because there is none. A cluster an older build left as `trust` is put back to peer on its next start.
- **Both DSNs are supplied:** two users (owner `opendox` for migrations, `opendox_runtime` for serving) over the one socket, with `port` spelled so a stray `PGPORT` cannot redirect libpq. They pass T071's three checks for the reason those exist: one dialect, one database, and never one credential in both settings.
  - **An operator DSN given beside `local` is refused by name.** It is added to T070's `HOSTED_ONLY_SETTINGS`, a holder reading on openxFactory#656 that Brett may overrule.
- **A second entry point on the same state directory is refused**, naming the running pid. One install's database belongs to one entry point at a time.

### The migrations gap (assigned to T072 by the holder)

- The migrations were not package data. `migrations/` sits at the repository root, and only the image copies it (`WORKDIR /app`), so `pip install "opendox[local]"` run outside a checkout had nothing to apply.
- Now `pyproject.toml`'s `[tool.setuptools.data-files]` maps `migrations/*.sql` into the wheel's data directory (`share/opendox/migrations`). **The root `migrations/` does not move.**
- `config.migrations_dir` resolves an unset `OPENDOX_MIGRATIONS_DIR` in this order:
  1. `migrations` wherever the working directory has one (a checkout, or the image's `/app`), which is **today's default, unchanged**;
  2. otherwise the copy the installed distribution's `RECORD` lists (`packaged_migrations_dir`);
  3. for an editable install, which installs no data files, the source tree's own `migrations/`.
- The canonical digest gate is what proves any copy found is the pinned one.
- `test_a_wheel_install_migrates_its_bundled_server_outside_a_checkout`:
  - builds this package's wheel offline (`--no-build-isolation`, `--no-index`);
  - installs it under a `--prefix` outside the checkout;
  - runs from a directory with **no** `migrations/`, asserting that `opendox` is the wheel's copy and that the migrations dir is under the prefix's `share/opendox`;
  - migrates the bundled server there (`applied == ["0001", "0002"]`).

### The server package (RULED, openxFactory#656 `5916000030` item 2: *"pixeltable-pgserver (Recommended)"*)

- **`pixeltable-pgserver` 0.6.0**, the maintained fork of `pgserver`, uploaded 2026-07-14. Apache-2.0, as its dist-info `LICENSE` and OSI classifier say. It carries **PostgreSQL 16.14** under the PostgreSQL License (`initdb --version` and `postgres --version` from `pixeltable_pgserver/pginstall/bin`). It also carries an 18.4 under `pginstall18/`, which this package does not use.
- **Only its binaries are used**, found with `importlib.util.find_spec("pixeltable_pgserver")` without importing it. Its own manager is not used, because:
  - it daemonizes through `pg_ctl`, against (i);
  - it stops from `atexit`, which SIGTERM never runs, against (iv);
  - it may put the socket under the user's runtime directory, opened to 0777, against 13.1.
- **Linkage, re-verified on the installed wheel** (`readelf -d`, `ldd`):
  - `postgres` needs `libz`, `libpthread`, `librt`, `libdl`, `libm`, `libc`;
  - `initdb` needs those less `libz` and `libdl`, plus the wheel's own vendored `libpq`. That libpq resolves through `RPATH $ORIGIN/../../../pixeltable_pgserver.libs` and itself needs only `libc`, `libm` and `libpthread`;
  - the server's loadable modules need `libc`, and one needs the vendored libpq.
  So the system libraries are the C library and libz only: no system PostgreSQL, no ICU.
- **Its floor and its size:**
  - Wheels exist for **cp310 to cp314**, on Linux x86_64 and aarch64, macOS and Windows.
  - The Linux wheels are tagged `manylinux_2_27` and `manylinux_2_28`, so they need **glibc 2.27 or later**. That is the tag's floor. The highest GLIBC symbol any binary or module needs is 2.25, by `objdump -T`.
  - Each wheel is about **24.7 MB** (the cp312 x86_64 wheel is 24,704,230 bytes), because it carries two server majors.
- It pulls in `fasteners`, `platformdirs`, `psutil` and `typing-extensions`, which nothing here imports. All four were already pinned.
- **Superseded:** `pgserver` 0.1.4 carried PostgreSQL 16.2 and had no wheel after cp312 (Copilot r4139811507 and r4139811528, both now resolved with this ruling cited). Also rejected: `postgresql-binaries`, which links the system's ICU and untars at first use, and `pgembed`, which is PostgreSQL 17.

### Outside `src/` and `tests/`

- **`pyproject.toml`:** the `local` extra, the `test` extra joining it, `setuptools>=70.1` in `test` (the wheel test's offline build; 70.1 is the first release that builds a wheel with no `wheel` package), and the data-files map. It is a single-writer file (T057 → T072). #58 (T057) adds package data there, and the merge-from-main round takes it.
- **`constraints-cpython312-linux.txt`,** in its own commit as its header asks. It was extended under its own pins in a clean 3.12.3 venv (`psutil==7.2.2`, `platformdirs==4.12.2`, `fasteners==0.20` and `setuptools==84.0.0` new). At `84a6c041` it was re-resolved in a clean environment under the pins less `pgserver`. The one line that moved is `pgserver==0.1.4` → `pixeltable-pgserver==0.6.0`.
- **`deploy/` — one line, and the task requires it.** `deploy/compose/.env.example` gains `OPENDOX_STATE_DIR=`, because `test_every_runtime_setting_is_documented_in_env_example` requires every `SETTINGS` entry there. The compose stack is hosted and never reads it. **`docs/`: untouched.**
- **Not touched:** `serve.py` (T073 adds the `install` block) and `validate.yml`. It already installs `.[runtime,test]`, and `test` now carries `local`.
- **Existing tests changed:**
  - `tests/test_doxbench_entrypoint.py` stands the bundle in, with a tripwire. Its cases test the model port, which reads nothing from the store (R1Q16 (ii)).
  - T070's `test_install_mode.py` and `test_install_mode_entrypoint.py` stop passing DSNs beside `local`.
  - `test_runtime_surface.py` declares `opendox.runtime.bundle` stdlib-only at import, because `opendox.cli` imports it and `opendox --help` runs with no extra installed.

## The falsifier

F13.1's `runtime status` block and its TCP-listener block, verbatim in their assertions (`f13-1-local.sh`), against a server `generate-and-open --local` started **in the background**, with no broker and no operator database, under `set -euo pipefail`.

**Since the merge round (`19e32f0c`), the start is the REAL entry point.** It is `python -m opendox.cli generate-and-open --local`, with the validator on, over T050's `tests/fixtures/plain-documents` copied into a fresh repository, as F13.1's preamble does. The stand-in driver is deleted. One deviation remains, and it does not weaken a check:
- ~~**Generation is stood in.**~~ Retired at `19e32f0c`. Until then the start went through `tests_runtime/local_entrypoint_driver.py`, because this stack's base predated T055/T056's standalone generate.
- **The pid comes from `runtime status`.** The TCP-listener block reads the server's pid from `runtime status`'s `database_bundle`, not from `caps.json`. `/capabilities`' `install` block is T073's, so F13.1's `caps.json` block is T073's to run.
- ~~**The corpus is a one-file stand-in.**~~ Retired at `19e32f0c`: it is T050's fixture now.

BEFORE is #67's head `b50e3b1`; AFTER is this branch:

```
=== BEFORE (b50e3b1)
ready=1
runtime status rc=1
AssertionError: no bundled database answered: None OPENDOX_DATABASE_URL is required and is not set: …
FAIL status-block
AssertionError: the bundle reports no server pid: None
FAIL tcp-listener-block
=== AFTER (this branch, set -euo pipefail, exit 0)
ready=1
runtime status rc=0
PASS status-block
  server pid 638810, sockets ['3692911'], TCP LISTEN rows: none
PASS tcp-listener-block
PASS stops-with-entry-point (pid 638810 gone)
--- server stdout:
  database /tmp/tmp.v0wydKm5Zm/postgres/run (bundled, pid 638810, migrations applied now: ['0001', '0002'])
```

T070's F13.1 refusal probes and F13.1's `load_settings` block still pass on this branch (`F13.1 REFUSALS + T070 PAIR: ALL PASSED`).

In the suite, `tests_runtime/test_bundled_postgres.py` runs the same two blocks on the same background launch, and adds three checks: the server's `PPid` is the entry point's pid (i), the socket directory is 0700, and SIGTERM ends the entry point with exit 0 and the server gone (iv). Beside that it has a SIGKILL case (the parent-death backstop), the second-server refusal, `runtime migrate` under `local`, the wheel case above, and the layout and refusal cases. **Under `CI` it fails rather than skips** if the server is missing, because `validate.yml` pins `EXPECT_SKIPPED=11` exactly.

## A mutant of each new refusal and guarantee, killed

Each mutant was applied alone, and `test_bundled_postgres.py` plus `test_install_mode.py` were run with `-x`, with a 240 s bound so a hang could not pass for a kill:

| mutant | killed by |
|---|---|
| M1 an operator DSN accepted beside local | `test_migrate_under_the_local_mode_uses_the_bundle_and_refuses_a_dsn` |
| M2 a socket path too long accepted | `test_a_state_dir_too_long_for_a_unix_socket_is_refused_naming_it` |
| M3 a relative state dir accepted | `test_a_relative_state_dir_is_refused_naming_it` |
| M4 the server opens a TCP listener (`listen_addresses=127.0.0.1`) | `test_the_entry_point_owns_a_migrated_server_with_no_tcp_listener` |
| M5 the server is not the entry point's child (`setsid -f`) | same |
| M6 `stop()` a no-op | `test_a_second_entry_point_on_the_same_state_dir_is_refused` |
| M7 no parent-death signal | `test_the_server_stops_even_when_the_entry_point_is_killed_outright` |
| M8 SIGTERM not read as an interrupt | `test_the_entry_point_owns_a_migrated_server_with_no_tcp_listener` |
| M9 started but not migrated | same |
| M10 the served DSN is the owner's (a collapse) | `test_the_two_dsns_are_two_users_over_the_one_socket` |
| M11 no packaged migrations found | `test_a_wheel_install_migrates_its_bundled_server_outside_a_checkout` |
| M12 the socket directory left 0755 | `test_the_entry_point_owns_a_migrated_server_with_no_tcp_listener` |
| M13 a second server on one state dir not refused | `test_a_second_entry_point_on_the_same_state_dir_is_refused` |

**A defect this PR's own test found in itself.** The first cut of the wheel case ran `pip install --prefix` without `--ignore-installed`. pip then read the suite's own editable `opendox` as the installed copy of the same project and **uninstalled it**, emptying the environment the suite runs in (measured: `pip list` lost `opendox` and both console scripts). The flag is now there, with a comment, and the case asserts afterwards that the suite's own `opendox` still resolves.

## The repo's own suite

Full `python -m pytest -q`, `LANG=C.UTF-8`, `CI=true`, against a `postgres:16` like `validate.yml`'s:

| | selected | passed | skipped | failed | errors |
|---|---|---|---|---|---|
| #60's head `f097fd8` | 2485 | 2474 | 11 | 0 | 0 |
| #67 (T070) `b50e3b1` | 2531 | 2520 | 11 | 0 | 0 |
| #67 (T070) `525f61c`, its fix round 2 | 2538 | 2527 | 11 | 0 | 0 |
| this branch before the merge, `c3a70a2` | 2545 | 2534 | 11 | 0 | 0 |
| `32db5d8` (merges `525f61c`) | 2552 | 2541 | 11 | 0 | 0 |
| `ac61596` (merges `02dadc5`) | 2554 | 2543 | 11 | 0 | 0 |
| `28bdccd` (fix round 4, and merges `859b37b6`) | 2580 | 2569 | 11 | 0 | 0 |
| `96b2699f` (fix round 5) | 2584 | 2573 | 11 | 0 | 0 |
| `a0fb7c8d` (merges `026f00ea`; fix round 6) | 2584 | 2573 | 11 | 0 | 0 |
| `0f77d5c1` (fix round 7) | 2595 | 2584 | 11 | 0 | 0 |
| `379fbb14` (fix round 8) | 2601 | 2590 | 11 | 0 | 0 |
| `84a6c041` (the carrier and peer authentication, as ruled) | 2613 | 2602 | 11 | 0 | 0 |
| `f8e6e9e9` (fix round 10) | 2619 | 2608 | 11 | 0 | 0 |
| `fe232fe` (merges #67's `c8fac05e`, carrying main `047bb4fa`), before its edits | — | — | — | 3 (the bundled background cases) | — |
| `19e32f0c` (the merge round's edits) | 3195 | 3184 | 11 | 0 | 0 |
| `6eb0bbdb` (merges #67's `cdf7382b`; the env probe expects the child's own state dir) | 3204 | 3193 | 11 | 0 | 0 |
| `fedfa75d` (fix round 11, `0488f5bd`, then merges #67's `d1de1fd9` with no file change) | 3210 | 3199 | 11 | 0 | 0 |
| `f66e5f82` (fix round 12, `3426c753`, then merges #67's `105f2f12`) | 3215 | 3204 | 11 | 0 | 0 |
| `a9854078` (in-process local cases get their own state dir), with local and broker settings, a runner `OPENDOX_STATE_DIR` and a 90-character `XDG_STATE_HOME` exported | 3215 | 3204 | 11 | 0 | 0 |
| `21bde1af` (the adversarial review's M1, L1, L2, L3 and replication note) | 3236 | 3225 | 11 | 0 | 0 |
| `28e195b9` (fix round 13: the carrier is found as the installed distribution's own files) | 3240 | 3229 | 11 | 0 | 0 |
| `57b7ed8f` (fix round 14, `bf9bcb08`, then merges main `66ff7257`) | 3327 | 3316 | 11 | 0 | 0 |
| `058d96ef` (fix round 15) | 3328 | 3317 | 11 | 0 | 0 |
| `085ba0b0` (merges main `2fc714d2`, #62 T079) | 3346 | 3335 | 11 | 0 | 0 |
| `3ebccb3c` (fix round 16) | 3350 | 3339 | 11 | 0 | 0 |
| `52a2b8dc` (merges main `9a490405`, #74 T081) | 3399 | 3388 | 11 | 0 | 0 |
| `4a9dbe95` (fix round 17) | 3402 | 3391 | 11 | 0 | 0 |
| this branch, `c04690f8` (fix round 18, `8986158e` and `c04690f8`) | 3404 | 3393 | 11 | 0 | 0 |

`EXPECT_SKIPPED=11` holds exactly, and the floors allow the rise unchanged. The new module adds about 32 s to the run (ten cases, each server start about 1.5 s).

**Merging #67's fix rounds.** `95fe16f` merges `32683e8`, and `32db5d8` merges `525f61c`: `runtime migrate` and `reset` refuse what a local install cannot be.
- `525f61c` and this PR both rewrite `load_migration_settings`. The conflict resolves to this PR's structure: under `local`, the refusal is asked first, and only then is the bundle's migration DSN read. T070's reason is carried into the comment.
- The two merged cases set the local shape as this PR defines it, with the mode and the state dir and no operator DSN. Beside `local` a DSN is itself refused here, so a case that set one would have tested the DSN refusal instead of the broker or bind refusal it names.
- A mutant that drops the refusal from the migration loader fails all 7 merged cases.
- `ac61596` merges `02dadc5`, cleanly. With the runtime extra absent, `status`'s early return now reports a local install's broker as not configured. The merged case uses this PR's local shape and also asserts that `database_bundle` is reported on that return: present for local, `null` for hosted.

## Fix rounds 4 and 5: Copilot's twelve threads (`5e52872`, `96b2699f`)

Copilot reviewed `95fe16f`, `32db5d8` and `ac61596a` and opened twelve threads. **Ten are fixed, answered and resolved.** The new cases are in `tests_runtime/test_local_lifecycle.py` (new, hermetic), plus two in `test_bundled_postgres.py`.
- **Migrations** (r4139811473, r4139880241).
  - An explicit `OPENDOX_MIGRATIONS_DIR` is used as given.
  - Unset, a **local** install uses only its own installation's copy, never the working directory's. That copy is the source tree `__file__` came from first, then the `RECORD` of the distribution that holds the running module. Where there is none, it is refused.
  - A **hosted** install's default is unchanged (13.6).
- **The pid** (r4139811555, r4139938402).
  - A pid is believed only when `/proc` proves it is an executable named `postgres` running in this data directory. A proven-stale lock is removed before the launch.
  - Where there is no `/proc`, nothing is believed, and PostgreSQL's own interlocks stand. The price is a `status` with no pid on macOS and the BSDs, recorded in the thread.
- **initdb** (r4139880213): it runs into an attempt directory that is renamed into place only on success. Abandoned attempts are removed. A non-cluster `data/` is refused and left alone.
- **start()** (r4139880279): every phase is one guarded operation, and every failure is the one named refusal, with its phase and class.
- **Interrupts** (r4139880267): SIGTERM or Ctrl-C anywhere in the local lifecycle is a clean stop, exiting 128 + the signal number. A served run still exits 0.
- **Refusal wording** (r4139880298): broker settings and DSNs are two classes, each with its own reason.
- **State dir** (r4139938444): an unknown `~user`, or no home, refuses naming `OPENDOX_STATE_DIR`.
- **Test helper** (r4139811584): bounded by a selector. With a silent 8 s child and a 1 s deadline, it returned after 1.0 s where the old loop took 8.0 s.

Evidence:
- Against `ac61596`'s source, 17 of round 4's first 18 cases fail; the one that passes is the unchanged hosted default. Round 5's cases fail against `28bdccd`.
- Mutants: 23 of round 4 and 5 of round 5, all killed (`runs/mutants-t072-r4.txt`, `-r5.txt` in the writer's workdir).

**Round 6** (`a0fb7c8d`): Copilot's review at `28bdccd9` opened two more threads.
- The unknown-`_serves` point was already fixed at `96b2699f`; it is answered and resolved.
- The pyproject note named a function that no longer exists. It now names `installation_migrations_dir`, and the packaging case checks every `opendox.runtime.config.<name>` pyproject names.
- `4aed6278` merges T070's `026f00ea`, a docstring change.

**Round 7** (`0f77d5c1`): Copilot's review at `a0fb7c8d` opened three threads, all fixed and resolved. SonarCloud raised one reliability finding.
- **libpq's environment.** Every `PG*` variable is lifted out of `os.environ` for the duration and put back afterwards, around `generate-and-open --local`'s lifecycle and the runtime verbs under `local`. `PGHOSTADDR`, `PGSERVICE` and `PGOPTIONS` can no longer redirect the bundle's connections. The real entry point and `status` are proven with all three set.
- **The socket's path.** The resolved state tree must be this user's own and writable by no one else. Every ancestor must be owned by the user or root, and sticky where others can write it; a group-writable ancestor of the user's own group is allowed. Symlinks inside the tree are refused before any chmod.
- **A relative HOME** is refused for the default state directory.
- **SonarCloud S6466** (reliability): `server_binaries` no longer indexes a list.
- Evidence: 8 new cases fail against `a0fb7c8d`, and 11 mutants are killed.

**Round 8** (`379fbb14`): Copilot's review at `0f77d5c1` opened two threads, both fixed and resolved.
- A group-writable ancestor is refused whatever its group, because a primary group can have other members.
- The configured path is checked as configured, as well as resolved: both chains' ancestors, every link's owner, and no `..` anywhere.
- Evidence: 5 new cases fail against `0f77d5c1`, and 5 mutants are killed.

**Round 9** (`84a6c041`) applies Brett's two rulings, openxFactory#656 [`5916000030`](opensoft/openxFactory#656 (comment)) items 2 and 3: the carrier and peer authentication, as described above.
- **The server confirms it**, in its own views:
  - `pg_hba_file_rules` has exactly the one peer rule (with `map=opendox`) and the two rejects;
  - `pg_ident_file_mappings` has exactly the two mappings, for this OS user;
  - `system_user` is `peer:<os user>` for both roles.
- **The map decides.** The same OS user asking for a role outside the map is refused (`peer authentication failed`). A non-root suite cannot connect as a second OS user, so for that case the map, read back from the server, stands: it names no other user.
- **Evidence:** 13 new cases fail against `379fbb14`. 9 mutants are killed:
  - initdb trust;
  - no map;
  - a trust rule;
  - a wildcard system user;
  - a third role;
  - no re-assertion;
  - any name admitted;
  - files 0644;
  - the old carrier.
  The auth mutants are also killed by the real-server cases alone.

**Round 10** (`f8e6e9e9`): Copilot's review at `84a6c041` raised three points, all fixed.
- An existing `postgres/data` joins the tree check, a broken link included (r4147680113, resolved).
- Missing parent directories are created exactly 0700 whatever the umask. `mkdir(parents=True)` under umask 0002 made them group-writable.
- Readiness requires the data directory's lock file to name the launched child, so a racing loser cannot adopt the winner's socket.
- Evidence: 6 new cases fail against `84a6c041`, and 4 mutants are killed.

**SonarCloud S2115, ACCEPTED, as ruled.**
- **Issue:** `AaDvyyOCiqwq-gAw53M3`, python:S2115, "Add password protection to this database", on `src/opendox/runtime/config.py` `DatabaseBundle.dsn`.
- **New status:** `accept` (SonarCloud now reports it `RESOLVED`). Set with the SonarQube tool on openxFactory#656 `5916000030` item 3's authority.
- **Rationale:** the DSN has no password because the server authenticates Unix-socket connections by PEER. The kernel verifies the connecting uid (`SO_PEERCRED`), and `pg_ident.conf` maps only this install's OS user to the two roles. The socket directory is 0700, the server has no TCP listener (`listen_addresses` is empty), and every host connection is rejected. The same rationale is in the DSN's docstring.
- **Gate:** after the change, SonarCloud reports the PR's quality gate `OK` on every condition.

## Merge-from-main round (`fe232fe`, `19e32f0c`; 2026-10-02)

Phase 2 has landed. This branch now carries #67's `c8fac05e`, which carries #60's `adeb6fed` and main `047bb4fa` (T054 to T058, T055's follow-up #70 and T056's standalone test). Git auto-merges `pyproject.toml` (main's validator package data beside this PR's local extra and data files), `src/opendox/cli.py` and `tests/test_doxbench_entrypoint.py` without a conflict. Four edits followed, all in `19e32f0c`:
1. **The stand-ins in `tests_runtime/local_entrypoint_driver.py` go.** The driver is deleted. Its stand-ins patched names T055 has since replaced, so on the merged tree they stood in for nothing, and all three background cases failed: the real corpus-root check refused the stand-in corpus. `test_bundled_postgres.py` now runs the real entry point over T050's fixture, with the validator on.
2. **Every cheap refusal comes before the database start.** Main's T055 added `_refuse_empty_source_options`, so the local path asks it before it builds the bundled server. `tests/test_projection_seams.py`'s empty-option case carries a tripwire bundle, so a regression neither starts a server nor passes.
3. **No child touches the user's state directory.** T070 gave four phase-2 callers `--local`, and here `--local` starts the bundled server, whose `OPENDOX_STATE_DIR` defaults to the user's `~/.local/state/opendox`. `tests/standalone_child.py` now gives every child a fresh, short, private state directory under `/tmp` and removes it when the child stops. Measured before: three children of T056 and T058 initialized a cluster in the (sandboxed) default state home.
4. **T056's case 3 asserts it:** while serving, its bundled server's data directory is under the child's own state directory, and the directory is gone after the stop.
- **Mutants**, all four killed: the refusal dropped; no private state dir; the dir not removed; the fixture not a repository.
- **Full suite:** `3195 selected, 3184 passed, 11 skipped, 0 failed`. Nothing is left under `~/.local/state/opendox` or `/tmp/odx-child-*`.

## Merge of #67's fix rounds (`94254b18`, `6eb0bbdb`; 2026-10-02)

`94254b18` merges #67's `cdf7382b`, which carries #60's `c39d960e` (a PostgreSQL scheme libpq would not read as a URI is refused). `cdf7382b` itself means a `--local` caller inherits none of the runner's runtime settings. There were two docstring and setup conflicts, and both were resolved by keeping both sides:
- **`tests/standalone_child.py`:** the code merged cleanly in the needed order. The child's environment first drops every `SETTING_NAMES` entry, and only then is `OPENDOX_STATE_DIR` set to the child's own directory.
- **`tests/test_projection_seams.py`:** the empty-option case scrubs the settings and keeps this PR's bundled-server tripwire.

`6eb0bbdb` changes #67's harness probe. On #67 it asserted that a child sees no runtime setting. Here every child is given exactly one, its private state directory. The probe now also exports a runner state directory, and asserts three things:
- the child sees exactly `OPENDOX_STATE_DIR` among the runtime settings;
- its value is the child's own `Child.state_dir`, not the runner's;
- the directory is gone once the child has exited.

Evidence:
- **Mutants**, all four killed, under an exported hosted install's settings:
  - the child keeps the runner's settings;
  - the scrub runs after the state dir is set;
  - the runner's own state dir is passed through;
  - the in-process case does not scrub.
- **Exported settings:** the child-driven modules (`tests/test_standalone_generate_path.py`, `tests/test_post_render_validator.py`) pass whole with a hosted install's settings and a runner `OPENDOX_STATE_DIR` exported.
- **Full suite:** `3204 selected, 3193 passed, 11 skipped, 0 failed`. Nothing is left under `~/.local/state/opendox` or `/tmp/odx-child-*`.

## Fix round 11: nothing is made through a path the tree check would refuse (`0488f5bd`, then `fedfa75d`)

Copilot's review at `19e32f0c` opened one thread, which is real (reproduced) and is now answered and resolved. `_prepare_directories` made the missing `postgres/run` before the tree check judged the path, so a component it refuses had already been written through: another user's link, or a 0777 directory. In a sticky parent such as `/tmp`, another user could also plant the state directory's name between the check and the `mkdir`. The fix:
- **What exists is judged before any write.** `_refuse_an_unsafe_tree(existing_only=True)` runs the link-ownership loop first, so even a broken foreign link is named. The whole tree is judged again afterwards, before the socket directory's `chmod`.
- **Missing components are made by descriptor.** `_make_private_directories` makes each one relative to its parent's descriptor and opens it with `O_NOFOLLOW`. `fstat` must show it is this user's alone before anything is made beneath it. A planted link, non-directory or foreign directory is the named refusal: never followed, never re-moded.
- **No `mkdir`/`chmod` window.** Each component is born 0700 under a umask of 077, and the umask is put back afterwards.

Evidence:
- **New cases:** six, in `tests_runtime/test_local_lifecycle.py`. All fail against `19e32f0c`'s `bundle.py` and pass here.
- **Mutants:** seven, all killed.
- **Full suite:** `3210 selected, 3199 passed, 11 skipped, 0 failed`.

`fedfa75d` merges #67's `d1de1fd9` (its healthy-local status case reads either DSN form). On this branch that case uses the bundled server, so the conflict resolves to this side and changes no file.

## Fix round 12: the auth files are exactly 0600, and the cluster runs on its own files (`3426c753`, then `f66e5f82`)

Copilot's reviews at `6eb0bbdb` and `fedfa75d` opened two threads, both real and now answered and resolved.
- **`write_authentication`'s 0600 was only a creation request.** The umask filtered it, and a stale temporary from an interrupted start kept its own mode or was written through as a link. Now the stale temporary is unlinked, the new one is opened `O_CREAT | O_EXCL | O_NOFOLLOW`, and its descriptor is `fchmod`-ed to exactly 0600 before anything is written. A link raced in after the unlink is a refusal, never followed.
- **A reused cluster's `postgresql.conf` could redirect `data_directory`, `hba_file` and `ident_file`**, for example to an outside `trust` file. The launch now pins all three on the command line, which outranks every configuration file.

Evidence:
- **New cases:** four in `tests_runtime/test_local_lifecycle.py` (umask, stale 0644, stale link, raced link) and one real-cluster case in `tests_runtime/test_bundled_postgres.py`. All but the raced-link case fail against `fedfa75d`.
- **Mutants:** six, all killed.
- **Full suite:** `3215 selected, 3204 passed, 11 skipped, 0 failed`.

`f66e5f82` merges #67's `105f2f12` cleanly. It adds an autouse fixture in `tests_runtime/conftest.py` that clears every runtime setting before each case, so a case wanting `OPENDOX_STATE_DIR` sets its own, as every one here already does.

## The adversarial review of `f66e5f82` (`a9854078` to `21bde1af`; 2026-10-02)

An adversarial review of `f66e5f82` found nothing high. It found one medium, three lows and a note. Each is fixed in its own commit, each with a new case that fails without it and mutants that are killed. One more hermeticity fix came first.

- **`a9854078`: the in-process local cases give themselves a short state directory.** The doxBench entrypoint fixture and the empty-option case in `test_projection_seams.py` scrubbed the settings, and so fell back to the runner's default state directory. When that is too long for a Unix socket, configuration refuses it before the case is reached. Measured with a 90-character `XDG_STATE_HOME`: 4 errors and 1 failure. Each now sets its own short `OPENDOX_STATE_DIR` and removes it afterwards. Nothing is made in it, because the database is stood in. Both mutants are killed.
- **M1, medium (`e214477d`): a comma in the state directory is refused.** PostgreSQL splits `-k` on commas, and libpq splits a decoded `host` on them. The review reproduced sockets in two unchecked directories, one of them 0777, while the checked 0700 directory stayed empty. `config.database_bundle` (every bundle's one derivation) now refuses a `,` from `OPENDOX_STATE_DIR`, `XDG_STATE_HOME` or `HOME`, without repeating the value.
- **L1 (`c4f5df3c`): the carrier is pinned to `pixeltable-pgserver>=0.6.0,<0.7`, and another major is refused by name.** Before an existing cluster is used, the server's own `postgres --version` is asked against its `PG_VERSION`. Another major, or a server that does not say, is a named refusal, before anything is written. 4 mutants are killed.
- **L3 (`b2d80e94`): the directory creation starts from is judged by its descriptor.** `_make_private_directories` now `fstat`-judges the base it opens with `_unsafe_because` before the first `mkdir`. That is the install's own rule for the state directory and below, and the ancestors' rule above it. This makes round 11's rule hold inside the function itself. 2 mutants are killed.
- **L2 (`9e2f3030`): the local verbs judge their socket before connecting to it.** `runtime status`, `migrate` and `reset` connected to whatever answered at the bundle's socket path. Reproduced here: `status` on a 0777 tree whose `run` linked to another bundle's socket reported that bundle's applied migrations and exited 0. The fix:
  - `bundle.refusal_before_connecting` asks the start's tree check (now `bundle.refuse_an_unsafe_tree`) of what exists. It then asks for a live server of THIS data directory: its own `postmaster.pid` must name a live `postgres` whose working directory is this data directory, listening at this socket directory.
  - **Status's `database` reads `"not probed: <reason>"`** for a local install that fails this check, including one with no server running. It used to read `"unreachable: …"`, found by connecting.
  - **`migrate` and `reset` refuse as `local-bundle-unverified`.** Hosted is unchanged.
  - The status database block is re-indented under the new branch; `git diff -w` shows only the branch.
  - 7 mutants are killed.
- **The note (`21bde1af`): no replication connection, logical or physical. Fixed, not only reworded.** `authentication_files`' docstring said a replication connection is refused. A physical one was refused, but a logical one (`replication=database`) was accepted as `peer:<user>`, and `IDENTIFY_SYSTEM` answered (measured). No `pg_hba.conf` rule can tell it from an ordinary connection. So the launch sets `max_wal_senders=0`, and the docstring now says both kinds are refused by the server. The mutant is killed.

**Full suite:** `3236 selected, 3225 passed, 11 skipped, 0 failed`. Nothing is left under `~/.local/state/opendox` or `/tmp/odx-*`.

## Fix round 13: the server is found as the installed distribution's own files (`28e195b9`)

Copilot's review at `f66e5f82` opened one thread, which is real and is now answered and resolved. `server_binaries` used `importlib.util.find_spec`, which follows `sys.path`. Under `python -m opendox.cli` that starts with the working directory, and a corpus checkout is where it runs. So a checkout holding an executable `pixeltable_pgserver/pginstall/bin/postgres` was run as the database server.

The carrier is now looked up by distribution name (`importlib.metadata`), on `sys.path` without the working directory. Both binaries must be files its RECORD lists, inside it and executable.

Evidence:
- **New case:** the working directory holds an importable package and a forged `.dist-info`, and the server is not taken from it.
- **Reworked lookup case:** four refusal shapes.
- **Before:** 6 of the 8 fail against `find_spec`.
- **Mutants:** 4 killed, 1 equivalent.
- **Full suite:** `3240 selected, 3229 passed, 11 skipped, 0 failed`.

## Fix round 14: a named platform gate, resolution failures as reasons, no pid behind a refused tree (`bf9bcb08`)

Copilot's review at `21bde1af` opened three threads, all real and now answered and resolved.
- **A named platform gate.** The bundle is a POSIX design, but the carrier ships Windows wheels, where a start failed as an `AttributeError`. `bundle.unsupported_platform()` names the missing primitives (`os.getuid`, `O_DIRECTORY`, `O_NOFOLLOW`, `os.fchmod`, `mkdir` with `dir_fd`, `socket.AF_UNIX`). A start and the local verbs' socket check ask it first.
- **Resolution failures are reasons.** `refusal_before_connecting()` names a symlink loop (`RuntimeError`) and an embedded NUL (`ValueError`) as reasons, beside `OSError`.
- **No pid behind a refused tree.** `bundle.report()` reports a pid only behind a verified tree. A `postgres/data` linked to another live bundle had reported that server's pid.

Evidence:
- **New cases:** three hermetic ones, and a `linked-data` shape with null-pid assertions in the real verbs case.
- **Mutants:** eight, all killed.

## Merge of main after #67 landed (`57b7ed8f`; 2026-10-03)

#67 (T070) landed as a squash, `66ff7257`, after #61 (T078, `8a98e317`). Main thus holds T070's content as one commit this branch's history never saw, and a plain merge against the old base `047bb4fa` conflicted in ten files where both sides carry the same T070 text.

So the merge is computed against the T070 state this branch already held, #67's `105f2f12`: `git merge-tree --write-tree --merge-base=105f2f12 HEAD origin/main`. It is recorded with both parents. The base is sound because `git diff 2526245 66ff725` (#67's last branch head against its squash) names only #61's three files. Against it the merge is clean:
- **18 files** take main's changes since `105f2f12`: #65 T088, #71 T085, #61 T078, and #67's `--local` in #71's test.
- **17 of them are byte-identical to main's.** `src/opendox/cli.py` is the one merged file: main's two `doxbench_defaults` registrations sit beside this branch's local path.
- **No standalone `generate-and-open` child main brought in lacks `--local`.** #71's case gained it in #67's merge. #65's lens test runs `generate` and `serve.build_server`. #61 runs no entry point. Under T072, #71's `--local` child starts a bundled server in `tests/standalone_child.py`'s private state directory.
- **Full suite:** `3327 selected, 3316 passed, 11 skipped, 0 failed`.

## Fix round 15: on Linux the parent-death signal is armed, or the server is not started (`058d96ef`)

Copilot's review at `57b7ed8f` opened one thread, which is real and is now answered and resolved. `ctypes` reports a failed `prctl()` by returning `-1`, never by raising (a seccomp denial, say), and the child ignored it. A killed entry point could then orphan the server, against R1Q16 (iv).

The fix:
- **A failed `prctl` is a refusal.** The child now raises on a nonzero return. `subprocess` re-raises that as `SubprocessError`, which `_launch` names as the refusal "could not be given its parent-death signal", with no server process left.
- **A Linux C library with no `prctl` is the same refusal**, where it used to fall back silently. Other platforms are unchanged.

Evidence:
- **New case (Linux):** a stand-in `prctl` that returns `-1`. The start refuses by name, and the stand-in server never runs.
- **Mutants:** both killed.
- **Full suite:** `3328 selected, 3317 passed, 11 skipped, 0 failed`.

## Merge of main after #62 landed (`085ba0b0`; 2026-10-03)

`085ba0b0` merges main `2fc714d2` (#62, T079). #62 changes only `doxbench_binding.py`, `doxbench_intake.py`, `doxbench_provider.py` and `test_model_provider_broker.py`, none of which this branch touches, so the merge is clean. It runs no `generate-and-open` child, so it needs no `--local`. Full suite: `3346 selected, 3335 passed, 11 skipped, 0 failed`.

## Fix round 16: a local install's served role and database are the bundle's own (`3ebccb3c`)

Copilot's review at `085ba0b0` opened one thread, which is real and is now answered and resolved. Under `local`, `OPENDOX_RUNTIME_PG_ROLE` could replace the bundle's served role in the settings. The migration run narrows the ledger privileges of exactly that configured role, while the bundle bootstraps `opendox_runtime` with the default DML and its served DSN connects as `opendox_runtime`. So `OPENDOX_RUNTIME_PG_ROLE=pg_read_all_data` left `opendox_runtime` able to rewrite `opendox_schema_migrations`.

`refuse_what_a_local_install_cannot_be` (asked by both loaders and by `generate-and-open --local`) now accepts two settings only unset or naming the bundle's own:
- `OPENDOX_RUNTIME_PG_ROLE` must be `opendox_runtime`;
- `OPENDOX_SERVED_DATABASE` must be `opendox`, since it declares the same identity.

Hosted is unchanged.

Evidence:
- **New case:** both settings, through both loaders.
- **Mutants:** both killed.
- **Full suite:** `3350 selected, 3339 passed, 11 skipped, 0 failed`.

## Merge of main after #74 landed (`52a2b8dc`; 2026-10-03)

`52a2b8dc` merges main `9a490405` (#74, T081), with no overlap. #74's standalone `generate-and-open` fixture already passes `--local`, since it landed after #67. Under T072 that child now starts a bundled server in the harness's private state directory.
- **`tests/test_chat_model_configuration.py`:** 49 passed.
- **Full suite:** `3399 selected, 3388 passed, 11 skipped, 0 failed`.

## Fix round 17: the `/proc` falsifier is gated, and a backslash in the map is pinned literal (`4a9dbe95`)

Copilot's review at `52a2b8dc` opened two threads, both now answered and resolved.
- **The `/proc` falsifier is gated.** `test_the_entry_point_owns_a_migrated_server_with_no_tcp_listener` reads Linux's `/proc`, so it now skips where `/proc` is absent, as the other `/proc` and PDEATHSIG cases do. On Linux, and in CI, nothing changes.
- **Backslashes are pinned literal, not escaped.** The thread suggested escaping backslashes in `pg_ident.conf`. Measured against the bundled PostgreSQL 16.14, `pg_ident_file_mappings` reads `"DOMAIN\alice"`, `"alice\"` and `"a\\b"` back as exactly those names, without error. The tokenizer treats a backslash specially only at the end of a line, never inside quotes, so escaping would map a different name.
  - The code is unchanged.
  - A real-server read-back case and a hermetic verbatim assertion pin the behaviour.
  - The escaping mutant is killed.

**Full suite:** `3402 selected, 3391 passed, 11 skipped, 0 failed`.

main has since taken #63 (T080, `1130e996`), whose five files (the doxbench model-provider family) do not overlap this branch. The PR stays MERGEABLE, and CI's merge ref includes it.

## Fix round 18: one schema for both DSNs, and an uninspectable live pid fails closed (`8986158e`, `c04690f8`)

Copilot's review at `4a9dbe95` opened two threads. The holder accepted both, and both are now answered and resolved.
- **`8986158e`: both bundle DSNs name `search_path=public`.** Left implicit, PostgreSQL's `"$user", public` let a reused cluster with a schema named `opendox` or `opendox_runtime` split the owner's ledger from the served role's reads.
  - **New real-server case:** both role-named schemas are created, then the bundle restarts. Both roles' `current_schema()` is `public`, they read one ledger, nothing is re-applied, and `status` is reachable with nothing pending.
  - **Before:** without the pin, the restart fails with `RuntimeAccessMissingError`.
  - **Mutants:** 2 killed.
- **`c04690f8`: a live pid `/proc` will not describe is unknown, not "not ours".** `_identity` raises `_Withheld` for anything but a vanished entry, and `_remove_a_proven_stale_lock` keeps the lock and refuses the start by name rather than unlinking a possibly live server's lock. With no `/proc` at all, PostgreSQL still judges.
  - **New case:** a live pid whose `/proc` links are withheld. It fails against `4a9dbe95`.
  - **Mutants:** 2 killed.

**Full suite:** `3404 selected, 3393 passed, 11 skipped, 0 failed`, run with `LANG=C.UTF-8` and no `GIT_*`/`XF_*`.

## Downstream, for the holder

- **Out of scope, and not T072's: `serve.py` cannot bind `::1`.** This is pre-existing, measured at #60's head `f097fd8`: `gaierror [Errno -9] Address family for hostname not supported`. `serve.LOOPBACK_HOSTS` lists `::1`, but `ThreadingHTTPServer` is IPv4. Recorded on #67 (T070) for a `serve.py` follow-up in that file's single-writer order. This PR does not touch `serve.py`.
- **Done at the merge round:** the driver's stand-ins are gone, and the probe runs the real entry point on `tests/fixtures/plain-documents`.
- **For T073 and T075, which stack on this PR:** a `--local` child built with `tests/standalone_child.py` gets its own state directory automatically. Any other `--local` caller must set `OPENDOX_STATE_DIR` itself.
- **T073** reads `database_bundle` from the server object the entry point keeps (`args.database_bundle`) for `/capabilities`' `install` block.
- **T074** runs F13.1 whole, `caps.json` block included.
- **Overlap** (`gh pr diff -R opensoft/openDox-code`): `pyproject.toml` (#58) and `cli.py` (#59, and the T058 writer after it). They have landed, and this stack took its merge-from-main round after them. `serve.py` is not touched here.

🤖 Generated with [Claude Code](https://claude.com/claude-code)


Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 3, 2026
#69 landed on main as 5e7ab00, a squash over 1130e99 (#63, T080). This
branch already holds #69's final pre-squash head c04690f (merged at
bc85f21). So the merge is computed with
`git merge-tree --write-tree --merge-base c04690f`: main's side contributes
only what main has beyond c04690f, which is #63's T080 change, and the
squash's copy of stack A is not merged a second time. Both parents are
recorded, with no rebase. There were no conflicts.

After it, #72's diff against main is T073's own files only.

Lane: openxfactory-4 (openXfactory-4-openDox_extraction)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 3, 2026
#69 landed as squash 5e7ab00 over 1130e99 (#63, T080). This branch
already contains c04690f, #69's final pre-squash head (bbd91a3). Merged
with git merge-tree --merge-base c04690f, because the squash's own
ancestry does not hold c04690f. Both parents are recorded, and nothing is
rebased. c04690f..5e7ab00 changes exactly #63's five files, so the
merged tree is main plus T075's four files and nothing else.

Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Arc: neutral-product-standalone-operability

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 3, 2026
…files; intent-feed.js is not owed (plan 034) (#73)

Lane: openxfactory-4 (openXfactory-4-openDox_extraction)

Arc: neutral-product-standalone-operability

Plan 034 (`specs/034-opendox-standalone-operation/tasks.md`, read at openxFactory `main` `2140f5a7`), phase-3 slice **P3-E, the door**, its first task:

- **T075** (#1144's 10.2 and 10.2a): *"The entry point serves the 42-file bundle, reachable in a browser from an openDox-only install. `intent-feed.js` is not owed (10.2a is a declaration, recorded in the PR)."*
- **Falsifier:** F10.1's fetch, as T007 batch H amends it (openxFactory#1206 → `f99a2097`): a `.[local]` install and `--local`, which start the bundled server. Widened here to every one of the 42 files.
- **After:** T056, T038, T063, T069, T072, T007 (batch H). **T063 has landed (#1218 → a883bbf6); T072 landed (#69 → 5e7ab00).** This PR now targets `main`. It was stacked on #69's branch (`build/034-p3i-t072-bundled-postgres`) and branched at #69's merge-from-main head `19e32f0c`, which carries main `047bb4fa`, and #69's later heads are merged in, never rebased: `6eb0bbdb` as `a7cf2665`, `fedfa75d` as `cac12d91`, `21bde1af` (the adversarial-review round, which pins `pixeltable-pgserver>=0.6.0,<0.7` in the same `pyproject.toml`) as `a3cf5e9a`, `28e195b9` (fix round 13) as `0457b383`, and `085ba0b0` (fix rounds 14 and 15, plus main `2fc714d2` after #67 T070, #61 T078 and #62 T079 landed) as `d7511388`, `3ebccb3c` (fix round 16) as `1e642b21`, and `52a2b8dc` (main `9a490405`, after #74 T081 landed, which edits the bundle's `views/doxbench-chat.js`) as `0b0d762c`, and `c04690f8` (fix rounds 17 and 18, #69's final pre-squash head) as `bbd91a3f`. Those merges were clean plain merges. The two `pyproject.toml` hunks are apart, so that merge needed no resolution and keeps both. After #69's squash `5e7ab003` landed on main (over `1130e996`, #63 T080), main was merged with `git merge-tree --merge-base c04690f`, recording both parents (`308523d9`), never rebased. `c04690f8..5e7ab00` changes exactly #63's five files, so against `main` this diff is T075's four files alone.

**Ruled:**
- R1Q22 (a), `5817152735`;
- batch H's F10.1 amendment: R1Q15 (b), R1Q16 (iii), `5850003126`;
- the drafting word, `5960162524` ("Draft all of them now (Recommended)").

Claimed on openxFactory#656 in [`5960231968`](opensoft/openxFactory#656 (comment)). Authored ahead as a DRAFT under `5960162524`. T063 and T072 have since landed. The holder posts READY.

## What the measurement found, at #69's head `19e32f0c`

The suite's own runs use an editable install, which puts `src/` on the path, so each of them serves the tree's 42 files. `pip install ".[local]"` installs a **wheel**, and the wheel is what was measured, from a directory outside the checkout:

- **The wheel carried 41 of the tree's 42 web files.** `vendor/.gitkeep` was missing. setuptools expands package data with the standard library's `glob`, and `web/**` never matches a name that starts with a dot. The census (`tests/fixtures/web_boundary_census.yaml`) counts that file, and `pyproject.toml`'s own note says the glob "ships exactly what that census counts". Slice S5 had measured the same omission and let it stand as a directory keeper (`tests/test_gate_loop_contributed.py`). 10.2 counts it among the 42, so it ships.
- **The installed `opendox generate-and-open --local --no-open` served the other 41**, each with the tree's bytes, and answered `GET /vendor/.gitkeep` with 404. F10.1's own fetch of `/` passed. The content types were `text/javascript`, `text/css` and `text/html`. The bundled server started, migrated `['0001', '0002']`, and stopped on SIGTERM with exit 0.
- **Nothing else was missing.** The static route (`SimpleHTTPRequestHandler`, over `opendox/web/` beside the installed `opendox/cli.py`) already serves whatever the wheel carries. **So `serve.py` and `cli.py` are NOT edited**, and this PR joins neither single-writer order.

## What changes

- **`pyproject.toml`**: `opendox = ["web/**", "web/**/.*"]`, and the note above the line now counts 42 files across two subdirectories. A pattern whose last part starts with a dot is the one that matches such a name, at any depth under `web/`. Measured: 42 files from pip's isolated build, 42 from `--no-build-isolation` over the `test` extra's setuptools 84.0.0, and 42 from a wheel built out of the sdist (whose tarball carries `src/opendox/web/vendor/.gitkeep`). This is T072's file in phase 3's single-writer table, and this PR is stacked after T072.
- **`tests_runtime/test_served_bundle.py`** (new, 3 cases, the in-suite falsifier):
  1. the wheel built from this checkout carries every file under `src/opendox/web/`, and nothing else there;
  2. F10.1's fetch, widened. The wheel is installed under a prefix of its own (T072's recipe), and its `opendox` console script runs `generate-and-open --local --no-open --port 0` from a directory that is not a checkout. The preconditions are checked first: none of the four siblings is importable, `opendox --help` exits 0, and the `WEB_DIR` it serves is the wheel's copy. Then `/` answers with `<html`, and each of the 42 files answers 200 with the tree's bytes and a type a browser accepts (a JavaScript MIME type for `.js`, `text/css`, `text/html`). The module graph a browser walks from `/` closes inside the bundle. The walk follows the page's `src`/`href`, then each module's static imports, then every dynamic `import()` target the bundle carries (`views/projection-index.js` is reached that way). Each module's imports are read with its comments removed.
  3. The walk's import reader, over the tree, with no server: its edges equal an independent line-by-line reading of every relative specifier, which is 80 edges. The edge a commented multi-line clause declares (`views/doxbench-editor.js` → `views/doxbench-state.js`) is named, and an in-memory mutant of only that specifier is caught. The process stops on SIGTERM with exit 0, and its bundled server is gone.
  - The child's environment drops every `OPENDOX_*`, `PG*`, `GIT_*` and `XF_*` name (the suite exports `XF_GATE_PRINCIPALS`). The fixture repository gets an explicit identity. The child is stopped by SIGTERM, which the local lifecycle reads as its interrupt, so a runner that ignores SIGINT (a `nohup` job) changes nothing. Under `CI`, a missing bundled server FAILS the module rather than skipping it, as `test_bundled_postgres.py` does. The short `/tmp` state directory's removal is retried, bounded, and asserted (`71d24af6`). A red mutant run had once left a `postgres/data/pg_wal/` behind, because a backend was still writing after its postmaster went.
- **`tests/test_gate_loop_contributed.py`** and **`tests/test_validator_input_set.py`**: each pinned the bundle's package-data line verbatim, and each now pins the new line. The recursive-glob guarantee and the setuptools floor are unchanged. S5's measurement in the first docstring (40 entries, 36 under `views/`) is kept as S5's. T075's re-measurement sits beside it: 41 entries and 37 views from `web/**` alone, and 42 with both patterns.

## 10.2a: `intent-feed.js` is not owed (the declaration)

`views/intent-feed.js` stays at openxFactory under RULED OQ-F (`not_moved / stays_openxfactory_adapter`), and it is **not owed to openDox**. openDox's replacement is `views/intent-binding.js`. That module reaches `./intent-feed.js` only by a dynamic `import()`, so an absent file is an absent binding, not a broken bundle. **It is not counted as a missing file.** The 42 do not include it, and the census has no row for it. Case 2 holds the declaration: across the whole bundle, the one module the graph reaches for and does not carry is `views/intent-feed.js`, reached only dynamically, and the installed server answers it 404.

## The falsifier: before and after

`f10-1-widened.sh` is #1144's F10.1 **as batch H amends it**: `pip install ".[local]"`, and `opendox generate-and-open --local …`, with every other line as written. T075's widening is appended after its last line. Each run starts from a clean checkout, in a FRESH venv, with every `OPENDOX_*`/`PG*`/`GIT_*`/`XF_*` variable unset. Two host preconditions apply, and neither is a change to the command:
- `OPENDOX_STATE_DIR=$HOME/.local/state/p3-t075`. This host's `/workspace/projects` is mode 777, and the bundle refuses a state directory under a world-writable, non-sticky ancestor, by name.
- A `python` → `python3` shim on `PATH`, for F10.1's `python -m venv`.

<details><summary>the script</summary>

```bash
# #1144 F10.1, as T007 batch H amends it (`pip install ".[local]"`, `generate-and-open --local`),
# with T075's widening appended after its last line: the fetch, made over every
# file of the bundle. Run with bash, from a CLEAN checkout of openDox-code only.
set -euo pipefail
W=$(mktemp -d)                                        # scratch space, resolved at run time (never a host path)
python -m venv --clear "$W/v10"                      # a FRESH environment: nothing already installed stands in
. "$W/v10/bin/activate"
pip install ".[local]"
if python -c "import openxdox" 2>/dev/null; then echo "FAIL: sibling present"; exit 1; fi
opendox --help >/dev/null                       # the console script MUST exist
export GIT_AUTHOR_NAME=fixture GIT_AUTHOR_EMAIL=fixture@example.invalid GIT_COMMITTER_NAME=fixture GIT_COMMITTER_EMAIL=fixture@example.invalid
R=$(mktemp -d)/plain-documents
cp -r tests/fixtures/plain-documents "$R"   # a FRESH repository (preamble)
git -C "$R" init -q
git -C "$R" add -A
git -C "$R" commit -qm fixture
opendox generate-and-open --local --repo-root "$R" --repository fixture --no-open --port 8080 &
SERVER=$!
trap 'kill "$SERVER" 2>/dev/null || true' EXIT   # cleanup cannot mask the verdict
ready=0
for _ in $(seq 1 30); do
  if curl -sf http://127.0.0.1:8080/ >/dev/null; then ready=1; break; fi
  sleep 1
done
test "$ready" -eq 1                              # a server that never started FAILS here
curl -sf http://127.0.0.1:8080/ > "$W/bundle.html"   # no pipeline: curl's status is the status
grep -qi '<html' "$W/bundle.html"                # and it is really the bundle
# ---- T075's widening: every one of 10.2's 42 files, by the same fetch ----
find src/opendox/web -type f | sed 's|^src/opendox/web/||' | sort > "$W/bundle.txt"
test "$(wc -l < "$W/bundle.txt")" -eq 42         # 10.2's count, from the tree
while read -r f; do
  curl -sf "http://127.0.0.1:8080/$f" > "$W/served" || { echo "NOT SERVED: $f"; exit 1; }   # curl's status is the status
  cmp -s "$W/served" "src/opendox/web/$f" || { echo "NOT THE TREE'S BYTES: $f"; exit 1; }
  echo "served $f"
done < "$W/bundle.txt"
test "$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8080/views/intent-feed.js)" = 404   # 10.2a: not owed
echo "F10.1 (as amended), widened to 42 files: PASS"
```
</details>

**Before**, at #69's head `19e32f0c`: F10.1's own fetch of `/` passes, and the widening stops at the first file the wheel does not carry (rc 1):

```
  database $HOME/.local/state/p3-t075/postgres/run (bundled, pid 1837880, migrations applied now: ['0001', '0002'])
  validation: opendox-snapshot: 0 violations, by opendox.validator, over its packaged copy opendox-snapshot (sha256 f9e3e111af1d)
  serving http://127.0.0.1:8080/index.html
served app.js
served index.html
served styles.css
NOT SERVED: vendor/.gitkeep
rc=1
```

**After**, at `67b8d9da` (rc 0). Re-run at `d61bb9e9`, `a7cf2665`, `cac12d91`, `a3cf5e9a`, `0457b383`, `d7511388`, `1e642b21`, `0b0d762c` and the final head `308523d9`: rc 0 each time, 42 served, `F10.1 (as amended), widened to 42 files: PASS`.

**A finding for T077, about F10.1's fixed `--port 8080`:** the first run at `a3cf5e9a` read `NOT THE TREE'S BYTES: views/doxbench-chat.js`. The body it fetched was 101660 B, while the installed file is 98535 B and equal to the tree's. That run's log has no `serving` line from its own server. Another run on this shared host already answered `127.0.0.1:8080` while this run's bundled server was still starting, and the readiness loop cannot tell whose server answered. The re-run adds one marked T075 guard line before the start, which is not F10.1 text: `if curl -s -o /dev/null http://127.0.0.1:8080/; then echo "FAIL: port 8080 already serves"; exit 1; fi`. Its log shows its own `serving http://127.0.0.1:8080/index.html` before the first fetch. The in-suite case binds `--port 0` and reads the URL its own server prints, so it is not exposed to this.

```
  database $HOME/.local/state/p3-t075/postgres/run (bundled, pid 2340484, migrations applied now: ['0001', '0002'])
  validation: opendox-snapshot: 0 violations, by opendox.validator, over its packaged copy opendox-snapshot (sha256 f9e3e111af1d)
  serving http://127.0.0.1:8080/index.html
served app.js
served index.html
served styles.css
served vendor/.gitkeep
served vendor/markdown-it.min.js
served views/account-menu.js
served views/board.js
served views/bullseye.js
served views/canvas-model.js
served views/canvas.js
served views/composed-model.js
served views/display.js
served views/doc-wheel.js
served views/docs.js
served views/doxbench-chat-model.js
served views/doxbench-chat.js
served views/doxbench-editor.js
served views/doxbench-save.js
served views/doxbench-state.js
served views/edit.js
served views/explorer.js
served views/funnel.js
served views/grouping.js
served views/helpers.js
served views/intent-binding.js
served views/lens-model.js
served views/lens.js
served views/lineage.js
served views/model.js
served views/notebook.js
served views/outline-model.js
served views/projection-index.js
served views/repo-selector-model.js
served views/repo-selector.js
served views/settings.js
served views/staging-workbench-model.js
served views/staging-workbench.js
served views/swb-model-intake.js
served views/view_extension.js
served views/viewer.js
served views/wheel-model.js
served views/wheel.js
F10.1 (as amended), widened to 42 files: PASS
rc=0
```

The in-suite falsifier, `LANG=C.UTF-8 python -m pytest -q tests_runtime/test_served_bundle.py`:
- before (this commit's `pyproject.toml` reverted to #69's): `2 failed`, case 1 with `assert ['vendor/.gitkeep'] == []`, and case 2 with `the installed entry point does not serve: [('vendor/.gitkeep', 'status 404')]`;
- after: `2 passed in 7.70s`.

## Mutants: 8 applied, 8 killed (at `0457b383`, and again at `d7511388`)

Each mutant was applied to the committed tree, `tests_runtime/test_served_bundle.py` was run, and the tree was restored.

| mutant | killed by |
|---|---|
| M1: the dotfile pattern dropped (`opendox = ["web/**"]`, #69's line) | both cases: `['vendor/.gitkeep']` omitted, then `status 404` |
| M2: the recursive pattern made single-level (`"web/*"`) | both cases: `vendor/markdown-it.min.js` and every `views/` file omitted, then 404 |
| M3: the static route serves `.js` as `text/plain` (a `DashboardHandler.extensions_map` override) | case 2: `('app.js', "served as 'text/plain'")`, and every other module |
| M4: `app.js` statically imports `./views/board-missing.js` | case 2's module graph: `('views/board-missing.js', 404)`. The per-file fetch alone passes this one. |
| M5: a `views/intent-feed.js` shipped in the bundle (10.2a broken) | both cases: `43 == 42`, and the declared absence `set() == {'views/intent-feed.js'}` |
| M6: a fake `openxdox` on the child's path | case 2's precondition: `a sibling is importable: importable siblings: ['openxdox']` |
| M7: `views/projection-index.js`, a carried dynamic target, statically imports `./missing-below-dynamic.js` | case 2's walk: `('views/missing-below-dynamic.js', 404)`. It **survived** the walk at `d61bb9e9`, which did not follow dynamic targets (Copilot's "previously missed" finding). |
| M8: in `views/doxbench-editor.js`, only the commented multi-line clause's specifier points at `./doxbench-state-missing.js` (Copilot, r4170756761) | case 2: `('views/doxbench-state-missing.js', 404)`; case 3: the named edge absent. It **survived** `a3cf5e9a`'s reader (`2 passed`). |

## The suite and CI

- **Locally**, at the final head `308523d9`, in a venv installed as `validate` installs it (`--only-binary :all: -c constraints-cpython312-linux.txt -e ".[runtime,test]"`), with `LANG=C.UTF-8`: **3443 passed, 177 skipped, 0 failed** (3230 / 177 / 0 at `bbd91a3f`, 3225 / 177 / 0 at `0b0d762c`, 3176 / 177 / 0 at `1e642b21`, 3172 / 177 / 0 at `d7511388`, 3066 / 177 / 0 at `0457b383`, 3061 / 177 / 0 at `a3cf5e9a`, 3035 / 177 / 0 at `cac12d91`, 3029 / 177 / 0 at `a7cf2665`). At `67b8d9da` it was 3020 passed and 177 skipped, which is #69's 3195 selected plus the 2 new cases. The 177 skips are the DB-backed cases on a machine with no `OPENDOX_TEST_DATABASE_URL`. CI runs them against its `postgres:16` service, and `EXPECT_SKIPPED` stays 11, because the new module never skips under `CI`.
- **CI:** `validate` green at `67b8d9da`, `d61bb9e9`, `a7cf2665`, `cac12d91`, `71d24af6`, `a3cf5e9a`, `0457b383`, `d7511388`, `1e642b21` and the final head `0b0d762c`. SonarCloud green at each since `d61bb9e9`.
- **SonarCloud:** at `67b8d9da` the quality gate failed on 3.4% duplication on new code (required ≤ 3%). Its one block was the pipe reader, copied from `test_bundled_postgres.py:160-175`. In `65b2058f` the entry point's streams go to files, and `_served_url()` polls the stdout file for the flushed URL line. The gate passed at `d61bb9e9`.
- **Copilot:** round 1 (at `67b8d9da`) raised 2 low findings, the stale "41 files" note and a precondition that did not name the sibling it found. Both are fixed in `d61bb9e9`, and both threads were answered and resolved. Round 2 (at `65b2058f`) had no new finding. It listed 2 "previously missed" ones, the dynamic-import walk and S5's stale counts. Both are fixed in `4df679b6` and answered in [this comment](#73 (comment)). Round 3 (at `4df679b6`) was "Approval recommended", with no finding. Rounds 4, 5 and 6 (at `a7cf2665`, `cac12d91` and `71d24af6`) were "Needs a closer look", with no finding, for the stacked, unlanded #69 alone. Round 7 (at `a3cf5e9a`) was "Changes recommended" on one finding: the import pattern missed a multi-line clause with commented quotes (r4170756761). It is fixed in `d99e5ccc` (M8 above), and the thread was answered and resolved. Rounds 8 through 11 (at `0457b383`, `d7511388`, `1e642b21` and `0b0d762c`) were "Needs a closer look", with no finding. Round 8 listed that thread as resolved. 0 unresolved review threads.

## Not in this PR

- T077 (the F10.1 run itself, quoted in its own PR) and T076 (the root `README.md`).
- One observation, not a gap: the static route takes its content types from the platform's `mimetypes` table. On Linux and on CI that table answers `text/javascript` for `.js`. A host whose table does not would fail case 2 (M3 shows it would be caught). Pinning the types would be a `serve.py` edit, so it is left to the holder.

🤖 Generated with [Claude Code](https://claude.com/claude-code)


Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 3, 2026
… a built-in credential's rules (plan 034) (#64)

Lane: openxfactory-4 (openXfactory-4-openDox_extraction)

Arc: neutral-product-standalone-operability

Plan 034, phase 3. This PR is not one of the plan's tasks. It follows T080 (#63), and gives the broker path the rules T080 gave a built-in credential. It is claimed on openxFactory#656, comment `5889279351`. Plan 034's T080 entry, read at openxFactory `main` `2140f5a7` and again, unchanged, at `main` `a883bbf6`, names this PR as the broker path's own draft, outside T080's scope.

**Ruled.** Brett Heap answered #63's closing question, *"Should the broker path follow it?"*, on 2026-09-29: *"Yes, separate phase-3 draft (Recommended)"*. It is recorded at openxFactory#656 comment `5890601202`: *"The broker token path gets T080's protections in openDox-code#64 (stacked after #63). T080's scope is unchanged."* It extends the T080 ruling *"Refuse unless loopback (Recommended)"*, which is recorded at openxFactory#656 comment `5880893901` and ends "The broker path is unchanged in this task". This is the separate draft.

**Ruled, for the runner.** Brett Heap answered this PR's second flag on 2026-09-29: *"Yes, add to #64 (Recommended)"*. It is recorded at openxFactory#656 comment `5901112350`, the lane's latest RULED comment, as item 2. When a broker misbehaves (non-zero exit, oversize answer, or timeout after writing), the shared runner's refusal carries no broker output, no cause and no context. This PR now does that for all four broker operations. To keep the diagnostics useful, each refusal names the operation and the failure class, and never the bytes. See *The broker runner* below.

**Batch K records the route rule in #1144** (openxFactory#1210 → `39f19145`). Brett Heap's word for the batch is `5916000030`, item 1: *"Yes, amendment batch K (Recommended)"*.
- The amendment is a dated note in requirement 17's body in #1144's spec delta, after its SHALL paragraph and above its scenarios. #1144's `tasks.md` points to it after 16.3's batch H addendum.
- The note narrows requirement 17's and scenario 17.1's *"any endpoint"* in one respect only: the route a credential travels by. A token a broker mints (`5890601202`) takes the rule that a credential the built-in resolver resolves takes (`5880893901`). Each is sent only over `https://`, or over `http://` to `127.0.0.1`, `[::1]` or `localhost`.
- A binding that would present either over `http://` to another host is refused when it is declared (`ENDPOINT_NOT_PRIVATE`), before anything is resolved or minted. The request that presents it follows no redirect, and over plain `http://` it takes no proxy.
- The note names this PR as the broker path's half, and #63 as the built-in resolver's. It rewrites no ratified text, and it says the runner ruling (`5901112350`, item 2) does not bear on the route.

**This was drafted ahead, and what it waited for has landed.** T063 has landed (openxFactory#1218 → `a883bbf6`), and T080 has landed (#63 → `1130e996`), after T078 (#61) and T079 (#62). So this PR may go READY. Its READY line is the holder's to post.

**Based on `main`.** The holder retargeted this PR to `main` when #63 landed. T100 (#82, another writer's PR) is stacked on this branch, so retarget #82 to `main` before this branch is deleted, because a merge with `--delete-branch` closes the PR stacked on it.

## What changes

At `main`, and unchanged at #63's head `3f14bb96`, the broker path had the four gaps #63's body listed for Brett. Each was measured over real sockets and a real broker child before it was closed, at `main` `2d116415` and at `3f14bb96` alike. Each is now closed by T080's own rule, reusing #63's helpers. #63's merge of `main`, `4948e6dd`, changes none of the stack's files, so the base reads the same there.

| the gap at `3f14bb96` | now |
|---|---|
| 1. A minted token could be declared over plain `http://` to any host. | Refused when the binding is declared, by the same predicate (`is_a_private_route`) and with the same fixed `ENDPOINT_NOT_PRIVATE` sentence. `mint` asks the predicate again before it asks the broker for anything. |
| 2. A POST answered 301, 302 or 303 reached the redirect's target as a GET carrying the token. With `http_proxy` set, a request to `127.0.0.1` went to the proxy with it. | The request follows no redirect (`DIAG_PROVIDER_REDIRECTED`), and over plain `http://` it uses no proxy. |
| 3. A refused connection chained urllib's `URLError`, and `do_open.headers`, `_send_request.headers`, `_send_output.msg`, `send.data` and `request.headers` held the token. | Every refusal of a turn that presented a token chains nothing: no cause and no context. |
| 4. The token was not checked. One outside latin-1 failed inside urllib as `DIAG_PROVIDER_UNREACHABLE`, which names the wrong party. One with a space or another non-ASCII character was sent as it was. | `mint` asks `_presentable` of it. Any other token is a malformed answer (`DIAG_BROKER_MALFORMED`), refused before the port holds it or any provider is contacted. |

**How, in the code:**
- `doxbench_binding._require_a_private_route` asks the rule of every record that presents a credential. Only the auth kind `none`, which presents nothing, keeps whatever route it declares. `ENDPOINT_NOT_PRIVATE` now names both credentials.
- `BrokeredProviderPort._call_provider` makes every provider request, on both paths. With a credential, it uses `_open_with_a_credential` (#63's opener, renamed) and raises any refusal afresh. #63's `_dispatch_without_a_broker` is rebuilt on it and behaves as before.
- The broker branch of `dispatch` answers an expiry outside every handler. The one re-mint and the one paid retry of the 2026-08-26 ruling do what they did, and a refusal raised by either keeps no context.
- `mint` reads the answer through `_minted_token`. Any refusal of it is raised again, afresh, after the answer has left the frame that read it. So the refusal of an unpresentable token keeps no frame that holds it, as the built-in resolver's refusal of an unpresentable value keeps none. The first flag below says what else this covers. Since the runner ruling, the wrapper that does this is `_broker_operation`, shared by all four operations.
- After Copilot's second review, an answer that could not be read at all is refused too, with the same fixed sentence:
  - an expiry that is no finite number (an integer past a float's range, `NaN`, `Infinity` or `-Infinity`), in `_parse_expires_at`;
  - an answer nested past the recursion limit, in `_answer_document`, for all four broker operations.

  Before, the first escaped `mint` as an `OverflowError` and the last as a `RecursionError`, each with the answer still in its frames. The three non-finite expiries minted a token that would never expire, or would always have expired.
- After Copilot's review at `e1a6cb0f`, a provider answer nested past the recursion limit is refused the same way, as `DIAG_PROVIDER_MALFORMED` (`da9e639a`; `a271d307` then names `ValueError` alone for the decode error, SonarCloud S5713).
  - The parse in `_post_to_provider` serves every path, so a broker's token, the built-in resolver's key and `none` each refuse.
  - Before, the parse escaped as a `RecursionError`, whose traceback kept the request with its authorization header. It did so at #63's head and at `main` too (measured; flag 12).
- `DIAG_PROVIDER_REDIRECTED`, `_DeclineRedirects`, `_presentable` and `_PresentedCredential` now name both credentials. The four gaps left `FIXED_DIAGNOSTICS` at eleven. The runner ruling adds a twelfth (below).

## The broker runner (the second ruling)

Measured at `788d764b`, this PR's head before the ruling, with a stand-in broker that wrote a fake token first:

| the broker | at `788d764b` | now |
|---|---|---|
| exits non-zero | refused, but the token stayed in the runner's frame: `answer`, and the child's `_fileobj2output` | `DIAG_BROKER_REFUSED`, keeping nothing |
| answers past the 64 KiB bound | the same, and refused as `DIAG_BROKER_MALFORMED`, like an answer of the wrong shape | `DIAG_BROKER_OVERSIZE`, a new sentence, keeping nothing |
| times out, with its output open or closed | the refusal chained the `TimeoutExpired`, whose `output`, and whose frames inside `subprocess`, held the token | `DIAG_BROKER_TIMEOUT`, with no cause and no context |
| answers in bytes that are not UTF-8 | no refusal at all: a `UnicodeDecodeError` escaped every operation, holding the token in `object` | `DIAG_BROKER_MALFORMED` (or `DIAG_BROKER_TIMEOUT` if it then hangs), keeping nothing |
| cannot be started | refused, chaining the `FileNotFoundError` | `DIAG_BROKER_UNREACHABLE`, with no cause and no context |
| writes without end (Copilot at `25788f91`) | read in full until the timeout, then refused as a timeout: the bound limited what was accepted, not what was read | refused with `DIAG_BROKER_OVERSIZE` as soon as it passes the bound, and killed |
| has a descendant holding its output open (Copilot at `b847ef3d` and `a603a032`) | at `e3eec6b1`, never refused: the reader waited on the pipe after the broker was killed (still waiting after 10 s, with a 0.5 s timeout). At `a603a032`, a descendant that left the group held the refusal for a 2 s grace and left a blocked reader thread, with its descriptor, behind. | refused at the timeout. The broker's process group is killed whole, and this process's ends of both pipes are closed, so no thread or descriptor is left behind. |
| never reads a credential larger than its pipe | at `a603a032`, the refusal waited past 10 s: the timeout began only once the credential was written | refused at the timeout, which now covers the credential's streaming too |
| its credential source fails mid-copy (the operator's input) | at `b847ef3d`, the broker was left running with its reader blocked, and the interpreter aborted at exit (`Fatal Python error: _enter_buffered_busy`) | the broker is reaped before the error goes on, and what it wrote is dropped. The error itself is unchanged (flag 9). |
| leaves a descendant in its group, holding none of its pipes, then exits non-zero or answers in no UTF-8 (Copilot at `a271d307`) | at `a271d307`, refused at once, but the descendant went on running, one more for each such call (measured: both still running a second later) | refused, and what is left of its group is killed while the broker is a zombie, so the group's id cannot have been reused |

At `788d764b` no refusal named its operation either. Two of the sentences said "so no token could be minted", whichever operation had failed.

**How, in the code:**
- The runner's work moves to `_run_broker`, which returns an answer or a sentence and raises no refusal. `subprocess_broker_runner` raises the refusal after it returns. So the refusal is outside every handler, in a frame that never held the child or its answer.
- One selector loop in the calling thread (`_answer_of`) streams the credential to the broker in `PIPE_BUF` writes and reads the answer, as `communicate` does on POSIX. There is no reader thread. One deadline covers both. At most one byte past `MAX_BROKER_ANSWER_BYTES` is read, straight into one list, so no other name holds it. The child's exit is awaited within what is left of the timeout.
- The broker starts in its own process group (`process_group=0`; the session and the terminal stay this process's). `_reap` kills that group, waits for the broker, and closes this process's ends of both pipes. Anything else that escapes the runner, such as a failing credential source, is reaped the same way before it goes on, and what the broker wrote is dropped.
- The answer is decoded as UTF-8, JSON's own encoding, where `communicate` used the locale's. An answer that is not UTF-8 is malformed.
- The four operations ask through one wrapper, `_broker_operation`, which replaces `mint`'s own. It raises every refusal again, afresh, with the operation named, after the answer has left its frame. So a runner that was injected is covered too, as are refused `intake`, `revoke` and `list` answers.
- `BrokerRefused` takes `operation=`, from `OPERATIONS` only and only beside a broker's sentence (`BROKER_DIAGNOSTICS`). Its message reads `broker <operation>: <sentence>`, for example `broker intake: the credential broker exited non-zero, and its answer is withheld by design`. `.diagnostic` is still the sentence alone, so every caller that compares it is unchanged.
- The broker's five sentences now name a failure class and no operation. `FIXED_DIAGNOSTICS` has twelve.
- Every refusal of the runner kills what is left of the broker's group: at the timeout, at the bound, after a non-zero exit, and after an answer that is not UTF-8 (`bbcb565e`, after Copilot at `a271d307`).
- The broker's exit is read with `os.waitid(…, WNOWAIT)` (`_exit_status_unreaped`, in `f8bc8aca`, after Copilot at `bbcb565e`).
  - This leaves the broker a zombie, so its pid, which is its group's id, cannot be reused before `_reap` signals the group.
  - `_reap` signals a group only while the broker is unreaped.
  - Its wait is bounded by `_REAP_GRACE_SECONDS`, 5 s, as `runtime/local_git_adapter` bounds its own. CPython reaps a broker that outlives that once its `Popen` is collected.
  - Where `os.waitid` does not exist, the exit is read with a reap, and no group is signalled.
- The runner's tail moves to `_settled`, which decodes the answer in place in the one list that holds it.

Files: `src/opendox/doxbench_binding.py`, `src/opendox/doxbench_provider.py` and `tests/test_model_provider_broker.py`, all in P3-B's row. The runner's five commits touch the last two only. `cli_model_binding.py`, `serve_workbench.py`, `validate.yml` and `pyproject.toml` are untouched.

## Merging #63, and then `main` (`63e534cb`)

`63e534cb` merges `main` `1130e996`, which is T080's squash (#63). This branch was stacked on #63's branch, so the merge takes `main` whole and replays none of #63's own commits. Its tree is the three-way merge of this branch and `main` over their old base, #63's `4948e6dd` (`git merge-tree --merge-base 4948e6d f8bc8ac 1130e99`), with three resolutions and nothing else:
- The test module's imports conflicted, each side adding one (`functools` here, `http.client` there). Both are kept.
- T080's route-neutral case declares an `http://` endpoint on another host for a broker's binding. Here a broker's minted token is held to a private route too, so the case declares that endpoint for the `none` binding alone.
- `ENDPOINT_SCHEME_REFUSED`'s comment names both credentials that `ENDPOINT_NOT_PRIVATE` covers on this branch.

The provider merged with no conflict:
- `_intake_reference`, where this branch reads an intake answer, holds a broker's reference to the record's rule (#63's M2).
- The shared transport maps `http.client.HTTPException` (#63's L4).

`main` also brought T078 (#61), T079 (#62), T070 (#67), T081 (#74), T085 (#71), T088 (#65) and T071 (#60). None of them changes this PR's code, and T081's changes to the test module are at cases this PR does not change.

Before that, `e1a6cb0f` merged #63's head `4948e6dd`, which carried `main` `047bb4fa` (phase 2's nine landings). Copilot's reviews of that merge and of the heads after it found four things, each fixed here (`da9e639a`, `bbcb565e` and `f8bc8aca`), and SonarCloud two more kinds (`f0d28a79` and `a271d307`).

**After the merge, two commits answer the holder and the adversarial review:**
- `b8323d01`, the adversarial review's L4 for the broker path. #63 maps an `http.client.HTTPException` in the shared transport to the unreachable sentence, and its case runs a broker's turn too, pinning that sentence only. Here every refusal of a request that carried a credential is raised afresh by `_call_provider`, so for a broker's turn the case also pins no cause, no context, and neither the key nor the token in any frame the refusal keeps.
- `e313437b`, the holder's Q2 (flag 13).

## Flags, for Brett and the holder

1. **One step past the fourth gap's letter.** The wrapper that keeps gap 4's own refusal clean covers every refusal of the mint answer. So a malformed answer beside a good token no longer keeps the token either. At `3f14bb96` it did: `mint.answer`, `mint.document`, `_answer_document.text` and `_answer_document.document`. Copilot's second review took the same rule to answers that raised something other than a refusal (see *How* above). This is T080's rule, that no frame a refusal keeps holds the raw credential, applied in the same function. The runner ruling then took it to all four operations. Narrowing it would need a separate cleanup for gap 4's refusal alone; say so if you would rather have that.
2. **Closed: the broker runner's own refusals.** This was the open question at `788d764b`. It is ruled (see *Ruled, for the runner*) and done (see *The broker runner*).
3. **A stored record can stop reading.** A broker binding already stored with plain `http://` to another host now refuses when the document is read. The entry point falls back to the harness declaration and says so. `model-binding edit` and `remove` read the whole document first, so they refuse too, and the operator edits the file by hand to an `https://` or loopback endpoint. T080's refusal of a key in a stored URL set the precedent (`test_a_stored_document_whose_endpoint_carries_a_key_does_not_read`).
4. **T080's scope pin is replaced.** `test_the_loopback_rule_is_the_built_in_resolvers_alone` pinned T080's scope by declaring a broker binding on two plain-`http://` routes. That half is gone. The case is now `test_a_none_binding_keeps_a_route_that_is_not_private`, over all nine routes. #63's body cites the old case among the broker path's pre-existing gaps, which this PR closes.
5. **A reading.** An opener installed process-wide with `urllib.request.install_opener` no longer serves a request that carries a credential. It already did not serve a built-in one. Nothing in openDox installs one.
6. **The operator reads new text.** `model-binding set-credential` and the console's intake route both show a broker refusal's `str()`. That now reads `broker <operation>: <sentence>`, and the broker's sentences are reworded. No test here or in openXdox-code compares the old text; every test compares the constants. `serve_workbench.py`'s intake comment still says a refusal carries "one of FIXED_DIAGNOSTICS and nothing else". The operation it now names comes from a closed vocabulary. The file is left alone: T084 owns it, and #59, which edited it, has landed.
7. **A twelfth sentence.** An answer past the bound had `DIAG_BROKER_MALFORMED`, and now has `DIAG_BROKER_OVERSIZE`, so the refusal names that class. The provider's own bound stays on `DIAG_PROVIDER_MALFORMED`. Say so if you would rather keep eleven, and the old class.
8. **Two cases past the ruling's three.**
   - An answer that is not UTF-8 was not a refusal at all at `788d764b`. It escaped as a `UnicodeDecodeError` holding the token. It is the same runner and the same rule, so it is refused here too.
   - Copilot's review at `25788f91` found the bound was checked only after the whole answer was read. The bound now limits the read itself.
9. **Not changed: the input side of `intake`.** This was measured at `25788f91` and again at `b847ef3d`. Since `e3eec6b1` the broker is reaped first, and the error is otherwise unchanged. When the credential's own source fails while it is copied to the broker, the error escapes the runner and is not refused:
   - A lone surrogate in the credential escapes as a `UnicodeEncodeError` whose `object` holds the credential. `sys.stdin` can yield one under `surrogateescape`. The stand-in broker still received, and stored, the credential without that character.
   - A source that fails to decode mid-copy escapes as a `UnicodeDecodeError`.

   This is the operator's input, not the broker's output, so it is outside the ruling. Should a follow-up refuse it?
10. **The runner is POSIX-only now.** The selector loop reads and writes pipes, which Windows' selectors cannot watch, and the process group needs `os.killpg`. On a platform without `killpg` the broker alone is killed. Nothing in this repository runs on Windows, and CI is Linux. Say so if the runner must also run there.
11. **Ruled: the operator's input stays without a time limit.** Copilot at `e75900ff` said a credential source whose `read()` blocks (`sys.stdin` in the CLI, the request body at the console) holds the loop, and the broker, past the deadline. That is an operator or a client still sending the credential, not a broker that misbehaves. Brett Heap ruled on 2026-09-30, openxFactory#656 comment `5916000030`, item 5: *"Leave unbounded (Recommended)"*. Operator input is outside the broker-runner ruling. No code changed for it.
12. **The nested provider answer is fixed here, for every path, and the holder decided it stays here** (2026-10-02). #63 and #64 land back to back after T063, so the window is short, and the fix changes the broker path's failure too, which T080's ruling leaves to this PR. `da9e639a` changes the parse that #63's built-in path and `none` share. Measured at #63's head `4948e6dd`, all three paths escape with a `RecursionError`, and so does `main`'s broker path at `047bb4fa`. #63's body says so.
13. **Every refusal kills what is left of the group, a refused answer included** (the holder's answer, 2026-10-02). Measured at `f8bc8aca`, a broker that left a helper in its group and exited 0 with an undeclared answer was refused as `DIAG_BROKER_MALFORMED`, and the helper was still running a second later. Since `e313437b`, the subprocess runner reads each operation's answer while the broker is a zombie, its exit read with `WNOWAIT` (`_settled`, given `read`).
    - A refusal of the answer kills the group, and then reaps the broker.
    - A successful answer reaps the broker alone and leaves the group, since a broker may leave a helper running on purpose.
    - `_broker_operation` hands its reader to the subprocess runner, and raises a refusal afresh, naming the operation, as before. A runner injected in its place, such as a test's, is given the argv alone, as before.
14. **SonarCloud's S3776 on `_answer_of` is known, and stays** (the holder's answer, 2026-10-02: no refactor in this PR). The runner's selector loop has a cognitive complexity of 28, where 15 is allowed. It was 31 before `f8bc8aca` moved its tail into `_settled`. The quality gate passes, and the runner's mutants pin the code. `e313437b` only passes `read` through it, and its complexity is unchanged.

For the holder, downstream:
- No downstream file changes. openXdox-code's `tests/test_doxchat_model_intake.py` builds its bindings on `https://` only, and asserts only that an intake refusal has a reason, not its text.
- The console's intake route builds its binding inside its existing `BindingRefused` handler (`serve_workbench.py`, left alone). So a posted broker binding on plain `http://` to another host is refused there with the fixed sentence.
- At `e313437b`, the only other open openDox-code PR that touches these three files is T100 (#82), stacked on this branch at `63e534cb`. A trial merge of this head with #82's `1ef4c71d` conflicts in one place, `_broker_operation`'s docstring, where each side added a paragraph, and #82's trust check, which goes before the argv is built. Both are kept by the obvious resolution.
- T081 (#74) has landed, and this branch carries it through `main` `1130e996`.

## The tests

### The four gaps' cases fail at `3f14bb96`

Each gap's cases ran with `3f14bb96`'s `src` (the base, whose broker path is `main`'s) and this branch's test module. They fail there on the behaviour itself, never on a missing name: `DID NOT RAISE`, the wrong sentence, a cause or a context set, a frame holding the token, or an `OverflowError` or `RecursionError` escaping. The controls beside them pass there too.

| gap | its cases | at `3f14bb96` |
|---|---|---|
| 1 | a broker binding refused on nine routes, by the constructor and from a stored record; `mint` asking no broker for a binding forced past the record; the operator door refusing one and storing nothing | 11 fail. 15 controls pass: six private routes, and nine `none` routes. |
| 2 and 3 | five redirect codes declined over real sockets, with nothing heard elsewhere; a stand-in proxy hearing nothing; a real refused connection keeping no frame that holds the token; four refusals (unreachable, refused, malformed, expired twice) and a refused re-mint, each with no cause and no context | 12 fail. T080's refactored redirect and proxy cases (6) pass. |
| 4 | eight unpresentable tokens refused before any request, with the catalog unavailable and nothing recorded; one outside latin-1, over a real socket; an unpresentable token kept in no frame; eight malformed answers (a bad instant, an undeclared key, JSON cut short, an expiry past a float, `NaN`, `Infinity`, `-Infinity`, nesting past the recursion limit) keeping no frame, cause or context | 18 fail. 2 controls pass: every printable ASCII character but the space is presented unchanged, and the declared answer mints. |

The proxy cases, T080's included, now drop `urlopen`'s cached global opener first (`_an_environment_proxy`). `urlopen` reads the proxy environment only when it builds that opener. Without the reset, the broker's proxy case passed at `3f14bb96` whenever an earlier test had built it.

T080's redirect and proxy scaffolds are shared with the broker's cases (`_a_provider_that_redirects`, `_an_environment_proxy`). T080's two route lists are shared parametrize marks (`ON_A_PRIVATE_ROUTE`, `NOT_ON_A_PRIVATE_ROUTE`). T080's node ids do not change.

### The runner's cases fail at `788d764b`

Each stand-in broker writes a fake token and marks that it did, then misbehaves. Each case checks the mark, so a slow start cannot pass it vacuously. Then it checks three things, in this order: no cause and no context; nothing kept (no frame local, no attribute of one, and nothing in the refusal itself holds the token); and the sentence, the operation and the message. With `788d764b`'s `src` and this branch's test module, 43 of the 44 cases fail, on the behaviour:
- 10 hold the token in a frame;
- 10 let a `UnicodeDecodeError` escape;
- 10 chain a `TimeoutExpired`;
- 4 chain a `FileNotFoundError`;
- 1 reads until the timeout;
- 5 name no operation;
- 1 prints the old text;
- 2 are the two updated counts.

| the runner's cases | cases | at `788d764b` |
|---|---|---|
| the shared runner, called directly, for six misbehaviours: exits non-zero, answers past the bound, times out, closes its output and times out, answers in no UTF-8, and answers in no UTF-8 and times out | 6 | 6 fail |
| each of the four operations, through the real runner, for the same six | 24 | 24 fail |
| a broker that writes without end, refused at the bound well inside a 5 s timeout | 1 | fails. It is also the one case that fails at `25788f91` (5.1 s measured). |
| a broker that cannot be started, by the runner and by each operation | 4 | 4 fail |
| an injected runner's refusal, named by each operation | 4 | 4 fail |
| the operation vocabulary, and a provider's sentence refused an operation | 1 | fails |
| the operator's `set-credential` door, with a broker that wrote and exited non-zero | 1 | fails |
| updated: twelve sentences; the bound's own sentence, with an answer at the bound as its control | 2 | 2 fail |
| a guard that the cases ask every declared operation | 1 | passes |

Five later cases answer Copilot and a finding of this PR's own, each measured at the head it answers:
- a descendant holding the broker's output, in its group and out of it (2). Each is refused at the timeout, leaves no thread and no descriptor, and an in-group descendant is killed. Both hang at `e3eec6b1`. The one that left the group takes 2.5 s at `a603a032`.
- a broker that reads 5000 bytes of a 1 MB credential and stops is refused at its 0.5 s timeout. At `a603a032` it held the refusal past 10 s.
- a credential source that fails mid-copy, in a child interpreter, exits 1 with its own error. At `b847ef3d` it aborts, 3 of 3 runs.
- the same in this process, after the answer has been read: what escapes keeps nothing the broker wrote, and the broker is not left running. It found a local, `chunk`, holding the answer, which is removed. Its two mutants are killed.

### This round's cases

- **The adversarial review's L4, for a broker's turn** (`b8323d01`): #63's six `http.client.HTTPException` cases, run for a broker's turn, now pin a fresh raise with no cause, no context, and no key and no token in a kept frame. With #63's transport (`05cb1c70`), where the broker path chains its cause, all six fail on that cause.
- **A refused answer kills what is left of the group** (`e313437b`), by each operation. A broker starts a helper in its group, holding none of its pipes, and exits 0 with an answer of no declared kind that carries the token. The refusal names the operation and keeps nothing, the group is signalled once while the broker is a zombie, and the helper is killed. All four fail at `63e534cb`, with `[] == ['Z']`.
- **A successful answer leaves the group alone** (`e313437b`), by each operation, as a control. A broker starts a helper and then becomes the fake broker in the same process. No group is signalled, and the helper still runs. All four pass at `63e534cb` too.

### Copilot's rounds after the merge of `main`

- **A provider answer nested past the recursion limit** (Copilot at `e1a6cb0f`), once for each credential source: a broker's token, the built-in resolver's key, and `none`. All three fail at `e1a6cb0f`, with the `RecursionError`.
- **A refusal kills what is left of the group** (Copilot at `a271d307`). The broker leaves a descendant holding none of its pipes, then exits non-zero or answers in no UTF-8. Both cases fail at `a271d307`, with the descendant still running. Since `f8bc8aca`, each also checks that the group is signalled once, while the broker is a zombie. With `bbcb565e`'s provider that reads `[None]`.
- **Where the exit cannot be read unreaped, no group is signalled** (Copilot at `bbcb565e`). With `os.waitid` removed, no `killpg` comes after the reap. With `bbcb565e`'s provider, one did.
- **A refusal waits on a killed broker only so long** (Copilot at `bbcb565e`). The broker's `wait` behaves like that of a broker stuck in uninterruptible I/O. With `bbcb565e`'s provider the case fails: `an unbounded wait`.

**Forty-seven mutants**, each run against the whole module, and each killed. The first thirty-five were run at `e75900ff`, and the last twelve at the heads named:

| mutant | tests that fail |
|---|---|
| the declaration's rule back to the built-in resolver alone | 10: the nine broker routes, and the operator door |
| `mint`'s own route check removed | 1: `test_mint_asks_no_broker_for_a_token_on_a_route_that_is_not_private` |
| the rule asked of `none` too | 10: the nine `none` routes, and T080's operator-door case |
| the credential opener kept for the built-in resolver alone | 6: the five broker redirects, and the broker's proxy case |
| the redirect-declining handler dropped | 10: every redirect, on both paths |
| the proxy bypass removed | 2: the proxy case, on both paths |
| a refusal re-raised with its chain, whatever was presented | 5: three chain cases, and both refused-connection cases |
| the expiry answered inside its handler, as at the base | 2: expired twice, and the refused re-mint |
| the token's check back to non-blank text | 10: the eight tokens, the real socket, and the frame case |
| each operation's refusals raised where they happen (no wrapper) | 42 |
| the answer kept in the operation's frame | 9: the eight malformed answers, and the unpresentable token's frames |
| the operation's refusal raised inside its handler | 43 |
| the finite check on an expiry removed | 4: an expiry past a float, `NaN` and both infinities |
| an integer past a float's range left to `float()` | 1: an expiry past a float |
| a `RecursionError` from the JSON reader not caught | 1: the answer nested past the limit |
| runner: the non-zero exit's answer handed back beside the refusal | 1: the runner's exit case |
| runner: the non-zero exit refused where it is seen, as at `788d764b` | 1: the same |
| runner: past the bound refused as `DIAG_BROKER_MALFORMED`, as at `788d764b` | 7 |
| runner: past the bound refused where it is seen | 2 |
| runner: the bound's check removed, so the whole output is read | 7 |
| runner: a timeout on the exit refused inside its handler | 1: the runner's closed-output case |
| runner: a timeout in the loop refused where it is seen | 2: the runner's two open-output timeouts |
| runner: no deadline on the child's exit | 5: the closed-output case, by the runner and by each operation |
| runner: the credential written whole, blocking past the deadline | 1: the broker that never reads the credential |
| runner: the decode error not caught | 5: no UTF-8, by the runner and by each operation |
| runner: a broker that cannot be started refused inside the handler | 4 |
| runner: anything else escaping, the broker not reaped | 1: the failing source, in this process |
| runner: anything else escaping, what the broker wrote kept | 1: the same |
| runner: a descendant, the broker killed alone | 1: the descendant in its group |
| runner: a descendant, the broker given no group of its own | 1: the same |
| the refusal names no operation | 34 |
| an undeclared operation accepted | 1: the vocabulary case |
| an operation named beside a provider's sentence | 1: the same |
| the message does not name the operation | 30 |
| (control) the presentability test too strict | 2: the printable-ASCII cases, on both paths |
| the provider's parse without `RecursionError` (`da9e639a`, and again at `a271d307`) | 3: the nested provider answer, on every path |
| the provider's parse without `ValueError` (`a271d307`) | 1: `test_a_refusal_of_a_turn_that_presented_a_token_chains_nothing[malformed]`, with a `JSONDecodeError` |
| runner: no reap after a non-zero exit (`bbcb565e`, and again at `f8bc8aca`) | 1: its own group case |
| runner: no reap after an answer in no UTF-8 (`bbcb565e`) | 1: its own group case |
| runner: the broker reaped before its group is signalled (`f8bc8aca`) | 2: both group cases |
| runner: `waitid` without `WNOWAIT`, so reading the exit reaps (`f8bc8aca`) | 2: both group cases |
| runner: the group signalled after a reap (`f8bc8aca`) | 1: the case without `os.waitid` |
| runner: the reap's wait unbounded (`f8bc8aca`) | 1: the bounded-wait case |
| a refusal of a credentialed request re-raised with its cause (`b8323d01`) | 19, among them the six broker L4 cases |
| runner: a refused answer reaped without the group kill (`e313437b`) | 4: the refused answer, by each operation |
| runner: a successful answer kills the group (`e313437b`) | 4: the control, by each operation |
| runner: the in-place read unused (`e313437b`) | 4: the refused answer, by each operation |

## Review

Each inline thread is answered with evidence and resolved.

| Copilot at | verdict | threads | answered in |
|---|---|---|---|
| `af457e66` | Needs a closer look | the redirect case's docstring read as repeating 303 | `a2c838a0`, the docstring only |
| `a2c838a0` | Changes recommended | an expiry past a float's range escaped `mint`, and `NaN` and the infinities were taken | `788d764b`, which also closed the `RecursionError` of the same class |
| `788d764b` | Needs a closer look | none. Its summary names the stacked prerequisites, which are the draft flag at the top. | |
| `25788f91` | Changes recommended | the answer's bound limited what was accepted, not what was read (high); this description was stale (low) | `b847ef3d`, and this description |
| `b847ef3d` | Changes recommended | a descendant holding the broker's output made `_reap` wait forever; this description was stale (read before it was updated) | `a603a032`, and this description. `e3eec6b1` between them reaps a broker whose credential source fails. |
| `a603a032` | Changes recommended | a descendant that left the group left a blocked reader thread and its descriptor behind each refusal; that reader could add to the answer after it was dropped | `e75900ff` |
| `e75900ff` | Changes recommended | a credential source whose `read()` blocks holds the loop past the deadline | ruled: left unbounded (flag 11). The thread is answered with the ruling and resolved, and Copilot was asked again at `e75900ff`. |
| `e75900ff`, asked again | Needs a closer look | none. Its summary asks for a final human review of the subprocess rewrite, and names the stacked prerequisite, which is the draft flag at the top. | |
| `e1a6cb0f`, the merge of `main` | Changes recommended | a provider answer nested past the recursion limit escaped `_post_to_provider` as a `RecursionError`, its traceback keeping the request | `da9e639a` |
| `f0d28a79` | Needs a closer look | none | |
| `a271d307` | Needs a closer look | none, and a note it had missed before: a non-zero or non-UTF-8 refusal left the broker's descendants running | `bbcb565e` |
| `bbcb565e` | Changes recommended | the group was signalled after the broker was reaped, so a reused pid could name another group; the reap's wait was unbounded | `f8bc8aca` |
| `f8bc8aca` | Needs a closer look | none. Its summary asks for a final human review once the stacked prerequisites land. | |
| `f8bc8aca`, asked again through the reviewer API | Needs a closer look | this description was stale: it still named `e75900ff` as this head | this description |
| `63e534cb`, the merge of `main` `1130e996` | Needs a closer look | none. Its summary asks for a final human review of the subprocess rewrite, and names the stacked draft dependencies, which have now landed. | |
| `e313437b`, L4's broker cases and Q2 | Changes recommended | this description was stale: it still said only the runner's refusals kill the group, and its head, totals and mutant count stopped before this round (moderate) | this description, pushed after the review; reply `4173524883` |

SonarCloud's check passed at each head, and its quality gate is OK. Its API read 0 issues at `af457e66` and at `788d764b`. At `e1a6cb0f` it read five, all from the runner's commits of 2026-09-30: four S5778 in the tests, fixed in `f0d28a79`, and one S3776. The catch `da9e639a` touched drew an S5713, fixed in `a271d307`. At `f8bc8aca` one remains, the S3776 on `_answer_of`, and at this head it is the same one, known and kept (flag 14). Its analysis of this head is pending.

## The repository's own suite

Local runs of the whole suite, as CI runs it (`python -m pytest -q`), use a PostgreSQL 16 service for `tests_runtime` and `LANG=C.UTF-8`:

| tree | passed | skipped | failed |
|---|---|---|---|
| `main` `1130e996` (T080 landed) | 3475 | 11 | 0 |
| `63e534cb`, the merge of `main` | 3585 | 11 | 0 |
| this head `e313437b` | 3593 | 11 | 0 |

`main` `1130e996`'s row is #63's last head `05cb1c70`'s run, and its tree is `1130e996`'s, byte for byte. Before this round, #63's `4948e6dd` read 3206, `e1a6cb0f` 3309 and `f8bc8aca` 3316. Before phase 2 landed, #63's `3f14bb96` read 2630, `788d764b` (before the runner ruling) 2686, and `e75900ff` 2733.

The +118 cases are all in `tests/test_model_provider_broker.py`:
- 24 for gap 1 (26 added, and T080's two-route scope case removed);
- 12 for gaps 2 and 3;
- 20 for gap 4, with the malformed answers;
- 47 for the runner;
- 3 for the nested provider answer, and 4 for the runner's later rounds: the two group cases, the case without `os.waitid`, and the bounded wait;
- 8 for the holder's Q2: the refused answer and its control, by each operation.

None skips, and skipped stays at the pinned 11. F16.1's record block still passes (`dialect and model declared; a raw key is refused in a field and in the URL`), and `tests/test_provider_boundary.py` passes (24).

CI's `validate` at this head (run `37129092647`) reads `selected=3735 passed=3724 skipped=11 failures=0 errors=0`, against the pins 2476, 2465 and exactly 11. That run checked out GitHub's merge of this head into `main` `5e7ab003`, where T072 (#69) landed after this round's merge. T072 changes none of this PR's files, and the merge is clean, so CI's count includes T072's cases. The floors are not raised here: `validate.yml` was outside the draft-ahead scope, as it was for #61 to #63.

🤖 Generated with [Claude Code](https://claude.com/claude-code)


Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants