diff --git a/AGENTS.md b/AGENTS.md index 355cf2c..006d5a1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -957,6 +957,99 @@ never being raised again. # Change Log +## 2026-09-04 (aw) โ€” Round 2's addendum: this machine has two AWS CLIs 23 minor versions apart, the script called whichever `PATH` chose, and it rewrites the whole distribution config + +**Not a third round.** `adversarial-reviewer` returned an addendum to round 2 +after entry (av) was committed, having gone back to check its own instrument. Two +findings, one blocking on the **next `--apply`** โ€” which is why they are acted on +rather than deferred. Entry (av) stands; nothing in it is retracted. + +--- + +### 1. ๐Ÿ›‘ The script called bare `aws`, and there are two + +`execFileSync('aws', โ€ฆ)` resolves from `PATH`. Measured 2026-09-04: +**`/opt/homebrew/bin/aws` = 2.34.53** and **`/usr/local/bin/aws` = 2.11.15** +(April 2023), both on `PATH`. + +**Why that is not cosmetic here.** `configure.mjs` reads the whole distribution +config, mutates it, and posts the whole thing back โ€” `update-distribution` is a +**full replace** โ€” and botocore parses a response against **its own** model, +dropping members that model does not know. An old CLI therefore reads a lossy +config and writes the loss back. **`--if-match` cannot catch it**: the ETag is +genuinely current; only the parse was stale. + +**The reviewer could not measure the blast radius and said so. It is now +measured.** Comparing the two installs' models for the same API version, the +older one does not know nine members across `DistributionConfig`, +`CacheBehavior` and `Origin` โ€” and `E1OK7G98KNKUTA` **carries two of them**: + +``` +DefaultCacheBehavior.GrpcConfig = {"Enabled": false} +CacheBehaviors[0].GrpcConfig = {"Enabled": false} +``` + +โš ๏ธ **Both are `{Enabled: false}`, which is the default โ€” so a round trip through +2.11.15 would write back the same effective value and change nothing observable +today.** That is the whole reason it needed finding rather than noticing: +**nothing would report it when that stops being true**, and the seven others +(`ConnectionMode`, `VpcOriginConfig`, `CacheTagConfig`, `AnycastIpListId`, +`TenantConfig`, `ViewerMtlsConfig`, `ResponseCompletionTimeout`) are one console +change away from being present. + +**The fix.** The script resolves the binary, reads `aws --version`, and prints +both as its **first line of output** โ€” beside the two `resolved Managed-โ€ฆ` lines +that already exist for the same reason. Below a floor it **exits 2 before any AWS +call**. Verified both ways: `/opt/homebrew/bin/aws (2.34.53)` proceeds; +`PATH=/usr/local/bin:$PATH` prints `/usr/local/bin/aws (2.11.15)` and exits **2** +with **zero** `resolved Managed-` lines, i.e. no call was made. The floor is the +version this was verified against โ€” older fails loudly, which is the safe +direction, and newer passes. **Lower it only with an entry here, never to make a +run go through.** + +### 2. A universal claim from one unnamed instrument + +Entry (av) recorded that *"no client-side validation of any kind stood between +that 182-character string and the CloudFront API"*, from reading +`botocore/validate.py`. Neither the runbook nor the module said **which** CLI +that was, and the second install is PyInstaller-frozen โ€” its `validate.py` is not +extractable, so it is **unchecked, not confirmed**. + +That is the shape `CLAUDE.md` corrected on 2026-09-01: *"no mechanism can X" is a +claim about every mechanism, including the ones you did not enumerate*, and the +preferred form was available. Both places now name the instrument and version โ€” +`aws-cli/2.34.53`'s bundled `validate.py` โ€” say plainly that the other is +unchecked, and lead with the claim that needs no qualification: **the +182-character `Comment` reached the API and came back `InvalidArgument`, so +nothing stopped it on the CLI that actually ran.** + +The same edit carries finding 3's carve-out into the module: every entry **with a +cited AWS source** is enforced by the service and by nothing local; the two +`[assumed]` `Name` entries are not known to be enforced at all. + +โš ๏ธ **This does not weaken `PAYLOAD_LIMITS`** โ€” the direct evidence for it is the +failed call, not the validator read. + +### 3. Corroboration the addendum also produced + +The second install's model independently confirms four constants entry (av) +sourced from one: `FunctionARN` `max: 108` and its pattern, and *"cannot be +longer than 128 characters"* on **both** `Comment` members. Two independent +models, same numbers. + +### 4. Verification + +`check` 0 ยท `build` 0 (23 pages) ยท `check:claims` 0 ยท `check:intake` 0 ยท +`og:proof` 0 ยท `lint` 0 ยท `router.test` 0 (30/30) ยท `policy-shapes.test` 0 +(53/53) ยท `spam-score.test` 0 (39/39) ยท minifier grep 1 (clean) ยท live dry run +**0**, 3 changes, 0 skips ยท stale-CLI refusal **2**, before any AWS call. + +**Nothing was applied and nothing was deployed.** The distribution still shows +`/api/*` on `b689b0a8-โ€ฆ`; the account still holds **1** custom response-headers +policy (the orphan) and **0** origin request policies. + +--- + ## 2026-09-04 (av) โ€” The second `--apply` got one policy further and failed on a 128-character cap that nothing local enforces; the orphan it left is reused by name, measured; the dry run gains the only pre-flight this script has; and ยง7's own record broke the table it lives in **Pouya's ruling of 2026-09-04**, after the second attempt: *"Fix: shorten BOTH diff --git a/docs/09-cutover-runbook.md b/docs/09-cutover-runbook.md index 2792386..3385dcf 100644 --- a/docs/09-cutover-runbook.md +++ b/docs/09-cutover-runbook.md @@ -329,6 +329,28 @@ dump** of the `*.pdf` behaviour it would add, 2 blank lines, the `N change(s) to distribution โ€ฆ` header and the `DRY RUN โ€” nothing was sent.` footer `[measured 2026-09-04, exit 0, nothing written]`. +๐Ÿ›‘ **THE FIRST LINE OF OUTPUT NAMES THE `aws` BINARY AND ITS VERSION. READ IT.** +This script **rewrites the whole distribution config**, and botocore drops +members its own model does not know โ€” so an old CLI reads a lossy config and +writes the loss back, on a distribution serving 23 pages and the intake form. +`--if-match` cannot catch that: the ETag is genuinely current. + +โš ๏ธ **THIS MACHINE HAS CARRIED TWO CLIs**, `2.34.53` and `2.11.15` (April 2023), +both on `PATH` `[measured 2026-09-04]`. The older model does not know +`GrpcConfig`, and `E1OK7G98KNKUTA` carries it on **two** behaviours โ€” both +`{Enabled: false}`, so the round trip is lossless *in effect* today, and nothing +would report it when that stops being true. It is also missing `ConnectionMode`, +`VpcOriginConfig`, `CacheTagConfig` and five more. The script now **refuses below +a floor** rather than leaving it to `which`: + +``` +resolved aws = /opt/homebrew/bin/aws (2.34.53) +``` + +If that line names `/usr/local/bin/aws` or a version below the floor, the run +exits **2 before any AWS call**. Run `which -a aws` and fix `PATH` โ€” do not +lower the floor to get through. + ๐Ÿ›‘ **READ THE EXIT STATUS, AND IT HAS THREE VALUES.** `0` โ€” everything this script manages was applied or is already present. `2` โ€” a usage error, before any AWS call. **`3` โ€” sections that could run did, and something was SKIPPED: read @@ -507,13 +529,18 @@ adding `Comment` to the drift check would throw on this very policy and block the run that attaches it. **WHY NOTHING CAUGHT IT LOCALLY, AND THIS IS THE GENERAL LESSON.** Measured -2026-09-04: **`botocore/validate.py` checks neither `max` nor `pattern`** โ€” its -`range_check()` reads only `min`, and the word `pattern` does not appear in the -file. And the 128 cap is not modelled as a constraint at all: on both policy -configs `Comment` is a bare `string`, and the number lives in the shape's -**`documentation` prose**. So **no client-side validation of any kind stood -between that 182-character string and the CloudFront API**, which is exactly why -the pre-flight below had to be built rather than relied upon. +2026-09-04 **against `aws-cli/2.34.53`'s bundled `botocore/validate.py`**: it +checks **neither `max` nor `pattern`** โ€” `range_check()` reads only `min`, and +the word `pattern` does not appear in the file. And the 128 cap is not modelled +as a constraint at all: on both policy configs `Comment` is a bare `string`, and +the number lives in the shape's **`documentation` prose**. + +โš ๏ธ **THAT NAMES ONE VALIDATOR, DELIBERATELY.** This machine also carries +`aws-cli/2.11.15`, which is PyInstaller-frozen and whose `validate.py` cannot be +read โ€” unchecked, not confirmed. The claim that holds without qualification is +the narrower and more useful one: **the 182-character `Comment` reached the API +and came back `InvalidArgument`, so nothing stopped it on the CLI that ran.** +That is why the pre-flight below had to be built rather than relied upon. **BOTH POLICY COMMENTS ARE NOW UNDER 80 CHARACTERS**, and the dry run enforces the limits it knows about โ€” `infra/cloudfront/policy-shapes.mjs`, diff --git a/infra/cloudfront/configure.mjs b/infra/cloudfront/configure.mjs index d922874..de9f15c 100644 --- a/infra/cloudfront/configure.mjs +++ b/infra/cloudfront/configure.mjs @@ -131,6 +131,65 @@ if (args.includes('--function-arn')) { } } +/** + * ๐Ÿ›‘ **WHICH `aws` RAN IS PART OF THE RESULT, BECAUSE THIS SCRIPT DOES A FULL + * READ-MODIFY-WRITE.** `update-distribution` replaces the whole config, and + * botocore parses the config it reads against **its own** model, silently + * dropping members that model does not know. So an old CLI reads a lossy config + * and writes the loss back โ€” on a distribution serving 23 public pages and the + * intake form. `--if-match` cannot catch it: the ETag is genuinely current. + * + * โš ๏ธ NOT HYPOTHETICAL ON THIS MACHINE. Measured 2026-09-04: two CLIs on PATH, + * `2.34.53` and `2.11.15` (April 2023). The older model does not know + * `GrpcConfig`, and `E1OK7G98KNKUTA` carries it on **two** behaviours. Both are + * `{Enabled: false}` today โ€” the default โ€” so the round trip happens to be + * lossless in effect, and **nothing would report it when that stops being true.** + * The old model is also missing `ConnectionMode`, `VpcOriginConfig`, + * `CacheTagConfig` and five more. + * + * The floor is the version this was verified against. Older fails loudly, which + * is the safe direction; newer passes. Lower it deliberately, with a Change Log + * entry, never to make a run go through. + */ +const AWS_CLI_FLOOR = [2, 34, 53]; +const awsBinary = execFileSync('command', ['-v', 'aws'], { + encoding: 'utf8', + shell: '/bin/sh', + stdio: ['ignore', 'pipe', 'inherit'], +}).trim(); +const awsVersionLine = execFileSync('aws', ['--version'], { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'inherit'], +}).trim(); +const awsVersion = (awsVersionLine.match(/aws-cli\/(\d+)\.(\d+)\.(\d+)/) ?? []) + .slice(1) + .map(Number); +if (awsVersion.length !== 3) { + console.error(`could not read a version out of: ${awsVersionLine}`); + process.exit(2); +} +const belowFloor = (() => { + for (let i = 0; i < 3; i += 1) { + if (awsVersion[i] !== AWS_CLI_FLOOR[i]) + return awsVersion[i] < AWS_CLI_FLOOR[i]; + } + return false; +})(); +console.log( + `resolved aws = ${awsBinary} (${awsVersion.join('.')})`, +); +if (belowFloor) { + console.error( + `\naws-cli ${awsVersion.join('.')} is below the floor ${AWS_CLI_FLOOR.join('.')}.\n` + + `This script rewrites the WHOLE distribution config, and botocore drops config ` + + `members its own model does not know โ€” so an old CLI reads a lossy config and ` + + `writes the loss back. Measured 2026-09-04: 2.11.15 does not know GrpcConfig, ` + + `which this distribution carries on two behaviours.\n` + + `Resolved binary: ${awsBinary}. Check \`which -a aws\` โ€” this machine has had two.`, + ); + process.exit(2); +} + /* stderr is NEVER suppressed and the exit status is always read โ€” the AWS CLI reports an expired session, a missing permission and a typo'd id all on stderr with a non-zero status, and swallowing that is how "it failed" becomes diff --git a/infra/cloudfront/policy-shapes.mjs b/infra/cloudfront/policy-shapes.mjs index c78384d..8d2e7c7 100644 --- a/infra/cloudfront/policy-shapes.mjs +++ b/infra/cloudfront/policy-shapes.mjs @@ -77,16 +77,26 @@ export function emptyObjectPaths(value, path = '') { } /** - * โš ๏ธ **NOTHING LOCAL ENFORCES ANY OF THESE, WHICH IS WHY THIS TABLE EXISTS.** - * Measured 2026-09-04: `botocore/validate.py` checks **neither `max` nor - * `pattern`** โ€” `range_check()` reads only `min`, and the word `pattern` does - * not appear in the file โ€” and the caps that matter are not modelled as - * constraints anyway. On both policy configs `Comment` is a bare `string`, and - * the 128 lives in the shape's **`documentation` prose**. So a 182-character - * `Comment` left the machine unremarked and came back `InvalidArgument`, after - * section 4 had already created its policy. **Every limit here is enforced by - * the service and by nothing else**, which makes the dry run the only - * pre-flight there is. `docs/09` Part 3 carries both attempts. + * โš ๏ธ **NO VALIDATOR I COULD READ ENFORCES ANY OF THESE, WHICH IS WHY THIS TABLE + * EXISTS.** Measured 2026-09-04 against **`aws-cli/2.34.53`'s bundled + * `botocore/validate.py`**: it checks **neither `max` nor `pattern`** โ€” + * `range_check()` reads only `min`, and the word `pattern` does not appear in + * the file. And the caps that matter are not modelled as constraints anyway: on + * both policy configs `Comment` is a bare `string`, and the 128 lives in the + * shape's **`documentation` prose**. + * + * โš ๏ธ **THAT IS ONE INSTRUMENT, NAMED, NOT A CLAIM ABOUT EVERY MECHANISM.** This + * machine also carries `aws-cli/2.11.15`, whose install is PyInstaller-frozen + * and whose `validate.py` cannot be read, so it is **unchecked rather than + * confirmed**. `CLAUDE.md`: *"no mechanism can X" is a claim about every + * mechanism, including the ones you did not enumerate* โ€” so the honest form is + * this one. What is **direct evidence** either way: the 182-character `Comment` + * reached the API and came back `InvalidArgument`, so nothing stopped it on the + * CLI that actually ran. `docs/09` Part 3 carries both attempts. + * + * Every entry below **with a cited AWS source** is therefore enforced by the + * service and by nothing local. The two `[assumed]` entries are not known to be + * enforced at all. * * โš ๏ธ **THE ENTRIES THAT MATTER MOST GUARD *CLONED* VALUES, NOT LITERALS THIS * FILE AUTHORS.** A literal we write is reviewed when it is written; a value