# Honest limitations Conceding these plainly is itself a control — it prevents the false assurance that a green scan means safe content. The README carries a summary of the highest-impact items; this is the full list, each with the mechanism. - **Structural unsolvability at the text layer.** Pattern/lexicon detection is bypassable in isolation; character-injection and novel phrasings evade it. The *contract* (tool-less transform, capability isolation, fail-secure) carries the security — the lexicon is defense-in-depth, not a wall. - **A lone HIGH finding in trusted prose disposes to WARN, not quarantine.** Under `PRESET_TRUSTED_SOURCE`, trust-scaling downgrades a single HIGH to WARN, and one HIGH is not "compound" (escalation needs ≥2 findings at MEDIUM+). So a HIGH injection reproduced verbatim under a *trusted* policy persists with a WARN. This is by design: if your "trusted" sources can carry attacker-influenced text, run them as untrusted (or add a quarantine floor). - **The quarantine floor fires at MEDIUM+, and is a no-op under the shipped upload preset.** Through 0.3.0 `quarantine_default` floored *any* finding to QUARANTINE_REVIEW. That premise ("a finding is the exception") broke when the active-content detector made every ordinary markdown link a finding, so 0.3.1 raised the floor to MEDIUM+. Under `PRESET_USER_UPLOAD` (untrusted) a MEDIUM already escalates on trust alone, so the floor still changes no outcome there; it is live only for a caller-defined *trusted* policy that opts into `quarantine_default`. Documented so the preset is not over-read: a LOW finding on an upload now disposes WARN. - **Semantic / factual poisoning is invisible** to lexicon + entropy: a false claim in clean prose carries no suspicious token. **Highest impact for a wiki.** The `grounding` module ships only a `SourceGroundingCheck` *seam* — the deterministic core does not judge semantics; a `[judge]` implementation must be plugged in. - **Adversarial-ML evasion** can survive normalization; **tokenizer mismatch** between scanner and model leaves gaps. **Latent / dormant memory poisoning** is not judgeable at write time. - **Dormant / broken-link injection** in a linked corpus (e.g. an OKF bundle): a link to a not-yet-existing target passes a per-concept write-time scan clean — the payload is planted later, when that target is written. `link_graph` surfaces the *dangling* edge as the signal, but catching the payload needs cross-write re-scan over time (the caller's disposition call). - **OKF reserved files (`index.md` / `log.md`).** In a *received* bundle these are legitimate structure, so mode-b `import_bundle` scans their body and frontmatter (an injection in a directory listing is caught) rather than path-rejecting the conformant bundle. A front-end materialising individual uploads keeps the opposite rule (`allow_reserved=False`): a reserved basename is a listing-shadow and refused. - **OKF frontmatter is a restricted grammar: a mapping is expressible through four carriers, all of them key-allowlisted.** Gate T2 accepts a line-oriented subset deliberately — full YAML is a larger parse-attack surface than a write-time gate needs. The admitted carriers are the flow mapping as a value (`generated: { by: x, at: y }`), the flow mapping as a block-list item, the flow *sequence* of flow mappings (`sources: [{ id: a, resource: x }]`) and the block sequence of block mappings (SPEC.md §5.1's own form). **The routes that still fail, each on a different rule:** a top-level block *mapping* (`k:\n sub: v`) on the nested-mapping check, dotted keys (`k.sub: v`) on the key pattern, and the inline second colon (`k: sub: v`) on the mapping-construct check. (A flow sequence of plain *scalars*, `tags: [a, b]`, parses since `1.4.0`; it is a sequence carrier, not a mapping one — see the `tags` entry below.) **Every carrier is admitted only when every key is on an allowlist** (`by`, `at`, `from`, `to`, `id`, `title`, `author`, `usage_count`, `last_modified` — the keys SPEC.md @ `62432a09` §5.1/§5.2 names inside a mapping, plus `resource` and `usage_window` *under `sources` only*, see below) and every leaf is a plain scalar, itself run through the same value predicates as a top-level scalar. Nested collections, quoted leaves, duplicate keys, an empty or unclosed mapping, and `{a:b}` (which PyYAML 6.0.3 reads as the *key* `a:b`, not as a scalar) all raise. The form is expressible, never trusted: the allowlist inspects every key, which is the property that carried the security when the blanket refusal was doing the enforcing. **A block list may not mix scalar items and mappings** — YAML permits it, but a consumer iterating `sources` and reading `entry.get("id")` gets an `AttributeError` off the first `str`. What else survives is scalars and flat lists of strings. **Two routes used to degrade into a string instead of failing, and that defect is closed in `1.1.0`**: a block-sequence item carrying exactly one key (`sources:\n - uri: https://e.com/a` yielded the *string* `'uri: https://e.com/a'`) and the inline second colon (`attester: resource: attesters/sql_equality.py`, which a real YAML parser refuses outright). Both parsed "successfully" into the wrong *type*, and a pointer parked in one rode through in a key the `resource` allowlist never inspects — mode-b `import_bundle` returned WARN and wrote the merged concept verbatim. Both now FAIL_SECURE at T2, before the allowlist is reached. **What closed is the type confusion, not pointer-smuggling as a class:** T3 still inspects `resource` and nothing else, so an honest *string* under another key rides through exactly as before — `attester: attesters/sql_equality.py` is WARN, while the same path under `resource:` FAIL_SECUREs. That is by design (the string is scanned like any other frontmatter value under T1) and it is not what `1.1.0` changed. **The boundary is where YAML puts it**, ground-truthed against PyYAML 6.0.3: `": "` and a trailing `":"` open a mapping and are refused; a colon carrying neither a space nor a line end (`domain:security`, `https://e.com:8443/a`) does not and still parses, as does a quoted scalar (`- "uri: x"`). Quotes are retained rather than stripped — a divergence from YAML that remains, pinned in `tests/test_okf.py`. - **T2 constrains import, not emission.** The frontmatter grammar runs on `okf.import_bundle` (door C) only — `parse_frontmatter` is referenced nowhere in the door A/B persist path, so frontmatter that fails secure on import passes `screen_output` unremarked. The grammar therefore bounds what a consumer can *receive*, never what a producer can *emit*. Verified identical on 0.2.0 and 0.3.1. - **`sources` passes in both of its spec carriers; the per-entry `usage_window` does not.** The wall used to be total: both of v0.2's backward-breaking migration targets are mappings — `timestamp` → `generated.at`, and body `# Citations` → a `sources` block list of mappings — and a consumer measured **0 of 53** upstream concepts through the gate. `generated.at` stopped being a wall in `1.2.0`; `sources` stopped being one in `1.3.0`, which admits both the block sequence of block mappings (SPEC.md §5.1's own example) and the flow sequence of flow mappings (the form the OKF producers emit, measured 2026-09-02 by two consumers independently). **`resource` is allowlisted inside a `sources` entry and nowhere else.** `1.2.0` left it off on the argument that the parser could not tell `sources[].resource` (§5.1, a citation) from `executor.resource` / `attester.resource` (§10, a pointer to code to be run — door C). That premise was measured false: the owning key is in scope at every call site and was simply never threaded through. It is threaded now, so `executor: [{ resource: skills/run.md }]` and `attester:\n - resource: …` are refused on the allowlist through *every* carrier, including the two this opened. **What stays refused: a `usage_window` inside a `sources` entry.** SPEC §5.1 permits it per entry ("A single entry MAY carry its own `usage_window`"), and it is a mapping inside a mapping — depth 2, which this parser admits at no key. A bundle using the per-entry override is refused; the shared sibling `usage_window` (the §5.1 example's own form) passes. This is a registered conformance gap, not an oversight: no-nesting-past-depth-1 is a security property, and spending it was not what the fix was for. - **`sources[].resource` is scanned as text but never validated as a URL.** T3's https allowlist inspects the *top-level* `resource` and nothing else. It cannot be extended to `sources` entries without over-blocking conformant bundles: SPEC §5.1 explicitly permits a bundle-relative path, a path into `references/`, or a scope descriptor a consumer cannot follow at all (the OKF producers' own golden bundle emits `resource: fixture`). So a `sources` entry may carry `file://`, `javascript:` or any other string; it goes through T1's scan like any other frontmatter value, and nothing else. **A consumer that dereferences `sources[].resource` must validate it itself** — `okf.validate_resource_url` is exported for exactly that. The dangling-or-substituted `executor`/`attester` pointer question stays out of reach separately: both are top-level block mappings, a carrier that is still refused. - **`tags` parses in two of the three forms the OKF corpus writes it in; `description`'s folded form still does not — and neither is what caps the corpus.** Re-measured 2026-09-07/08 against the pinned upstream reference bundles (`_okf-upstream/okf` @ `3fcbb9f`, denominator **53** — every non-reserved `.md`, which is SPEC §11.1's own unit) and the pinned SPEC (`_okf-canonical` @ `ad30107`). This entry carried three claims that the measurement showed to be wrong, all three inherited from `1.2.0` and left standing for twelve days after `1.3.0` moved them; they are corrected here. - **A sequence-value type exists** — `_consume_block_list` has parsed block lists of plain scalars since `1.3.0`, and `1.4.0` adds the flow sequence of plain scalars (`tags: [a, b, c]`, SPEC §4.1's own skeleton). What is missing is not the type but the **indentation requirement**: `_consume_block_list` demands a leading space, and the corpus writes `- a` flush at column 0 (36/53 of `tags`, 44/53 of `sources`). Shape census of `tags` over the 53: 36/53 flush block sequence, 9/53 flow sequence, 8/53 single-line scalar. - **The number is 6/53, not 4/53.** `4` was the `1.2.0` parser's figure. Measured on `1.4.0`, the corpus goes from **0/53 to 6/53** — all six in `acme_retail`, being 6 of the 9 documents whose `tags` is a flow sequence; the other three stop on the mapping-key allowlist (`parameters.name` ×2, `not.term` ×1), not on `tags`. - **`tags`/`description` are not the binding constraint.** `description` alone releases **0/53** — 29/53 write it as a folded plain scalar over an indented second line, which the parser has no continuation-line model for, but removing that obstacle by itself lets nothing through. With all three surface forms closed the corpus still measures **6/53**, because **44/53 stop on `generated` written as a top-level block mapping**, and behind that wall sit 44/53 single-quoted scalars inside the mapping. That constraint is refused deliberately: it spends the no-nesting-past-depth-1 rule, which is a security decision and not a parser detail. **Said plainly: `1.4.0` takes this corpus to 6/53, not to 53/53.** The two neighbouring predicates were measured and deliberately **not** built — a flush-left block sequence and a folded plain scalar each release **0/53** on their own, and stacked on top of `1.4.0` they still measure 6/53. They would cost parser surface and buy no measured conformance. The full measurement, including the SPEC quotations and the per-candidate table, is `docs/2026-09-07-limitations-44-maaling.md`. - **A persist gate cannot cover execution risk.** OKF v0.2 introduces concepts whose purpose is to *name code to be run* (`runtime`, `executor.resource`, `attester.resource`). This library answers "is this safe to **store**"; executable code carries its risk at **run**. A file that is harmless to persist can be harmful to point at. Upstream defers the attester ABI and sandboxing to a future revision, so there is no runtime contract to gate against — the execution boundary is *unowned* across the stack rather than covered by anyone's roadmap, and no tightening of a write-time scanner would change that. - **A document that *describes* attacks is a false positive.** Content documenting prompt-injection payloads (security notes, this project's own corpus) trips carrier-strip / fail-secure. At the text layer "*about* an attack" and "*carrying* an attack" are indistinguishable; such content needs a deliberate, explicitly escaped path, never a silent allow. - **Bilingual text trips the Cyrillic/Latin homoglyph rule.** `homoglyph:cyrillic-latin-mix` (MEDIUM) flags a Latin letter adjacent to a Cyrillic look-alike, so genuine bilingual prose → MEDIUM → under untrusted → QUARANTINE_REVIEW — a real false positive for an inbox that expects multilingual content. **Conceded for 1.x: no fix is promised.** The rule fires on codepoint adjacency, which genuine bilingual prose produces as readily as a substitution attack does. Narrowing it is a calibration question, not an API one, so a fix may land in any 1.x release without breaking the contract — but none is scheduled, and a caller that ingests multilingual prose should raise its untrusted-tier threshold rather than wait for one. `SECURITY.md` lists this as a documented boundary, not a vulnerability. - **Insider in-place edits** by a trusted author are out of the untrusted-content threat model. - **Text-only.** The core is `text -> findings`: it parses no files (no `pypdf`/`python-docx`/archive deps). Extract text first, then scan it with the high-untrust upload provenance. OCR-embedded instructions and multimodal stego are out of scope beyond the sanitizer's character-layer stripping. - **Uploaded files: only the *extracted text* is scanned.** The dev-scoped OKF inbox showcase (`tests/test_okf_inbox_uploads.py` + `tests/inbox_frontend.py`; parsers in the `[dev]` extra, never core `dependencies`) reads `.txt`/`.md`/`.csv`/`.docx`/`.pptx`/`.xlsx`, folders and `.zip`, materializes an OKF bundle, then guards it. What survives extraction is **out of scope**: macros, OLE/embedded objects, OCR-needing images, font/render stego, encrypted files — the binary layer needs a separate scanner. The front-end owns the container threats it *can* see (zip-slip → path gate, zip-bomb → size cap, symlink refusal, CSV/XLSX formula-lead cells). **`.pdf` is a deliberate concession:** a top-level `.pdf` is *refused as unsupported* rather than half-scanned (a PDF parser is disproportionate for a dev showcase, and the OCR/stego it would smuggle is already out of scope). One known gap: the numeric `-`/`+` CSV false positive (a typed XLSX numeric cell does not trip it). - **Hidden styling is judged from the element's own `style` attribute, and white text alone is not a signal.** `html-obfuscation:tiny-font` reports a font below 4px (`HIDDEN_FONT_MAX_PX`; pt, em, rem and % converted at 16px per em), and `html-obfuscation:invisible-colour` reports text within 8 per channel of the background declared in the *same* attribute, or transparent. A background on a parent element, a `