Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
e5505a9
fix(containment): validate the codeset rename SOURCE and fold the Cor…
wshallwshall Aug 22, 2026
32fb311
docs(codesets): record what the rename-source guard changed, and drop…
wshallwshall Aug 22, 2026
c28ad75
fix(apiclient): percent-encode every interpolated path segment and al…
wshallwshall Aug 22, 2026
d31ea1b
test(codesets): pin every refusal branch on the rename source, not ju…
wshallwshall Aug 22, 2026
9d59ff4
test(corepoint): census escapes the out root cannot contain, and reco…
wshallwshall Aug 22, 2026
c52927b
fix(transports): refuse ech_egress where the SNI cannot be hidden, an…
wshallwshall Aug 22, 2026
142b926
feat(uploads): make the upload quota cross-process via a store reserv…
wshallwshall Aug 22, 2026
b3d1eb4
docs(uploads): the leaked-reservation self-heal claim was false, and …
wshallwshall Aug 22, 2026
77d03ca
fix(redact): the log redactor emitted the token it claimed to redact …
wshallwshall Aug 22, 2026
7898303
Merge remote-tracking branch 'origin/main' into claude/builder-2-6e58d6
wshallwshall Aug 22, 2026
cb2e79d
Merge remote-tracking branch 'origin/main' into claude/builder-2-6e58d6
wshallwshall Aug 22, 2026
091a9b7
feat(api): take the PHI search needle off the query string (BACKLOG #…
wshallwshall Aug 22, 2026
0e44e23
docs(crypto-gate): the ide/ exclusion is a fact about the LANGUAGE, n…
wshallwshall Aug 22, 2026
f385a6d
fix(remotefile): the SFTP connector proposed an HMAC over a disallowe…
wshallwshall Aug 22, 2026
4f46ece
docs(readme): the supply-chain note claimed signing coverage two of t…
wshallwshall Aug 22, 2026
70186e4
test(bash): resolve a bash that can SEE this process's files, in all …
wshallwshall Aug 22, 2026
e464906
test(bash): my own positive control was an identity and certified a b…
wshallwshall Aug 22, 2026
5f7be3e
fix(http-auth): refuse an HTTP Digest challenge that names a disallow…
wshallwshall Aug 22, 2026
87d7223
feat(smtp-auth): an approved AUTH mechanism, and only over an encrypt…
wshallwshall Aug 22, 2026
d2c1fd9
fix(xml-dsig): pass an accept-set instead of letting signxml's defaul…
wshallwshall Aug 22, 2026
e16b3e5
Merge remote-tracking branch 'origin/main' into claude/builder-2-6e58d6
wshallwshall Aug 22, 2026
76b000c
Merge remote-tracking branch 'origin/main' into claude/builder-2-6e58d6
wshallwshall Aug 22, 2026
925bc6b
fix(alerts): the alerts SMTP hop sent AUTH credentials in cleartext (…
wshallwshall Aug 22, 2026
cf8dfed
fix(smtp): a shipped control that had never run on the path it guards…
wshallwshall Aug 22, 2026
ca16181
fix(smtp): a policy refusal was reaching the worker as "our bug" (BAC…
wshallwshall Aug 22, 2026
f6796e8
Merge remote-tracking branch 'origin/main' into claude/builder-2-6e58d6
wshallwshall Aug 22, 2026
e7ae619
test(tooling): classify test_bash_resolver.py, which the partition gu…
wshallwshall Aug 22, 2026
7c20ddb
Merge remote-tracking branch 'origin/main' into claude/builder-2-6e58d6
wshallwshall Aug 22, 2026
4b78395
fix(tests): the resolver picked the bash that rewrites PATH (BACKLOG …
wshallwshall Aug 22, 2026
110c125
test(dependabot): self-certify that a prepended stub actually WON (BA…
wshallwshall Aug 22, 2026
90ffcb3
Merge remote-tracking branch 'origin/main' into claude/builder-2-6e58d6
wshallwshall Aug 22, 2026
fce7d0a
fix(tests): the wrapper predicate matched an ordinary Linux bash (BAC…
wshallwshall Aug 22, 2026
4eee9f8
webconsole: fetch-metadata isolation for /ui, including the Mount (#1…
wshallwshall Aug 22, 2026
8b5e833
Merge remote-tracking branch 'origin/main' into claude/builder-2-6e58d6
wshallwshall Aug 22, 2026
9e4c152
webconsole: connection names cannot escape a /ui path segment (#1107)
wshallwshall Aug 22, 2026
549e304
webconsole: encode the one role id that skips its own 404 lookup (#1107)
wshallwshall Aug 22, 2026
721635c
init writes a loadable config, and check can fail on one (#1318, #1320)
wshallwshall Aug 22, 2026
cb010d7
docs: the instruments that returned confident wrong answers, tracked
wshallwshall Aug 22, 2026
67843d6
docs: sharpen the marker rule to its actionable form
wshallwshall Aug 22, 2026
482ed77
Merge branch 'main' into claude/builder-2-6e58d6
wshallwshall Aug 24, 2026
f406877
Merge branch 'main' into claude/builder-2-6e58d6
wshallwshall Aug 24, 2026
2c2db57
Merge branch 'main' into claude/builder-2-6e58d6
wshallwshall Aug 24, 2026
4974ed6
Merge branch 'main' into claude/builder-2-6e58d6
wshallwshall Aug 24, 2026
c3c3a71
Merge branch 'main' into claude/builder-2-6e58d6
wshallwshall Aug 24, 2026
474845e
Merge branch 'main' into claude/builder-2-6e58d6
wshallwshall Aug 24, 2026
0a3f04b
Merge branch 'main' into claude/builder-2-6e58d6
wshallwshall Aug 24, 2026
6d1e4e7
Merge branch 'main' into claude/builder-2-6e58d6
wshallwshall Aug 24, 2026
7c50235
Merge branch 'main' into claude/builder-2-6e58d6
wshallwshall Aug 24, 2026
f71a9b0
Merge branch 'main' into claude/builder-2-6e58d6
wshallwshall Aug 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Instruments that returned confident wrong answers

Measured across one session, 2026-08-22, by several sessions working the same tree. Tracked here
rather than left in a coordination handoff because `<git-common-dir>/mefor-coord/handoffs/` sits
inside `.git`, which git cannot track by construction, so no rescue ref covers it (COMMON 5.6b).

**Every entry is a real measurement that was correct about what it measured and wrong about what it
was asked.** None is a typo or a slip. That is the point: the failures survive care, so the guard has
to be structural.

## The single shape

**An instrument answered a question adjacent to the one asked.** Name the question and the answer in
the same sentence and check they are the same sentence. That is SDS-3.8, and every entry below is an
instance of it.

## The catalogue

| The check | What it answered | What was asked |
|---|---|---|
| `grep ... \| head -60` | The first 60 lines of the result | How many sites exist. It FOUND four and PRINTED three |
| `grep -c <symbol>` | How many times a name occurs | Whether the code handles it. All 8 hits WERE the handling |
| A count on a settings model | That one real field took | Whether unknown keys are dropped. Only a BOGUS key discriminates |
| `git status` | Does this differ from HEAD | Is someone mid-edit. A restore-to-current renders identically |
| `git status` in a stale checkout | Differs from a HEAD 28 commits behind | Did anyone edit this |
| Two mail timestamps | The gap between two messages | What caused an event three minutes earlier |
| Reading a commit by SHA | What was true at that commit | What is true now. A SHA is a snapshot, a TIP is a state |
| `grep -i "superseded"` on a row | The word is present | The row's status. Both bodies used it as NARRATIVE |
| `parse_items` `is_open` | The BANNER state | Which row the authors say survives. Different questions |
| A `tail`-piped background run | Nothing until exit | Progress. The buffer hid a 0-byte file for a run that never started |
| A harness "exit code 0" | The wrapper's status | Whether the suite passed. It reported 0 over `3 failed` |

## Four rules that survived the session

**1. A zero needs a positive control -- AND the control proves the instrument, not the AIM.**
Confirming a pattern fires on a corpus establishes the grep works THERE. It cannot establish that
there is the right place to look. Both halves are needed and the second is the one that gets skipped.

**2. Not on origin means UNPUBLISHED, not UNREADABLE.** Its mirror: WHAT YOU READ IS NOT NECESSARILY
WHAT IS COMMITTED. Several checks stop at `origin/main` and report unknown when the answer sits in a
sibling worktree. Read through `git show <ref>:<path>` so the command NAMES the corpus -- but note
the limit: naming the corpus prevents drift, not misaim.

**3. Never filter a command whose output IS evidence** -- a count, an id, a receipt, a verdict. A
truncated COUNT is wrong immediately and might be caught. A truncated ARTIFACT is not wrong at all,
it is ABSENT, and absence surfaces only when something downstream needs it.

**4. A completeness claim is a liability.** Prefer "at least" (SDS-3.6). Two published population
claims in one session were unmeasured extrapolations from a real mechanism, and both had to be
withdrawn. The honest form is "the next one will do this silently", not "these are everywhere".

## Two shapes that are not instrument failures

**A STATUS MARKER MUST NOT BE A WORD THAT ALSO APPEARS IN NARRATION.** A row containing
"THIS ROW SURVIVES A CROSS-SUPERSEDE" was read as superseded by three separate readers grepping for
that word; a fourth read "SUPERSEDED BY #1326" in a body paragraph and took it for the row's status.
Both misreadings are presence-equals-meaning -- the positional-meaning defect CLAUDE.md section 11
gives as the reason not to use status glyphs, reproduced exactly, in prose.

The rule is not about pictographs. It is about tokens whose meaning depends on the sentence around
them, and PROSE IS NOT IMMUNE: a word carries its scope in the sentence, which is precisely why the
same word used as BOTH a status and a narration cannot. In this repo the banner alphabet is the status
channel and `parse_items` is its only correct reader; anything written in the body is narration, no
matter how emphatic. **Four seats grepped the body for a status word. None of them was careless.**

**Mutual deference has no fixed point, and neither does mutual assertion.** Two seats deferring to a
third produced two records of one defect; both then yielding to each other produced zero; both then
asserting produced two again. Same courtesy, three directions, never convergence. Only an ASYMMETRIC
decider settles it -- and by position rather than by judgement, since a merit tie-break between two
good options is still a tie.

## The one that generalises furthest

**A test that cannot fail in the direction of the bug will certify it.** Observed four times: a test
asserting a string the loader rejects; a gate reading a refusal and reporting `skipped`; a settings
object silently dropping the flag a test is named after; an assertion reading `.path` where httpx
decodes what the fix encodes. **Mutation testing is the only thing that answers "can this test fail
at all", and it caught two tests written by the person applying the rule.**
89 changes: 88 additions & 1 deletion messagefoundry_webconsole/_security.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,10 @@
import secrets

from starlette.datastructures import MutableHeaders
from starlette.responses import PlainTextResponse
from starlette.types import ASGIApp, Message, Receive, Scope, Send

from ._auth import browser_hardening_enabled, security_headers_context
from ._auth import _CROSS_ORIGIN_FETCH, browser_hardening_enabled, security_headers_context
from ._html import reset_csp_nonce, set_csp_nonce

#: The route (registered in :mod:`.routes.core`) the browser POSTs CSP violation reports to, and the
Expand Down Expand Up @@ -222,6 +223,92 @@ def _is_ui_html_path(path: str) -> bool:
return (path == "/ui" or path.startswith("/ui/")) and not path.startswith("/ui/static")


def _is_ui_fetch_scope(path: str) -> bool:
"""Every /ui path INCLUDING the static mount -- deliberately wider than :func:`_is_ui_html_path`.

The asset tier is exactly what a per-route validator cannot reach: ``/ui/static`` is mounted as a
Starlette ``Mount`` in :func:`.mount.mount_ui`, not registered as an ``APIRoute``, so a route
dependency never runs for it. That gap is the reason this check is middleware rather than a
dependency, so excluding the mount here would remove its only purpose.
"""
return path == "/ui" or path.startswith("/ui/")


#: A cross-site request that is a SAFE TOP-LEVEL NAVIGATION is allowed -- intranet links and the OIDC
#: callback are both cross-site by construction. Anything outside this set arriving cross-site as a
#: navigation is a CSRF form submission, which :func:`._auth.assert_same_origin` also refuses per-route.
_SAFE_NAVIGATION_METHODS = frozenset({"GET", "HEAD"})
#: ``object``/``embed`` pull a subresource into someone else's page while still reporting
#: ``Sec-Fetch-Mode: navigate``. That is framing, not navigation, so it does not get the carve-out.
_FRAMING_DESTINATIONS = frozenset({"object", "embed"})


class UiFetchMetadataMiddleware:
"""Refuse a /ui request the BROWSER ITSELF labels cross-site (BACKLOG #1122, ASVS 3.5.3).

It shares the membership set with :func:`._auth.assert_not_cross_site`, lifted to middleware so it
also covers the ``/ui/static`` Mount that route dependencies cannot see -- but it is NOT that check
at a wider scope, and building it as one is a defect the suite catches. That helper guards
hand-picked routes (a CSP report sink, state-changing POSTs) where nothing legitimate EVER arrives
cross-site; its name says FETCH because its callers have already established that. Applying the
bare set to every /ui request adds top-level NAVIGATIONS, which those callers never see.

**A CROSS-SITE TOP-LEVEL NAVIGATION IS LEGITIMATE AND MUST PASS.** An intranet link into the
console is one; so is the OIDC callback, where the IdP redirect back is cross-site BY
CONSTRUCTION. ``test_oidc_callback_survives_a_cross_site_navigation`` exists to say exactly that,
and warns that otherwise *"every real login would 403 while every hermetic test still passed"*.
So the refusal must read ``Sec-Fetch-Mode`` too, and fires only when the request is NOT a safe
top-level navigation. METHOD is part of safe: a cross-site navigation carrying a POST is a CSRF
form submission, and no supported flow makes one (the OIDC leg is a GET; ``response_mode=form_post``
is not implemented here). ``object``/``embed`` destinations are refused because they are framing
rather than navigation.

**ABSENT IS ALLOWED, AND THAT IS THE LOAD-BEARING HALF.** ``Sec-Fetch-Site`` is browser-populated:
an old browser, a user-agent's out-of-band reporting agent, and every non-browser client omit it
entirely. Failing closed on absence would refuse the shipped Windows tray's own ``GET /ui`` probe
(``tray/probe.py`` builds its client with no headers at all) and 332 headerless call sites in the
/ui test corpus -- MEASURED, both. ``_auth`` already records the same reasoning for the CSP report
sink, where a strict check "would 403 every modern report and silently blind the 3.7.5 canary".
So this rejects only a header that is PRESENT and says cross-site or same-site.

**403, NEVER 404.** ``tray/probe.py`` classifies 404 as ``DISABLED`` and every other status as
``ENABLED``, so a 404 here would make the tray report a healthy console as switched off. A later
"return 404 rather than disclose the route" hardening pass would look like an improvement and
silently break the tray; the tests pin both halves.
"""

def __init__(self, app: ASGIApp) -> None:
self.app = app

async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if scope["type"] != "http" or not _is_ui_fetch_scope(scope.get("path", "")):
await self.app(scope, receive, send)
return
# Read from the raw scope rather than building a Request: this runs for every /ui asset, and
# header names on the wire are lower-cased bytes by ASGI contract.
site = mode = dest = None
for key, value in scope.get("headers") or ():
if key == b"sec-fetch-site":
site = value.decode("latin-1")
elif key == b"sec-fetch-mode":
mode = value.decode("latin-1")
elif key == b"sec-fetch-dest":
dest = value.decode("latin-1")
if site is None or site not in _CROSS_ORIGIN_FETCH:
await self.app(scope, receive, send)
return
if (
mode == "navigate"
and str(scope.get("method", "")).upper() in _SAFE_NAVIGATION_METHODS
and dest not in _FRAMING_DESTINATIONS
):
await self.app(scope, receive, send)
return
await PlainTextResponse("cross-site request rejected", status_code=403)(
scope, receive, send
)


class UiSecurityHeadersMiddleware:
"""Pure-ASGI /ui browser-security hardening (see the module docstring)."""

Expand Down
9 changes: 8 additions & 1 deletion messagefoundry_webconsole/mount.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from messagefoundry.api._ui_seam import UiDeps

from . import STATIC_DIR, _auth, assert_engine_seam, pages
from ._security import UiSecurityHeadersMiddleware
from ._security import UiFetchMetadataMiddleware, UiSecurityHeadersMiddleware
from ._static import AllowlistedStaticFiles
from .routes import (
account,
Expand Down Expand Up @@ -101,3 +101,10 @@ def mount_ui(app: FastAPI, deps: UiDeps) -> None:
# See :mod:`._security`.
if not any(getattr(m, "cls", None) is UiSecurityHeadersMiddleware for m in app.user_middleware):
app.add_middleware(UiSecurityHeadersMiddleware)

# BACKLOG #1122 (ASVS 3.5.3): the cross-site refusal lifted from a route dependency to middleware,
# because /ui/static is a Mount rather than an APIRoute -- a dependency never runs for it, so the
# asset tier was the one /ui surface the per-route check could not reach. Same re-mount guard as
# above, and the same append-by-pattern contract.
if not any(getattr(m, "cls", None) is UiFetchMetadataMiddleware for m in app.user_middleware):
app.add_middleware(UiFetchMetadataMiddleware)
23 changes: 23 additions & 0 deletions messagefoundry_webconsole/pages/_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

from __future__ import annotations

from urllib.parse import quote


def _num(value: object) -> str:
"""Render a count/None as text ('—' for None)."""
Expand All @@ -19,3 +21,24 @@ def _secs(value: float | None) -> str:
if value is None:
return "—"
return f"{value:.0f}s"


def _seg(value: object) -> str:
"""Percent-encode ONE path segment, INCLUDING ``/`` (ASVS 1.2.2, BACKLOG #1107).

``quote`` defaults to ``safe="/"``, which leaves the single character a path segment turns on.
Measured rather than reasoned: ``quote("IB/ACME")`` returns it UNCHANGED, so a name carrying a
slash silently becomes two segments and addresses a different route.

CONNECTION NAMES are why this is not theoretical. They are unconstrained free text --
``Registry._add`` checks only for a duplicate, and no charset gate exists -- so the "every
interpolated id is a ``uuid4().hex``" argument that covers most /ui interpolations is FALSE for
them. The remaining id-carrying sites are deliberately NOT routed through here yet: that argument
is probably true of them, but it rests on a data-grammar invariant no line of URL-building code
asserts, and deciding it is a separate piece of work.

NOT for a path legitimately carried in a QUERY parameter. ``_auth``'s reauth ``next`` uses
``safe="/"`` on purpose, and routing it through here would break it -- the reason the research on
#1107 says to partition these sites by READING each one rather than by a blanket builder.
"""
return quote(str(value), safe="")
9 changes: 7 additions & 2 deletions messagefoundry_webconsole/pages/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
)

from .._html import Markup, el, page, register_nav, rows_table
from ._common import _seg

__all__ = [
"ad_groups_page",
Expand Down Expand Up @@ -360,7 +361,11 @@ def role_form_page(
description if description is not None else (role.description or "" if role else "")
)
perm_checked = checked if checked is not None else (role.permissions if role else ())
action = f"/ui/roles/custom/{role.id}/update" if role else "/ui/roles/custom"
# _seg, NOT bare interpolation: on a rejected submit `ui_role_update` rebuilds this page from
# `CustomRoleInfo(id=role_id, ...)` where role_id is the RAW path param -- a ValidationError on
# CustomRoleRequest short-circuits before `update_custom_role` runs, so the 404 lookup that
# constrains every OTHER id on this surface never happens (BACKLOG #1107, ASVS 1.2.2).
action = f"/ui/roles/custom/{_seg(role.id)}/update" if role else "/ui/roles/custom"
form = el(
"form",
el("label", "Name", el("input", name="display_name", value=name_value, autofocus=True)),
Expand All @@ -378,7 +383,7 @@ def role_form_page(
"form",
el("button", "Delete role", type="submit"),
method="post",
action=f"/ui/roles/custom/{role.id}/delete",
action=f"/ui/roles/custom/{_seg(role.id)}/delete",
class_="ctl",
)
)
Expand Down
6 changes: 3 additions & 3 deletions messagefoundry_webconsole/pages/connections.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from messagefoundry.api.models import ConnectionEventInfo, ConnectionRow

from .._html import Markup, el, page, rows_table, text
from ._common import _num, _secs
from ._common import _num, _secs, _seg

__all__ = [
"bulk_control_result",
Expand Down Expand Up @@ -61,7 +61,7 @@ def _name_cell(r: ConnectionRow) -> Markup:
el(
"a",
"ⓘ",
href=f"/ui/connection/{quote(r.name)}",
href=f"/ui/connection/{_seg(r.name)}",
class_="detail-link",
title="Connection details",
aria_label=f"Details for {_display_name(r.name)}",
Expand Down Expand Up @@ -197,7 +197,7 @@ def _flag_cell(r: ConnectionRow) -> Markup:
aria_label=("Unflag " if r.flagged else "Flag ") + name,
),
method="post",
action=f"/ui/connections/{quote(name)}/flag",
action=f"/ui/connections/{_seg(name)}/flag",
class_="ctl flagform",
)

Expand Down
5 changes: 3 additions & 2 deletions messagefoundry_webconsole/pages/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from messagefoundry.parsing.tree import TreeNode

from .._html import Markup, el, page, rows_table, text
from ._common import _seg

__all__ = [
"dead_letter_pending",
Expand Down Expand Up @@ -615,7 +616,7 @@ def dead_letters(data: DeadLetterList) -> Markup:
"form",
el("button", f"Replay all dead — {ch}", type="submit"),
method="post",
action=f"/ui/dead-letters/{ch}/replay",
action=f"/ui/dead-letters/{_seg(ch)}/replay",
class_="ctl",
)
for ch in channels
Expand All @@ -625,7 +626,7 @@ def dead_letters(data: DeadLetterList) -> Markup:
"form",
el("button", f"Replay {ch} → {dest}", type="submit"),
method="post",
action=f"/ui/dead-letters/{ch}/{dest}/replay",
action=f"/ui/dead-letters/{_seg(ch)}/{_seg(dest)}/replay",
class_="ctl",
)
for ch, dest in pairs
Expand Down
Loading
Loading