1
0
Fork 0
Commit graph

34 commits

Author SHA1 Message Date
701a4a47c7 release(0.3.3): the lexicon ReDoS fix, and a correction to 0.3.2's claim
Version synced across the four locked points (pyproject, __version__, README
badge, README install pin) + CHANGELOG.

Corrections this release carries, both measured rather than reasoned:

- docs/LIMITATIONS.md said the script-tag change removed "the last"
  quadratic-backtracking site on the output path. It did not. Corrected, with
  the 334.7s gate measurement that falsifies it.
- README's coverage line claimed 126/126 classes; the matrix reports 128/128.
  Stale since before v0.3.2. Test badge was 642, actual 666.

New residual recorded (LIMITATIONS, now 30 items, README synced): the sweep
flags on timing above a 1.5ms noise floor at N=8000, so an arm hiding under it
could still cost ~23s at the cap. What this supports is "no arm worse than ~23s",
not "no quadratic arm remains" -- and the blind spot is demonstrated, since a
generic-payload pass found only one of the two patterns.

666 tests green, coverage matrix 128/128 with 6/6 gaps holding, exit 0.
2026-07-31 21:51:00 +02:00
75ae48277b release(0.3.2): the ReDoS fix, with both residuals measured against the v0.3.1 tag
Version sync in all four places (pyproject, __version__, README badge, README
install pin) plus the changelog entry for cff0437.

Two claims that were about to ship in the release note did not survive being
measured against a v0.3.1 worktree, and are corrected in LIMITATIONS first:

- "Report-only, so the cost is a review, not a block" was wrong twice. HIGH under
  a low-trust preset is fail_secure, not a review -- report-only means the text is
  never mutated, not that a finding cannot block. And the new script-tag false
  positive costs no consumer a disposition at all: any text containing a literal
  `<script>` already produced active:raw-html at HIGH on 0.3.1, so the same prose
  disposed fail_secure under PRESET_USER_UPLOAD before this change and after it.
  The fail-open that was closed is narrower for the same reason -- it existed only
  in scan_lexicon called on its own; through either composed gate, raw-html
  already caught the unclosed tag. What changed is the label, not the outcome.

- "The realistic long value is still caught by egress:jwt-token" was true and
  hid the part that matters. Measured at the 257-char boundary: a generic long
  password still trips entropy:base64-blob at CRITICAL, disposition unchanged.
  A JWT used as a DB password is the case that moves -- its remaining detections
  top out below CRITICAL, so the any-tier block is lost and PRESET_TRUSTED_SOURCE
  drops from fail_secure to quarantine_review. PRESET_USER_UPLOAD still
  fail_secures. Recorded as a behaviour change in the changelog, not buried.

The first probe for that boundary used a 300-char run of "A" and found nothing on
either version: all-same-char values are suppressed as placeholders. The probe was
wrong, not the pattern.

662 passed, coverage matrix exit 0, LIMITATIONS still 29 items = README's 29.
2026-07-31 21:16:45 +02:00
cff043787d fix(output): 19 quadratic regex runs on the output path, worst ~5.7h at the cap
The output gate claimed LLM10 self-safety on the grounds that its patterns have
no nested quantifiers. True, and irrelevant: nesting is not what makes these
blow up. A run in front of a REQUIRED literal, reachable from a short anchor, is
enough -- crafted input repeats the anchor and never supplies the literal, so
every start position rescans the tail. Quadratic, not exponential, and the
max_scan_chars cap does not help: it bounds the input, and quadratic work on a
bounded input is still hours.

Measured, not argued. `<a:` x 100_000 took 23.4s in AUTOLINK_RE alone; the
composed gate on that payload took 458.7s, extrapolating to ~5.7 hours at the
1_000_000-char input the gate itself accepts. Size-matched ordinary prose runs
0.31s, so the separation is 18x-660x -- unlike the blob in the neighbouring
test, which is the *faster* side of prose and never exercised backtracking.

Two fixes, chosen per pattern rather than uniformly:

- active_content + lexicon JSON (15 runs): exclude the character that opens the
  pattern's own anchor (`[` for markdown, `<` for tags), so a run cannot reach
  past the next start position and the per-start costs telescope. Verified to
  cost no recall: long URLs, long alt text, and `<` inside a quoted attribute
  all still match. Bounding instead would have been linear too but wrong here --
  the content is attacker-controlled, so padding past a bound would be a
  one-line bypass of the EchoLeak class this table exists to catch.
