1
0
Fork 0

feat(okf): scan reserved index.md/log.md in mode-b import, not path-reject (review MAJOR #2)

A received OKF bundle MAY legitimately carry index.md (directory listing, read
first under progressive disclosure) and log.md (update history) at any level
(spec §3.1/§6/§7). import_bundle previously hard-rejected those basenames in the
T4 path gate, so a conformant third-party bundle was over-blocked in full
(FAIL_SECURE) — and because the reject fired before scan_concept, index.md's
body (the highest-priority injection surface) was never scanned.

import_bundle now defaults allow_reserved=True: reserved basenames are scanned
as structural files (path-safety checks — traversal / absolute / backslash / .md
— still apply). The shadow-reject (an *upload* masquerading as index.md) is
preserved: the front-end passes allow_reserved=False so a materialized upload
landing on a reserved basename is still refused. That front-end opt-in was
required to keep the shadow-reject once the default flipped (not in the plan's
Filer set; traced from the code).

- okf.py: validate_concept_path/_validate_concept/import_bundle gain the
  keyword; validate_concept_path default stays False (strict standalone).
- tests: +3 (legit index/log admit; injection in index.md body caught;
  okf_version frontmatter admits). Per-concept-iteration test switched to a
  traversal vector; mode-b showcase's index.md surface reframed from
  reserved-name-reject to index.md-body-scan.
- README honest-limits + CLAUDE.md context note the mode-b/upload distinction.

Suite: 341 -> 344 passed. Core invariant intact (dependencies=[]).
This commit is contained in:
Kjell Tore Guttormsen 2026-07-15 06:43:50 +02:00
commit 0772dafb70
6 changed files with 105 additions and 18 deletions

View file

@ -151,7 +151,7 @@ def _scannable_regions(frontmatter, body):
return regions
def validate_concept_path(path):
def validate_concept_path(path, *, allow_reserved=False):
"""Validate a bundle-relative concept path and return its concept-ID.
T4 path / reserved-name gate. The concept-ID is the path with the ``.md``
@ -164,6 +164,13 @@ def validate_concept_path(path):
lets ``Index.md`` shadow ``index.md``;
- non-``.md`` files (not a concept document).
``allow_reserved`` (default ``False``) keeps this a strict concept-path
validator: a reserved basename is not a concept and is rejected. A mode-b
bundle import passes ``allow_reserved=True`` because a *received* bundle MAY
legitimately carry ``index.md`` / ``log.md`` as structural files the caller
then scans their body rather than persisting them as concepts. The path-safety
checks (traversal / absolute / backslash / ``.md``) still apply either way.
Raises :class:`OKFPathError` on any of these; returns the concept-ID string.
"""
if not path or not isinstance(path, str):
@ -181,7 +188,7 @@ def validate_concept_path(path):
raise OKFPathError("malformed path segment in %r" % path)
basename = segments[-1]
if basename.lower() in _RESERVED_BASENAMES:
if not allow_reserved and basename.lower() in _RESERVED_BASENAMES:
raise OKFPathError("reserved filename may not name a concept: %r" % basename)
if not basename.lower().endswith(".md"):
raise OKFPathError("a concept document must be a .md file: %r" % path)
@ -338,7 +345,7 @@ class BundleResult:
return "\n".join(lines)
def import_bundle(bundle, *, origin=Origin.EXTERNAL, channel=Channel.AUTOMATIC):
def import_bundle(bundle, *, origin=Origin.EXTERNAL, channel=Channel.AUTOMATIC, allow_reserved=True):
"""Validate a received OKF bundle concept-by-concept before merge (mode b).
``bundle`` maps concept path (e.g. ``tables/users.md``) to its raw document
@ -348,18 +355,27 @@ def import_bundle(bundle, *, origin=Origin.EXTERNAL, channel=Channel.AUTOMATIC):
rejected (FAIL_SECURE) and recorded, but iteration continues, so the caller
sees every issue in the bundle, not only the first. The bundle disposition is
the most severe across its concepts.
``allow_reserved`` (default ``True``) reflects that this is the mode-b
*received-bundle* path: ``index.md`` / ``log.md`` are legitimate structural
files (OKF spec §3.1/§6/§7) that MAY appear at any level, so they are scanned
(their body is the highest-priority injection surface) rather than
path-rejected over-blocking a conformant third-party bundle is itself a
failure mode (brief principle 5). A front-end materialising individual
*uploads* passes ``allow_reserved=False``: there a reserved basename is a
shadow of the directory listing and must be refused.
"""
results = tuple(
_validate_concept(path, bundle[path], origin, channel)
_validate_concept(path, bundle[path], origin, channel, allow_reserved=allow_reserved)
for path in sorted(bundle)
)
aggregate = _most_severe(r.disposition for r in results)
return BundleResult(results, aggregate, link_graph(bundle))
def _validate_concept(path, doc, origin, channel):
def _validate_concept(path, doc, origin, channel, *, allow_reserved=True):
try:
concept_id = validate_concept_path(path)
concept_id = validate_concept_path(path, allow_reserved=allow_reserved)
except OKFPathError as exc:
return ConceptResult(path, None, Disposition.FAIL_SECURE, None, Report(), str(exc))
try: