fix: assert which aws binary runs, because this script rewrites the whole config
Build and deploy / build-and-deploy (push) Failing after 4s
Build and deploy / build-and-deploy (push) Failing after 4s
Round 2 returned an addendum after the previous commit, having re-checked its
own instrument. Two findings, one blocking on the next --apply.
This machine carries two AWS CLIs on PATH: /opt/homebrew/bin/aws 2.34.53 and
/usr/local/bin/aws 2.11.15 (April 2023). configure.mjs called bare `aws`, so
PATH decided. That matters because update-distribution is a full replace and
botocore parses a config against its own model, dropping members it does not
know — an old CLI reads a lossy config and writes the loss back, and --if-match
cannot catch it because the ETag is genuinely current.
Measured, which the review could not do: the older model is missing nine
members, and E1OK7G98KNKUTA carries two of them — GrpcConfig on the default
behaviour and on /api/*, both {Enabled: false}. So a round trip through the old
CLI would write back the same effective value and change nothing observable
today. That is precisely why it needed a guard rather than a look: nothing
reports it when that stops being true.
The script now resolves the binary, prints it and its version as the first line
of output, and exits 2 below a floor before any AWS call. Verified both ways.
Second finding: entry (av) claimed "no client-side validation of any kind"
stood between the 182-character Comment and the API, from reading one
validate.py without naming which CLI it came from — and the other install is
frozen, so it is unchecked rather than confirmed. That is CLAUDE.md's named
shape. Both records now name the instrument and version and lead with the claim
that needs no qualification: the Comment reached the API and came back
InvalidArgument, so nothing stopped it on the CLI that ran.
The second install's model independently confirms FunctionARN max 108, its
pattern, and the 128 on both Comment members.
Nothing was applied to the distribution and nothing was deployed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Md3GndFqWPzK78xAoebsg5
This commit is contained in:
co-authored by
Claude Opus 5
parent
a07193d561
commit
07a8ff6989
@@ -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
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user