- connstr egress (4 runs): bound the password at MAX_CONNSTR_VALUE. The
  exclusion fix is unavailable -- the anchor character is `/` and passwords
  containing `/` are the common case (measured: they match today). The residual
  miss is a credential over 256 chars; a token that long is still caught by
  egress:jwt-token.

hybrid-xss:script-tag had neither option: its run is the script BODY, which may
legitimately contain `<`. It now matches the opening tag and drops the
`</script>` requirement. That also closes a fail-open -- `<script>alert(1)`
unclosed was silently missed -- at the cost of flagging prose that merely
mentions `<script>`, now documented.

Found by the composed-gate test staying red after every individual scanner was
already linear: the lexicon's six html-obfuscation patterns were the remaining
813x. A per-scanner test alone would have shipped that.

662 passed (was 642), and faster than before the fix.
2026-07-31 18:31:58 +02:00
8deca93ee1 docs(plan): scope the self-safety claim -- ReDoS duty is the lexicon path, not output
PLAN.md's test-plan line read as if both scan paths carried ReDoS coverage.
f74245f measured that the output path's "pathological" payload is the FASTER
side against size-matched ordinary prose (0.93x / 0.96x, order swapped), so it
does not exercise catastrophic backtracking at all.

Leaving the line unscoped would be the same defect c0899e9 just retracted in
LIMITATIONS.md: a committed claim resting on a premise now known to be false.

Names which path carries which duty and says plainly that a crafted payload for
the output regexes does not exist. No new limitation bullet -- 27 stays 27.
2026-07-31 17:23:52 +02:00
c0899e9417 docs(limitations): withdraw the "two variables" explanation -- 394 was a scoping artifact
Yesterday's-shape correction, one commit old. e25de56 explained the 2400-vs-2401
gap by saying the corpus "grew from 389 files to 394 between the two runs", so
the larger snapshot returned one fewer URL -- two variables moving at once.

The consumer self-corrected the same day and the correction reproduces here
against their tree, not against their message:

  find skills -path '*/references/*' -name '*.md'   389
  find skills -name 'SKILL.md'                        5
  find skills -name '*.md'                          394

394 is every .md under skills/; 389 is the references/** path the measurement
actually scoped to. One snapshot counted two ways, not two snapshots taken at
different times -- and the 5 SKILL.md files contributed no unique URLs.

So the corpus never grew, only the script moved, and the one-URL gap is back to
unexplained (the original harvester is gone from a scratchpad). Everything else
in their measurement stands unchanged: 2400 distinct URLs, 0 firings on the
entropy branch, worst legitimate token H=4.301 at len=78.

The do-not-merge rule is unaffected and now rests on less: two counts of the
same knowledge base, one URL apart, no account of why. Never summed, never
quoted as one figure.

27 items unchanged, README stays in sync.
2026-07-31 17:20:27 +02:00
e25de560f9 docs(limitations): two variables moved between the 2400 and 2401 counts, not one
The previous commit said the two corpus counts differ by harvester over the same
knowledge base. Checked the provenance rather than leaving it asserted: 2401
traces to ms-ai-architect's 2026-07-26 message, 2400 to their 07-31 re-run, so
the attribution was right -- but the knowledge base was not held constant. Their
file count moved 389 -> 394 between the runs.

That inverts how the delta reads. A larger snapshot returning one fewer distinct
URL is not a rounding difference between two scripts; two variables moved at
once, so neither count explains the other and the one-URL gap is not evidence of
anything in particular.

Strengthens the do-not-merge rule instead of weakening it. The earlier wording
would have let a reader treat the counts as near-identical measurements of one
thing, which is the merge it was written to prevent.
2026-07-31 15:43:34 +02:00
05ee26aec8 docs(limitations): the entropy headroom is 0.099, not 0.36 -- measured in the field
ms-ai-architect re-measured after retracting their entropy refutation (their
original finding scored whole filenames as one unit, so it was never a finding
about our rule). The retraction confirms the branch; the re-measurement produced
a number we did not have, and it goes against us.

The sharpest legitimate token in their 2400-URL corpus is a 78-character
percent-escaped lovdata title at H=4.301. That is 0.099 from the 4.4 floor where
this bullet previously implied 0.36 -- the mechanism was already ours, but the
worked example understated how thin the margin gets in real Norwegian
government paths.

Verified against our own code before recording, not reconstructed: our real
_URL_TOKEN_RE emits it as a single 78-character token, our shannon_entropy
returns 4.301, and is_ordinary_url returns False. Their three calibration
examples also reproduce to three decimals here.

Threshold stays. Their own conclusion too -- moot per URL, since the % rule
already disqualifies it before entropy is consulted.

Records the 2400-vs-2401 provenance split explicitly. The counts come from two
different harvesters over the same knowledge base and differ by one URL; the
neighbouring hex bullet cites 2401. Left unstated, a later reader would
reasonably merge them into one corpus figure, which is exactly the
prose-vs-measurement conflation this file exists to prevent.
2026-07-31 15:39:15 +02:00
434b74e766 docs(limitations): close the slugger question -- both consumers read, both whitelists
okf delivered the code read we asked for: materialize.py:71 is
`[^a-z0-9]+ -> "-"`, a whitelist, so their generated-path percent-escape
exposure is structurally zero. Verified independently against their source
rather than taken on the message: the regex is where they said, both their
worked examples reproduce against the real implementation, and the only
urllib.parse.quote in their tree builds a sqlite file: URI that never touches
a filename.

That retires the "still open for the one consumer whose slugger we have not
seen" clause. The Loekkene example already in this bullet was linkedin-studio's
run output, not a prediction -- checked before rewriting, because the two
clauses read as if they contradicted each other and only one was stale.

Adds the honest half that closing the question exposes: we have now read two
whitelist sluggers and zero encodeURIComponent-class ones, so the
"produces escapes systematically" arm of this axis is still a prediction from
the transform, not a field observation.

The {tenant} placeholder result goes on the generated-path side, deliberately
not appended to the %7B sentence -- that one is a link-corpus harvesting
artifact, and merging them would re-conflate the two axes this bullet exists
to separate.

No test is owed here: the claim pins okf's regex, which this suite cannot
assert. It is verifiable only by re-reading their source.
2026-07-31 15:35:48 +02:00
910a12455a docs(limitations): correct the percent-escape framing -- slugger class, not language
A consumer corrected a claim we shipped this morning. We wrote that both
zero-measuring corpora were English, and used that to explain the third corpus's
10 Norwegian hits as a sampling bias. One of those two is a Norwegian repo with
Norwegian content, so the stated explanation was wrong.

Their measurement separates two axes we had conflated. For third-party LINK
corpora -- URLs collected from other people's sites -- language does predict, and
the Norwegian legal/government sources are where the escapes were. For GENERATED
paths the predictor is slugger class: a whitelist slugger ([^a-z0-9]+ -> "-")
cannot emit an escape in any language because it discards the character before
anything encodes it, while encodeURIComponent produces them systematically as soon
as titles are non-ASCII. Their own slugger returns a structural zero on Norwegian
input, not a statistical one.

So non-ASCII language is a confounder for encode-vs-whitelist, and slugger class is
the thing that is actually testable at a consumer -- a one-line code read rather
than a corpus census. Reframed accordingly, with the earlier conflation named so
the correction is visible rather than silently rewritten.
2026-07-27 20:20:45 +02:00
2d5a9cd0c7 docs(okf): the mapping class is inexpressible, and T2 binds import not emission
A consumer measured our frontmatter gate against 0.2.0 and asked us to confirm
against a newer guard, correctly noting that a divergence would be a version
difference rather than a contradiction. Measured at 0.3.1: no divergence. Both
their results reproduce exactly.

Two things are sharper than what we documented yesterday:

ALL THREE ROUTES TO A MAPPING FAIL, each on a different rule -- flow {k: v} on the
disallowed value-start indicator, block on the nested-mapping check, dotted keys on
the key pattern. So the mapping CLASS has no expressible form; it is not a choice
between two shapes where one is better. That matters because OKF v0.2's `generated`
IS a mapping (`by` is required when it is present), so it cannot be expressed at
all. Pinned by a test asserting the three failures stay distinct.

T2 RUNS ON DOOR C ONLY. parse_frontmatter is referenced nowhere in the door A/B
persist path, so the same frontmatter that FAIL_SECUREs through import_bundle
passes screen_output unremarked. The grammar bounds what a consumer can RECEIVE,
never what a producer can EMIT -- which is the question a consumer's emitter design
was blocked on.

Also corrects one of our own rows: we reported "sources block list" as a single
FAIL_SECURE case. The parser distinguishes three shapes -- flat scalars parse
correctly, one key per item misparses silently to a string, two keys per item hard-
rejects. A one-element `verified:` block list passes today.

631 -> 642 passed. README item count synced.
2026-07-27 09:11:22 +02:00
684ce3a45f docs(url-shape): make the rule reconstructable, and record what three corpora measured
Three consumers reconstructed is_ordinary_url from prose we sent in coordination
messages and each produced a different wrong number on a real corpus: one omitted
the base64 20-char floor and fired on path words like /blog/; one omitted the
opaque-token condition entirely and undercounted; one computed Shannon entropy
over whole filenames instead of tokens and concluded the 4.4 floor over-blocks
ordinary documents. Same cause each time -- our prose described the rules without
their tokenizer.

docs/URL-SHAPE.md states the algorithm in order, spells out the separator class
and all three length floors, and lists the three reconstruction errors as worked
counter-examples. Its example table is parsed and asserted against the real
predicate by tests/test_url_shape_doc.py, so the reference cannot drift from the
code -- all 18 rows verified load-bearing.

LIMITATIONS.md brought current with the field measurements:

- Percent-escape is no longer zero. Two English corpora measured 0; a 389-file
  Norwegian/Microsoft corpus found 10, all Norwegian (%C3%B8, %C3%A5 are just
  o-slash and a-ring). It is a non-ASCII-language tax, and both zero-measuring
  corpora being English was a sampling bias invisible from inside.
- The query over-block now has THREE disjoint benign populations: utm_* tracking,
  content identity (?v=, ?channel_id=), and Microsoft Learn's ?view= version
  selector. No parameter-level remedy covers any two, which moves this from a
  conclusion to a settled constraint on 0.4.0.
- Legitimate CDN asset ids trip the hex branch permanently; the branch is otherwise
  precise (no other FP in 2401 distinct URLs) and stays.
- Raw HTML with a relative URL attribute is HIGH though it reaches no external
  host, and end tags are counted.
- OKF frontmatter: a one-key block-sequence item is silently misparsed to a string
  where two keys hard-reject, so a pointer can ride past the resource allowlist.
  Consequence: a conformant OKF v0.2 concept cannot traverse door C at all, since
  both backward-breaking migration targets are nested. Fail-secure, but a
  compatibility wall that needs a deliberate parse-safety decision.
- A persist gate cannot cover execution risk, and that boundary is unowned.

New behaviour claims are pinned by tests so a closed concession fails and forces
this doc to be updated. 593 -> 631 passed.
2026-07-27 08:56:24 +02:00
956c835d38 docs(limitations): bound two URL-shape false positives with field measurements
Two consumers measured the 0.3.1 URL-shape rule against real corpora on the
same day. Record what they found, and pin the dispositions both of them
inferred wrongly.

Percent-escape FP: zero occurrences across both corpora (0/347 external URLs
in a 527-document vendor-docs corpus, 0/81 in a capture store). This bounds
the shape rather than closing it -- a consumer that slugs filenames from
titles produces %20 systematically, and that corpus is still unmeasured.

Query FP: the over-block that actually occurs. The two corpora hit disjoint
benign populations -- 16/16 publisher-authored utm_* tracking versus 35/35
content identity (?v=, ?channel_id=) where the parameter IS the resource.
An allowlist keyed on tracking-parameter names resolves the first entirely
and the second not at all, so no parameter-level remedy covers both. That
is input to the 0.4.0 axis-separation scope, not a fix here.

Both consumers reported dispositions they had inferred rather than run, and
both were wrong: a query-carrying link is MEDIUM, so it is held or warned,
never hard-failed. tests/test_wiring.py now pins that, plus the active-tag
gate -- counting raw HTML tags overcounts what the gate flags, since
formatting markup is inert and only name/on*=/URL-attr tags are active.

593 passed.
2026-07-25 20:56:14 +02:00
55ce6265d4 docs(plan): state the gate requirement precisely -- fixture set, not tagged artifact
okf asked outright whether the v1.0 gate requires 'the tagged artifact resolves
0.3.1' or 'the fixture set passes against 0.3.1'. It is the second, and the
answer is locked here so a later session cannot quietly upgrade the ask into a
release request against their operator.

They refused to widen their published range to run the measurement, and the
refusal is better than the ask it answers: widening a published range in order
to generate the evidence that would justify widening it is circular, and a red
result would then have meant unwinding a release to fix a test setup. The
measurement has to be able to say no. Their scratch-venv method -- outside the
project environment, --no-deps, guard installed from the v0.3.1 tag, resolved
version asserted from importlib.metadata -- satisfies the gate exactly.
2026-07-25 20:02:41 +02:00
6b705736cd docs(plan): record two consumer notification promises as release constraints
Both were given to named consumers who are building on current behaviour, and
both were about to live only in STATE.md -- which is gitignored, so a promise
kept there does not survive a machine. Breaking either silently is a release
defect, not a preference.

1. Any change to how PRESET_USER_UPLOAD grades ordinary links and remote images
   requires notice to linkedin-studio before it ships; they pin v0.3.1 at wiring.
   Applies to 0.4.0: the axis separation must change disposition, not grading.
2. Closing the relative-target asymmetry requires notice to llm-ingestion-okf;
   it is a Door C property rather than a guard gap, but closing it changes what
   arrives at their persist gate.

Also records a mechanism we had not modelled: okf reports percent-escapes
reaching filenames through a slugger, so %20 is manufactured systematically from
document titles rather than being incidental as it is in the documentation URLs
the FP was calibrated against.
2026-07-25 20:00:25 +02:00
d39033676a docs(plan): the v1.0 gate cannot be opened by a green result alone
okf pins llm-ingestion-guard >=0.2,<0.3 in [project].dependencies and pins the
uv source to tag v0.2.0 -- verified at their v0.4.0 tag and at their HEAD. The
cap excludes 0.3.1, so the committed Door B+C fixture run cannot reach the
version the gate names. It does not fail either: uv resolves v0.2.0, the
constraint holds, and the fixtures come back green because v0.2.0 never had the
regression. Green from an unmodified tree is the one outcome that proves nothing
and reads as proof.

The gate now requires the result to name the resolved guard version, and records
that transitive arrival does not count as an integration -- okf v0.4.0 makes the
guard a mandatory runtime dependency, so a consumer can acquire us without
choosing us, and today lands on v0.2.0 without the 0.3.0 hardening or the 0.3.1
fix. Silent under-defend, not a break.
2026-07-25 19:57:41 +02:00
10a37d104e docs(plan): retarget the Session G integration gate at v0.3.1 2026-07-25 15:45:45 +02:00
207b2c1679 docs(plan): record Session H as landed — 0.3.1 tagged with measurements 2026-07-25 15:45:14 +02:00
6e9b8168e3 fix(calibration): grade active content on URL shape, not construct type
v0.3.0 made the untrusted upload path unusable: measured on both doors, an
ordinary remote image fail_secure'd and an ordinary link/autolink/refdef
quarantined, so only documents without external references persisted.

Two independent defects compounded; neither fix works alone:

1. `markdown-image: HIGH` fired on any external image. The exfil primitive is a
   URL that moves bytes outward, not an image. `is_ordinary_url` now grades on
   shape - http(s)/protocol-relative, no query, no userinfo, no percent-escape,
   no opaque host label or path segment -> LOW; anything data-carrying keeps the
   carrier's severity. raw-html and data: URIs stay HIGH unconditionally.
   Opacity reuses entropy's primitives; floors calibrated against real doc URLs
   (worst legit token H=4.08, exfil segments 4.36-4.54) and frozen in
   calibration.

2. The quarantine_default floor fired on ANY finding, a premise that broke when
   every ordinary link became a finding. It now fires at MEDIUM+ - a no-op for
   every detector that shipped before 0.3.0 (no LOW/INFO exists), which is what
   makes this a patch rather than a minor.

The corpus blind spot that let this pass 522 green tests is closed: the FP
corpus carries realistic markdown and is asserted on the OUTPUT gate under
PRESET_USER_UPLOAD, with a counter-corpus of exfil-shaped URLs that must still
block. Beaconing and short opaque segments are conceded in LIMITATIONS and
asserted by the coverage matrix rather than papered over.

No new public API; no new preset (0.4.0 work); allow_reserved default unchanged.
2026-07-25 15:36:02 +02:00
da7421e6c8 docs(plan): scope Session H — 0.3.1 regression fix for active-content calibration
v0.3.0 makes the untrusted upload path refuse ordinary documents. Measured on
both doors under PRESET_USER_UPLOAD: one ordinary markdown image reaches
fail_secure, one ordinary link/autolink/refdef reaches quarantine_review, and
only a document with no external references persists.

Root cause is three independent defects that compound. Severity tracks
construct type rather than URL shape, so every external image is HIGH when the
exfiltration primitive is actually a URL that carries data outward. The
quarantine floor fires on any finding at all, a premise that broke once every
link became a finding. And the false-positive corpus could not have caught
either: it holds no markdown links or images, asserts only under
PRESET_TRUSTED_SOURCE where everything warns anyway, and runs _scan_input, so
the output gate where active_content lives is never exercised by it.

Both code changes are required together -- verified that fixing severity alone
still quarantines via the floor, and fixing the floor alone still fail-secures
on a HIGH image.

Records the version reasoning too: 0.3.1 is honest as a patch because the
lexicon carries no LOW/INFO patterns (40 high, 22 medium, 21 critical) and no
other detector emits LOW, so raising the floor to MEDIUM+ is a no-op for every
finding that existed before 0.3.0. The new middle preset stays out of this
release; it is additive API and belongs in 0.4.0.
2026-07-25 15:11:12 +02:00
7025759789 docs(plan): record the revised v1.0 gate and the 0.3.0 cut ahead of Session G
PLAN-v1.md said consumer integration was NOT part of the v1.0 sequence, and
Session G listed A-F as its only dependency -- both of which are now false.
The gate change lived only in STATE.md, which is local-only and overwritten
each session, so the plan was the wrong side of the truth.

Records: 1.0.0 now gates on the first real integration coming back green
(llm-ingestion-okf step 4 against v0.3.0), not on our own suite; the reasoning
(522 self-authored tests prove the code matches the design, not that the
design survives contact -- our first external contact found both a
tag-vs-main divergence and three undocumented behaviour changes); and that
one validated `integrated` outweighs five `planned` declarations, so we do not
wait on repos that may never arrive.

Also fixes Session G's now-stale mechanics: its CHANGELOG entry can no longer
"list A-F" because A/A2/B already shipped under [0.3.0]; the version-sync file
list gains the README status line and install pin; and verification gains an
anonymous clean-venv install check against the new tag, since README must
never advertise a command we have not run.
2026-07-25 12:27:01 +02:00
3dda1f68fa docs(readme): add concrete 'What it protects against' catalogue; split limitations
Answers the gap that the README said what it does NOT stop (a long limitations
section) but never plainly listed what it DOES. Add a 'What it protects against'
section high up: attack classes grouped by OWASP anchor (LLM01 injection + 83
lexicon classes + carriers, LLM02 egress, LLM05 EchoLeak, LLM06 agency, LLM10
fail-secure, OKF T1-T6, container front-end), each driven by a live coverage-matrix
payload. Move the full honest-limitations list to docs/LIMITATIONS.md; README keeps
a high-impact summary + link. Net: protection and limits read in balance, 261 -> 216
lines. Coverage 126/126 and 522 tests unchanged; every class listed is real.
2026-07-15 19:09:04 +02:00
a1f3fe1983 docs(adoption): reusable consumer adoption brief for OKF second-brain repos
Self-contained brief a consumer repo can plan an inclusion from: what the
guard is (write-time, not query-time), the two bookends + 8-step contract,
the shipped OKF adapter (import_bundle mode-b, per-concept gates), how to
verify (coverage matrix -> 126 classes), how to depend (stdlib-only core),
and a planning checklist for WHEN/WHERE to wire it (untrusted boundary, not
first-party onboarding). Every claim verified against v0.2 code.
2026-07-15 12:35:09 +02:00
66f3cbf4f5 docs(pdf): concede .pdf as a deliberate design boundary (Session F1)
Reframe .pdf from a 'known gap'/TODO to a deliberate concession across the
honest-limits and OKF-upload docs. A top-level .pdf drop is already refused as
an unsupported format (inbox_frontend.py raises on the else branch), not
half-scanned; adding a PDF parser + reportlab (solely to author white-on-white
test fixtures) is disproportionate for a dev-scoped showcase, and the OCR /
font-render stego carriers a PDF would smuggle are out of scope regardless.

- README honest-limits: .pdf = concession, not TODO; only the numeric CSV FP
  remains a known gap.
- docs/PLAN.md: upload table .pdf row marked 'conceded'; honest-scope paragraph
  names .pdf; assertions tightened to 'every accepted format'.

Closes the last format gap before v1.0 freeze (Session G). No code touched;
357 tests green.
2026-07-15 10:15:39 +02:00
1625f3893b docs: version-sync + SECURITY/CONTRIBUTING + honest-limits (Session E)
- README: tests badge 275->357; status v0.1->v0.2 (repo is 0.2.0; the v1.0
  bump belongs to the Session G freeze, not this docs pass); add three
  honest-limits — lone-HIGH-in-trusted-prose->WARN, vacuous quarantine-floor,
  Cyrillic/Latin homoglyph-mix false positive.
- docs/BRIEF.md: drop "No code yet" pre-implementation framing -> implemented v0.2.
- docs/OKF-INGESTION-BRIEF.md 4: correct cross-link control language —
  absolute https / references/ targets are spec-permitted, not rejected.
- Add SECURITY.md (private Forgejo disclosure) + CONTRIBUTING.md (stdlib-only
  core, Iron-Law TDD, no trailers, Forgejo-only invariants).
2026-07-15 10:08:24 +02:00
4a9cfd2bbe docs: reframe novelty claim to composite write-time contract (review MAJOR #3)
Replace the unverified/absolute novelty statement with the defensible
four-part-contract form, verified against a focused adversarial PyPI+GitHub
survey (2026-07-15):

- BRIEF §11: 'assumed, not verified' -> verified survey with sources. Names
  aig-guardian (real, query-time; blurs only the minimal-dep-library
  differentiator), GuardLLM (nearest neighbour, runtime hardening, no
  scan-before-persist / capability isolation / fail-secure), and ipi-scanner
  (orphaned placeholder repo, recorded for honesty not as prior art).
- README: differentiator moved from 'library vs hosted/model' to the full
  four-part write-time contract.
- PLAN §27-31: drop the unverifiable 'the first' superlative.

Also promotes the v1.0 session plan (PLAN-v1.md) and the cross-model review
(review-2026-07.md) into docs/ on the open/ mirror, referenced by PLAN.md's
re-sequencing addendum.
2026-07-15 09:22:38 +02:00
896ab4034a docs(plan): showcase covers realistic upload formats — two-stage extract→guard, dev-scoped parsers, honest binary-layer limits 2026-07-06 10:18:43 +02:00
ba7fc68c05 docs(plan): re-sequence v0.2+ streams — defer consumer integration, add OKF inbox showcase as next build
Ground-truth pass over the named flagship consumer (portfolio-optimiser's
'OKF-upload-inbox') found it does not exist as a seam: both optimiser siblings
are frozen at release, carry their own OKF layer, receive no external bundles,
and take no dependency on this guard. Defer consumer integration (stream 2)
until the guard is mature; mature it here first, Node-port-friendly. Next
concrete build is the in-repo OKF inbox showcase (mode-b import_bundle as a
receive/quarantine gate), spec'd under 'The OKF inbox showcase'.
2026-07-06 10:12:50 +02:00
43368e9684 docs(plan): lock v0.2+ stream sequencing (OKF->consumers->Node) + dependency rationale 2026-07-06 07:30:09 +02:00
5cd9b451fb docs(okf): capture OKF-ingestion security brief as v0.2 future work
Google's Open Knowledge Format (OKF v0.1) is the LLM-wiki pattern this
library guards. Capture a security brief for OKF ingestion as tracked
future work -- NOT shipped scope. The v0.1 core is format-agnostic text;
OKF surfaces beyond the body (YAML frontmatter, resource URLs, cross-link
graph, file path / reserved names, format-level authenticity) are uncovered.

- Coverage claims verified against the code at 5397ba1 (brief section 9): no
  YAML parse in src, neutralize defangs but has no reject-gate, no path/graph
  logic, disposition machinery exists.
- OKF confirmed real at the format level (Google Cloud, 2026-06-12; spec
  GoogleCloudPlatform/knowledge-catalog).
- Design move: an OKF adapter ON TOP of the format-agnostic core; the core
  stays text->findings.
- Section 8 lists 8 v0.2 tasks (T1-T8); T8 (surface two OKF residual risks in
  README honest-limits) can land independently.
2026-07-06 07:07:09 +02:00
a55404460a docs(plan): add end-to-end showcase pipeline test as the final deliverable
One realistic content sample carrying many vulnerabilities at once, run through
a mock ingestion pipeline (sanitize -> lexicon+entropy+decode-rescan -> output
gate -> disposition); assert every planted vuln is caught and disposition fails
secure. Doubles as the README worked example. Added to build-order step 11 and
the test strategy; tracked as task #11. Inspiration: llm-security/examples/*.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K8GmKRCdsPjWYAKWsNgeQS
2026-07-04 17:23:17 +02:00
a9c4ccd8c7 feat: scaffold package + report and sanitize modules (TDD)
Build order steps 1-2 of docs/PLAN.md:
- pyproject.toml (llm-ingestion-guard, stdlib-only core, extras [ml]/[judge]/[dev]), LICENSE (MIT)
- report: Finding/Report/Severity/Source shared type (pure data)
- sanitize: carrier stripping (zero-width, BIDI, Unicode-tag, HTML comment,
  data: URI) with the byte-identical / removes-only invariant
- docs/PLAN.md: v1 implementation plan (positioning A, gap-expanded scope,
  llm-security reuse map)

15 tests passing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K8GmKRCdsPjWYAKWsNgeQS
2026-07-04 09:24:20 +02:00
d983aa3c95 docs(brief): fold in ms-ai-architect Layer B learnings (first output-side impl)
Based on the working, tested Layer B ingestion-gate in ms-ai-architect (2026-07-04):
- A: correct consumer 2 — it fetches authored Learn docs + code samples via the
  microsoft-learn MCP, NOT the open Q&A forum / MSDN / Stack Overflow (that claim
  was unverified/overstated); low-trust surface is intra-document. Repo/name
  reconciled: the consumer is ms-ai-architect, no separate "MS AI Security plugin".
- B: §4.7 — trust tiers WITHIN a document (code sample / localized string vs
  authored prose), not only across sources; carriers + critical block in any tier.
- C: §12 — consume the lexicon as pure imported functions, not the llm-security
  scan CLI (which under-covers Markdown prose + base64-in-code-block, verified).
- D: §4.6 — fail-secure extends to scanner-unavailable: un-scannable ⇒ BLOCK.
Adds ms-ai-architect as the second reference implementation (output side).
2026-07-04 08:32:38 +02:00
41473d9db2 docs(brief): add MS AI Security plugin as second consumer; source-trust disposition
Second target consumer named: MS AI Security plugin, an ingestion
pipeline over Microsoft Learn content that includes user-generated Q&A
(learn.microsoft.com/answers, plus ingested MSDN/Stack Overflow). This
is the high-untrust case where the contract is load-bearing, not
hygiene. New design principle 4.7: disposition scales with source trust
(pinned changelog = WARN; open UGC = quarantine/hard-fail on high
severity). Day-1 rationale: architectural controls are cheap to design
in, expensive to retrofit (consumer 1 is proving that at its A13).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HPAmFyEVWbwvmSNVdXTu4d
2026-07-04 06:26:55 +02:00
b505f70485 docs: design brief for reusable LLM ingestion-pipeline security library
Brief-stage repo. docs/BRIEF.md defines a minimal, framework-agnostic
library that packages the write-time ingestion contract (sanitize ->
fence -> tool-less quarantined transform -> per-stage capability
isolation -> scan output before persist -> fail-secure) as reusable
code. Positioned honestly against query-time guardrails (LLM Guard,
NeMo, Rebuff, Vigil) with a prior-art verification log. Reference
implementation: claude-code-llm-wiki Stage B.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HPAmFyEVWbwvmSNVdXTu4d
2026-07-04 06:18:32 +02:00