Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
124 commits
Select commit Hold shift + click to select a range
3144ae8
Ignore the add-on per checkout, and derive the loopback file
generoi-deploy Aug 30, 2026
7ba9d63
Fix the install regression, and test the three fixes nothing tested
generoi-deploy Aug 30, 2026
72d9123
The upgrade hook, and asking the container instead of grepping files
generoi-deploy Aug 30, 2026
61a3a41
Order the copy-db tests so the first copy still runs against an empty…
generoi-deploy Aug 30, 2026
4b4c3c2
Model the URL parser, not just the bytes
generoi-deploy Aug 30, 2026
ac2266c
Make --slug stick, and stop removal reaching into sibling worktrees
generoi-deploy Aug 30, 2026
cd4d964
Reject over-long labels, and say what removal leaves behind
generoi-deploy Aug 30, 2026
3489673
Locate the authority instead of guessing where it starts
generoi-deploy Aug 30, 2026
34fbdc7
Offer the locator every URL in a value, not only the first
generoi-deploy Aug 30, 2026
fe2e064
Fold hosts the way the browser does, and stop init reporting success …
generoi-deploy Aug 30, 2026
7be9371
Make both matchers reach every surface, and stop the narrowing inverting
generoi-deploy Aug 30, 2026
6a2d242
Bound the host scan, reach the request direction, and apply the web c…
generoi-deploy Aug 30, 2026
ae741b8
Give the browser model an oracle, and fix the two leaks it found
generoi-deploy Aug 30, 2026
a155028
Measure scaling, since none of the four quadratics changed a byte of …
generoi-deploy Aug 30, 2026
a4cb8a8
Carry scheme and port through the splice, and stop guessing either
generoi-deploy Aug 30, 2026
97d3d07
Drive `check` past the container gate, which nothing had ever done
generoi-deploy Aug 30, 2026
816bd05
Rewrite text/plain and XML, scan every project, and make check ask
generoi-deploy Aug 30, 2026
5aae351
Retry the probe, so a cold start is a race rather than a refusal
generoi-deploy Aug 30, 2026
d219645
Add the second base scheme the oracle was missing
generoi-deploy Aug 30, 2026
f8e3d51
Stop guarding the URL model with three narrower models
generoi-deploy Aug 30, 2026
d8b6103
Cross the host mutators, and make non-ASCII a host byte
generoi-deploy Aug 30, 2026
9657b5a
Stop the new checks crying wolf, and make `rewrite` the same engine a…
generoi-deploy Aug 30, 2026
7f564aa
Wait for the router before asserting what it serves
generoi-deploy Aug 30, 2026
cb3446a
Read every spelling the forward pass can emit, and decode where the p…
generoi-deploy Aug 31, 2026
b93a725
Ask the scheme question per host, not per map
generoi-deploy Aug 31, 2026
1dac433
Ask the application which database it reads, and stop diff passing on…
generoi-deploy Aug 31, 2026
8d2940d
Round-trip every corpus shape through every surface, not just href
generoi-deploy Aug 31, 2026
8ff7e09
Make the instruments measure what they report
generoi-deploy Aug 31, 2026
69dee2e
Put the two encodings above the URL parser into the corpus
generoi-deploy Aug 31, 2026
f49ffd3
Correct the record on how a reference-encoded origin is covered
generoi-deploy Aug 31, 2026
4d28039
Catch composed encodings, and stop the checks pointing at remedies th…
generoi-deploy Aug 31, 2026
8c01f37
Demote the database gate: keep the signal, give up the authority
generoi-deploy Aug 31, 2026
2029ac6
Stack the views, bound the bracket scans, and make every view report
generoi-deploy Aug 31, 2026
ffdf1f9
Make the corpus say how much it covers, and the verdict match the proxy
generoi-deploy Aug 31, 2026
4c4aec8
Assert Validate's fixed-point promise where the views are reachable
generoi-deploy Aug 31, 2026
5dd3a30
Run wp-cli from the docroot, and compare declarations instead of file…
generoi-deploy Aug 31, 2026
a22e0f8
Ask Docker for the rivals the directory scan cannot see
generoi-deploy Aug 31, 2026
d5e8c36
Name the project the way DDEV names it, in the new collision scan too
generoi-deploy Aug 31, 2026
570fd41
Move the last two request-body arms to the request direction
generoi-deploy Aug 31, 2026
db58100
Share the reference decode, and make the allocation ceiling a tracked…
generoi-deploy Aug 31, 2026
6892491
Compare canonical origins, and test overlap rather than equality
generoi-deploy Aug 31, 2026
d81ac74
Undo an allocation optimisation that was a leak, and count what is re…
generoi-deploy Aug 31, 2026
128d9fa
Let the database settle the parent-declares question, and stop refusi…
generoi-deploy Aug 31, 2026
5176a0c
Strip reference-spelled controls in every decoder, not only the fall-…
generoi-deploy Aug 31, 2026
53e2dc9
Stop the WP_HOME warning dying to SIGPIPE, and let the subset test speak
generoi-deploy Aug 31, 2026
7f3eec3
Make the diff scorer run the proxy's arms, and stop corrupting serial…
generoi-deploy Aug 31, 2026
d171bdb
Repair serialized length prefixes instead of skipping the body
generoi-deploy Aug 31, 2026
659ae87
Recurse on the input, scan both spellings in one pass, and repair eve…
generoi-deploy Aug 31, 2026
691d9d4
Decline a stale length rather than believe a false boundary
generoi-deploy Aug 31, 2026
afc712e
Walk the grammar, and refuse a parse that leaves a string's tail behind
generoi-deploy Aug 31, 2026
5b39788
Replace a test that could not fail, and pin the depth-limit decline
generoi-deploy Aug 31, 2026
9c4808c
Repair only a value that occupies its whole field
generoi-deploy Aug 31, 2026
3aeec6d
Scope a decline to one field, and commit before declining on shape
generoi-deploy Aug 31, 2026
fca97c3
Assert served serialized payloads parse, and handle the rest of the g…
generoi-deploy Aug 31, 2026
f4e02dc
Rewrite custom payloads, and take field boundaries from the content type
generoi-deploy Aug 31, 2026
6d0db63
Repair in the HTML arm too, and stop blaming the proxy for the database
generoi-deploy Aug 31, 2026
ee65a8a
Pair the delimiters, and refuse to measure what cannot be measured
generoi-deploy Aug 31, 2026
50d56b9
Repair the serialized spelling Elementor actually ships
generoi-deploy Aug 31, 2026
a1a7bf0
Look for a nested payload wherever it starts, not only at offset zero
generoi-deploy Aug 31, 2026
cdf74a1
Measure what a decline was quietly getting wrong
generoi-deploy Aug 31, 2026
214784e
Report every note a page earned, and say what went wrong in the summary
generoi-deploy Aug 31, 2026
b0b2be3
Branch only where the readings actually diverge, and measure where the
generoi-deploy Aug 31, 2026
a7b47bb
Look inside a string the way the string is actually written
generoi-deploy Aug 31, 2026
d383140
Fix the percent openers, which had never fired
generoi-deploy Aug 31, 2026
0dd9d34
Stop treating `"` as ambiguous under esc_attr
generoi-deploy Aug 31, 2026
7b8e25d
Match PHP inside a string, and repair the request line and headers
generoi-deploy Aug 31, 2026
e5a9f25
See a payload glued to the label in front of it
generoi-deploy Aug 31, 2026
e430b7b
Let the enclosing parse choose between readings
generoi-deploy Aug 31, 2026
43a4f0e
Settle what a Set-Cookie carries, since PLAN said both things
generoi-deploy Aug 31, 2026
677673b
Run the decoder views from inside the serialized walk, on every path
generoi-deploy Aug 31, 2026
3b9913f
Re-emit a delimiter exactly as it arrived
generoi-deploy Aug 31, 2026
dbdfc2c
Ask the decoder that finds hosts where a field ends
generoi-deploy Aug 31, 2026
c8fa558
Read a value that was HTML-escaped and then percent-encoded
generoi-deploy Aug 31, 2026
c6052d0
Close the rest of the transport-by-escaping product
generoi-deploy Aug 31, 2026
f7ed4c5
Read the hex spelling of a JSON quote, and stop enumerating
generoi-deploy Aug 31, 2026
8584711
Keep a proxy flag the developer set by hand
generoi-deploy Aug 31, 2026
fec762b
Say when a value was rewritten in a spelling nothing could read
generoi-deploy Aug 31, 2026
83f0a8a
Rebuild the unread-value check, which was wrong in both directions
generoi-deploy Sep 1, 2026
7762bf9
Make check look at the page it already fetched
generoi-deploy Sep 1, 2026
849f0a8
Give check a verdict, and ask the engine what an origin looks like
generoi-deploy Sep 1, 2026
bee6822
State what a decline actually promises
generoi-deploy Sep 1, 2026
a400b53
Say so before crawling the client's live site
generoi-deploy Sep 1, 2026
8346569
Narrow the scorer's self-redirect carve-out to the proxy's
generoi-deploy Sep 1, 2026
c97ff7d
Dial this project's web container, not whichever one answers
generoi-deploy Sep 1, 2026
3f29c9d
Make the guardrail and the dialer ask one function
generoi-deploy Sep 1, 2026
fd90503
Name the hostname DDEV advertises; stop failing on one dns-prefetch
generoi-deploy Sep 1, 2026
b2f4044
Bound the line-count check instead of deleting it; fix the note's con…
generoi-deploy Sep 1, 2026
0db7ec6
Check that loopback containment exists; pair diff's bases by site
generoi-deploy Sep 1, 2026
49ea066
Ask the size question in bytes; stop reports asserting what they skipped
generoi-deploy Sep 1, 2026
4d464de
Fit each condition to its defect; warn when the image predates the en…
generoi-deploy Sep 1, 2026
b58848a
Read the escape WordPress writes into every block delimiter
generoi-deploy Sep 1, 2026
3e052b8
Give each budget its own bound, and stop the suite failing on a slow …
generoi-deploy Sep 1, 2026
cebe774
Read the escape an IDN needs, and stop the safety notice arming the trap
generoi-deploy Sep 1, 2026
835234f
Preserve the spelling an IDN was declared with, on the way back
generoi-deploy Sep 1, 2026
8de582d
Change all three arms, not two, and preserve the spelling actually wr…
generoi-deploy Sep 1, 2026
90432b3
Make the bases the map, and write the separator back as it was found
generoi-deploy Sep 1, 2026
0a5cc36
Refuse on measured production links, and read the separator's bytes
generoi-deploy Sep 1, 2026
7518510
Stop enumerating separator encodings; copy the span verbatim
generoi-deploy Sep 1, 2026
ccca0c4
Ask the page, not the map; and a JSON escape is not a host boundary
generoi-deploy Sep 1, 2026
c146fba
A backslash is not one thing: give the escape alphabet its surface
generoi-deploy Sep 1, 2026
2810244
A list is not a rule: finish round 54's surface split, and stop follo…
generoi-deploy Sep 2, 2026
76c8b47
Draw the grid, and see the states check could not see
generoi-deploy Sep 2, 2026
94151d8
One alphabet, one window, and a refusal that fits the state it names
generoi-deploy Sep 2, 2026
bdbd41b
A census nobody printed, a scorer that agreed with itself, and a fix …
generoi-deploy Sep 2, 2026
395d131
Both auditors found nothing large; here is what they found instead
generoi-deploy Sep 2, 2026
941399c
The remedy queried the wrong database, and text/plain is not a styles…
generoi-deploy Sep 2, 2026
959332a
A counter where the parser has a stack, and it failed toward the leak
generoi-deploy Sep 2, 2026
0d0b29a
An `<mtext>` inside `<svg>` is not MathML's, and I read it as HTML
generoi-deploy Sep 2, 2026
3ca9a5a
A vocabulary is not a tag name, a port is not a string, and a form en…
generoi-deploy Sep 2, 2026
383b3a8
`<style` is a tag name, `*` is not `%2A`, and a field can hold two sp…
generoi-deploy Sep 2, 2026
74e6efa
A grid instead of examples, the mglyph carve-out, and a splice instea…
generoi-deploy Sep 2, 2026
5a5d392
An attribute named `=`, a widget in base64, and a report that only lo…
generoi-deploy Sep 2, 2026
bf5a3ff
The detector I wrote to end the silence was itself silent
generoi-deploy Sep 2, 2026
6425b7e
A detector pointed one way, and a leak PLAN said was measured that is…
generoi-deploy Sep 2, 2026
60c797b
`base64` is six characters, so the header silenced the detector it wa…
generoi-deploy Sep 2, 2026
493cf1b
A bare origin at the end of a line was never mapped, in either direction
generoi-deploy Sep 2, 2026
254c3a2
The question was never value-or-prose: it is whether anything parses …
generoi-deploy Sep 2, 2026
014ab80
The locator was never on the axis, and the sweep is not always backin…
generoi-deploy Sep 3, 2026
dba5606
The JSON arm called the locator by a surface name that was not its own
generoi-deploy Sep 3, 2026
1763c36
A tab is two things at once, and the report cannot be its own witness
generoi-deploy Sep 3, 2026
68f4f32
Two arms of one gate, and a pointer that fired on the pages it was me…
generoi-deploy Sep 3, 2026
10b8017
The position map was 8 bytes a byte to carry one fact every 272
generoi-deploy Sep 3, 2026
80a8d0d
Rename client fixtures, scrub the corpus, and stop the add-on suite r…
generoi-deploy Sep 3, 2026
bbda333
Untrack the design record and the pilot notes
generoi-deploy Sep 3, 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
12 changes: 10 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,16 @@ jobs:

# The add-on command is shell and holds the opinions the binary refuses
# to: the slug, which checkout the map comes from, which hostnames web
# keeps. It needs no Docker and no DDEV, so it belongs here rather than in
# the e2e job that takes twenty minutes.
# keeps. It needs no Docker, no DDEV and no network, so it belongs here
# rather than in the e2e job that takes twenty minutes.
#
# The no-network half of that was untrue until it first ran here: seventy-
# four `check` cells left the real curl on PATH and asked the machine
# whether the variant answered, so eleven of them passed on a developer's
# laptop — where a local DDEV router answers on 443 — and failed on a
# runner where nothing does. The suite now poisons curl behind the fake one
# and asserts nothing fell through, so the property this comment claims is
# checked rather than assumed.
- name: the add-on command
run: make test-addon

Expand Down
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,11 @@

# local agent scaffolding, not part of the design record — PLAN.md is authoritative
/GOAL.md

# The design record and the pilot notes. Kept out of the public repo because
# they are written against named client deployments; the comments throughout the
# code cite them by section, and those citations still name the decision even
# where the document itself is not here to open.
/PLAN.md
/docs/m0-preflight.md
/docs/m6-pilot.md
235 changes: 200 additions & 35 deletions README.md

Large diffs are not rendered by default.

153 changes: 153 additions & 0 deletions cmd/hostshift/audit_r41_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package main

import (
"strings"
"testing"
)

// The live-crawl guardrail and the dialer must agree about what `--resolve`
// covers, and they normalise in opposite directions.
//
// cmdDiff decides whether to warn with:
//
// if ok && strings.EqualFold(h, cb.Hostname()) && p == port {
//
// while corpus.Run's DialContext decides whether the fetch is redirected with
// an exact map lookup:
//
// if to, ok := o.Resolve[addr]; ok {
//
// where `addr` is what net/http hands the dialer — which is the *punycode*
// host (Transport calls idnaASCII on the URL host before it builds the connect
// address) with its case preserved.
//
// So the guard folds case and the dialer does not; the dialer folds IDNA and
// the guard does not. Both directions are wrong, and one of them is the
// direction that matters: the guard reports "covered" and stays silent while
// the crawl falls through to real DNS and fetches -n pages from the client's
// live production site. That is what HEAD's own commit message calls the
// unacceptable failure — "a guardrail a typo can disable is worse than none,
// because it reads as confirmation" — for the two spellings it did check, and
// it is still true for these two.
//
// Measured against `www.hämeenlinna.fi` on the machine this was written: with
// `--resolve www.hämeenlinna.fi:443:127.0.0.1:9` hostshift printed no warning
// and the canonical fetch returned `status 301` from the live site. The hosts
// here are under `.invalid` (RFC 2606, guaranteed NXDOMAIN) so the test itself
// never leaves the machine.
//
// The assertion is the invariant rather than a table of expected verdicts: the
// warning must be silent exactly when the fetch was in fact redirected. A
// spelling that silences the warning without redirecting the fetch is the leak;
// a spelling that redirects the fetch and warns anyway is the false alarm that
// teaches a developer to scroll past it.
func TestTheLiveCrawlWarningDescribesWhereTheCrawlActuallyGoes(t *testing.T) {
const variant = "https://wt-a--client.ddev.site"
// 127.0.0.1:9 is discard: a redirected fetch fails with "connect:
// connection refused" naming that address, and one that was not redirected
// fails with "no such host". The two are distinguishable in the report,
// which is what makes this test able to check the dialer rather than
// assume it.
const local = "127.0.0.1:9"

cases := map[string]struct{ base, resolve string }{
// The control. Lowercase ASCII, right host, right port: this is the
// spelling the existing test covers, and it works.
"a plain host": {
"https://www.example.invalid",
"www.example.invalid:443:" + local,
},
// An IDN canonical, written the way a person writes it — and the way
// PLAN's own prose writes .fi client domains. net/http punycodes it
// before dialling, so this key never matches.
"an IDN spelled in unicode": {
"https://www.hämeenlinna.invalid",
"www.hämeenlinna.invalid:443:" + local,
},
// The same IDN spelled the way the dialer will ask for it. This one
// does redirect the fetch, and the guard warns anyway.
"an IDN spelled in punycode": {
"https://www.hämeenlinna.invalid",
"www.xn--hmeenlinna-q5a.invalid:443:" + local,
},
// A hostname copied out of somewhere that upper-cased it. EqualFold in
// the guard says covered; the exact map lookup in the dialer does not.
"a host in a different case": {
"https://www.example.invalid",
"WWW.EXAMPLE.INVALID:443:" + local,
},
// The other branch of the invariant, so the table is not degenerate:
// a --resolve that genuinely does not cover this crawl. Every spelling
// above is a *misspelling* of the right host, and once those are folded
// they all redirect — leaving nothing to exercise the "warned, and
// rightly" side. This is the curl mistake the guard exists for.
"a --resolve for another host entirely": {
"https://www.example.invalid",
"other.invalid:443:" + local,
},
}

// Which cases *should* land locally. The invariant below — silent iff the
// crawl went local — is true of a build where the guard and the dialer are
// both wrong in the same way, so it cannot be the only assertion: with the
// resolve map keyed on the raw spelling, the IDN case goes to DNS *and*
// warns, and the invariant holds while the flag does nothing.
shouldLandLocal := map[string]bool{
"an IDN spelled in unicode": true,
"an IDN spelled in punycode": true,
"a host in a different case": true,
}

var redirected, notRedirected int
for name, c := range cases {
code, out, errOut := run(t, "", cmdDiff,
"--canonical-base", c.base,
"--from", "https://www.example.fi", "--to", variant,
"-n", "1", "--timeout", "3s", "--resolve", c.resolve)
_ = code

warned := strings.Contains(errOut, "not pointed anywhere local")
// Where the fetch actually went. Not inferred from the flag — read out
// of the failure the crawl reported.
wentLocal := strings.Contains(out, local)
wentToDNS := strings.Contains(out, "no such host")
if wentLocal == wentToDNS {
t.Fatalf("%s: the crawl's destination is not legible, so this test would "+
"assert nothing:\nstdout:\n%s\nstderr:\n%s", name, out, errOut)
}
if wentLocal {
redirected++
} else {
notRedirected++
}
if want, ok := shouldLandLocal[name]; ok && wentLocal != want {
t.Errorf("%s: --resolve %q names the host being crawled, so the fetch "+
"should have gone to %s; it went to real DNS instead, which means "+
"the flag silently did nothing\nstdout:\n%s", name, c.resolve, local, out)
}

// The invariant. Silent means "this crawl is pointed somewhere local",
// and that has to be true.
if warned == wentLocal {
if wentLocal {
t.Errorf("%s: --resolve %q sent the crawl to %s and hostshift warned "+
"that it was \"not pointed anywhere local\" anyway — a false alarm "+
"on the one message that must stay worth reading\nstderr:\n%s",
name, c.resolve, local, errOut)
} else {
t.Errorf("%s: --resolve %q did NOT cover the crawl — it fell through to "+
"real DNS — and hostshift printed no warning, so under "+
"production-canonical it would have fetched the client's live site "+
"while reading as confirmation that it had not\nstdout:\n%s",
name, c.resolve, out)
}
}
}

// Not a degenerate table: both outcomes are present, so neither branch of
// the invariant above is unreachable.
if redirected == 0 || notRedirected == 0 {
t.Fatalf("the table exercised only one outcome (%d redirected, %d not), "+
"so the invariant was only half-tested", redirected, notRedirected)
}
}
Loading
Loading