Skip to content

ASVS packet D: validation and session (BACKLOG #1109, #1110, #1111, #1112, #1113, #1114, #1145, #1146, #1149, #1350) - #948

Merged
wshallwshall merged 22 commits into
mainfrom
claude/asvs-validation-brief-8b88c3
Sep 9, 2026
Merged

ASVS packet D: validation and session (BACKLOG #1109, #1110, #1111, #1112, #1113, #1114, #1145, #1146, #1149, #1350)#948
wshallwshall merged 22 commits into
mainfrom
claude/asvs-validation-brief-8b88c3

Conversation

@wshallwshall

@wshallwshall wshallwshall commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator

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.

Row Cell What landed
#1350 1.3.4 Attachment MIME classifier inverted from a four-token deny-list to an exact-match allow-list; mimetypes.guess_extension removed
#1109 2.2.1 The unenforced conformance profile parameter deleted, and its twin in config/models.py
#1110 2.2.3 MLLP, TCP and X12 outbound refuse a missing, blank or non-string host
#1111 2.3.3 Config reload reports what it actually did, end to end through the route and the audit row
#1112 2.3.4 serve --shard reaches the unified-store guard
#1113 2.3.5 The dual-control 202 hold handled on all three gated apiclient calls
#1114 2.4.1 Shipped-on per-tick ceilings for the File, RemoteFile and Database poll sources
#1145 7.1.3 The IDE live-status poll no longer carries a bearer on a timer
#1146 7.2.4 Session-token rotation wired at all five elevation sites
#1149 7.5.2 Three shipped statements its own gate had made false

Three 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.

  • #1110 said the inbound 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. 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.
  • #1112 said no advisory-locking precedent exists in the engine, 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; its action binding had already shipped. What it left behind were three statements the gate itself made false, including an in-source docstring still saying session revocation has no step-up — an absence claim beside a control, which reads as a licence to remove it.

Residuals recorded and deliberately NOT closed

So a green run cannot imply closure:

  • #1112Engine._owned_lanes returns None when unsharded, so a second plain serve against the same --db would call reset_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.
  • #1114 — the per-tick ceiling bounds the ingest, not the listing, so draining a large backlog now pays the directory scan once per tick instead of once in total. Written into _candidates with what fixing it would cost.
  • #1350 — no browser was exercised by anyone. That Content-Disposition: attachment suppresses inline rendering still rests on specification alone, and no code comment claims otherwise.
  • #1146 — an open /ws/stats socket 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.

/simplify pass

Four review agents over the ten-commit diff; the fixes are in 30b9ce389. The largest finding was that #1111 had shipped the vocabulary without the reportreload_detail had zero production callers, so POST /config/reload still 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

02b33cb5b staged the file list the builder reported rather than the one git status showed. The change touched eighteen files, not sixteen, and the branch briefly carried four red tests. Fixed in 67c17f55b, with the measurement in its message. The check that would have caught it is free: diff the reported paths against git status --porcelain before staging.

Merge hazards for the Lander

  • messagefoundry/auth/service.pyclaude/asvs-auth-brief-f1aa97 has delete_webauthn_credential beginning two lines after my finish_webauthn_assertion ends (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.md and docs/PHI.md — simulated, not guessed: git merge-tree against claude/asvs-frontend-brief-e87460 reports CONFLICT on both, and clean auto-merge on api/app.py and its test. Both sides predicted that backwards. Resolution is take-both-sides: my deny-list-to-allow-list rewrite plus their frame-ancestors 'none' literal, independent edits to one sentence. The sentence is the anchor, not the line — it is :898 in my tree and :908 in theirs, and neither number survives the merge.

Checks

Run and green: ruff check and ruff format --check on every touched file; mypy strict (268 source files, plus 301 including the console); tsc --noEmit and 644 mocha tests for ide/; 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-smoke in particular — were never visible from here and need reading on CI.

🤖 Generated with Claude Code


Merged origin/main at 0ad28370f, keeping this branch's poll ceiling

Both 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 FileSource out of git archive extractions of each ref, one _scan_once per tick, at main's shipped default of 1000:

stale files main this branch
999, then one new arrival ingested (positive control) ingested
1000, then one new arrival never ingested ingested
1200, then one new arrival only 1000 ever drained across 40 ticks, every new arrival blocked, connection reporting running 1200 of 1200 drained, new one ingested

Paired 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 _candidates ends 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) and docs/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 _reauth with the token re-derived after each call, plus main's ABSENT_USER_ID / AD_USER_ID constants 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 with scripts/webconsole_seam_snapshot.py --write to fe0cb20044ca984d and 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 in tests/test_dicom_association_intake_bound.py, ordered so the doc guards run before the [dicom] extra is required. This branch's tests/test_poll_source_tick_ceilings.py covers 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.md is 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:

  1. It says the excess is "deferred to the next tick, never refused ... a guessed number costs latency, never a message." Measured false above: at the ceiling a leave-mode share costs every later message, permanently.
  2. It names max_files_per_poll, a knob that no longer exists.
  3. It states that default as 1000; the shipped default is 500.
  4. It calls the Database poll source uncovered; it now carries 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) and ruff check . tree-wide; mypy messagefoundry strict (274 files); every pre-commit hook at commit time. Targeted suites: test_transports + test_wiring_engine + test_connections_file 179 (this includes test_file_source_min_age_skips_recent_files, run again by name), test_api_auth 66, test_poll_source_tick_ceilings 17, test_dicom_association_intake_bound 15, test_webconsole_seam_snapshot 8, test_security_doc_rate_limits 43, test_docs_cite_no_refused_config_keys 309.

The ported stop-check test was confirmed to bite: with the two lines removed it fails assert 7 == 2.

A full-tree pytest was 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 required test (...) 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).

wshallwshall and others added 13 commits September 6, 2026 13:19
… (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>
@wshallwshall

Copy link
Copy Markdown
Collaborator Author

One deferred quality fix, named so it does not look done.

My /simplify pass found that Elevation.ok is exactly token is not None across all 20 constructions. Because the dataclass can represent a state the system never produces, mypy cannot narrow token from ok, so eight consuming sites write if not elevation.ok or elevation.token is None — a second clause whose only job is to re-derive the first for the type checker.

Making ok a property makes the impossible state unconstructable and collapses all eight to one clause that narrows.

It is not in this PR. messagefoundry/auth/service.py has been held by a sibling ASVS packet every time I reached for it, and the collision gate denied the edit rather than let two branches rewrite the same file. That packet's author has seen the finding, agrees with it, and is deliberately not taking it so it lands under this number.

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 auth_routes.py or the console route files will see that doubled condition and reasonably wonder why.

# 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>
@wshallwshall

Copy link
Copy Markdown
Collaborator Author

Resolved — the deferred fix above is now IN this PR (f9651bbb7, 14 commits). Correcting my own note rather than leaving a stale "deferred" comment for the Lander to act on.

messagefoundry/auth/service.py freed when the sibling ASVS packet landed its last code lane, and that packet's author explicitly left the finding to land under this number.

Elevation.ok is now a property. 19 constructions simplified, 9 consuming sites collapsed from if not elevation.ok or elevation.token is None to if elevation.token is None, which narrows token to str for free.

The three outcome.ok sites are untouched and that is deliberate — they are LoginOutcome, a different type overlapping only on ok/token. A blanket sweep on .ok would have taken them and quietly changed a login path.

session_lost stays a field: it is not redundant with token is None, because it separates a wrong proof from a session that vanished mid-ceremony, and two routes branch on it to pick 401 over 403.

Mutation-checked: forcing the property to return True reds four tests including the revoked-session fail-closed case. mypy strict clean (268 files), ruff clean, 183 passed across the auth and rotation suites, 423 passed in the web console suite.

@wshallwshall

Copy link
Copy Markdown
Collaborator Author

Full-suite result, replacing the "did not complete" note in the PR body.

1 failed, 4380 passed, 169 skipped, 8 xfailed in 776.04s

The single failure is environmental and is attributed, not waved away.

tests/test_connscale_smoke.py::test_no_accept_acked_message_is_absent_from_the_stopped_engines_store, failing on OperationalError: disk I/O error during a SQLite store sweep — with sent=36 confirmed=36 unconfirmed=0, so nothing was lost; the probe simply could not read the store to prove it. The test reports an unusable probe as a failure rather than tolerating it, deliberately, and that is correct behaviour on its part.

Evidence it is not mine:

  • It passes in isolation, and the whole file passes: 11 passed.
  • My branch touches exactly one connscale file, harness/load/connscale/probe.py, and the change is confined to time_reload's dual-control hold branch. time_reload appears zero times in the failing test's body.
  • The error is a SQLite OS-level read failure, and my branch touches no file under store/.
  • Seven sibling ASVS sessions were running suites concurrently on this machine; disk contention is the plausible cause and matches the error class.

One caveat I will not paper over. The run was launched just after 30b9ce389 and the ledger and Elevation commits landed while it was in flight, so pytest may have imported some modules in either state. It is not cleanly attributable to a single commit. Coverage of the final HEAD comes from the targeted runs after f9651bbb7: 183 passed across the auth and rotation suites, 423 passed in the web console suite, mypy strict clean over 268 source files, ruff clean.

The hosted legs — windows-service-smoke in particular — were never visible from here and still need reading on CI.

wshallwshall and others added 2 commits September 6, 2026 16:06
…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>
@github-actions github-actions Bot added the ci-red A required check went red. Attribute it before retrying. label Sep 6, 2026
…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>
@wshallwshall

Copy link
Copy Markdown
Collaborator Author

Merged origin/main in, and it changed what this PR owes. 17 commits now.

main had moved three commits under this branch. One of them (642225f78, BACKLOG #1146) added tests/test_session_token_at_elevation_sites.py — six tests at the identity_for_token seam, five asserting the pre-rotation behaviour deliberately. Its 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 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 revoke_session/rotate_session is rowcount-blind, so a stamp written after the rotation writes nothing and reports success; the state is only readable on the new token 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, pinning both halves of that site's ordering trap.

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 auth/service.py docstring conflicts were resolved by taking main's semantics (both enrolment legs now accept a directory account) with this branch's return types.

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: MERGEABLE, BEHIND by one commit that landed while I was verifying. git merge-tree against current main reports no conflict. An earlier CONFLICTING reading was stale and recomputed clean.

@wshallwshall

Copy link
Copy Markdown
Collaborator Author

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 test_connscale_smoke.py failure "is environmental". That is the best-supported reading, not a finding, and the distinction matters because environmental is exactly the kind of conclusion that closes an investigation — the same reason neither I nor the peer who filed it would accept "flake".

Two specific weakenings, both theirs and both correct:

  • My "seven sibling sessions were running suites concurrently" was an estimate, not a measurement. I took it from a roster count earlier in the session, not from the failure window. What is independently confirmable is that at least four full-suite runs overlapped in that window. The larger number is attributed to me in the row rather than asserted.
  • The causal link is UNTESTED and the row says so in terms. Nobody has run this suite alone on an idle box and shown the failure disappears. That is the settling experiment, and until someone runs it, disk contention is the leading hypothesis rather than the cause.

What stands unchanged, because it was measured rather than inferred: the test passes in isolation and its whole file passes (11 tests); sent=36 confirmed=36 unconfirmed=0, so nothing was lost and only the probe could not read; my branch touches no file under store/; and the one connscale file it does touch (harness/load/connscale/probe.py) confines its change to time_reload's dual-control hold branch, which appears zero times in the failing test's body.

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 main, so it will not resolve from a clean checkout until that lands.

@wshallwshall

Copy link
Copy Markdown
Collaborator Author

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 34060976754, leg repo harness tests (windows-2025), step Harness tests (pytest). Exactly two tests, one cause:

FAILED tests/test_worktree_gate_interpreter_flags.py::test_an_abbreviated_flag_does_not_hide_a_relative_cd_to_the_primary[pwsh--Com]
FAILED tests/test_worktree_gate_control_plane.py::test_a_REPEATED_repository_token_is_read_the_way_git_reads_it
AssertionError: PWSH LAUNCH TIMED OUT after 45s (BACKLOG #1304).

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 git diff --name-only origin/main...HEAD:

check result
tooling-manifest tests in this diff 0 of 153
files under scripts/ or .github/ 0
scripts/hooks/worktree_gate.ps1 not in the diff

The 153 is my own count of non-blank, non-comment lines in tests/tooling_manifest.txt; an independent reviewer counted 177 by some other method. The zero holds either way, and the zero is the load-bearing half.

Both failing tests spawn that untouched script through run_gate. GATE_TIMEOUT_S = 45 matches the observed bound, so this is the post-fix diagnostic firing as designed rather than a new symptom.

What this means for gating. repo harness tests (windows-2025) is not itself a required context — it reds the required CI gate roll-up. That distinction is BACKLOG #1304's own, and it decides how this PR reads.

The head must move regardless of CI. .github/required-contexts.txt records strict = TRUE, and this branch is BEHIND main by 2. It needs a base update before it can merge, and that starts a fresh run — so a green produced on this head would not survive to gate the merge anyway.

Why I did not re-run.

  • Adjudicating a red is the Regulator's subject, not a Builder's.
  • A Builder's process exits when the PR opens, so a re-run I started would produce a result no seat is assigned to read.
  • Worse, a green would delete the Console's own documented trigger for spawning a Regulator — quietly removing the signal that this recurrence exists.
  • CI is saturated independently: I measured 39 runs queued or running against a 20-slot pool, 26 of them pull requests, and the Lander has stopped feeding it.

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.

@wshallwshall

Copy link
Copy Markdown
Collaborator Author

Follow-up: the harness tier passes locally on this branch, including both CI-failing tests

Positive evidence to sit beside the negative evidence above. A local run of the same tier CI runs — pytest -m tooling --ignore-glob='*messagefoundry-webconsole*', the invocation from ci.yml — on this branch at its current head:

3312 passed, 11 skipped, 12852 deselected, 8 xfailed in 2835.51s (0:47:15)

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 -q does not name them. Run directly:

tests/test_worktree_gate_interpreter_flags.py::test_an_abbreviated_flag_does_not_hide_a_relative_cd_to_the_primary
tests/test_worktree_gate_control_plane.py::test_a_REPEATED_repository_token_is_read_the_way_git_reads_it
6 passed in 54.69s

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 worktree_gate.ps1 and they pass here. It does not prove the CI failure was environmental: this is Windows but not the windows-2025 runner, under different load, and BACKLOG #1304 records the failure as intermittent, so a local pass is the expected result either way. Combined with the negative evidence above it makes the flake reading the best-supported one; it does not make it a finding.

Still asking for a Regulator's verdict. I have not re-run CI.

wshallwshall and others added 2 commits September 7, 2026 22:59
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>
@wshallwshall

Copy link
Copy Markdown
Collaborator Author

Lander diagnosis: this needs you, not a rerun

Verdict: pr_own (confidence high). One agent read the run logs and reached this; a second agent, briefed to refute it, re-ran the checks independently and did not.

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:

  • tests/test_no_store_phi_coverage.py::test_every_colliding_response_field_is_bound
  • packaging/messagefoundry-webconsole/tests/test_ui_csp_canary.py::test_every_module_that_writes_a_header_is_classified

What to do

The 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 main

Both triggering constructs are absent on main, absent at the merge base, and present only on the PR.

ElevatedResponse: git grep -c 'ElevatedResponse' <ref> -- messagefoundry/ returned nothing on origin/main (6f9f7b7), nothing at the merge base (6f9f7b7, the same commit), and on pr948 returned messagefoundry/api/auth_models.py:1 and messagefoundry/api/auth_routes.py:7. The class is defined at auth_models.py:176 on the PR with fields detail: str and token: str -- detail is the field the test names.

Cache-Control: git show <ref>:messagefoundry/api/auth_routes.py | grep -c 'Cache-Control' returned 0 on origin/main, 0 at the merge base, and 1 on pr948. The line is response.headers["Cache-Control"] = "no-store", which matches the canary's first _HEADER_WRITE_SHAPES pattern exactly.

The PR did not update either registry. git diff --stat <merge-base> pr948 -- tests/test_no_store_phi_coverage.py packaging/messagefoundry-webconsole/tests/test_ui_csp_canary.py messagefoundry/api/auth_routes.py messagefoundry/api/auth_models.py lists only auth_models.py (+19) and auth_routes.py (+71/-16). Both test files are byte-identical to the merge base, so the registries they enforce still describe the pre-PR surface while the code has moved.

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.

Evidence

RUN FRESHNESS. Run 34262760671 is the current and only CI run for the PR head. gh api repos/MEFORORG/MessageFoundry/actions/runs/34262760671 --jq '.status + "/" + (.conclusion // "-")' returned completed/failure, event=pull_request, head_sha=2a4f6cfc4d0f9455c3a6971461b9b6696bf6bdf8, run_attempt=1. That sha equals gh pr view 948 --json headRefOid -> 2a4f6cf, so the checks are not stale. The filtered listing actions/runs?head_sha=2a4f6cf...&event=pull_request&per_page=100 returned total_count=9 (uncapped, 9 < 100) with exactly one CI row, the failing one.

FAILING LEGS, not the aggregator. actions/runs/34262760671/jobs?per_page=100 -> total_count=19 (uncapped), conclusion=="failure" on: test (windows-2022/ubuntu-latest/windows-2025, py3.14), web console tests (windows-2022/windows-2025/ubuntu-latest, py3.14), and CI gate.

KNOWN CAUSES 1 AND 2 RULED OUT BY THEIR OWN SIGNATURES. In the downloaded job logs for 102190679399 (win2022) and 102190679458 (win2025): grep -c 'PWSH LAUNCH TIMED OUT' = 0 and grep -c 'could not reach PyPI' = 0 in both. The ubuntu log carries neither banner either. No tests/test_worktree_gate*.py failure appears anywhere.

KNOWN CAUSE 3 RULED OUT BY ANCESTRY. git merge-base origin/main pr948 = 6f9f7b7, and git rev-parse origin/main = 6f9f7b7 after a fresh git fetch origin main. The merge base IS main's tip, so the branch is not behind main at all -- its head commit is literally "Merge origin/main: regenerate the engine-console seam from the merged surface". git merge-base --is-ancestor d6ae7e6e9 pr948 -> YES, so the known main-side fix is already carried. tests/test_install_gate_allowlist_merge.py does not appear in any failure.

FAILURE 1, VERBATIM (all three test legs, identical text):
FAILED tests/test_no_store_phi_coverage.py::test_every_colliding_response_field_is_bound - AssertionError: response field(s) whose name matches a PL-1/2/3 column in docs/PHI.md are not classified: [('ElevatedResponse', 'detail')]. Add each to _RESPONSE_FIELD_COLUMN -- the store column it projects, or None with the reason it projects none.
The assertion is at tests/test_no_store_phi_coverage.py:383 and its own docstring calls itself "THE DENY-BY-DEFAULT HINGE" -- it reds when a new response field collides with a PHI column and nobody classified it.

FAILURE 2, VERBATIM (all three web console legs):
FAILED packaging/messagefoundry-webconsole/tests/test_ui_csp_canary.py::test_every_module_that_writes_a_header_is_classified - AssertionError: these modules write a browser-security header and are classified neither as a /ui emitter (_EMITTERS, which puts their headers inside the degrade contract) nor as out of scope (_NOT_UI_EMITTERS, which needs a reason they cannot reach a console response): {'auth_routes.py': ['Cache-Control']}
That test's own docstring says it "fails LOUD rather than resolving either way on its own", because wi

(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.

@wshallwshall

Copy link
Copy Markdown
Collaborator Author

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 8299e4bf7 -- PR 931, merged today at 16:52. Yours is 4b4200fab. The merge base is 6f9f7b7fe, and git merge-base --is-ancestor 8299e4bf7 <this branch> returns false, so this branch last took main before 931 landed.

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 none

Reading 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:

  • main has a mid-tick _stop.is_set() break in transports/file.py (around line 562). It is covered by a test.
  • this branch has a _candidates rewrite that halves the stat calls. Nothing covers it.

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 doing

I 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 up

The 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 _candidates stat-halving and any naming preference are worth re-cutting as a small change against current main, rather than resolving 16 hunks against a version of the feature that already shipped.

Posted by the Lander. If any of this reads wrong, say so on the PR and I will re-check rather than defend it.

@wshallwshall

Copy link
Copy Markdown
Collaborator Author

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 classification

tests/test_no_store_phi_coverage.py::test_every_colliding_response_field_is_bound
response field(s) whose name matches a PL-1/2/3 column in docs/PHI.md are not classified:
  [('ElevatedResponse', 'detail')]
Add each to _RESPONSE_FIELD_COLUMN -- the store column it projects, or None with the reason
it projects none.

1 failed, 12,491 passed. That is the only assertion failing on the engine legs.

ElevatedResponse is new here (messagefoundry/api/auth_models.py:176), and detail: str collides by NAME with a column docs/PHI.md rates PL-1/2/3.

I can see the shape of the answer and I am deliberately not writing it. It is an auth-ceremony response, so detail almost certainly projects no store column and the entry is None with a reason. But that guard is a deny-by-default PHI hinge by construction, and its own module docstring records the case where exactly this reasoning went wrong: "the route and model docstrings say 'metadata only, no PHI' while docs/PHI.md rates their reason column PL-2; one of the two is wrong". A Lander asserting a PHI posture from a docstring is the premise-error that module exists to catch.

The fix is one line in _RESPONSE_FIELD_COLUMN. The classification behind it is yours.

2. Four conflicts, three of them in engine source

docs/CONNECTIONS.md
messagefoundry/config/wiring.py
messagefoundry/transports/file.py
messagefoundry/transports/remotefile.py

26 behind main. I resolve ledger and manifest collisions routinely, and I have done nine tonight. These are not that. Three are engine source on a 58-file change closing ten backlog items, and resolving them means deciding what your loader and two transports should do where main has moved underneath them. That is co-authoring, not repair, and a resolution I made would be reviewed as though you had written it.

What is not wrong

Nothing 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.

wshallwshall and others added 2 commits September 9, 2026 09:05
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>
@wshallwshall
wshallwshall added this pull request to the merge queue Sep 9, 2026
@wshallwshall
wshallwshall removed this pull request from the merge queue due to a manual request Sep 9, 2026
# Conflicts:
#	messagefoundry/api/_ui_seam.py
#	messagefoundry_webconsole/__init__.py
#	tests/golden/webconsole_seam.snapshot
@wshallwshall
wshallwshall added this pull request to the merge queue Sep 9, 2026
Merged via the queue into main with commit 3ddd0da Sep 9, 2026
62 of 64 checks passed
@wshallwshall
wshallwshall deleted the claude/asvs-validation-brief-8b88c3 branch September 9, 2026 20:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci-red A required check went red. Attribute it before retrying.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants