Skip to content

Answer one address per route, and name a destination the operator can read - #41

Open
Deep-Sixed wants to merge 3 commits into
mainfrom
claude/confident-mayer-865xft
Open

Deep-Sixed wants to merge 3 commits into
mainfrom
claude/confident-mayer-865xft

Conversation

@Deep-Sixed

@Deep-Sixed Deep-Sixed commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

Two defects found by running careersignal serve and driving it, not by reading the diff. Both were green under the existing suite.

Head: 9f1021c
Base: main @ d0a40f9
Two commits and a base merge, five files changed. No behaviour outside the loopback surface changes.

commit what
2abf871 the address rule — server.py, test_web_surface.py, docs/web-surface.md
79d2864 the packet's destination row — screens.js, test_web_frontend.py
9f1021c merge of main after #40 landed

1. A leading // was a working alias for every address

BaseHTTPRequestHandler.parse_request rewrites a leading //… to /… before a handler runs — gh-87389, which stops a client reading a redirect to //host/path as an absolute URI. This surface inherited it, and tail — written to keep empty components rather than filter them — never saw the spelling it refuses, because the rewrite came first.

Observed against a running serve on a50e83c:

GET  //api/v1/session                          200   (also /// and ////)
POST //api/v1/opportunities/{id}/status        409 status_conflict   <- reached the command

Auth, Host, Origin and the token all still applied, so nothing was open — but docs/web-surface.md and tail's own docstring both stated the opposite in absolute terms, and test_the_command_address_is_singular_rather_than_a_family_of_aliases sends /api/v1//…, …//{id}/…, …//status and a trailing slash: every spelling the rewrite leaves alone. The suite was green while the claim was false.

The fix. The target is read from the raw request line and split on the first ?. urlsplit leaves this path with it — its rules are a URI reference's rather than a request target's, so it reads //api/v1/session as the authority api, which is the same alias arriving by a second route. In a request line that string is an ordinary absolute path whose first component is empty: beneath no root, reaching nothing. Origin form is an absolute path, so _origin_form is now a leading /; absolute, authority and asterisk form all begin with something else and are still 400.

target before after
GET //api/v1/session, ///, //// 200 404 No such resource
POST //api/v1/opportunities/{id}/status reached the command 405 No command at this address
GET // 200, the page 404 — // is not /
GET /api/v1/session, GET / 200 200
GET http://host/api/v1/session 400 400
GET /api/v1//session, /api/v1/session/ 404 404

Both new tests fail against the previous behaviour — checked by pointing _target back at self.path:

test_a_leading_empty_component_is_not_a_spelling_of_the_command_address  FAILED
test_a_read_address_is_singular_too                                     FAILED  (200 != 404)

test_nothing_outside_the_packaged_asset_names_can_be_served already parametrised //etc/passwd and expected 404. It passed before only because the rewrite made it /etc/passwd, which is not a packaged asset name; it now passes for the reason it was written for.

The new command test withholds its body, for the reason #40's third commit gives: the refusal is settled on the request line, so the bytes would go unread, and closing on unread bytes is a reset rather than a clean shutdown on Windows.

2. The approval packet published null · null

The destination row concatenated action.provider and action.provider_namespace. Those are what a recorded decision bound, and until one exists the API reports both as null — correctly, because nothing is bound. Concatenated, that put the word "null" twice in the one pane whose stated job is to say what an approval would bind, directly under a note reading "Approving binds this packet to the destination named below" and beside a button reading Approve for controlled. An already-decided row rendered controlled · controlled beside it.

The launch always knew the answer: /session reports decision_target, the re-approval control already compares against it, and the button's label is built from it.

before:  destination   null · null
after:   destination   controlled · controlled

The recorded pair is now read only when there is a decision to read it from; an undecided packet names the launch destination; before the session arrives the row says —. Neither value is computed in the browser.

test_the_packet_names_a_destination_rather_than_publishing_a_missing_one pins the guard and forbids the unguarded concatenation by name, so reintroducing it fails rather than merely reading differently. It fails against the previous line.

Verification on 9f1021c

1688 tests (1686 on main after #40, → 1688), Ruff check and format clean, verify_tree 100 publishable text files / 5 reviewed renders / 1 reviewed screenshot, verify_secrets PASS, verify_wheel PASS.

Both fixes were also re-checked at the surface on a fresh database: the address table above was driven over a raw socket, and the packet was read in headless Chromium — destination reads controlled · controlled undecided and after approving, the page contains no rendered null, and there were no console or page errors.

Not changed here: docs/verification.md still records 1517 tests in its Tests row. That drift predates this branch and is not what either fix is about.

🤖 Generated with Claude Code

https://claude.ai/code/session_012ViK1h7GSCZtShZqeknnVJ

Deep-Sixed and others added 3 commits September 25, 2026 23:32
`BaseHTTPRequestHandler.parse_request` rewrites a leading `//...` to `/...`
before a handler runs (CPython gh-87389, which stops a client reading a
redirect to `//host/path` as an absolute URI). Inherited, that made
`//api/v1/...` a second spelling of every address this surface answers --
reads and the command addresses alike -- while `tail`, which is written to
keep empty components rather than filter them, never saw the spelling it
refuses, because the rewrite had already happened.

Observed on a running `careersignal serve`: `GET //api/v1/session` answered
200, `///` and `////` with it, and `POST //api/v1/opportunities/{id}/status`
reached the command. The existing alias test sends `/api/v1//...`,
`...//{id}/...` and a trailing slash -- every spelling the rewrite leaves
alone -- so it stayed green throughout.

The target is now read from the raw request line and split on the first `?`.
`urlsplit` is gone from this path with it: its rules are a URI reference's
rather than a request target's, so it reads `//api/v1/session` as the
authority `api`, which is the same alias arriving by a second route. In a
request line that is an ordinary absolute path whose first component is
empty; it is beneath no root and reaches nothing. Origin form is exactly an
absolute path, so `_origin_form` is now a leading `/` -- absolute, authority
and asterisk form all begin with something else and are still refused 400.

`//api/v1/session` is 404 and `POST //api/v1/.../status` is 405, the same
answers the other empty-component spellings already gave. Both new tests
fail against the previous behaviour.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ViK1h7GSCZtShZqeknnVJ
The approval packet built its destination row by concatenating
`action.provider` and `action.provider_namespace`. Those are what a
*recorded* decision bound, and until one exists the API reports both as
null -- correctly, because nothing is bound. Concatenated, that published
the string "null · null" in the one pane whose stated job is to say what an
approval would bind, directly under a note reading "Approving binds this
packet to the destination named below" and beside a button reading "Approve
for controlled". Observed in a browser against a running serve; an
already-decided row rendered "controlled · controlled" beside it.

The recorded pair is now read only when there is a decision to read it from.
An undecided packet names the launch destination instead -- `decision_target`
from `/session`, the same session fact the re-approval control already
compares against and the same one the button's label is built from -- so the
note above it is true and the two controls agree. Before the session has
arrived there is nothing to name and the row says so. Neither value is
computed here; both arrive from the server, which is the rule this layer
keeps.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ViK1h7GSCZtShZqeknnVJ
Brings in #40 (the 0003-era upgrade regression). Its change to
test_web_surface.py withholds the body on the one refuse-before-read call
site that still sent it; the new leading-empty-component test added here
already does the same, for the same reason.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ViK1h7GSCZtShZqeknnVJ
@Deep-Sixed
Deep-Sixed marked this pull request as ready for review September 26, 2026 00:07
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