T100, 16.3a: a served repository's bindings are trusted per machine (plan 034) - #82
Merged
Merged
Conversation
`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>
…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>
…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>
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>
…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'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's review of openDox-code#63 at d240fd5 (high severity): _post_to_provider took the credential as a raw string. At main the provider-call frame held a MintedToken, whose repr redacts. A traceback keeps its frames, and an error reporter that records a frame's locals records them by their repr, so the raw string was one repr away from a log. - The credential now travels as _PresentedCredential, whose repr and str say nothing, on both paths. The minted token is wrapped at its two call sites and the built-in value as it is read. So every frame of this module that carries a credential to the provider holds only the wrapper, as it held a MintedToken at main. - A refusal of a request that carried a built-in credential is raised afresh, outside every handler, with no cause and no context. Measured with a refused connection: urllib's own frames (do_open.headers, _send_request.headers, send.data and others) hold the bearer in their locals, and a chained cause keeps those frames. - A value refused as unpresentable is deleted from the reading frame before the refusal is raised. It can still be most of a key. The broker path keeps its chained cause, and so keeps urllib's frames, as the 2026-09-28 ruling leaves that path. The PR body records that. Three new cases: over a real refused socket, for an unpresentable value, and for the broker path's frame. Each walks every frame the refusal keeps, through its causes and contexts. Three mutants were each killed: the wrapper's repr disclosing, the chained cause kept, and the unpresentable value kept. 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 review of openDox-code#63 at 1b0fb3f: the opener for a built-in credential still installed urllib's ProxyHandler, which reads the environment's proxies. Measured: with http_proxy set, a request addressed to 127.0.0.1 went to the proxy, credential header and all. A plain-http route is private only because it stays on this host, so the loopback ruling of 2026-09-28 is broken by any proxy. _open_without_redirects becomes _open_for_a_built_in_credential. It still declines every redirect, and a plain-http request now goes direct through ProxyHandler({}), whatever the environment names. An https:// request may still use the environment's proxy, because a proxy reaches it only by CONNECT and the credential stays inside TLS. The broker path keeps the default opener, as the ruling leaves that path. One new case over real sockets, with a stand-in proxy that hears nothing. With the bypass removed, the stand-in proxy answered the turn, so the mutant was 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>
…ures keep no context Two remarks in Copilot's overview of openDox-code#63 at 286655f. Its threads were none. - "Broker-returned reserved references can raise uncaught errors." T080 reserves the env: and keyring: forms for the built-in resolver. A broker whose intake answer carried a reference in one of them would make the next declaration of the record see two resolvers. The console's intake route replaces the reference outside the handler that catches a refused binding, so there it would be an uncaught error. hand_off_credential now refuses such an answer as malformed (DIAG_BROKER_MALFORMED), which both entry points already catch. serve_workbench.py is left alone. - "Keyring failures retain exception context." The resolver raised its keyring refusal inside the handler, "from None", which hides the backend's error but keeps it as __context__, together with the backend's frames. The refusal is now raised after the handler, with no context. One new case with two real broker scripts, and one assertion added to the failing-backend case. Two mutants 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>
…T080) A broker binding over plain http:// to any host but this one is now refused when it is declared, with the one ENDPOINT_NOT_PRIVATE sentence a built-in credential earns on the same route. `mint` asks the same predicate before it asks the broker for anything, as the built-in resolver does before it reads, so a binding forced past the record still mints nothing. Only the auth kind `none`, which presents no credential, keeps a route that is not private. This is the first of the four broker-path gaps openDox-code#63 listed for Brett. His word of 2026-09-29, given in-session on #63's closing question: "Yes, separate phase-3 draft (Recommended)". The T080 ruling it extends is recorded at openxFactory#656 comment 5880893901. ENDPOINT_NOT_PRIVATE now names both credentials. T080's two route lists become shared parametrize marks, so the broker cases reuse them and T080's node ids do not change. T080's case that pinned the old scope (a broker binding declared on these routes) keeps only its `none` half, over all nine. Tests: 17 broker cases and 9 `none` cases added, 2 removed. With T080's head 3f14bb9 as the source, the 11 gap cases fail (DID NOT RAISE) and the 15 controls pass. Arc: neutral-product-standalone-operability Lane: openxfactory-4 (openXfactory-4-openDox_extraction) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…hains nothing Gaps 2 and 3 of the four broker-path gaps openDox-code#63 listed for Brett, closed with T080's own rules for a built-in credential (his word of 2026-09-29, given in-session). One port method, `_call_provider`, now makes every provider request, on both paths. A request that carries a credential (a broker's minted token, or what the built-in resolver read) goes through `_open_with_a_credential`, which is T080's opener renamed. It declines every redirect, and over plain http:// it uses no proxy. Its refusal is raised afresh, with no cause and no context. The auth kind `none` presents nothing, so it keeps the default opener and its refusal's chain, 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. DIAG_PROVIDER_REDIRECTED and the redirect handler now name both credentials. Measured at T080's head 3f14bb9, over real sockets and a real broker child: - a POST answered 301, 302 or 303 reached the redirect's target as a GET that carried the token; - with http_proxy set, a request to 127.0.0.1 went to the proxy with it; - 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. Tests: 12 broker cases. With 3f14bb9 as the source all 12 fail, and T080's two refactored cases pass. T080's redirect and proxy cases now share their scaffolds with the broker's (`_a_provider_that_redirects`, `_an_environment_proxy`). The proxy scaffold drops urlopen's cached global opener, so the proxy environment is read the way a process started with it reads it. The ENDPOINT_NOT_PRIVATE sentence is rewrapped, with the same text. Arc: neutral-product-standalone-operability Lane: openxfactory-4 (openXfactory-4-openDox_extraction) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…keeps it Gap 4 of the four broker-path gaps openDox-code#63 listed for Brett, closed with T080's own rule for a built-in credential (his word of 2026-09-29, given in-session). `mint` now asks `_presentable`, the built-in resolver's own test, of the token: non-empty printable ASCII with no whitespace. Any other token is a malformed answer (DIAG_BROKER_MALFORMED). It is refused before the port holds it or any provider is contacted, so the catalog reads unavailable and no mint is recorded. At T080's head 3f14bb9: - a token outside latin-1 failed inside urllib as DIAG_PROVIDER_UNREACHABLE, which names the wrong party, with do_open.headers and putheader.values holding it; - one with a space or another non-ASCII character was sent as it was. The refusal keeps no frame that holds the token, as the built-in resolver's refusal of an unpresentable value keeps none. `mint` reads the answer through `_minted_token` and raises any refusal again, afresh, after the answer has left its frame. That covers every refusal of the mint answer, which goes one step past the fourth gap's letter: a malformed answer beside a good token also kept it at 3f14bb9 (mint.answer, mint.document, _answer_document.text and _answer_document.document). Not changed: the broker runner's own refusals (a non-zero exit, an answer past the bound, a timeout) still keep what a misbehaving broker wrote. The runner is shared by all four broker operations and sits outside the provider call the ruling names. The PR flags it for Brett. Tests: 15 cases. With 3f14bb9 as the source the 13 gap cases fail, and the 2 controls pass (every printable ASCII character but the space is presented unchanged; the declared answer mints). 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 two unpresentable-token literals outside ASCII are written as backslash-u escapes, as the rest of this module writes them, so the source adds no non-ASCII character. The gap-1 control and the operator door's case each gain a line saying what they hold. The `none` case says how T080 once pinned its scope there (two routes, with a broker binding). No test changes what it runs. 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 review of openDox-code#64 at af457e6 (thread 4133295457): the docstring's "301, 302 or 303" wrapped so that it read as repeating 303, and "a 307 or 308 read as the provider refusing" was hard to parse. It now says that at T080's head urllib followed only the 301, 302 and 303 codes, with a GET that still carried the token, and refused a 307 or a 308 with DIAG_PROVIDER_REFUSED, as the base probe measured. No test changes what it runs. 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 review of openDox-code#64 at a2c838a (thread 4133360345): `_parse_expires_at` let an integer past a float's range escape as an OverflowError, and took NaN and the infinities as an expiry. `mint` re-raises only a BrokerRefused afresh, so the OverflowError escaped with the answer still in its frames. That defeats this PR's rule that no refusal of a mint keeps the answer. A NaN or infinite expiry also minted a token that would never expire, or would always have expired. An expiry must now be a finite number, or an ISO-8601 instant as before. Anything else is a malformed answer (DIAG_BROKER_MALFORMED), which `mint` raises again holding nothing. The same measurement found one more escape of the same class: arrays nested past the recursion limit fit inside the 64 KiB answer bound (30,000 of them in 60 KB), and json.loads' RecursionError escaped `_answer_document`. That is now malformed too, for all four broker operations. Tests: 5 cases join the malformed-answer case (an expiry past a float, NaN, Infinity, -Infinity, and an answer nested past the limit), and each answer is held inside the runner's bound. With 3f14bb9 as the source all 8 of that case's params fail. Three mutants are killed: the finite check removed (4 fail), the overflow left to float() (1), and RecursionError not caught (1). Arc: neutral-product-standalone-operability Lane: openxfactory-4 (openXfactory-4-openDox_extraction) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Brett Heap's word of 2026-09-29 (openxFactory#656, comment 5901112350): "Yes, add to #64". When a broker misbehaves (it exits non-zero, answers past the size bound, or times out after writing), the shared runner's refusal carries no broker output, no cause and no context, for all four operations. The refusal names the operation and the failure class, and never the bytes. What was measured at 788d764, with a broker that wrote a fake token first: - exit non-zero, or past the bound: the token stayed in the runner's frame (answer, and the child's _fileobj2output). Past the bound read as MALFORMED. - timeout: the refusal chained the TimeoutExpired, whose output and whose frames inside subprocess held the token. - an answer that is not UTF-8: a UnicodeDecodeError escaped holding the token, and no refusal was raised at all. - no refusal named its operation, and two of the sentences said "so no token could be minted" for intake, revoke and list too. The runner's work moves to _run_broker, which returns an answer or a sentence and raises no refusal of its own. subprocess_broker_runner raises the refusal after that call returns, so it is outside every handler and in a frame that never held the child. A decode error is caught, and so is a decode error while a refused child is reaped. The four operations now ask through one wrapper, _broker_operation. It raises each refusal again, afresh, with the operation named, so an injected runner is covered too. BrokerRefused takes operation=, but only from OPERATIONS and only beside a broker's sentence (BROKER_DIAGNOSTICS). Its message reads "broker <operation>: <sentence>", and .diagnostic is unchanged. The broker's sentences no longer name an operation. An answer past the bound has its own sentence, DIAG_BROKER_OVERSIZE, so there are twelve fixed diagnostics. Tests: 36 new cases. They cover the shared runner for five misbehaviours, each of the four operations for each misbehaviour, a broker that cannot be started, an injected runner, the operation vocabulary, and the operator's set-credential door. With 788d764 as the source, 37 cases fail: the 36 new ones except the one guard, plus the two tests that were updated. 14 new mutants are killed, and all 16 earlier ones are still 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's review of #64 at 25788f9 (high): communicate() read all of a broker's output before MAX_BROKER_ANSWER_BYTES was checked. So the bound limited nothing in memory, and a broker that wrote without end was read until the timeout and refused as a timeout. _run_broker now reads the answer on a reader thread, at most one byte past the bound. A broker that writes that byte is refused with DIAG_BROKER_OVERSIZE and killed at once. The thread starts before the credential is written, so the answer drains while intake's stdin is streamed. The runner then waits for the child's exit within what is left of the timeout. The answer is decoded as UTF-8 (JSON's encoding; the locale's before), and an answer that is not UTF-8 is malformed. Every refusal is still raised by subprocess_broker_runner after _run_broker has returned, so it keeps no cause, no context and nothing the broker wrote. Tests: a broker that writes without end is refused at the bound, well inside a 5 s timeout. At 25788f9 it read until the timeout (5.1 s measured), and that is the one case that fails there. A sixth misbehaviour, a broker that closes its output and then hangs, is refused as a timeout by the runner and by each operation. With 788d764 as the source, all six new cases fail too (43 of the 44 runner cases). The runner's mutants are rewritten for the new code. All 31 mutants are killed, among them the whole output read as at 25788f9, and no deadline on the child's exit. Arc: neutral-product-standalone-operability Lane: openxfactory-4 (openXfactory-4-openDox_extraction) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
b847ef3's reader is a daemon thread. When the credential's own source failed mid-copy (the operator's input, which the ruling does not reach), the error escaped with the child still running and the reader blocked on it, and the interpreter aborted at exit ("Fatal Python error: _enter_buffered_busy"), measured 3 of 3 times. _run_broker now reaps the child before any other exception goes on, and drops what the child wrote. The work after the reader starts moves to _answer_of, unchanged. The runner's tests also assert that no refusal keeps a frame holding the child. That kills three mutants that the reap had made equivalent: a refusal raised where it is seen, for the exit, the bound and the timeout. Tests: the failing source in a child interpreter exits 1 with its own UnicodeDecodeError and no fatal error. At b847ef3 it aborts. In this process, with a broker that wrote first, what escapes keeps nothing the broker wrote. All 33 mutants are 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>
…boundedly Copilot's review of #64 at b847ef3: a descendant that inherits the broker's standard output kept the pipe open after the broker was killed, so the reader, and the refusal, waited forever. That was measured at e3eec6b: a broker with such a descendant and a 0.5 s timeout was still not refused after 10 s. The broker now starts in its own process group (process_group=0; the session and terminal stay this process's). A refusal kills the whole group, and waits at most BROKER_REAP_SECONDS (2 s) for the reader. A descendant that left the group is left to the daemon reader, and the refusal does not wait on it. The reader reads the descriptor with os.read, so a reader still blocked at interpreter exit holds no buffered lock that finalization needs. Tests: a descendant in the group, refused within the timeout plus the grace, and one that left it (setsid), refused within the grace plus a margin. Both hang at e3eec6b. The failing-source case now also asserts that the broker is not left running. All 36 mutants are killed, among them the broker killed alone, no group of its own, and an unbounded wait for the reader. 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 review of #64 at a603a03: a descendant that left the broker's process group left the timed-out reader thread blocked, with its pipe descriptor, behind every refusal, and that reader could still add to what had been read after received was cleared. The reader thread is gone. One selector loop in the calling thread streams the credential to the broker's stdin in PIPE_BUF writes and reads the answer, as communicate does on POSIX, under one deadline that now covers the credential's streaming too. A refusal kills the broker's process group and closes this process's ends of both pipes. So a descendant that left the group costs no thread and no descriptor, and nothing can add to the answer once the loop stops. The answer is read straight into received, so no other local holds it. BROKER_REAP_SECONDS is gone. Measured at a603a03: - a broker whose descendant left the group was refused only after the 2 s reap grace (2.5 s with a 0.5 s timeout), and left its reader behind; - a broker that never read a 1 MB credential held the refusal past 10 s, because the timeout began only after the credential was written. Tests: the descendant case now also asserts that no thread and no descriptor is left behind, and that an in-group descendant is killed. A broker that reads 5000 bytes of a 1 MB credential is refused at its 0.5 s timeout. The failing-source case now lets the answer be read first. That exposed a local, chunk, holding the answer in the escaping traceback, and it is removed. All 35 mutants are 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>
openDox-code main moved from 2d11641 to 047bb4f with phase 2's nine landings, T050 to T058. None of them changes a file this branch changes, 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>
T078's branch now carries openDox-code main 047bb4f, phase 2's nine landings. Neither those landings nor T078's merge change a file this branch changes, 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>
T079's branch now carries T078's merge of openDox-code main 047bb4f, phase 2's nine landings. None of them changes a file this branch changes, 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>
… bindings never trusted, one list reading, dangling links refused (plan 034) Copilot at openDox-code#82, review at 7012cda, and the adversarial self-pass the holder asked for before this push. - r4174783197 (high). A printed trust command fell back to a JSON-quoted operand for a non-printable or non-ASCII value, and a shell still runs `$(...)` and backticks inside double quotes. At 7012cda a repository directory named with a newline, a terminal escape or a non-ASCII character plus `$(touch CANARY)` made CANARY when its printed command was pasted (red run). Now a printed command carries only an id the model catalog accepts (no shell or option parser acts on its characters) and paths quoted by shlex.quote where every character is printable. A non-printable root is named `.`, to be run from the repository's root, and `--bindings` is named where the binding was read from a document given by one. An id the catalog refuses gets no command. The `--` logic is gone: no printable id begins with `-`. - r4174783280. A binding whose id or label the catalog refuses was trusted by `trust`, and the next start failed in brokered_catalog. doxbench_trust.unservable_because now judges the very catalog the factory declares, before any policy is asked: verdict_for refuses such a binding (a host policy and an old store entry included), recorded_for refuses it, so `add`, `edit` and `trust` write nothing, and the start declares the refusing port over an empty catalog. - r4174783250. `list` reads the bindings document once, and its disclosure, trust lines and console lines are all of that reading. - r4174783301. A link this user owns that points at nothing, as the state directory or above it, is refused by name, and any OSError the store's tree raises that no check named is a TrustStoreRefused naming the store, in `record` and in `verdict`. - The trust-state walk (pending, approved, undeclared, unreadable declarations, a stale record). `list` gains a console line naming the one binding a console serving the repository declares, by the factory's own rule. The console's model approval says the binding is available only where the registered policy admits it, and otherwise APPROVED_UNTRUSTED_NOTICE. The fixed sentences that quote a command (the refused turn's, the rail's, the approval's) now name the required `--repo-root`, each parsed by the real parser in a test. The rail's JavaScript twin changes on its one line, so the census is unchanged. Every fix has a case that fails at 7012cda (32 failed) and a mutant. 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
…on (plan 034) (#85) Lane: openxfactory-4 (openXfactory-4-openDox_extraction) Arc: neutral-product-standalone-operability Plan 034 (`specs/034-opendox-standalone-operation/`): a **T102 follow-on**, under no task of its own. T102 (#81) landed as `0116293a`, so its After line is met. Claimed on openxFactory#656 in [`5973692022`](opensoft/openxFactory#656 (comment)). **DRAFT.** The holder posts READY, and the landers merge. **The finding, F1 of the holder's T096 dry run:** > Switching the rail's loaded document reads /workbench/thread. A standalone plane has no branch session, so it answers 403 `thread_capability_unavailable`, and Chromium logs one console error per switch, which is undeclared. **The ruling,** [`5973854291`](opensoft/openxFactory#656 (comment)) on openxFactory#656: > **Ruling (i): fix the product.** The rail wires its thread read only when `workbenchGate.session` exists, the same condition as its refusal transport. A composed host, which has a branch session, keeps reading threads as today. A separate openDox-code PR carries it (a T102 follow-on, under no task of its own). It lands before T087 and before T101's version bump, because it changes the shipped bundle. Option (ii), declaring the 403 in T096's oracle, was rejected. ## The change `src/opendox/web/app.js` gains `doxbenchThreadSeam(session, consoleTokenOf, injectedFetch)`, beside the thread loader it chooses. The doxBench bundle's `thread` is now `doxbenchThreadSeam(workbenchGate.session, () => caps?.console_token)`. That is the same `workbenchGate.session` whose presence picks the Save transport over `refusalTransport()`. - **With a session column** (a composed host), it is `createDoxBenchThreadLoader`, unchanged: the same route, query, header, and reading of a refusal. - **With none** (a standalone plane), it is a seam that answers `null` and sends nothing. **Why not an absent seam.** The ruling's words, "wires its thread read only when…", could be read as leaving `thread` off the bundle. That would regress the rail. `switchThread` (`views/doxbench-chat.js`) reads an absent `thread` as the pre-§11 rail, which returns early and keeps the previous document's transcript across a switch. Carrying one document's turns into another's next request is the defect the switch exists to close. So a plane with no session column still gets a seam, and it answers "no readable thread" itself. Its `null` is the answer the 403 produced, so the rail still adopts the empty transcript, without the request or its console error. No file other than `app.js` changes in the product. ## Tests **`tests/test_thread_read_by_session.py` (new), 7 cases:** 1. **The seam, under node, from `app.js`'s own text.** The module cannot be imported whole, because it boots the page at its last line. The harness takes the loader, the seam and the two constants they read, and runs them with an injected fetch. - No session column (`null` or `undefined`): a function that answers `null` and sends nothing. - A session column: one request to `/workbench/thread?repository=fixture&ref=main&tile_kind=cluster&tile_id=g1&document=b.md`, with the console-token header and the thread answered. A 403 still reads as `null`. 2. **The switch, in the real shell.** This reuses T102's shell harness (`mount`, `expand`, the DOM instrument) up to its first scenario. A document is loaded through its docs tile, then the rail's loaded-document selector (`select.doxchat-loaded`) is switched to every other entry once. - **Standalone** (the null columns, `editing by scope`): each switch asks the seam, and no switch sends a thread request. - **A composed host** (the contributed create and session columns, the gate live): each switch sends one thread request, with the usual query. 3. **The composition.** `app.js` composes the seam from `workbenchGate.session`, the bare loader is no longer wired directly, and the rail still adopts the empty transcript on a `null` answer. **`tests/test_workbench_edit_by_scope.py`:** the shell harness's `mount` takes an optional `thread`, and `_stage` can write extra modules beside the views. Both are additive, and the module's 44 cases are unchanged. **`tests/fixtures/web_boundary_census.yaml`:** the `app.js` row is re-measured (`1725 -> 1752`) with a provenance sentence, and the totals are re-derived. **Mutants**, at `c7b2635e`. Each is applied to `app.js` in the committed tree, and an anchor that is not found aborts the run. Then the new module, `test_doxbench_thread_switch.py` and `test_workbench_edit_by_scope.py` run. All four are killed, each by a named failure. | Mutant | Result | |---|---| | the guard dropped: every plane reads the thread (the F1 defect) | 2 failed | | no seam at all without a session column (the pre-§11 rail) | 3 failed | | the bare loader wired again, past the seam | 1 failed | | the guard inverted: a composed host loses its thread read | 4 failed | `c7b2635e` exists because the second mutant first ERRORED the seam cases (the harness called the `null`) rather than failing them. The harness now records a seam that is not a function, and the switch's counter hands no seam down as none. **The whole suite**, locally, with `LANG=C.UTF-8 python -m pytest -q` and the basetemp under `~/.local/state`, run under nohup and polled in the foreground: - at the head `c7b2635e`: `3776 passed, 177 skipped`, 0 failed; - at `9959f89a`: the same. The skip count does not move, and `validate.yml` is not edited. ## AT-R1, the browser half **At the head, with the T096 prep harness** (`run-pass.sh` and `t096_drive.py`, as for #81). The integration is the head itself, because main `0116293a` is its base. The venv was reinstalled from it (117 installed files compared, 0 differ). Pass a (`plain-documents`) and pass b (plain notes, no front matter) each passed all 15 checks, with `FAILED CHECKS: []`. - Step 7 passes: the no-model note shows, the turn is refused 403 `model_capability_unavailable`, and both editors are typed. - Zero `pageerror`, nothing undeclared, and no 5xx. The pill reads `editing by scope`. That harness does not switch the rail's document. So F1 is measured with the dry run's own browser half, which does. **F1, before and after, with the dry run's own browser half.** These are `run-at-r1.sh` and `at_r1_browser.py` from the holder's t096-dry evidence, byte-identical copies (sha256 `b4b654ba697a…` and `b719aa17101d…`), run with `HALVES=browser`. That browser half reads the console token from T104's private copy, so it ran on #84's head. Its driver switches `select.doxchat-loaded` through every held buffer, and does not declare the thread 403. | Commit (local, never pushed) | Pass | Rail switches | Thread reads | Thread 403s | Undeclared console errors | Verdict | |---|---|---|---|---|---|---| | `d4b99436`: #84's head, main without this fix | (a) | 1 | 1 | 1 | 1 | FAIL, "nothing undeclared" | | `d4b99436` | (b) | 4 | 4 | 4 | 4 | FAIL, "nothing undeclared" | | `78816a4e`: `d4b99436` plus this PR's four files, nothing else | (a) | 1 | 0 | 0 | 0 | PASS | | `78816a4e` | (b) | 4 | 0 | 0 | 0 | PASS | So, on a standalone plane, the browser logs 0 console errors on a switch. ## Not in this PR - **#82** (T100) touches `views/doxbench-chat.js`, and this PR does not. - **#84** (T104) merged into `78816a4e` with one conflict: the census's class-A total. It was re-derived. Whichever of the two lands second re-derives it the same way. Lane: openxfactory-4 (openXfactory-4-openDox_extraction) 🤖 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>
A plain merge: the branch holds none of #85's commits. One conflict, the web census's class-A total: main's 18486 (18459 + #85's 27 lines in app.js) against this branch's 18499 (18459 + T100's 40 lines in views/doxbench-chat.js). RE-DERIVED from the merged tree's rows, every one of which matches its file: 18526 = 18459 + 27 + 40. EXPECT_SKIPPED is untouched by #85 and stays main's 11. Arc: neutral-product-standalone-operability Lane: openxfactory-4 (openXfactory-4-openDox_extraction) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…034) Two of round 5's mutants survived at db77c4e, each a defence the cases did not yet reach: - "an id the catalog refuses is printed in a command": the refusals of a catalog-refused id carried REASON_UNSERVABLE, so `command_safe_id` was never the only guard. The hostile-id case now also asks the provider's own refusals (`require_admitted` with no verdict, and with a verdict for another binding) and `trust_command` itself: no command for such an id on any path. - "a binding trust cannot repair is told to trust": the over-long-label case checked the reason but not the absence of a command. Its refused turn, factory notice and `list` now print REMEDY_UNSERVABLE and no trust command, though the id itself is a valid one. Both mutants are killed with these cases (mutants-run-13). Arc: neutral-product-standalone-operability Lane: openxfactory-4 (openXfactory-4-openDox_extraction) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ever to trust (plan 034) Copilot at openDox-code#82, review at e05c475 (r4175203889). A binding whose id or label the model catalog refuses is never trusted: verdict_for refuses it and recorded_for never records it. But two fixed sentences still sent the operator to `trust`, a command that can never succeed for it: - the console's model approval answered APPROVED_UNTRUSTED_NOTICE for an approved binding past the catalog's bounds; - a served turn on such a binding (its refusing port lists nothing) was refused with UNTRUSTED_TURN_MESSAGE. Each now names the actual remedy and no command that trusts: APPROVED_UNSERVABLE_NOTICE, judged first under any policy and reading no store, and UNSERVABLE_TURN_MESSAGE, chosen by doxbench_trust.turn_message_for from the refusing port's verdict, which is held within the released failure envelope's 500-character message bound. REMEDY_UNSERVABLE, which the refusals, the notice and `list` print, names the verbs that correct a binding, `edit` (the same id) or `remove` then `add` (a new id), each of which records trust for what it writes. Every case fails first at e05c475 (7 failed), and each change has a mutant. Arc: neutral-product-standalone-operability Lane: openxfactory-4 (openXfactory-4-openDox_extraction) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A plain merge: the branch holds none of #76's commits. One conflict, in validate.yml's triple, and only in its comments: #76 re-pinned the floors (3977 / 3966) and wrote why EXPECT_SKIPPED holds at 11 for T082; T100's block wrote why it holds at 11 for T100. Both reasons are kept, T082's first, and T100's last sentence now names T082's floors in place of the superseded 3327 / 3316 reading. EXPECT_SKIPPED stays 11. The web census is untouched by #76 (no file under src/opendox/web/), so class A stays 18526, which tests/test_web_boundary.py re-derives from the rows. tests/test_chat_model_configuration.py merged cleanly. Arc: neutral-product-standalone-operability Lane: openxfactory-4 (openXfactory-4-openDox_extraction) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…val reads the trust seam once (plan 034) Copilot at openDox-code#82, review at 735d0c1. - r4177946237. `os.open`'s mode is filtered by the umask. Under 0777 the lock file was born 000: the first record went through the descriptor it had open, and every later record was refused ("cannot be opened"), so the store was unusable. Once its owner and mode are judged, the lock file is set to exactly 0600 through that descriptor, as `_write` already sets the store's temporary file. - r4177946288. The approval asked `is_registered()` and then `verdict_for()`, which reads the seam twice. A host that unregistered between the two reads would have had openDox's default installed in its place by an act that promised not to, and the default's store's answer given as the host's. `doxbench_trust.registered_verdict_for` reads the registration once, under the seam's lock (`_registered_now`), registers nothing, and gives that policy's verdict alone. `verdict_for` and it share one judging path (`_judged`), with the catalog check first. Both cases fail at 735d0c1, and each change has a mutant. Arc: neutral-product-standalone-operability Lane: openxfactory-4 (openXfactory-4-openDox_extraction) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…on a FIFO (plan 034) Copilot at openDox-code#82, review at 42c98f9 (r4178064601). A read-only open of a FIFO blocks until a writer comes. With a FIFO in the store's place, `list`, the start and every verdict waited forever, before the descriptor's type check could refuse it. The store and its lock file are now opened O_NONBLOCK, so a FIFO is refused by the type check on the descriptor that was opened, never by a second look at the path. A regular file reads the same either way. O_NONBLOCK joins the primitives the platform check names. The case fails at 42c98f9: the store's verdict waited on the FIFO (red run). The lock file's case passes there too, because Linux opens a FIFO read-write without waiting; it holds the refusal on platforms that wait. 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
marked this pull request as ready for review
October 4, 2026 15:45
Contributor
Author
|
READY at adb19f1 — Lane: openxfactory-4 (openXfactory-4-openDox_extraction) Holder: T100 (16.3a): a served repository's bindings are trusted per machine. Its After line is met, and the head merges main
|
There was a problem hiding this comment.
Sorry @brettheap, your pull request is larger than the review limit of 150,000 diff characters
Contributor
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.





Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Arc: neutral-product-standalone-operability
Plan 034 T100. #1144 box 16.3a: a served repository's bindings are trusted per machine. RULED by Brett Heap on 2026-10-02 in openxFactory#656 comment
5962785556, item 2: "Trust per machine (Recommended)". The text this conforms to is T007 batch M (openxFactory#1219, merged ascc775fea): box 16.3a and F16.1's batch M block. What openxFactory registers when it hosts openDox was RULED on5970369724; that registration is T094's (below).Phase 3. The base is
main. T100's After line (T080, openDox-code#64 and T084) is met: #64 landed as8e377823, and #77 (T084) ase49b17c3.04bcb68e.main8e377823withgit merge-tree --merge-base 04bcb68e, both parents recorded.maine49b17c3(T084, 4.3: the last deferred reaches through declared seams; consumer_reach retired (plan 034) #77),390e2c28(T103, every loopback route checks the Host (DNS rebinding) (plan 034) #80),0116293a(T102, the staging workbench offers the editors and the chat rail by scope (plan 034) #81),c4b55cc4(T102 follow-on: the chat rail reads a thread only with a branch session (plan 034) #85) andca9e1bd5(T082, 16.5: every other surface works with no model (plan 034) #76) with ordinary merge commits. The branch held none of T102, the staging workbench offers the editors and the chat rail by scope (plan 034) #81's, T102 follow-on: the chat rail reads a thread only with a branch session (plan 034) #85's or T082, 16.5: every other surface works with no model (plan 034) #76's commits.Nothing was rebased. The diff against
mainis T100's 12 files only.The defect
The entry points read the bindings document of the repository they SERVE (
declared_model_port_factoryoverbindings_path(checkout_root)). A binding written into that file by hand, or arriving with a clone, needed no approval. The adversarial review of 2026-10-02 showed two things:broker_argvof["/bin/sh", "-c", "id > $PWD/pwned"]ran on the first chat turn.env:reference sent an unrelated secret of the operator's to the file's endpoint as a bearer token (repo_binding_exfil.py).The first two cases of
tests/test_model_binding_trust.pyare those two findings, run as the review ran them. Onmain8e377823without this change, they fail for the defect's own reasons:The rule, and where it is enforced
A binding read from the served repository runs a broker, resolves a credential reference (
env:,keyring:or a broker's), or contacts its endpoint ONLY after the operator has trusted THAT EXACT binding on THIS machine. The auth kindnoneis included, because it still sends chat content to the endpoint the file chose.src/opendox/doxbench_trust.py(new, standard library only).sha256of the binding's canonical full record). The canonical record isas_record(), every field, sorted, compact, ASCII. So any edit untrusts the binding, and so does the same file under another root.model-binding-trust.json, in openDox's state directory (OPENDOX_STATE_DIR, through T072, 13.1: the bundled PostgreSQL server, the local install's own child (plan 034) #69'sconfig.state_dir). It holds no credential and is written owner-only, through an exclusive, no-follow temporary file and a rename.O_NONBLOCK), so a FIFO in either place is refused by the type check on the opened descriptor rather than waited on.MAX_TRUST_STORE_BYTES). A record that would outgrow it is refused before anything is replaced, so every trust already held stays held.model-binding-trust.lock, beside the store, across its read, its change and its replace. So two processes recording at once keep both trusts, and neither restores a form the other replaced. The lock file gets the same link and permission checks as the store, and is then set to exactly 0600, whatever the umask. Where no lock can be taken, the record is refused by name and nothing is written. Readers take no lock: the replace is atomic.OPENDOX_STATE_DIR, before anything is written. So is one that cannot be resolved, such as a link loop.unsupported_platform(), following T072, 13.1: the bundled PostgreSQL server, the local install's own child (plan 034) #69's form). The primitives areos.getuid,O_DIRECTORY,O_NOFOLLOW,O_NONBLOCK,fchmod,mkdirwithdir_fd, andfcntl.flock. So every verdict reads untrusted rather than raising.OSErrorthe store's tree raises that no check named: it becomes aTrustStoreRefusednaming the store and the system's short word for it, inrecordand inverdict.A binding the model catalog refuses is never trusted (
unservable_because). Its id or its label is one the catalog cannot list, so no turn could use it. The check builds the same catalog the factory declares (brokered_catalog), and it runs BEFORE any policy is asked:verdict_forrefuses such a binding, even under a host policy that trusts every binding or with a store entry recorded earlier. The start declares the refusing port over an empty catalog and never fails.recorded_forrefuses such a binding, soadd,editandtrustrecord nothing and write nothing.REMEDY_UNSERVABLE): correct the binding withopendox model-binding edit(to keep the id) orremovethenadd(to change the id). Each of those records trust for the binding it writes.Every answer a policy gives is held to the binding AND the root asked about.
verdict_forpasses on a verdict for this root that admits exactly this binding, or an untrusted one for exactly this record at this root. Anything else becomes an untrusted verdict for THIS binding at THIS root:So every refusal names the right binding, root and command, and the per-repository key holds against a host policy too.
recorded_forrefuses by name (TrustNotRecorded) a policy that declines to record, records another binding or root, or raises. It names what a policy raised, never its words. Only openDox's own store'sTrustStoreRefusedpasses through as written, and only from exactlyMachineTrust, not a host's subclass. A host policy's refusal of any class,BindingRefusedincluded, is named by its class alone.doxbench_install. The factory asksverdict_forabout the first approved binding (trust_gated_model_port_factory).UntrustedBindingPort. Its catalog lists the bindingavailable: false, every dispatch is refused by name, and the factory writes a notice on stderr naming the id and the command that trusts it.In depth,
doxbench_provider. These all refuse a binding that no verdict covers:mint,hand_off_credential,revoke,list_references), in_broker_operation, before the argv is built and before either runner's branch;dispatchandcatalog.A verdict covers only the id and digest it was given for.
cli_model_binding.addandeditrecord trust for the binding they write. They record it FIRST, so a store or a policy that refuses leaves nothing written and nothing printed as "trusted".trust <id>(positional, no--yes) prints what it trusts, then records trust for exactly that record: the broker argv it would run, the endpoint, the auth kind and the credential REFERENCE, never the credential. It resolves no reference, spawns nothing and contacts nothing.The command every refusal, notice and
listprints isopendox model-binding trust --repo-root ROOT [--bindings DOCUMENT] ID. It is printed only from operands a POSIX shell reads back exactly (trust_command):.,_and-, beginning with a letter or a digit, so no shell expands it and no option parser reads it as an option;shlex.quote, and only where every character is printable.A root that is not printable is never printed: the command names it
., to be run from that repository's root.--bindingsappears where the binding was read from a document given by one (list --bindings, or the factory'sbindings_path). Run as printed, the command trusts exactly the binding it names.set-credentialis refused before its broker spawns when the binding is untrusted, and it leaves the binding untrusted. It re-trusts a TRUSTED binding after rewriting its reference.listreads the bindings document ONCE, and derives everything it prints from that reading. It adds two lines per binding:listwas given another document, the line says the console does not read it.Every value a repository wrote is printed in a JSON string's form, by
list,trust, the refusals and the notice. A newline or\x1b[2Jin a field cannot forge or hide a line.serve_workbench.model_unavailablewith a FIXED sentence (UNTRUSTED_TURN_MESSAGE) saying how to trust it. The catalog's shape is closed, so the reason travels in the refusal, the notice andlist. Where the catalog cannot list the binding, the sentence isUNSERVABLE_TURN_MESSAGEinstead, which names the remedy and no command that trusts (turn_message_for). Both fit within the released failure envelope's 500-charactermessagebound.APPROVAL_NOTICE("becomes an available catalog entry") only where the registered trust policy admits the binding it approved. That is a governed host's approval, or a binding this machine trusts. Otherwise it answersAPPROVED_UNTRUSTED_NOTICE, a fixed sentence saying the binding is not yet trusted and how to trust it. A binding the catalog cannot list is checked first, under any policy and without reading a store, and answersAPPROVED_UNSERVABLE_NOTICE, which names the remedy and no command that trusts. Approval asks only a policy that is already registered, so where nothing is registered it registers nothing and reads no store. It reads the registration once, under the seam's lock (registered_verdict_for), so a host that unregisters meanwhile never has the default installed in its place.ideation/dashboard/model-declarations.yamlnames, and that broker belongs to no binding. So it asks the registered policy its OWN question,intake_verdict_for, which no binding's trust can answer.MachineTrust.intake_verdict) always says no, refused by name (intake_refused, reasonINTAKE_BROKER_UNTRUSTED, a fixed sentence). That happens before any byte of the body is read, and the body is drained unread.intake_verdict, an optional third callable on the seam. A policy without one admits no intake.The chat rail (
web/views/doxbench-chat.js). It has its own visible line,UNTRUSTED_BINDING_REMEDY(Python twindoxbench_trust.UNTRUSTED_BINDING_REMEDY), beside T081, 16.4: "no model configured" is a state, shown before any turn (plan 034) #74's no-model line."opendox model-binding list --repo-root <repository>", which shows whether each binding is trusted, and"opendox model-binding trust --repo-root <repository> <id>". It also says that a binding already trusted, which a provider's refusal also leaves unavailable, is unavailable for the reason the console printed when its provider refused.--repo-rootis in every quoted command.--repo-rootis required by everymodel-bindingverb. Each command a fixed sentence quotes (the refused turn's, the rail's and the approval's) is parsed by the real parser in a test, with its placeholders filled in.test_doxbench_privacy.pybans that word from both chat modules.Bindings stay committable. The bindings document is unchanged, and no trust is ever read from it.
Whose rule: the neutral default, and the seam
doxbench_trustis a policy seam (register/register_default/current/policy/unregister), with the same window rule asprojection_seams: the default is replaceable until it is read, a host over a host is refused, and the same registration again is a no-op. A policy carriesverdictandrecord, and optionallyintake_verdict. openDox's strict per-machine store is the NEUTRAL default.Why the default is registered lazily, which departs from R1Q10 (a)'s entry-point registration (accepted by the holder): the CONSUMERS register it, the first time one asks and only where nothing is registered yet. Those consumers are
declared_model_port_factory, themodel-bindingverbs and the intake hand-off. The console's approval is not one of them: it asks only a policy that is already registered. So this PR adds no hunk tocli.pyorserve.py, which stay out of the phase-3 single-writer order. It also fails closed: a bare process is held to the strict default too. A host's registration at process start wins.How this relates to 4.2's seam tests. Each seam module's tests enumerate and test that module's own seams:
tests/test_projection_seams.py,tests/test_doxbench_seams.py(the doxBench validators and rail), and #77'stests/test_column_seams.py(SEAMS = (cs.gate, cs.scope, cs.kickoff, cs.register)). No test enumerates every seam of the package, so none needs this one added.doxbench_trust.current()refuses by naming its own seam and the registration call (TrustPolicyNotRegistered), as 4.2's discipline asks, andtests/test_model_binding_trust.pyholds that.tests/test_consumer_reach.pyderives its record over the whole package and passes with the new module, which imports no sibling.What T094 must register
RULED by Brett Heap on openxFactory#656 comment
5970369724, "Governance approval (Recommended)": openxFactory registers its own policy,GovernedBindingTrust, as a sixthseams()entry with an undo. Under it:record()writes nothing.intake_verdictmust answer too, as the policy answers for a binding with no declaration. The console intake asks that question and no other, so without it the governed host's intake would be refused. With it, the intake stays as it is today, which the ruling keeps._GovernedHostPolicyintests/test_model_binding_trust.pyis that policy as a test-local stand-in. The tests that compose it in process:test_a_governed_host_policy_keeps_the_governed_flow[approved|undeclared]: the factory resolves the brokered port, and a turn reaches the listener, exactly as before this change.test_a_governed_host_policy_keeps_the_console_intake: the governed intake runs its broker.test_a_governed_host_policy_refuses_a_pending_declaration: the pending and unreadable cases, andrecorded_forrefusing a policy that declines.In the same checkout, the strict default refuses each of these until
trust, and always refuses the intake.Evidence
main8e377823test_the_edits_cover_every_field_of_the_record), 1 xfailed, 56 errors (ImportError: cannot import name 'doxbench_trust'). The two defect cases fail as quoted above.first-binding.42c98f9dO_NONBLOCK. The lock file's FIFO case passes there too, because Linux opens a FIFO read-write without waiting.735d0c14MachineTrustafter a host's teardown and answeredAPPROVAL_NOTICEfrom its store.e05c475c7012cda3CANARYwhenshran their printed command: a newline, a terminal escape and a non-ASCII character, each followed by$(touch CANARY). The factory raisedInvalidCatalogEntryErroron a trusted binding past the catalog's bounds, andrecordraisedFileNotFoundErrorthrough a link to nothing.735d0c14(the merge's tree, run just before it was committed); the trust module alone again atadb19f1eadb19f1etests+tests_runtime), in a venv installed by CI's own command (pip install --only-binary :all: -c constraints-cpython312-linux.txt -e ".[runtime,test]")adb19f1eOPENDOX_TEST_DATABASE_URL). #76 reads the same skips on its own local run.a6c2a844(the 10 alone), then the head (in the whole suite above)testextra, so it lacked thelocalextra'spixeltable-pgserver, and generate-and-open with--localrefused ("the local install's PostgreSQL server is not installed"). In the CI-command venv, all 10 pass, with a short TMPDIR (~/.local/state/t1) and with the long one (~/.local/state/t100-tmp) alike. The socket-path length was not the cause here.mutate_t100.py, one textual edit each)adb19f1emutants-run-17).tests/ideation-dashboard -m "not postgres", in a clone namedopenxFactory, TMPDIR holding the basetemp)main1f670bc3, openDox code leg atmain8e377823, then at T1007a04590dmodel_port_factoryand commits no bindings document, so its governed flow never reaches the gate before T094. The later rounds change only the store, how verdicts are held, and the rail's sentence.The mutants killed:
dispatchandcatalog; and a verdict admitting by id alone.addandeditrecording no trust, or writing before recording;set-credentialskipping the gate, or not re-trusting what it rewrote;trustrecording nothing or printing nothing;list, the disclosure and the refusals printing raw values;listsaying nothing of trust.listsays why;MachineTrustsubclass passing its refusal text;--it mutated: no id the catalog accepts begins with-, and no command is printed for one that does.listjudging a second reading;list's command, the factory's notice and the refusing port each omitting the document read;recordorverdictraw;listcommand or the rail'strustcommand dropping--repo-root.The F16.1 batch M cases each serve a fresh
git initwith a freshOPENDOX_STATE_DIR. They run over three bindings in turn:env:reference whose environment records every name read;keyring:reference whose stand-in backend records every lookup.The listener records every request. One test per case of the block.
set-credentialon anenv:orkeyring:binding is refused by the refusal it already had, "names no broker", which also comes before any read or spawn. Namingtrustthere would point at a command that cannot make the verb work.Review
Every Copilot finding was accepted and fixed with a case that failed first and its mutant, then answered on its thread and resolved:
1ef4c71d, fixed in7a04590d:r4173513738: a policy that declines to record;r4173513761: the lock across processes;r4173513782: the intake's own question;r4173513795: a verdict for another binding.7a04590d, fixed ine4b145d1:r4173876800: a platform without the store's primitives;r4173876823: an unresolvable state directory;r4173876849: what the rail line sayslistshows.8270dffc, fixed ine4b145d1:r4174310794: a verdict for another root;5402101086's "previously missed" item: a host policy's refusal text.cb691b18, fixed in24a1c25e:r4174632006: only the exactMachineTrustpasses its refusal;r4174632060: the printed trust command runs as printed;r4174632086: the write bound.7012cda3, fixed indb77c4ea:r4174783197: a printed command a shell reads back exactly;r4174783250:listreads its bindings once;r4174783280: a binding the catalog refuses is never trusted, and never fails the start;r4174783301: a link to nothing, and any unnamedOSError, refused by name.e05c475c, fixed ina6c2a844:r4175203889: a binding the catalog cannot list is told its remedy, at the approval and in a refused turn, and never to trust it.735d0c14, fixed in42c98f9d:r4177946237: the lock file is 0600 whatever the umask;r4177946288: the approval reads the trust seam once.42c98f9d, fixed inadb19f1e:r4178064601: the store and its lock file are opened without waiting on a FIFO.The adversarial self-pass, in the same commit. It covered two things:
list, plus each command a fixed sentence quotes. Hostile values in every interpolated operand were run throughsh,bashandzsh.It found three gaps, each now fixed with its case and mutant:
listdid not say which binding a console declares;model-bindingcommands without the required--repo-root.SonarCloud. The quality gate's one failure was
python:S5332: the trust disclosure spelled a plain-HTTP URL scheme, and now says "plain HTTP". The two functions over the cognitive-complexity bound (_unsafe_because,_refuse_an_unsafe_tree) are split into named parts, with the same rules. The gate passes from7a04590don.CI's triple
EXPECT_SKIPPEDis 11,main's own value. It moved to 14 for three strict-xfail cases, each waiting on a draft and naming it:It stepped back by one, with its reason in the workflow, as each draft reached this branch (
1ef4c71d,32646871,78d1e904). All three cases now run and pass. The floors are not moved.The web census's class-A total is re-derived from the merged tree at every merge of
main. It is 18526 at the head:main's 18486 plus this change's 40 lines inviews/doxbench-chat.js(1890 to 1930). Round 5 changed that file within one line, so its count holds, and #76 touched no web file. The floors are #76's re-pin (3977 / 3966, T082). T100 moves neither, since it only adds cases. The merge kept both reasons forEXPECT_SKIPPEDholding at 11, T082's and T100's. #84 (T104) also movesEXPECT_SKIPPEDand class A. Whichever of the two lands second re-derives both values from its own merged tree.#77, and other notes
5961364221item 1). So this file's intake cases use a stand-in host that registers a host gate atopendox.column_seams.gate, as T084, 4.3: the last deferred reaches through declared seams; consumer_reach retired (plan 034) #77's own tests do.model-binding addin a CHILD with a privateOPENDOX_STATE_DIR. The parent's factory reads its own store, so a turn there still reads the binding untrusted until that store trusts it.runtime/bundle.py's helpers take a different signature, raiseBundleRefusedwith the bundle's wording, and the module imports heavier modules. They are kept in step by rule, and the tests hold both.Gates
pyflakesis clean over the changed modules.tests/test_provider_boundary.pypasses. No module outsidedoxbench_providerspells its banned needles.🤖 Generated with Claude Code