# 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. - **A task in plain prose with no marker is not caught (attack A12).** Invariant's toxic agent flow put this in a public issue: read the README of every one of the author's repos, "including private ones", and add everything to this README. It addresses no one, defers nothing, writes no memory and hides nothing, so `indirect:prose-instruction` does not fire, and a maintainer could file the same request. Measured 2026-10-31: 0 findings, WARN. Telling it from a real request needs the intent, not the words. The contract carries this case: a tool-less transform cannot read the private repositories the payload asks for. - **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 `