Skip to content

Stop conditions: triage divergence (65% agreement) and a real truncation hole - #18

Merged
amic25 merged 10 commits into
mainfrom
post-merge/queue-1
Jul 29, 2026
Merged

Stop conditions: triage divergence (65% agreement) and a real truncation hole#18
amic25 merged 10 commits into
mainfrom
post-merge/queue-1

Conversation

@amic25

@amic25 amic25 commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Documentation only. No code changed, nothing fixed — both outcomes are declared stop conditions in the queue.

1. Hand triage materially disagrees — 65% strict agreement

Re-triaged a deterministic stratified sample of 20 of the 73 field findings.

13/20 strict agreement. 5/20 divergence, and all five run the same direction: earlier TP, now FP. That is systematic optimism, not noise.

Two recurring causes:

  • Committed ≠ compromisable. A PostHog phc_ project key and a Supabase anon key are published deliberately. Reporting them as Critical "credential compromise" is wrong even though a credential is literally committed.
  • Framework internals ≠ application configuration. allow_delegation = True inside crewAI's own _create_manager_agent is the framework implementing its documented mode.

The claim that "roughly 12 of 17 gating findings are genuine" does not survive. On this sample only four are TPs and two of those are weak.

Caveat stated plainly: this is the same reader re-reading, not an independent triage. A same-reader re-read diverging 25% is a floor on the error, not a measurement of it.

2. Truncation is a real detection hole

The 1,039,776-line equivalence run proved nothing about over-cap lines — the harness contained ln = ln[:4096], truncating every line before comparison. The only population that could show a regression was excluded by construction. Fourth occurrence of this class of harness error.

Only 1 line in 1,039,808 was actually over-cap, so the equivalence claim is barely weakened — but detection past the cap is broken:

key_at_100       AG001 findings=1  truncated_lines=0
key_at_3000      AG001 findings=1  truncated_lines=0
key_at_5000      AG001 findings=0  truncated_lines=1   <-- missed
key_at_20000     AG001 findings=0  truncated_lines=1   <-- missed

A credential past character 4096 of a minified line is not detected. The scan reports truncated_lines=1 and exits 0. Recall defect in the flagship category, on exactly the file shape where inlined keys occur.

Not done

Queue items 2, 4, and 10 consume the field number and are blocked behind a re-triage decision. Items 5–9 are independent but the queue is ordered and both stop conditions fired at items 1 and 3.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Q5nXA9BJKdmye9a9FaHdc6

…ion hole

Re-triaged 20 of the 73 field findings. Strict agreement with the earlier
read is 13/20, and all five divergences run the same direction - earlier
true positive, now false positive. That is systematic optimism, not noise,
so the "roughly 12 of 17 gating findings are genuine" claim does not
survive. The 233 -> 73 headline is directionally right; its quality split
is not trustworthy enough to optimise against.

Two causes recur: a committed credential that is public by design is not a
compromise, and a framework implementing its own documented capability is
not an application granting one.

Separately, the 1,039,776-line equivalence run proved nothing about lines
over the 4096 cap - the harness truncated every line before comparing, so
the only population that could show a regression was excluded. Testing
past the cap shows a credential at offset 5,000 of a minified line is not
detected at all. truncated_lines reports it and the scan still exits 0.
That is a recall hole in the flagship category, on exactly the file shape
where inlined keys occur.

Nothing fixed, per instruction. Also lists the claim surfaces that live
outside version control, where grep cannot reach.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5nXA9BJKdmye9a9FaHdc6

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5081c36939

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread WORKLOG.md
Re-triaged a deterministic stratified sample of 20 of the 73 field findings (round-robin
by rule so every rule appears), reading each with four lines of surrounding context.

**Strict agreement: 13/20 = 65%. Divergence: 5/20 = 25%. Two further partials.**

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exclude changed verdicts from strict agreement

The reported 13/20 strict-agreement rate counts rows #11 and #19 as agreements even though both changed from borderline to TP (weak); meanwhile, rows #6 and #15 changing from borderline to FP are treated as partials. Under the stated strict criterion, only 11 rows retain the same verdict, so the headline agreement rate should be 55%, or the document should define and apply a non-strict equivalence consistently.

Useful? React with 👍 / 👎.

Comment thread WORKLOG.md
Comment on lines +805 to +807
**Methodological caveat, stated plainly:** this is the same reader re-reading, not an
independent triage. Genuine independence needs a second person. A same-reader re-read
diverging 25% is a floor on the error, not a measurement of it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat reread divergence as instability, not an error floor

Without an independent adjudicator or ground truth, a same-reader reread disagreement does not establish a lower bound on classification error: either the earlier or later verdict may be wrong, and both may use the same flawed assumptions. The 25% figure supports label instability, but calling it a floor on error overstates the evidence used to justify this stop condition and directly conflicts with the following claim that it is not an error measurement.

