Skip to content

fix: make a first run diagnose itself instead of failing quietly - #41

Open
snad1 wants to merge 4 commits into
mainfrom
fix/cli-first-run-diagnostics
Open

fix: make a first run diagnose itself instead of failing quietly#41
snad1 wants to merge 4 commits into
mainfrom
fix/cli-first-run-diagnostics

Conversation

@snad1

@snad1 snad1 commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

What prompted this

A first-run session that failed at every step with one line, three times:

Could not reach the Provx server at http://localhost:8000.

The server was not running, so nothing here was broken. But the session surfaced
four defects worth fixing, and one of them was silently dangerous.

The changes

The unreachable message was a dead end. It named the fact and stopped: no
cause, no next step. It now classifies the cause from the failure's type -
nothing listening or unresolvable, no answer in time, or a generic connection
failure - and says what to try. The exception's own text stays out of it; it
carries connection internals an operator cannot act on (PX-ERRORS).

A password was typed for nothing, twice. provx login and provx admin create prompted first and discovered the unreachable server afterwards. They now
check the server is there immediately before the prompt appears, and only then,
so a piped or --password-env password still costs exactly one request. Nothing
changes about how the secret is read (PX-SECRETS).

An engagement could be created that scans nothing and calls it a pass. This is
the one that matters. scope_allow: ["nidlo-stage"] with target
https://nidlo-stage.snadstudio.com was accepted. An allow rule matches an exact
host or, as *.domain, a domain and its subdomains, so that engagement covered
nothing: every target skipped at scan time, run reported completed, zero
findings. A scope mistake that reads as a passing result is the worst failure this
product can have. POST /engagements now refuses it.

The check reuses the SDK matcher rather than restating it: ScopePolicy grows
covers_host, holding the deny-first decision is_in_scope already made, and
is_in_scope delegates to it (PX-SCOPE, Q-11). The dangerous-range gate is
deliberately not applied at creation - a loopback or RFC-1918 target is legitimate
to configure, and refusing to reach one belongs at scan time where the
override is logged. Targets are named by index, never quoted back: the 422 travels
through the handler that exists to stop submitted input landing in proxy logs and
CI output (PX-SECRETS).

Compose could not start on a machine that already runs Postgres or Redis. The
published ports collided and the only fix was editing a tracked file. Both are now
${POSTGRES_PORT:-5432} / ${REDIS_PORT:-6379}, matching what BACKEND_PORT and
FRONTEND_PORT already did. Defaults unchanged, container ports unmoved, so
DATABASE_URL and REDIS_URL are unaffected.

Tests

647 passed, ruff and mypy --strict clean.

  • Each httpx failure class produces its own cause phrase and exit 4, and the
    exception text never reaches the output
  • login / admin create against an unreachable server read no password at all;
    a piped password still makes exactly one request
  • covers_host matches exact host and *.domain, deny beats allow, empty allow
    permits nothing
  • Creation refuses an uncovered target, a denied target, and a non-web URL; the
    quickstart pair, a wildcard, a path-bearing URL, and a lab target still create;
    the 422 body contains neither the target URL nor the allow rule
  • The two scan-time enforcement tests now plant their out-of-scope target directly
    in the database, so they test the gate that has to hold rather than the
    validator in front of it

Verified against a live stack

$ provx engagement create --name "Nidlo stage demo" --scope nidlo-stage \
    --target https://nidlo-stage.snadstudio.com
The server rejected the request. targets[0] is outside this engagement's scope; it
matches no scope_allow rule, or a scope_deny rule excludes it. An allow rule is an
exact host, or *.domain to include subdomains [validation_error]

$ provx --server http://localhost:9999 login --username root
Could not reach the Provx server at http://localhost:9999: nothing is listening
there, or the host does not resolve. Start it with 'docker compose up', or point at
another server with --server URL or $PROVX_SERVER.

The second prints no password prompt.

Docs

docs/QUICKSTART.md §6 demonstrated the old permissive behaviour, so it is
rewritten around the refusal; four troubleshooting rows added. packages/cli/README.md
notes that pipx install does not overwrite an existing install.

snad1 added 4 commits August 18, 2026 10:44
…password

Three commands in a row printed "Could not reach the Provx server at
http://localhost:8000." and stopped there. The message named the fact and
nothing else: no cause, no next step, and in two of those three cases only
after the operator had already typed a password at a hidden prompt.

The failure now carries a cause classified from the exception's type -
nothing listening or unresolvable, no answer in time, or a generic
connection failure - plus what to do next. The exception's own text stays
out of it: it carries connection internals an operator cannot act on
(rule PX-ERRORS).

`login` and `admin create` now check the server is there immediately
before the prompt appears, and only then, so a piped or --password-env
password still costs exactly one request. Nothing changes about how the
secret is read (rule PX-SECRETS).

Also strips Pydantic's "Value error, " prefix from a 422 message, which
reads as noise in front of a sentence written for the operator.

Signed-off-by: Solomon Nii Amu Darku <snad1@users.noreply.github.com>
…gets

`scope_allow: ["nidlo-stage"]` with target https://nidlo-stage.snadstudio.com
was accepted. An allow rule matches an exact host or, as *.domain, a domain
and its subdomains, so that engagement covered nothing: every target was
skipped at scan time and the run reported completed, zero findings. A scope
mistake that reads as a passing result is the worst failure this product can
have, so it is now refused at the point it is made.

The check reuses the SDK matcher rather than restating it: ScopePolicy grows
covers_host, holding the deny-first rule decision that is_in_scope already
made, and is_in_scope delegates to it (rules PX-SCOPE, Q-11). The
dangerous-range gate is deliberately not applied at creation - a loopback or
RFC-1918 target is legitimate to configure, and refusing to reach one belongs
at scan time where the override is logged.

Targets are named by index, never quoted back. The 422 travels through the
handler that exists to stop submitted input being reflected into a response
body that lands in proxy logs and CI output (rule PX-SECRETS).

The two scan-time enforcement tests now plant their out-of-scope target
directly in the database, which tests the gate that has to hold rather than
the validator in front of it.

Signed-off-by: Solomon Nii Amu Darku <snad1@users.noreply.github.com>
…g the file

A developer machine that already runs Postgres on 5432 or Redis on 6379 could
not bring the stack up: the published ports collided, and the only fix was an
edit to a tracked file. Both are now `${POSTGRES_PORT:-5432}` and
`${REDIS_PORT:-6379}`, matching what BACKEND_PORT and FRONTEND_PORT already do.

Defaults are unchanged, and the container ports do not move, so DATABASE_URL
and REDIS_URL are unaffected either way.

Signed-off-by: Solomon Nii Amu Darku <snad1@users.noreply.github.com>
The config-drift guard requires every variable injected into the backend to be
either read by Settings or listed as intentionally excluded. POSTGRES_PORT and
REDIS_PORT are host port mappings that Compose consumes and the app never sees,
which is exactly what BACKEND_PORT and FRONTEND_PORT already record.

Signed-off-by: Solomon Nii Amu Darku <snad1@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant