ASVS packet D: validation and session (BACKLOG #1109, #1110, #1111, #1112, #1113, #1114, #1145, #1146, #1149, #1350) - #948
Conversation
… (BACKLOG #1149)
#1149 landed the session-terminate action binding: both JSON routes and both
console twins now take the reauth-only action factory, so a login-seeded window
no longer unlocks a terminate. Three shipped statements were written before that
gate existed and still describe its absence.
pages/account.py said revoking one's own sessions is "cookie-authenticated
self-service (no step-up)". That is now false. A stale absence claim sitting
beside a control is worse than silence: it reads as a licence to remove the gate
for consistency.
SECURITY.md's session-inventory section enumerated all four routes and never
mentioned the password re-proof gate, and asserted the current session is "only
revocable via Sign out". The second is a property of the console PAGE, not of the
endpoint: revoke_own_session checks ownership and nothing else, so on a first
deployment DELETE /me/sessions/{id} would accept the caller's own current session
id and revoke it.
That last sentence is derived rather than asserted. The new test drives the real
route; a mutation adding the current-session guard the prose implied makes the
route answer 404, which reds its 200 assertion. The sibling ownership test is the
control.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…alls (BACKLOG #1113) The engine answers 202 with a PendingApprovalResponse when [approvals].enabled holds an operation for a second approver. The shared client raised only at 400 and above, so the hold arrived as success and each of the three gated calls then mishandled it a different way. replay_dead_letters and reload_config parsed it bare, bypassing the module's own _decode. Both result models require every field and the hold body supplies none, so on a first deployment with the gate on a correctly-working hold would raise an unhandled pydantic ValidationError out of the client -- breaking the contract that file's docstring says _decode exists to preserve. purge_connection did use _decode and would have reported the hold as engine version skew. Three behaviours for one wire state was the underlying defect. All three now return `<result> | PendingApprovalResponse`, discriminated on the status code and decoded through _decode, so a malformed body of either shape is still an ApiError and never a bare ValidationError. That mirrors the engine's own route signatures and the console's existing isinstance narrowing. Callers: harness/monitor.py reports a hold as a status line rather than an error. probe.py's time_reload would have timed a held reload as a fast one, reading as the O(connections) cost getting cheaper; it returns None on a hold. Mutated both ways. Reverting one call to the bare model_validate reds three tests; reporting every 2xx as held reds exactly the three negative controls. The 2.3.5 cell is an owner-ruled permanent partial. This is the row's "fix them regardless of what the verdict does" half and claims nothing about the verdict. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…KLOG #1350)
ASVS 1.3.4. An attacker-influenced image/svg+xml OBX-5.2 attachment is accepted,
stored and served, so the prior not-applicable rationale -- that the engine
neither accepts nor renders SVG -- was false on the accepts conjunct.
The control was a four-token deny-list (html, xml, script, svg) plus a multipart
rule. Its completeness was an unprovable negative, and application/hta is the
counterexample: it carries none of those tokens, so it was declared verbatim.
_safe_attachment_content_type now returns the CANONICAL key from a ten-entry
exact-match allow-list, so no attacker-influenced byte reaches the Content-Type
header at all. Everything else is application/octet-stream. Completeness is now a
property of a short reviewable list.
The download-name extension came from mimetypes.guess_extension, which reads the
HOST registry on Windows -- measured here, guess_extension("application/hta")
returns ".hta". A served filename would then be a property of the machine the
engine happens to run on rather than of the product. The extension now comes from
the same table, defaulting to .bin, and app.py no longer imports mimetypes.
The allow-list decides what is DECLARED, never whether a file is served: an
unrecognized type downloads exactly as a refused one does. The CSP, the
unconditional Content-Disposition, nosniff and the middleware are untouched.
application/pdf STAYS on the list, decided in writing above the table. The clause
this control answers is about executing in the APPLICATION ORIGIN; PDF script runs
in the viewer against the document, so a downgrade narrows nothing while costing
the type hint on the commonest clinical attachment. What would narrow the
local-open threat is content scanning, which this route does not do.
Differential control: restoring the deny-list behind the new names reds 19 of 94.
Still unmeasured, and no comment claims otherwise: no browser was exercised. That
Content-Disposition suppresses inline rendering rests on specification alone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…r (BACKLOG #1109) parsing/validate.py took `profile: object | None = None` and its own docstring said it was "accepted but not yet enforced". config/models.py carried the twin, `Validation.profile: str | None`, whose comment promised an operator that naming a conformance profile would do something. Both are false affordances. A Handler author passing a profile would reasonably believe conformance was being checked, and nothing said otherwise at runtime. That is the shape CLAUDE.md section 11 forbids: a control-shaped thing that is not a control. `object | None` also accepts anything, so mypy strict could not help. Measured before removing: `profile=` reaches validate() ZERO times, against a positive control of 13 for the sibling `expected_version=` in the same run. All 41 `profile=` hits in the tree are harness load profiles and DR callbacks. The parameter was keyword-only, so no positional call could reach it either. The model field was reachable from no authoring path -- inbound() never passed it, so connections.toml could not set it -- and nothing read it. ENFORCE was ruled out on evidence, not by default: no conformance-profile type ships anywhere, no ADR covers it, HL7-VALIDATION.md never mentions it, and the feature is BACKLOG #78, re-scored twice to demand-gate. RAISE was ruled out too -- with zero callers the branch is dead by construction, and it would keep advertising a keyword that does nothing. Removing the model field changes no construction: Validation takes Pydantic's default extra="ignore", so Validation(profile=...) was silently ignored before and is silently ignored now. Also corrected the Validation docstring, which said `strict` runs "hl7apy profile validation". hl7apy does STRUCTURAL validation and takes no profile -- that sentence was the source of the confusion. The tolerant default is untouched. validation.strict still ships False. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Engine.reload swapped the live graph and then ran three steps that could raise:
the reference-set reconcile, the provenance fingerprint, and the cluster version
bump. The fingerprint was wrapped but caught only OSError. So on a first
deployment a raise in any of them would surface to the caller as a FAILED reload
while the new graph was already live -- and at the bump, possibly after other
nodes had been told to converge.
Each step was asked whether it could move BEFORE the swap, rather than assuming
none could:
reference sync NO. _make_reference_runner reads its specs through a lambda
closing over the live registry, so pre-swap it would
materialize the OLD graph's reference sets and a reload that
ADDS a set would leave it unarmed. Moving it changes what it
means.
fingerprint YES, and it moved. config_fingerprint_detail is a pure offline
fold over the directory bytes and never reads the live graph,
so computing it early is meaning-preserving. It also narrows
the gap between the bytes load_config read and the bytes this
reload is credited with, and puts the local import's
ImportError -- which `except OSError` never covered -- on the
honest side of the swap. Only the assignment stays after.
cluster bump NO. The bump TELLS other nodes to converge. Bumping first
would announce a config this node had not applied.
What cannot move is now reported instead of swallowed. reload_detail returns a
ReloadOutcome with three discriminable states: a raise means nothing was applied;
applied with no failures is clean; applied WITH named failures means the graph is
live and a step did not finish. Engine.reload keeps its exact signature and
Registry return, so no caller moves.
Both catches log and name the step, never `pass`. CancelledError derives from
BaseException, so cooperative cancellation still propagates.
Mutated three ways: reverting the reference-sync guard, reporting the swap itself
as degraded (the dishonest direction -- caught by the pre-swap negative control),
and moving the fingerprint back. Each reds a different test.
Follow-on NOT built, because a live peer holds api/app.py: POST /config/reload
still calls reload() and reports a degraded apply as plain success. It needs
reload_detail, a degraded flag on ReloadResult, and the step names in the audit
detail. The dual-control executor needs the same switch.
This does not move ASVS 2.3.3 -- the approvals executor replay-safety, the store
transaction boundary and DR recovery limbs are untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… and X12 (BACKLOG #1110)
The three dialing destinations defaulted a missing host to 127.0.0.1. A defaulted
peer is one the operator never chose: on a first deployment the delivery would
dial THIS machine, and where the same engine runs a listener on that port it would
SUCCEED into its own intake rather than failing -- surfacing as a misdelivered feed
rather than a connection error. All three now refuse at construction, matching
EmailDestination, DirectDestination and DicomScuDestination. build_check_registry
builds every deployed outbound, so the refusal fires at check and dry-run.
THE ROW'S OWN JUSTIFICATION IS FALSE AND IS CORRECTED HERE. #1110 says the inbound
half of this rule is enforced while the outbound half is not, and that asymmetry is
what makes it a defect. Both halves are enforced, 350 lines apart in the same file:
config/wiring.py:4461 already refuses an absent host for MLLP, TCP and X12 outbound,
through the shared build_outbound_connection core, so both the code-first surface
and connections.toml refuse. Measured through the TOML loader, which is the GUI's
own save target.
WHAT IS GENUINELY UNGUARDED AT EVERY LAYER IS A BLANK HOST, and nobody had named
it. Wiring tests `settings.get("host") is None`, so `host=""` passes it and the
connector kept the empty string. That is not loopback: measured on this machine,
getaddrinfo("") resolves to the host's own LAN interfaces (192.168.4.27,
192.168.9.1), so a blank host would dial the engine's own box OFF-LOOPBACK. X12 was
worse -- its str() turned a None host into the literal hostname "None".
The connector is the layer that owns the peer address, so it is the layer that must
not fabricate one. wiring.py's `is None` test is left alone deliberately: tightening
it to a falsy test would need to stay falsy rather than isinstance, because
host=env(...) puts a truthy EnvRef there, and that file is large and contended.
Destination-versus-source was established per site from the enclosing class and its
register_* call, not from line numbers. The `s.get("host") or "127.0.0.1"` lines in
MLLPSource, TcpSource and X12Source are untouched -- loopback is the correct default
for a LISTENER bind, and a source guard test pins that it still is.
docs/CONNECTIONS.md already documented host as required for all three, so the code
contradicted the shipped documentation.
Nothing here claims to move ASVS 2.2.3. The row is explicit that re-scoring on this
would be the same trap wearing a config-plane hat.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…1112) require_unified_store refuses a >1-engine-shard config on a non-server store (ADR 0063). It had exactly two call sites, both the supervisor path, which checks before spawning. The direct `serve --shard` entrypoint filtered the registry and called nothing, so two hand-run shard processes over one SQLite file would bypass the guard the supported path enforces. The check goes in the registry_filter closure because that is the only place on the serve path that sees the UNFILTERED registry, so it knows the whole engine-shard universe; it also fires on every reload, not just startup. A pre-flight load was rejected on measurement: load_config calls _exec_module unconditionally with no sys.modules reuse, so it would execute the operator's config modules TWICE on every sharded start. The ValueError is re-raised as WiringError because /config/reload catches that specifically for a 422, where a bare ValueError would be a 500. THIS NARROWS; IT DOES NOT CLOSE. Route (A) and a store-open single-writer lock are NOT substitutes -- neither subsumes the other, which is why this is the right arm to build rather than the cheap one: a lock cannot see a LONE `serve --shard a` against a >1-shard config. One writer, so nothing trips -- but filter_registry_for_shard arms ADR 0073 lane ownership whenever the config declares >1 shard, so lanes owned by shards nobody started would get no delivery consumer at all: ACKed at ingress, never delivered, nothing reporting it. This guard refuses that. this guard cannot see two plain `serve` processes on one SQLite file. There is no engine-shard universe to refuse. A WORSE UNGUARDED PATH, FOUND WHILE ENUMERATING AND NOT CLOSED HERE. Engine._owned_ lanes returns None when registry.shard_id is None, so an UNSHARDED second serve calls reset_stale_inflight(owned=None) -- "every inflight row at startup is this node's own crash residue". On a first deployment a second plain serve against the same --db would re-pend every in-flight row store-wide, including a live sibling's. Recorded in the source and the test module so a green run cannot imply closure. The enumeration ran with both controls in the same pass: the plain-serve store open was found (api/app.py:5967) and the supervisor correctly returned zero, so a 12-site result is a measurement rather than a pattern that matches one spelling. ONE PREMISE THIS ROW CARRIES IS FALSE AND IS CORRECTED. "No advisory-locking precedent exists in the engine" rests on a six-token grep that still returns zero -- but messagefoundry/tray/instance.py is a single-instance guard on a Local\ named mutex via ctypes CreateMutexW (ADR 0113), which none of those tokens match. It is Windows-only and outside the engine packages, so the store-open lock still needs a POSIX arm and a stale-lock policy for the six admin CLIs that legitimately open the store. It is ADR-shaped and belongs in its own item. Mutation-tested: deleting the call reds both positives; refusing every sharded start reds all three negative controls, which is what tells a guard from a wall. Nothing here claims ASVS 2.3.4 moves; its remaining limb is a server-DB rig. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…KLOG #1145) Two files in ide/src gave opposite answers to one question. statusBar.ts opens with a load-bearing block whose first item says its poll sends NO TOKEN, because "a bearer on a 15s timer would keep refreshing the engine's session idle clock and make its 30-minute idle timeout unreachable forever (CWE-613)". liveStatus.ts did exactly that, on a 5-to-10-second timer. The chain, read rather than assumed: GET /connections is gated by plain require(Permission.MONITORING_READ); require() resolves the bearer with identity_for_token(bearer_token(request)) at the activity=True default, which calls touch_session. So on a first deployment, with liveStatus.enabled turned on, the idle cap would not bind while a VS Code window stayed open -- only the 12-hour absolute cap would. Keeping the bearer safe is not available to a client. There are four identity_for_token call sites in api/: three take the activity=True default and one is a hardcoded activity=False WebSocket keepalive. No header, query parameter or route lets a CALLER ask for activity=False -- it is a per-route server-side decision. That surface is an engine-side change and is named, not built. So the bearer is dropped. Tokenlessness is now DATA -- LIVE_STATUS_PLAN carries authenticated: false and CI asserts it -- rather than a comment that a later edit can contradict, which is how the two files diverged in the first place. Accepted cost, stated where it bites: against an auth-enabled engine the rows stay undecorated, so decorations land only where /connections answers tokenless. The setting ships OFF by default, what is given up is a status word and a count on a tree row, and the full monitor remains the web console, which reads the same data under activity=False. A second defect fell out. The 401 branch called clearToken. With no bearer sent, a 401 means the route demands auth, not that the session died -- clearing on it would sign the operator out from a timer over a request their session took no part in. Removed; auth.withAuth still clears on a 401 that DID carry the token. Two documents asserted the defect was fine and are corrected. ADR 0091's shipped-status bullet called this "auth is passive"; passive there meant never prompts, and it was not passive about the idle clock. docs/SECURITY.md enumerated the PHI-scoped token holders in a closed list omitting the extension, which is the only holder putting the token in durable OS-managed storage that outlives the process -- now an "at least" form per SDS-3.6. SECURITY.md's claim that the idle clock is refreshed only by user-driven requests needed no edit: it was false of this poll before and is true after. The code change restores the documented claim. Unverified by execution, and both the comment and the ADR say so: the idle-clock claim rests on reading identity_for_token's signature and the route's dependency chain, not on running an engine. ide checks: tsc --noEmit clean, esbuild clean, 643 mocha tests passing. Both new controls mutation-tested. ASVS 7.1.3 does not move -- the Kerberos ticket-lifetime limb is untouched and the cell carries its signed acceptance to 2027-01-14. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rces (BACKLOG #1114) FileSource scanned every glob candidate with no cap, RemoteFileSource did the same over its listing, and DatabaseSource materialised the whole poll result set with fetchall. On a first deployment a long-unattended drop directory or table would be taken whole in one tick. All three now take at most 500 items per tick, SHIPPED ON, operator-overridable; 0 or None disables, and a negative value is refused at build (accepted, it would make the ceiling true on the first candidate of every tick, so the connection would report running and ingest nothing for ever). WHY THESE MAY DEFAULT ON WHEN THE NETWORK-LISTENER PACER MAY NOT. Deferral on a poll source is not a drop: a file the scan does not reach is still in the drop directory, a row the poll does not fetch is still in the table and unmarked, and the next tick takes it. Nothing is quarantined, errored or accepted-and-dropped, so the count-and-log invariant is untouched -- an item that was never received has no disposition to record. That is the whole difference from the MLLP pacer, which ships off by deliberate ruling. Stated in the code and the docs, because a later reader will otherwise "fix" the inconsistency the wrong way. THE NUMBER IS ANCHORED ON THE REPO'S OWN MEASUREMENTS, not picked. docs/THROUGHPUT records ~450 msg/s at intake and ~60 end-to-end; SYSTEM-REQUIREMENTS records ~97 sustained and ~107 burst as the highest ever measured from one engine process. At the shipped poll intervals 500 per tick is 500/s on File and 100/s on the other two, so the ceiling sits at or above every rate this engine has been measured achieving and cannot throttle a feed it could otherwise have kept up with. Ingesting faster than the engine drains delivers nothing sooner; it moves the backlog from the source system into the store. capture_max_rows=100 was deliberately NOT reused: it bounds a captured response body, a different axis, and 100 rows per 5-second poll is 20/s, below the engine's own measured sustained rate. The name shape and construction idiom were matched; the value was not. FAIR PROGRESS. Only an item the tick FINISHED with charges the budget -- handed to the pipeline, or quarantined. Every arm that leaves an item in place for a later retry (locked or vanished file, scan-hook malfunction, handler failure, and the unsafe-listing-name refusal) deliberately does not charge, because charging them would let one permanently stuck item that sorts early eat the whole ceiling every tick and starve the healthy items behind it. Two tests pin this and both red when those arms are made to charge. The ceiling bounds the INGEST, not the listing: _candidates still globs and sorts the whole directory, because taking the first N in order requires seeing them all. Two anchors in the row point at the CAPTURE path, not the poll path, and are corrected: the poll fetchall was at database.py:1181, and the capture row ceiling is at :899/:952. The defect itself reproduced exactly. Every new test proved load-bearing by mutation, each restored: removing the break reds the File and RemoteFile volume tests, deleting the untouched candidates reds the second-scan test alone, restoring fetchall reds two DB tests, and charging either transient arm reds its fairness test. Nothing here claims ASVS 2.4.1 moves. The row is explicit that a builder should not build to move it; the two acts that would are the owner's. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…KLOG #1146) ASVS 7.2.4 asks for a new session token on authentication INCLUDING re-authentication, with the current one terminated. Initial authentication minted fresh; re-authentication did not. Five sites stamped elevation onto the SAME token hash -- reauth, verify_mfa, confirm_mfa_enrollment, finish_webauthn_registration, finish_webauthn_assertion -- so on a first deployment a pre-MFA token captured before the second factor would be elevated in place to a fully authenticated session. The primitive existed and had zero production callers. It is not a toolkit wired for effect: rotate_session updates the row keyed on the old hash and returns its rowcount, so the old hash stops resolving in the same transaction that mints the new one and the verb's terminate limb comes free. THE ORDERING IS THE WHOLE CONTROL, and it lives in ONE place. Every session UPDATE except revoke_session and rotate_session is rowcount-blind, so a stamp issued after a rotation writes nothing and reports success. One private _elevated() is the sole caller of _rotate_session_token, so the rule is stated once rather than in nine route handlers. Both site-specific traps are handled: in reauth, _factor_binding_is_blocked resolves by the OLD token and fails closed, so it is decided before the rotation while the action grant is minted after against the new hash; in verify_mfa the whole three-write group lands first. Fails closed. A rotation on a vanished or revoked session returns session_lost, which the routes map to 401 rather than the 403 a wrong proof gets -- a correct password must not be reported as incorrect because the session died mid-ceremony. Both outcomes are audited; the failure row is the more interesting one. THE CONSOLE WAS THE SHARP SURFACE. /ui/reauth can rotate TWICE in one request, so the local token is rebound between them and the cookie is stamped through a helper taking the token as an argument, precisely because the variable moves mid-handler. The cookie is re-set on every post-elevation path INCLUDING the error exits -- a correct code followed by a wrong password rotates once, and without that the browser would be stranded on a dead cookie mid-ceremony. The apiclient now shares a token cell by reference instead of copying it into for_polling's clone, and that docstring's justification is rewritten rather than left asserting something rotation made false. A response with no usable token leaves the current one alone: a version skew must not become a sign-out. NOT DONE ON THE CHEAP SUBSET. A 2026-07-25 owner ruling names two JSON routes, and building precisely those would have looked like building to the owner's own words while leaving the three legs that turn an MFA-pending session into an MFA-satisfied one un-rotated -- and for POST /ui/mfa the passkey assertion is the ONLY leg, so the cell would have read "rotates on re-authentication" while missing the passkey path entirely. The pass rests on behaviour, not on the marker. The absence claim keys on the primitive's own call pattern, so wiring it for effect would flip the marker and change nothing. tests/test_session_rotation_wiring.py asserts a pre-elevation token stops authenticating at the moment of elevation, one case per site. The negative control was mutation-checked: making reauth rotate unconditionally reds test_a_failed_elevation_rotates_nothing[reauth] and nothing else, so it can fail. WEBSOCKET, reasoned and recorded in _elevated's docstring rather than left to a report: a rotation would drop an open /ws/stats socket at the next revalidation tick, since the keepalive re-validates the token captured at handshake. That is fail-closed and correct. app.js wires ws.onclose to resume the HTTP poll, which carries the new cookie, so completing MFA would cost the live push for the rest of that page's life -- a liveness regression, not correctness or data loss. That is why the bounded reconnect is deferred rather than built. Also repaired: the test file cited a BACKLOG number that cannot resolve from a public checkout. Replaced with the subject, per docs/LEDGER-GATE.md. No number invented. Deferred, named: IDE sign-in supersession, login supersession on the three console cookie-minting legs, written rationales for both bearer login legs, the console self-session revoke identifier, and the bounded WebSocket reconnect. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…LOG #1146) 02b33cb changed five AuthService methods to return Elevation and updated the call sites, but staged an explicit sixteen-file list. The change actually touched eighteen: tests/test_auth_hardening.py and tests/test_last_admin_guard.py were edited and not staged, so the branch shipped four red tests. Measured, not assumed: at 02b33cb those two files give 4 failed, 32 passed; with these edits, 36 passed. Both are the same mechanical adoption every other caller took. `_reauth` now returns `(response, token_to_use_next)` -- a successful re-auth re-keys the session, so a caller holding the old bearer 401s on its next request, and a refusal rotates nothing and hands the incoming token back. THE PROCESS DEFECT IS MINE AND IS WORTH NAMING. I staged the file list the builder REPORTED rather than the one `git status` showed. A report is a claim about the work; the tree is the work. The two agreed for every other row today and diverged here, silently, because a shorter list still commits cleanly and the tests I chose to run did not include either file. The check that would have caught it costs nothing: diff the reported paths against `git status --porcelain` before staging. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…#1111, #1113, #1114, #1145) Four review agents over the ten-commit diff. What they found that was worth fixing: **#1111 -- the reload outcome was vocabulary without a report.** Three reviewers independently found ReloadOutcome, ReloadStepFailure and reload_detail had ZERO production callers: POST /config/reload still called reload(), which projects the failures away, so a degraded apply was still answered as clean success -- the exact defect the commit named. The route and the dual-control executor now call reload_detail, ReloadResult carries `degraded` and `failures`, and the failed steps reach the AUDIT row as well as the response body. The audit half matters more than the body: the response goes to one caller once, and a reload whose reference sets never re-armed has to be findable afterwards. Mutation-checked -- dropping failed_steps from the audit reds the new test while the response assertions stay green. api/app.py was contended when #1111 landed and is free now. **#1114 -- the database poll fetched a message body to choose a log word.** The ceiling fetched `poll_max_rows + 1`, the usual probe idiom for "is there more". A row here can carry a body (`body_column`), so on every poll that probe row was marshalled out of the driver and discarded. Now it fetches exactly the ceiling and treats a full batch as the signal, which cannot tell "exactly N remained" from "more remain" -- so the message says so rather than naming a remainder. The negative-control test sat exactly ON the boundary and would have stopped being a control; it now runs three rows against a ceiling of four. **#1114 -- the ceiling multiplied a redundant stat.** Under `sort="mtime"` the min-age filter and the sort key were the same stat, read twice per candidate. That was always waste; the ceiling made it worse by turning one tick into many over a shrinking set. Decorate-sort-undecorate reads it once. The larger finding -- that the ceiling bounds the ingest and not the listing -- is real, is NOT fixed here, and is written into `_candidates` with what fixing it would cost, because it changes what the per-candidate screens mean for the budget. **#1145 -- the tokenless poll would 401 forever.** Dropping the bearer made that failure deterministic rather than transient: LIVE_STATUS_PLAN is a compile-time constant, so against an auth-enabled engine every tick 401s and waiting changes nothing. It now stands the timer down, gated on a TOKENLESS 401 specifically -- an authenticated 401 is a dead session, a different fact that must not silently stop the poller. applySettings() re-arms. **#1113 -- one of three elevation adopters bypassed the shared adopter.** Two reviewers flagged it. `confirm_mfa` assigned `self._token` directly, which was safe only by accident of `MfaConfirmResponse.token` being a required str elsewhere -- a property of a different file, not of the rule `_adopt_rotated` states. Also: `resolve_poll_ceiling` takes `Any` rather than `object`, dropping a `type: ignore` a later reader would have had to decide whether to trust. NOT APPLIED, and the disagreement is the reason. Two reviewers proposed extracting the three outbound-host refusals into a shared helper; a third measured that the identical idiom already appears at twelve pre-existing sites in transports/, so extracting only the three new ones would create a SECOND idiom rather than remove one. Skipped on that evidence. Likewise `_at_ceiling`, where the two log messages differ substantively (one is PHI-redacted), and the shared-test-fake extraction, which is real but is churn this branch should not carry. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…were wrong (BACKLOG #1109, #1110, #1111, #1112, #1113, #1114, #1145, #1146, #1149, #1350)
Ten rows, all still OPEN. Every one carries Closing-act: scorecard-rescore, which
lives in the separate vault clone and is the Lander's act by the owner ruling of
2026-09-05. A builder cannot close these and did not try. Landed code plus an open
row is the intended outcome here, not a shortfall.
TWO ROWS ASSERTED SOMETHING FALSE AT HEAD, and the corrections are the part worth
reading. Both were the JUSTIFICATION rather than the finding, which is the harder
kind to catch: the defect reproduces, a reviewer confirms it, and nothing
downstream ever tests the reasoning.
#1110 said the inbound outbound-host rule is enforced while the outbound half is
not, and called that asymmetry the defect. Both halves are enforced at
config/wiring.py:4461. What is genuinely unguarded at every layer is a BLANK
host: wiring tests `is None`, so host="" passes, and getaddrinfo("") resolves to
the machine's own LAN interfaces.
#1112 said no advisory-locking precedent exists in the engine, resting on a
six-token grep that still returns zero. tray/instance.py is a single-instance
guard on a Local\ named mutex via ctypes CreateMutexW, which none of those tokens
match.
#1149 needed no build at all -- its action binding had already shipped. What it
left behind were three statements that its own gate made false, including an
in-source docstring still saying session revocation has no step-up. That is the
same defect class in reverse: prose asserting the absence of a control that exists.
Two rows record a residual that is NOT closed, deliberately, so a green run cannot
imply closure: #1112's unsharded second-serve path, which would re-pend every
in-flight row store-wide, and #1114's listing cost, which the per-tick ceiling
multiplies rather than bounds.
#1114 and #1145 say in terms that they do not move their cells; #1113's cell is an
owner-ruled permanent partial and this was its "fix them regardless" half.
Anchors are on symbols, not line numbers. Several this packet touched had drifted
again since August, and #1109's own citations now point at lines its fix deleted.
The notes carry no branch name: this file is public, and the leak gate correctly
refused a worktree slug as an internal project name. The date is what a reader
needs; the pull request carries the provenance.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
One deferred quality fix, named so it does not look done. My Making It is not in this PR. I will add it as a further commit here when the file frees. If it does not free before this PR is otherwise ready, treat it as a known residual on #1146 rather than an oversight — the shipped code is correct, just more verbose at the eight call sites than it needs to be. Flagging it here because a reviewer reading |
| # Still best-effort: an unreadable bundle leaves provenance unknown rather than refusing | ||
| # an otherwise-applicable config. Recorded as a step failure so the caller is not told the | ||
| # reload was clean when GET /config/provenance will report nothing. | ||
| log.warning("config fingerprint before reload failed for %s: %s", path, exc) |
| # the failure surface is open-ended. Logged and reported, never swallowed. Per-set source | ||
| # failures are already isolated inside sync_all (last-good kept); what lands here is the | ||
| # arm/converge machinery, which leaves the sets stale but the graph correct. | ||
| log.warning("reference sync after reload failed for %s: %s", path, safe_exc(exc)) |
…146)
The last finding from this branch's /simplify pass, deferred until now only because
messagefoundry/auth/service.py was held by a sibling ASVS packet every time it was
reached for. That packet's author reviewed the finding, agreed, and deliberately
left it to land under this number.
`ok` was a second spelling of `token is not None`, and the invariant held across
all 19 constructions without exception. The cost was not the field. Because the
dataclass could represent a state the system never produces, mypy could not narrow
`token` from `ok`, so every consuming site wrote
if not elevation.ok or elevation.token is None:
where the second clause carried no information the first did not -- it existed to
re-derive the invariant in a form the checker accepts. As a property the
combination cannot be constructed, and one clause narrows `token` to `str` on the
happy branch for free.
Nine sites collapse. The three `outcome.ok` sites are LoginOutcome, a DIFFERENT
type that overlaps only on `ok`/`token`, and are untouched -- named here because a
blanket sweep on `.ok` would have taken them and quietly changed a login path.
`session_lost` is NOT redundant with `token is None` and stays a field: it
distinguishes a wrong proof from a session that vanished mid-ceremony, which two
routes branch on to pick 401 over 403. `recovery_codes` stays on the shared type
for the one site that populates it, because a second return channel would
reintroduce the shape divergence the class exists to prevent.
Mutation-checked rather than assumed: forcing the property to `return True` reds
four tests, including the revoked-session fail-closed case, so the derivation
carries weight rather than restating something already pinned elsewhere.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Resolved — the deferred fix above is now IN this PR (
The three
Mutation-checked: forcing the property to |
|
Full-suite result, replacing the "did not complete" note in the PR body. The single failure is environmental and is attributed, not waved away.
Evidence it is not mine:
One caveat I will not paper over. The run was launched just after The hosted legs — |
…LOG #1146) f9651bb made `ok` a derived property and rewrote 19 constructions. There were TWENTY. The one it missed is the ternary's else-branch in /ui/reauth's code leg, so on HEAD a re-auth submitted with an EMPTY code raises `Elevation.__init__() got an unexpected keyword argument 'ok'` -- established by execution, not by reading. That is a 500 on a real operator path. BOTH INSTRUMENTS I USED SHARED ONE BLIND SPOT, which is the part worth recording. I enumerated with `grep -rn 'Elevation(' messagefoundry/` and typechecked with `mypy messagefoundry`. Both are scoped to the ENGINE package; the missed site is in `messagefoundry_webconsole/`. Measured now: 19 constructions under `messagefoundry/`, 20 including the console. So the count I checked against was the count my own instrument defined, and it agreed with itself. The refactor changed a type consumed across a package boundary, so the population was never the engine package. `mypy messagefoundry messagefoundry_webconsole` reports it immediately -- 301 source files rather than 268. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…LOG #1146) Changing five AuthService methods to return `Elevation` changed the engine-side contract the web console builds its deps bundle from, so the seam digest moves to 3dc2d790c35d9368. ALL THREE FILES SHIP TOGETHER AND THAT IS NOT TIDINESS. The console is a separately built wheel holding exactly one accepted seam, so a partial update is a hard startup refusal (UiSeamMismatch) for a deploying site rather than a warning -- which is the point of the mechanism, and the reason the generator and the hand-set constant are two steps rather than one. Generated with `scripts/webconsole_seam_snapshot.py --write` for the engine constant and the golden; `SUPPORTED_ENGINE_SEAMS` set by hand to match, as the procedure the gate prints requires. The gate caught exactly what it exists for: a cross-package contract change made in the engine and not carried to the console. My #1146 commit changed the return types and did not move the seam, and nothing in the engine-scoped checks I ran would ever have said so. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rote for it main moved three commits under this branch. One of them (642225f, BACKLOG #1146) added tests/test_session_token_at_elevation_sites.py: six tests at the identity_for_token seam, FIVE of which assert the pre-rotation behaviour on purpose. Its own docstring says why -- "each names the inversion that lands with the rotation wiring, so wiring a site turns exactly one test red and the wiring commit flips one assertion rather than deleting a file." This branch IS that wiring commit, so this merge flips them. All five now assert that the pre-elevation token stops authenticating and the elevated state is readable on the new one. The sixth, which the file says must never invert, is untouched and still passes: the self-service password re-proof already satisfies the terminate limb because change_password revokes everything. Reading the elevated state off the NEW token is not cosmetic. Every session UPDATE but revoke_session/rotate_session is rowcount-blind, so a stamp written after the rotation writes nothing and reports success -- the state is only readable there if the stamps landed first. The reauth arm additionally asserts the ADR 0077 grant is reachable on the new token and NOT on the retired one, which pins both halves of that site's ordering trap. main's note on #1146 concluded the wiring was NOT built because no site is free -- rotating anywhere needs the new token to reach the client in the same response. That reasoning was correct and is why this branch built the delivery contract, the apiclient adoption and the console cookie re-set together rather than hunting for a cheapest site. The ledger note records that rather than dropping the paragraph. Two directory-account tests that arrived with main (BACKLOG #1144) needed the same adaptation: confirming an enrolment now rotates, so the satisfied state is read on the returned token. Two docstring conflicts in auth/service.py resolved by taking main's SEMANTICS with this branch's return types -- main relaxed both enrolment legs to accept a directory account, and the pre-merge text on this side still said they refuse one. mypy strict clean over 301 source files (engine AND console -- the narrower engine-only run is what let a console call site through earlier on this branch), ruff clean, 492 passed across every affected auth, rotation and doc suite. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Merged
This PR is that wiring commit, so the merge flips all five. The sixth — which the file says must never invert — is untouched and still passes. Reading the elevated state off the NEW token is the load-bearing part, not cosmetic. Every session UPDATE but I kept main's reasoning rather than dropping it. Its #1146 note concluded the wiring was NOT built because no site is free — rotating anywhere needs the new token to reach the client in the same response. That is correct, and it is exactly why this branch built the delivery contract, the apiclient adoption and the console cookie re-set together instead of hunting for a cheapest site. The ledger note records that supersession explicitly. Also carried: two directory-account tests from BACKLOG #1144 needed the same adaptation, and two Verification after the merge: mypy strict clean over 301 source files — engine and console, since the engine-only run is what let a console call site through earlier on this branch — ruff clean, and 492 passed across every affected auth, rotation and doc suite. Merge state: |
|
Correcting my own attribution of the full-suite failure above. It is now filed as BACKLOG #1467 with better hedging than I gave it, and my comment claimed more than the evidence supports. I wrote that the Two specific weakenings, both theirs and both correct:
What stands unchanged, because it was measured rather than inferred: the test passes in isolation and its whole file passes (11 tests); Two independent runs on different branches hit the same mechanism, which is stronger than either alone — but two observations of a symptom are not a diagnosis. #1467 exists on the federation packet's branch and is not yet on |
CI is red, diagnosed, and handed to a Regulator. I did not re-run.(Reposted — an earlier version of this comment was corrupted by shell command substitution and has been deleted. Same content, verified this time.) The failure. Run The test says what it is: a PROCESS LAUNCH that never returned. It is NOT an assertion failure and NOT evidence that the gate's behaviour changed: no gate logic ran. Negative evidence, so a Regulator does not have to re-derive it. Measured on
The 153 is my own count of non-blank, non-comment lines in Both failing tests spawn that untouched script through What this means for gating. The head must move regardless of CI. Why I did not re-run.
One measurement I could not complete, stated rather than guessed. I tried to compute this leg's recent base rate across other branches. The last-60-run window is currently 32 skipped, 21 in progress and 7 cancelled, with no completed failures in it — that is the pool jam, not evidence the flake is rare. Read it as a non-measurement, not a zero. I also did not add a recurrence note to #1304. That item has already replaced PR-blockage counting with a base rate carrying a denominator, and explicitly discarded its own earlier "blocked a prose PR twice" framing. Adding "a third PR" would walk it back to the framing its author dropped, and would be a bare count with no denominator. Asking for: a Regulator's verdict on this red. Nothing in the branch needs fixing for it. |
Follow-up: the harness tier passes locally on this branch, including both CI-failing testsPositive evidence to sit beside the negative evidence above. A local run of the same tier CI runs — Zero failures, exit 0. That figure alone would not have been evidence, so I checked the part that makes it one. A tier-wide pass only speaks to these two tests if these two tests were in it, and Both pass, all six parametrised cases, on the branch head that went red in CI. What this does and does not establish. It rules out a deterministic defect introduced by this branch — the tests exercise the untouched Still asking for a Regulator's verdict. I have not re-run CI. |
Six conflicts, in three shapes.
THE SEAM WAS REGENERATED, NOT PICKED. `_ui_seam.py`, `messagefoundry_webconsole/__init__.py`
and `tests/golden/webconsole_seam.snapshot` each carried a different digest per side.
Choosing either is wrong: the digest is derived from the merged API surface, which is
neither branch's. `scripts/webconsole_seam_snapshot.py --write` produced `58167cb8cc8875e7`,
and the console side is set to it in this same commit as the generator instructs. `--check`
returns clean.
`docs/CONNECTIONS.md` and `docs/PHI.md` -- both sides describe the SAME route and DIFFERENT
controls on it, and both are true of the merged code, so neither could simply win:
* This branch rewrites the mechanism from a deny-list to an ALLOW-LIST. The merged code
agrees -- `_safe_attachment_content_type` ends `return key if key in
_INERT_ATTACHMENT_TYPES else _DEFAULT_ATTACHMENT_MIME` -- so main's wording, which still
describes downgrading a list of browser-active subtypes, is stale. That direction is the
point of the change: a deny-list asks a reviewer to prove no further executable type
exists, and `application/hta` is the counterexample that says they cannot.
* Main independently added `frame-ancestors 'none'` to the attachment CSP, and the merged
`_ATTACHMENT_CSP` agrees. This branch predates it and its policy string is short by that
directive.
So each file keeps this branch's block with main's addition spliced in: the directive, the
"and no framing" parenthetical, and the reason it is named explicitly rather than left to
the header floor -- `frame-ancestors` takes no fallback from `default-src` (ASVS 3.4.6).
Verified after the splice that both files name the allow-list AND `frame-ancestors 'none'`.
`docs/BACKLOG.md` -- item #1146 was edited on BOTH sides, which my ledger resolver refuses
by design rather than guessing. Read by hand: both sides delete the same research narrative,
and this branch additionally replaces it with a BUILT record for 2026-09-06 that opens by
stating the superseded paragraph was sound when written. This branch's version therefore
subsumes main's deletion rather than contradicting it, and is taken whole. The item stays
OPEN; its closing act is a scorecard re-score, which no builder performs.
Checks run: `pytest tests/test_webconsole_seam_snapshot.py tests/test_session_rotation_wiring.py
tests/test_attachment_download_api.py` -- 114 passed; `webconsole_seam_snapshot.py --check`
clean; `mypy messagefoundry` -- no issues in 270 source files; `ruff check` clean;
`backlog_status_check.py` reports 686 items, each declaring exactly one status, with #1146
appearing once.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… surface Two conflicts, both resolved from the merged result rather than by picking a side. The seam digest is DERIVED from the engine's API surface, and both sides changed that surface, so both had computed a different digest. Neither value is right for the merge. Resolved by taking main's copies, then re-running scripts/webconsole_seam_snapshot.py --write over the merged tree and setting the console pin by hand as the generator instructs. All three now read 42fb8b34514ffc0f. 93 seam tests pass, 7 skipped. The ledger conflict was positional and cost this branch nothing: its side of the conflict was empty (it had only deleted a trailing blank line) while main appended new items, so main's side was taken whole. This branch's own 37-line amendment sits far above the conflict and is untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Lander diagnosis: this needs you, not a rerunVerdict: Failing jobs: test (ubuntu-latest, py3.14), test (windows-2022, py3.14), test (windows-2025, py3.14), web console tests (ubuntu-latest, py3.14), web console tests (windows-2022, py3.14), web console tests (windows-2025, py3.14), CI gate (aggregator, fails because the six legs above failed) Failing tests:
What to doThe author must fix it, in two edits on the branch. First, add an entry for ('ElevatedResponse', 'detail') to _RESPONSE_FIELD_COLUMN in tests/test_no_store_phi_coverage.py (the dict starts at line 125) -- either the store column it projects, or None with the stated reason it projects none. The neighbouring ('SimpleMessage', 'detail'): None, # a literal operation-result string is the closest precedent, since ElevatedResponse.detail looks like the same composed-in-the-route-body case. Second, classify messagefoundry/api/auth_routes.py in packaging/messagefoundry-webconsole/tests/test_ui_csp_canary.py -- add it to _EMITTERS if its headers belong inside the /ui degrade contract, or to _NOT_UI_EMITTERS (line 530) with a reason it cannot reach a console response. That test refuses to guess between the two on purpose, so it is a judgement call for the author, not a mechanical addition. How this was attributed to the branch rather than to mainBoth triggering constructs are absent on main, absent at the merge base, and present only on the PR. ElevatedResponse: Cache-Control: The PR did not update either registry. Confirming the negative direction: because the merge base equals main's tip, main's own tree contains neither construct, so both tests pass on main by construction -- there is no main-side defect to inherit. EvidenceRUN FRESHNESS. Run 34262760671 is the current and only CI run for the PR head. FAILING LEGS, not the aggregator. KNOWN CAUSES 1 AND 2 RULED OUT BY THEIR OWN SIGNATURES. In the downloaded job logs for 102190679399 (win2022) and 102190679458 (win2025): KNOWN CAUSE 3 RULED OUT BY ANCESTRY. FAILURE 1, VERBATIM (all three test legs, identical text): FAILURE 2, VERBATIM (all three web console legs): (truncated) Posted by the Lander while draining the merge queue. If any of this reads wrong, say so on the PR and I will re-check rather than defend it. |
Lander: this feature is already on main, built independently. Please read before resolving.Your conflict is not positional and it is not about naming. All 16 hunks across four files are one feature implemented twice. Both this branch and main implement BACKLOG #1114, the per-tick poll ceiling. Main's version landed as commit I merged 931, and I should say so plainly. I priced it beforehand and it named exactly one pull request it would break: this one. I merged it anyway, reasoning that 948 was already failing its own tests so nothing was lost. That was true about the collision and blind to what the collision was. I checked that the two branches conflicted; I did not check that they were the same feature. Why a quick resolution here would be worse than noneReading the conflict, the visible disagreement in most hunks is which knob name to use. Underneath that, each side carries a correction the other does not, and both sit inside hunks whose surface content is the naming argument:
So taking either side wholesale silently drops one of them, and dropping yours leaves no signal at all -- no test fails, nothing reports it. A resolver scanning for "which knob name won" reads straight past both. What I am not doingI am not resolving this. Two implementations of one feature is a decision about which design survives and which corrections carry forward, and that is yours. This is the one conflict of the four I re-examined tonight that came back genuinely authorial, and it survived an adversarial pass that was specifically briefed to argue it was mechanical. What would help whoever picks it upThe useful question is probably not "how do I merge these" but "is anything in this branch still wanted now that 931 has landed" -- and if so, whether the Posted by the Lander. If any of this reads wrong, say so on the PR and I will re-check rather than defend it. |
|
Lander: two blockers, and I am not taking either. Both are yours by design, and here they are precisely. 1. One test, and it is a PHI classification1 failed, 12,491 passed. That is the only assertion failing on the engine legs.
I can see the shape of the answer and I am deliberately not writing it. It is an auth-ceremony response, so The fix is one line in 2. Four conflicts, three of them in engine source26 behind What is not wrongNothing else. The remaining red checks are all downstream of the one assertion above. Suggested order: classify the field first, since it is one line and unblocks every engine leg; then rebase and take the four conflicts with the engine context you have and I do not. |
Two required legs were red on this branch for the same reason: a new response surface landed without the classification its guard requires. tests/test_no_store_phi_coverage.py -- ElevatedResponse.detail is bound to None (projects no store column). MEASURED from the call sites, not from the field name or the class docstring: the model is constructed at exactly two sites, both engine-authored string literals, and neither route reads the store. messagefoundry/api/auth_routes.py:393 detail="re-verified" (/me/reauth) messagefoundry/api/auth_routes.py:423 detail="verified" (/auth/mfa-verify) packaging/messagefoundry-webconsole/tests/test_ui_csp_canary.py -- auth_routes joins _EMITTERS rather than _NOT_UI_EMITTERS. It writes exactly one browser security header, Cache-Control at line 171, and app.py already contributes that name, so the emitted set gains no entry and no new degrade-contract bucket is needed. _NOT_UI_EMITTERS was rejected on evidence: an entry there must state that no browser ever reaches the response, and these routes sit on the same origin that serves /ui, so that reason would be false. The seventeen merge conflicts against main are deliberately untouched; the poll-ceiling collision needs an owner decision, recorded on the PR. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ck and DICOM bound
Both sides built a per-tick poll ceiling for the same item and the two are
mutually exclusive. This keeps the branch's and drops main's, because main's
permanently stalls a documented after_read="leave" share.
Measured by driving the real FileSource out of git archive extractions of each
ref, at main's shipped default of 1000:
999 stale files then one new arrival -> ingested (positive control)
1000 stale -> the new arrival is never ingested
1200 stale -> only 1000 ever drained across 40
ticks, every new arrival blocked,
the connection reporting running
The branch drains 1200 of 1200 and ingests the new one, at a ceiling of 500 and
at 1000. Mechanism: main's _candidates ends in _within_tick_ceiling(files),
while the leave-mode dedup skip runs later in _scan_once, so already-ingested
files spend the budget on every tick forever. The branch charges the budget on
disposal instead, so a dedup skip costs nothing.
How each conflicted path was resolved:
- transports/file.py and transports/remotefile.py take the branch's files.
Main's only non-ceiling change to either was the in-loop stop check, ported
here with its test, which now lives beside the ceiling break it is the
sibling of. Read with the ceiling disabled, so a low ceiling cannot end the
scan and pass it for the wrong reason; it fails on the two lines removed. The
min-age filter survives in both sort arms.
- config/wiring.py and docs/CONNECTIONS.md are resolved hunk by hunk, not whole
file. All 11 of their conflicts are the knob rename pair. Main's DICOM
association knob and its FHIR search docstring auto-merged and survive.
- tests/test_api_auth.py carries both contracts: the branch's rotating _reauth
with the token re-derived after each call, and main's id constants plus the
comment on why no malformed-id probe belongs on that route.
- api/_ui_seam.py, messagefoundry_webconsole/__init__.py and the golden seam
snapshot conflicted on a hash. Neither side's value describes the merged
surface, so the seam is regenerated to fe0cb20044ca984d.
tests/test_poll_and_association_intake_bounds.py asserted main's retired knob
and died at import, taking its DICOM association coverage down with it. It is
deleted, and that coverage plus the security-prose drift guards are carried to
tests/test_dicom_association_intake_bound.py. The branch's
tests/test_poll_source_tick_ceilings.py already covers the shipped knob on all
three poll sources, including the Database poll the deleted module never
reached.
docs/SECURITY.md's ingest row now describes a retired knob at a retired default
and still calls the Database poll uncovered. It is left unedited for its own
change; the PR body records it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts: # messagefoundry/api/_ui_seam.py # messagefoundry_webconsole/__init__.py # tests/golden/webconsole_seam.snapshot
ASVS packet D: validation and session. Ten rows, thirteen commits, every row stays OPEN — each carries
Closing-act: scorecard-rescore, which lives in the separate vault clone and is the Lander's act (owner ruling 2026-09-05). Landed code plus an open row is the intended outcome, not a shortfall.mimetypes.guess_extensionremovedprofileparameter deleted, and its twin inconfig/models.pyserve --shardreaches the unified-store guardThree ledger rows asserted something false, and that is the finding worth reading
Each was the justification rather than the conclusion — the defect reproduces, a reviewer confirms it, and nothing downstream ever tests the reasoning.
config/wiring.py:4461. What is genuinely unguarded at every layer is a blank host: wiring testsis None, sohost=""passes, andgetaddrinfo("")resolves to the machine's own LAN interfaces. On a first deployment that would dial the engine's own box off-loopback, and where it runs a listener on that port the delivery would succeed into its own intake.tray/instance.pyis a single-instance guard on aLocal\named mutex via ctypesCreateMutexW, which none of those tokens match.Residuals recorded and deliberately NOT closed
So a green run cannot imply closure:
Engine._owned_lanesreturnsNonewhen unsharded, so a second plainserveagainst the same--dbwould callreset_stale_inflight(owned=None)and re-pend every in-flight row store-wide, including a live sibling's. Worse than the path this PR guards, and not fixed here._candidateswith what fixing it would cost.Content-Disposition: attachmentsuppresses inline rendering still rests on specification alone, and no code comment claims otherwise./ws/statssocket does not survive a rotation. Fail-closed and correct; the client falls back to the HTTP poll, so it is a liveness regression rather than data loss. The bounded reconnect is deferred./simplifypassFour review agents over the ten-commit diff; the fixes are in
30b9ce389. The largest finding was that #1111 had shipped the vocabulary without the report —reload_detailhad zero production callers, soPOST /config/reloadstill answered a degraded apply as clean success. Now wired through the route, the dual-control executor,ReloadResult, and the audit row.One finding was skipped because the reviewers disagreed and the dissent was better evidenced. Two proposed extracting the three outbound-host refusals into a shared helper; a third measured that the identical idiom already appears at twelve pre-existing sites in
transports/, so extracting only the three new ones would create a second idiom rather than remove one.A defect of mine, disclosed
02b33cb5bstaged the file list the builder reported rather than the onegit statusshowed. The change touched eighteen files, not sixteen, and the branch briefly carried four red tests. Fixed in67c17f55b, with the measurement in its message. The check that would have caught it is free: diff the reported paths againstgit status --porcelainbefore staging.Merge hazards for the Lander
messagefoundry/auth/service.py—claude/asvs-auth-brief-f1aa97hasdelete_webauthn_credentialbeginning two lines after myfinish_webauthn_assertionends (2831 / 2833). Inside git's default context, so expect a conflict despite the functions being disjoint. Neither side changes the other's signatures; that branch's author has this in their PR body too.docs/CONNECTIONS.mdanddocs/PHI.md— simulated, not guessed:git merge-treeagainstclaude/asvs-frontend-brief-e87460reports CONFLICT on both, and clean auto-merge onapi/app.pyand its test. Both sides predicted that backwards. Resolution is take-both-sides: my deny-list-to-allow-list rewrite plus theirframe-ancestors 'none'literal, independent edits to one sentence. The sentence is the anchor, not the line — it is:898in my tree and:908in theirs, and neither number survives the merge.Checks
Run and green:
ruff checkandruff format --checkon every touched file;mypystrict (268 source files, plus 301 including the console);tsc --noEmitand 644 mocha tests foride/; the web-console suite (423 passed); and per-row suites — 216 across the auth and MFA files, 232 and 247 across the transport files, 198 across the apiclient and reload files, 96 attachment, 17 rotation, 16 poll-ceiling, 12 profile.Not complete at the time of writing: a full-tree
pytest. It is running and had reached 4 percent; earlier attempts died to CPU contention from the sibling ASVS sessions sharing this machine, and one produced an unattributable failure because the shared worktree held another builder's in-flight edits. I am not quoting a number I cannot attribute. The hosted legs —windows-service-smokein particular — were never visible from here and need reading on CI.🤖 Generated with Claude Code
Merged
origin/mainat0ad28370f, keeping this branch's poll ceilingBoth sides built a per-tick poll ceiling for #1114 and the two are mutually exclusive. Main's is dropped. It permanently stalls a documented
after_read="leave"share at exactly its ceiling.Measured by driving the real
FileSourceout ofgit archiveextractions of each ref, one_scan_onceper tick, at main's shipped default of 1000:runningPaired control at one scale, ceiling 50 over 60 stale files and 12 ticks, the implementation the only variable: main drained 50 and never saw the new file; the merged tree drained 60 and ingested it.
Mechanism: main's
_candidatesends in_within_tick_ceiling(files), while the leave-mode dedup skip runs later in_scan_once. Already-ingested files spend the budget on every tick, forever. This branch charges the budget on disposal, so a dedup skip costs nothing.How each of the 20 conflicts was resolved
transports/file.py,transports/remotefile.py(5 hunks) take this branch's files verbatim. Verified rather than assumed: main's only non-ceiling change to either was the two-line in-loop stop check, ported here with its test. The min-age filter survives in both sort arms.config/wiring.py(9) anddocs/CONNECTIONS.md(2), hunk by hunk. All 11 are the knob rename pair. Main's DICOM association knob (3 sites) and its #1243 FHIR search docstring auto-merged and survive; so does this branch's #1350 attachment MIME allow-list prose.tests/test_api_auth.py(1) carries both contracts: this branch's rotating_reauthwith the token re-derived after each call, plus main'sABSENT_USER_ID/AD_USER_IDconstants and the comment on why no malformed-id probe belongs on that route.api/_ui_seam.py,messagefoundry_webconsole/__init__.py,tests/golden/webconsole_seam.snapshot(3) conflicted on a hash. Neither side's value describes the merged surface, so the seam was regenerated withscripts/webconsole_seam_snapshot.py --writetofe0cb20044ca984dand the console constant set to match in the same commit.One test file deleted, its live coverage carried
tests/test_poll_and_association_intake_bounds.py(main's) asserted the retired knob and died at import, taking its DICOM association coverage down with it. Deleted. Its DICOM tests, its factory-reachability tests and its security-prose drift guards now live intests/test_dicom_association_intake_bound.py, ordered so the doc guards run before the[dicom]extra is required. This branch'stests/test_poll_source_tick_ceilings.pycovers the shipped knob on all three poll sources, including the Database poll the deleted module never reached.A shipped security-record defect, reported and deliberately NOT fixed here
docs/SECURITY.mdis not in the conflict set, so its ingest row auto-merges and carries main's wording. That row is now wrong in four ways, and only the first was known when this merge started:max_files_per_poll, a knob that no longer exists.poll_max_rows.docs/BACKLOG.md's #1114 landing record repeats defects 2 and 3 in the present tense.The poll-ceiling half of the drift guard that would have caught this is withdrawn rather than silently rewritten, with the reason in the new test module's docstring. Restoring it belongs with the change that rewrites the row. This is filed as a finding, not built here.
Checks run on the merge
Green:
ruff format --check .(1308 files) andruff check .tree-wide;mypy messagefoundrystrict (274 files); every pre-commit hook at commit time. Targeted suites:test_transports+test_wiring_engine+test_connections_file179 (this includestest_file_source_min_age_skips_recent_files, run again by name),test_api_auth66,test_poll_source_tick_ceilings17,test_dicom_association_intake_bound15,test_webconsole_seam_snapshot8,test_security_doc_rate_limits43,test_docs_cite_no_refused_config_keys309.The ported stop-check test was confirmed to bite: with the two lines removed it fails
assert 7 == 2.A full-tree
pytestwas NOT completed here and no number is quoted for it. It was started twice and stopped both times: the first run overlapped a deliberate source mutation used to red-test the ported stop check, so its result would not have described this tree; the second reached 6 percent in about fifteen minutes on a shared machine. The three requiredtest (...)legs on CI are the check to read, along with the hosted-only legs. The ledger parses clean (492 items, no duplicate numbers, no doubled banners).