Useful? React with 👍 / 👎.

claude and others added 9 commits July 29, 2026 12:57
…the rules

A credential inlined in a minified bundle sat past the 4096-char bound and
was invisible. That is the flagship category failing on the file shape
where the leak most often is. Bounds now live per rule: AG001 declares
UNBOUNDED and reads lines whole, everything else stays capped.

That is only safe because it was measured. tools/measure_linearity.py
reports growth exponents of 0.98-1.02 for every AG001 pattern, worst case
40ms on a 1MB line - and it validates itself against the pre-fix cubic
AG004 pattern first, exiting without reporting if that control fails to
register. The harness deciding whether a pattern may run unbounded is the
last place a silent pass is acceptable.

Truncation is declared rather than gated. Every report carries which lines
were clipped, by how much, against which bound; --fail-on-incomplete opts
into exit 2. Default exit codes unchanged. The five field projects turn out
to contain 286 lines no bounded rule read in full, 270 in crewAI alone -
previously invisible.

From the triage: a credential published by design is capped at Low with a
message saying so, because reporting a PostHog project key as a critical
compromise is wrong even though a credential is committed. Vendored and
site-packages paths downgrade like fixtures. shell=True with an argument
list now describes the portability bug it actually is - POSIX passes only
argv[0] and discards the rest - instead of an injection risk that is not
there.

Corpus precision falls from 100% to 72.2%, recall unchanged. The rules did
not get worse. Six field false positives are folded in as true negatives,
and the previous 100% was measured against cases written after the bugs
were known. AG003 and AG006 scored perfectly while being ~98-100% false
positives in the field; that gap was a corpus validity failure and is now
five reproducible CI failures, none of them fixed yet.

The 73 field findings ship as a dataset with the upstream commits, the
sampling method, and the same-reader bias stated plainly - including that
all five divergences moved the same direction. It is not cited in the
README and must not be.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5nXA9BJKdmye9a9FaHdc6
…, rewrite README

Three things the previous unit left resting on memory rather than
enforcement.

Coverage computing against the tightest applied bound was caught by
noticing, not by test. Now pinned by two rules bounded at 100 and 4096 over
a 200-char line, verified to fail when min is flipped back to max. The
companion test's docstring claimed it discriminated the same inversion; it
does not, because positive bounds are filtered first. Corrected rather than
left, and it now names the test that does.

measure_linearity discovers unbounded rules and their patterns by
introspection and stresses each with inputs built from its own literal
runs, so a contributor's pattern is gated without anyone remembering to
add it. --check runs in CI. It earned that immediately: the first
env-assignment pattern written in this session used two unbounded
quantifiers around an alternation and measured exponent 2.00 - 918ms on
32KB, roughly 16 minutes extrapolated to 1MB, on a rule that reads lines
unbounded. Rewritten to capture the name once and test it in Python.

Four of the five corpus failures are fixed and the fifth is accepted with
reasoning. AG003's function_map clause is removed rather than narrowed: it
flagged a function map's existence, not an over-broad one, and a clause
that cannot express the breadth the rule is named for does not belong in
it. AG002 needed two fixes - resolving module constants was useless while
the check demanded every argument be fixed, since exec(code, namespace)
passes a globals dict second. AG008 is a strict xfail: separating a tool
deleting a caller's path from a function deleting its own temp file needs
data flow, and a name-based guess would be a blind spot an attacker could
name their way into.

Corpus precision 72.2% -> 93.3%, recall unchanged.

.env is now scanned. A secrets scanner that cannot read the canonical
secrets file is indefensible. Values there are unquoted, which the quoted
pattern misses entirely, so there is an env-only pattern - requiring quotes
is what stops the general one matching password = get_password().

README rewritten around what make bench reproduces. Every number in it was
checked against a command, which caught three wrong claims: the labelled
count, an assertion that every true negative came from real-world
measurement when 6 of 18 did not, and a recall-trade list crediting this
unit with a narrowing from earlier work.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5nXA9BJKdmye9a9FaHdc6
The push carrying 443e34c did not fire a pull_request synchronize event, so
PR #18 shows no checks despite CI passing on a manual dispatch of the same
tree. Empty commit to re-fire the event.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5nXA9BJKdmye9a9FaHdc6
PR #18 was DIRTY, not un-triggered. main advanced with six Dependabot
merges while this branch also edited ci.yml, and pull_request workflows run
against refs/pull/N/merge - which GitHub cannot compute while a branch
conflicts. That, not a dropped webhook, is why no checks reported.

One conflict: this branch inserted the measure_linearity gate immediately
above upload-artifact@v4, and main bumped that action to v7. Kept both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5nXA9BJKdmye9a9FaHdc6
.env support went in with two corpus files and no coverage of the shapes
that actually occur. Adding them found two false positives immediately.

`SERVICE_TOKEN=$OTHER_TOKEN` was reported as a critical committed secret.
The placeholder filter matched `${VAR}` but not a bare `$VAR`, and an
unbraced shell reference is a pointer to a value held elsewhere, not the
value. `sk-proj-replace-this-before-running` in a .env template was
reported too - it carries no word the filter recognised.

Both are value-shape problems, not file-classification ones, so the fix is
in the placeholder filter rather than in how templates are treated. That
distinction matters: a genuine credential committed to .env.example is a
real leak and is still reported, at reduced severity. Suppressing by
filename would have hidden it.

The new .env.local case covers commented-out credentials, empty values,
braced and unbraced interpolation, values under the length floor, and
ordinary configuration - each failing to fire for a different reason.
Verified non-vacuous: reverting the filter drops corpus precision to 82.4%
and names both cases.

AG001 declares UNBOUNDED, so the linearity gate was re-run after touching
its patterns: still linear, 9 patterns across 1 rule.

README now frames 93.3% over 33 files as a regression gate rather than a
precision estimate. A corpus whose negatives were selected from observed
failures is biased towards passing, and saying so is the difference between
evidence and marketing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5nXA9BJKdmye9a9FaHdc6
tests/test_rules.py asserted AG006 should fire on `fetch(url)`. That
assertion was wrong, and a wrong assertion is worse than no assertion,
because it defends the defect against exactly the change that fixes it -
narrowing AG006 broke this test, which is how it surfaced.

It is now tests/corpus/true_negatives/js_fetch_local_url.js, so the rule is
measured on the case rather than asserted about. The positive direction
stays covered by `fetch(user_input)`.

Recorded in DECISIONS.md as the sixth recall trade, and called out as the
sharpest of them: a genuinely attacker-controlled URL reaching `fetch(url)`
is now missed. It was taken because the rule cannot see provenance at all -
`url` is the ordinary name for a variable holding a URL, and treating the
name as evidence produced false positives on every call in the field
sample and true positives on none. Closing that properly needs data flow,
not a longer name list.

The trade table said "five times" while listing six. Fixed.

The README's field-derived-versus-written split is left unquantified here:
the number is currently inferred by sniffing prose in the manifest, which
already miscounts this new entry. Unit C replaces that with an explicit
marker, and the count follows from it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5nXA9BJKdmye9a9FaHdc6
…parately

A corpus whose negatives were selected from failures it already saw is
biased towards passing. The blended 93.3% flatters, and nothing in the
repository distinguished the cases drawn from real projects from the ones
composed while reading the rules.

Every entry now declares origin: field or written, enforced by bench rather
than inferred. The previous inference sniffed prose in `why` for "measured"
or "observed", and it miscounted the moment an entry said "field finding"
instead - which happened one commit ago.

bench --field-only scores the 13 field-derived cases alone, and runs in CI
next to the full pass. Both are in the README, because publishing only the
higher one is the thing this split exists to prevent.

Also added a "behave as labelled" count. Precision over the field-derived
subset is structurally skewed - a field false positive becomes a corpus true
negative, so that subset is nearly all negatives and its precision figure
says less than it appears to. How many cases behave as labelled survives
that: 33 of 34 overall, 12 of 13 field-derived, the same AG008 failure in
both.

Two labels corrected while doing this: .env.local and .env.sample are
`written`, not `field`. The false positives they caught were found by
composing them, not by scanning anything.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5nXA9BJKdmye9a9FaHdc6
DECISIONS.md mentioned this in a clause. It deserves the numbers, because
it is the strongest available argument for the gate being CI rather than a
convention.

The .env pattern written minutes after the gate itself used two unbounded
quantifiers around an alternation - exactly the shape the gate exists to
catch. It measured exponent 2.00, 918ms on 32KB, extrapolating to roughly
16 minutes on one 1MB line, on the only rule that reads lines unbounded.
Rewritten: exponent 0.99, 9.3ms at 1MB.

The point on record is not that a mistake happened. It is that the author
of the gate, fully aware of why it existed, immediately wrote the thing it
guards against and did not notice until a machine measured it. A review
checklist could not have caught that - the reviewer wrote the checklist.

CONTRIBUTING's harness-failure count goes from four to five: the
equivalence run that truncated every line before comparing belongs on that
list, since it excluded the only inputs that could have shown the
regression.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5nXA9BJKdmye9a9FaHdc6
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5nXA9BJKdmye9a9FaHdc6
@amic25
amic25 merged commit 6852786 into main Jul 29, 2026
12 checks passed
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.

2 participants