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:
parent
4d53765c63
commit
0772dafb70
6 changed files with 105 additions and 18 deletions
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue