Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ deliberate — replace `<version>` with the current release shown at the top of
needs (each is opt-in and lazy-imported):

```bash
pip install -e packaging/messagefoundry-webconsole # the browser web console (/ui) — the operator UI; most operators want this (source tree: not on PyPI yet)
pip install "messagefoundry-webconsole==<version>" # the browser web console (/ui) — the operator UI; most operators want this
pip install "messagefoundry[postgres]==<version>" # PostgreSQL store backend (production server DB)
pip install "messagefoundry[sqlserver]==<version>" # SQL Server store backend (+ OS-level ODBC Driver 18)
pip install "messagefoundry[sftp]==<version>" # SFTP transport for the REMOTEFILE connector
Expand Down
5 changes: 1 addition & 4 deletions docs/INSTALL-GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,10 +249,7 @@ It ships as a separate, version-matched wheel (`messagefoundry-webconsole`) that
in-process; install it alongside the engine and turn on `[api].serve_ui`:

```powershell
pip install -e packaging/messagefoundry-webconsole # the /ui web console, into the same venv
# NOTE: the console is NOT on PyPI yet — install it from the source tree, as above. Installing it
# by bare name from an index would resolve an UNCLAIMED distribution, i.e. whatever a third party
# has uploaded under that name, with its build backend executing at install time (ASVS 15.2.4).
pip install "messagefoundry-webconsole==0.2.15" # the /ui web console, into the same venv
# then set [api].serve_ui = true in your service settings and (re)start the engine
```

Expand Down
2 changes: 1 addition & 1 deletion docs/MENTAL-MODEL.md
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ Keep the message store on a fast *local* disk, not a network share — the stage

## 13. Deployment & operations

- **Install:** the supported production artifact is the signed, version-pinned PyPI wheel (pip install "messagefoundry==0.1.0"); then messagefoundry init scaffolds your own config repo (ADR 0017). Extras are opt-in: \[postgres\], \[sqlserver\], \[harness\] (the PySide6 test harness), \[sftp\]. The `/ui` web console installs alongside as the separate `messagefoundry-webconsole` distribution — **from the source tree** (`pip install -e packaging/messagefoundry-webconsole`) until the release phase publishes it; the name is not yet claimed on PyPI.
- **Install:** the supported production artifact is the signed, version-pinned PyPI wheel (pip install "messagefoundry==0.1.0"); then messagefoundry init scaffolds your own config repo (ADR 0017). Extras are opt-in: \[postgres\], \[sqlserver\], \[harness\] (the PySide6 test harness), \[sftp\]. The `/ui` web console installs alongside as the separate `messagefoundry-webconsole` distribution, published to PyPI on its own `webconsole-v*` cadence.

- **Run headless:** python -m messagefoundry serve --config samples/config --db ./messagefoundry.db --env dev — API on http://127.0.0.1:8765 (GET /connections, /messages, /stats, WS /ws/stats).

Expand Down
2 changes: 1 addition & 1 deletion docs/SERVICE.md
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,7 @@ same-origin at `/ui` (not part of the service runtime — a separate, version-ma
mounts in-process):

```powershell
pip install -e packaging/messagefoundry-webconsole # into the engine venv (not on PyPI yet)
pip install "messagefoundry-webconsole" # into the engine venv
# set [api].serve_ui = true in the service settings, then (re)start the service
```

Expand Down
2 changes: 1 addition & 1 deletion docs/USER-GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ pip install -e ".[dicom]" # DICOM C-STORE SCP + codec — headers/SR only
pip install -e ".[otel]" # OpenTelemetry/OTLP export seam (the /metrics endpoint itself needs no extra)
```

(For a deployment wheel, the same extras apply: `pip install "messagefoundry[harness]==0.1.0"`, and the web console installs from the source tree with `pip install -e packaging/messagefoundry-webconsole` — it is **not published to an index yet**, so installing it by bare name would resolve an unclaimed distribution.) SQLite is the zero-dependency default — you need no extra to run the sample config.
(For a deployment wheel, the same extras apply: `pip install "messagefoundry[harness]==0.1.0"`, and the web console installs as its own wheel `pip install "messagefoundry-webconsole==0.2.15"`.) SQLite is the zero-dependency default — you need no extra to run the sample config.

### 3. Run the engine headless (dev)

Expand Down
3 changes: 1 addition & 2 deletions messagefoundry/api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4942,8 +4942,7 @@ async def _reauthorize() -> Identity | None:
# install, which is what actually works today and resolves no index at all.
raise RuntimeError(
"serve_ui requires the web console, which is not installed. It ships as a separate "
"distribution: install it from the source tree with "
"`pip install -e packaging/messagefoundry-webconsole`, or set [api].serve_ui=false "
"distribution: `pip install messagefoundry-webconsole`, or set [api].serve_ui=false "
"to run JSON-only."
) from exc

Expand Down
12 changes: 7 additions & 5 deletions packaging/messagefoundry-webconsole/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ surface (`security`/`models`/`auth_models`/`_ui_seam`), `messagefoundry.auth`, a
## Install

```
pip install -e packaging/messagefoundry-webconsole # from the source tree, alongside the engine
pip install messagefoundry-webconsole # alongside the engine
# or from a checkout, for development:
pip install -e packaging/messagefoundry-webconsole
```

> **Not on PyPI yet.** This distribution has never been published, so its name is **unclaimed**.
> Installing it by bare name from an index would resolve whatever a third party has uploaded under
> that name — and an sdist executes its build backend during `pip install`, before any engine process
> exists. Install from the source tree until the name is registered (ASVS 15.2.4).
> The distribution name is **registered on PyPI** and published only by this repository's
> `release.yml` over PyPI Trusted Publishing (OIDC, no API token), on its own `webconsole-v*` tag.
> Claiming the name is what forecloses the dependency-confusion substitution an unclaimed name invites
> (ASVS 15.2.4).

A plain `pip install messagefoundry` stays byte-identical: with the console absent and `serve_ui`
default-off, the JSON API is unchanged; `serve_ui=true` without the console fails LOUD at startup.
Expand Down
79 changes: 68 additions & 11 deletions tests/test_install_instruction_provenance.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,23 @@
_ROOT = Path(__file__).resolve().parent.parent

#: Distributions this repository builds that are **not yet published to any index**. Until a name is
#: claimed, no shipped text may tell a user to install it by bare name. Remove an entry here the day
#: the name is registered -- and note that reserving it is sufficient; an empty project cannot be
#: squatted.
_UNPUBLISHED_DISTRIBUTIONS = frozenset({"messagefoundry-webconsole"})
#: claimed, no shipped text may tell a user to install it by bare name.
#:
#: **Currently empty, and that is the goal state, not a disabled guard.** All three distributions this
#: repo builds are registered on PyPI (`messagefoundry`, `messagefoundry-harness`,
#: `messagefoundry-webconsole` -- the last claimed 2026-07-29 by the first `webconsole-v*` release,
#: closing the ASVS 15.2.4 exposure where README.md instructed an index install of an unclaimed name).
#: An empty set means "nothing unpublished is being advertised", which is exactly what should hold.
#:
#: The set exists for the NEXT distribution: add its name here the moment `packaging/<name>/` lands and
#: before any doc references it, then remove it once the first release claims the name.
#: `test_a_new_distribution_must_be_classified` fails if a packaged distribution is neither listed here
#: nor demonstrably published, so this cannot silently rot back to vacuous.
_UNPUBLISHED_DISTRIBUTIONS: frozenset[str] = frozenset()

#: Distribution names registered on PyPI and published by this repo's release workflow. Listing one
#: here asserts the name is CLAIMED -- the property that makes a bare-name install instruction safe.
_PUBLISHED_DISTRIBUTIONS = frozenset({"messagefoundry-harness", "messagefoundry-webconsole"})

#: Files whose install commands are shipped to, or executed by, someone other than a maintainer.
#: CI workflows and internal handoffs are excluded deliberately: they install from the source tree by
Expand Down Expand Up @@ -156,27 +169,71 @@ def test_the_unpublished_list_does_not_rot() -> None:
)


def test_a_new_distribution_must_be_classified() -> None:
"""Stops the guard rotting back to vacuous now that the unpublished set is empty.

Every distribution under `packaging/` must be either (a) listed in `_UNPUBLISHED_DISTRIBUTIONS`,
so shipped text may not instruct an index install of it, or (b) known-published, so it may. A NEW
`packaging/<name>/` that is neither fails here -- which is the window the ASVS 15.2.4 exposure
lived in: a distribution existed, docs referenced it by name, and nobody had claimed the name.

Mutation: add a `packaging/<new>/` directory without touching this file. Red: named below.
"""
packaged = {
p.name for p in (_ROOT / "packaging").iterdir() if p.is_dir() and not p.name.startswith(".")
}
unclassified = sorted(packaged - _UNPUBLISHED_DISTRIBUTIONS - _PUBLISHED_DISTRIBUTIONS)
assert not unclassified, (
f"packaged distribution(s) classified as neither unpublished nor published: {unclassified}. "
f"Add each to _UNPUBLISHED_DISTRIBUTIONS (and keep docs on a path install) until its name is "
f"registered, then move it to _PUBLISHED_DISTRIBUTIONS. An unclaimed name that shipped docs "
f"reference by name is the dependency-confusion window (ASVS 15.2.4)."
)


#: A name that will never be published, used ONLY to exercise the detector. The parametrized cases
#: below used the real `messagefoundry-webconsole`, so the day that name was claimed on PyPI three of
#: them inverted and the guard-the-guard failed for being RIGHT. The discrimination under test is
#: path-vs-index, which has nothing to do with which names happen to be published.
_SYNTHETIC_UNPUBLISHED = "messagefoundry-notreal"


@pytest.mark.parametrize(
("line", "flagged"),
[
('pip install "messagefoundry-webconsole==0.1.0"', True),
("pip install messagefoundry-webconsole", True),
("uv pip install messagefoundry-webconsole", True),
("pip install -e packaging/messagefoundry-webconsole", False),
("uv pip install --system -e packaging/messagefoundry-webconsole", False),
(f'pip install "{_SYNTHETIC_UNPUBLISHED}==0.1.0"', True),
(f"pip install {_SYNTHETIC_UNPUBLISHED}", True),
(f"uv pip install {_SYNTHETIC_UNPUBLISHED}", True),
(f"pip install -e packaging/{_SYNTHETIC_UNPUBLISHED}", False),
(f"uv pip install --system -e packaging/{_SYNTHETIC_UNPUBLISHED}", False),
('pip install "messagefoundry==0.1.0"', False),
# A PUBLISHED name must NOT be flagged, whatever form it takes -- the whole point of claiming
# a name is that a bare-name install of it becomes safe to instruct.
('pip install "messagefoundry-webconsole==0.2.15"', False),
("pip install messagefoundry-webconsole", False),
],
)
def test_the_detector_separates_index_installs_from_path_installs(line: str, flagged: bool) -> None:
"""Guard-the-guard, and the reason it is worth having: the whole value of the check above is the
path-vs-index distinction. A detector that flagged both would be turned off; one that flagged
neither would be decorative. This pins the boundary in both directions, with no file I/O, so it
keeps working wherever the suite runs."""
keeps working wherever the suite runs -- and against a SYNTHETIC unpublished name, so it does not
invert the day a real name is claimed."""
unpublished = _UNPUBLISHED_DISTRIBUTIONS | {_SYNTHETIC_UNPUBLISHED}
hits = [
m
for m in _BARE_NAME_INSTALL.finditer(line)
if "-e" not in m["flags"]
and "--editable" not in m["flags"]
and m["name"].lower().replace("_", "-") in _UNPUBLISHED_DISTRIBUTIONS
and m["name"].lower().replace("_", "-") in unpublished
]
assert bool(hits) is flagged, f"{line!r}: expected flagged={flagged}, got {bool(hits)}"


def test_the_synthetic_probe_name_is_not_a_real_distribution() -> None:
"""The probe above is only meaningful while its name is fictional. If a `packaging/` directory ever
uses it, the parametrized expectations silently stop testing what they claim to."""
packaged = {p.name for p in (_ROOT / "packaging").iterdir() if p.is_dir()}
assert _SYNTHETIC_UNPUBLISHED not in packaged, (
f"{_SYNTHETIC_UNPUBLISHED!r} is now a real distribution — pick another fictional probe name"
)
Loading