Contracts
guard-contract.md
Behavioural Contract: Untrusted-Segment Containment Seam
This mission has no network/API surface. The "contracts" are the behavioural guarantees of the canonical seam at each sink. Each is a testable assertion.
C-GUARD-1 — segment grammar (assert_safe_path_segment)
- Input: a string segment.
- Accept: non-empty pure-ASCII label, no separators, not
./.., not absolute. - Reject (
ValueError): empty, whitespace, non-ASCII, contains/or\,./.., leading-dot traversal forms, absolute path.
C-GUARD-2 — fail-closed slug (safe_mission_slug)
- Input:
(slug | None, fallback). - Output:
slugwhen it passes C-GUARD-1; otherwisefallback(trusted), plus one WARNING. - Never raises.
C-GUARD-3 — containment (ensure_within_any)
- Input: a built path + the trusted-root set.
- Behaviour:
resolve(strict=False)(follows symlinks), then assert the resolved path is within at least one trusted root. - Reject (
ValueError): a path that resolves outside all trusted roots — including a symlink whose target escapes. - Accept: a path (or symlink) that resolves to a location inside a trusted root.
C-SINK-READ — resolver read sinks (store.py, aggregate.py)
- Given an untrusted slug, when resolving identity from
<root>/<slug>/meta.json, - then the slug passes C-GUARD-1 and the resolved meta path passes C-GUARD-3 before any
read_text; - on failure: return
None(skip the record), emit one WARNING, read nothing outside the root.
C-SINK-WRITE — derived-view write sinks (progress/lifecycle/views)
- Given an untrusted slug from EITHER source —
snapshot.mission_slug(event log) OR themeta.jsonslug viaresolve_mission_identity— when buildingderived_dir/<slug>, - then BOTH sources are sanitized: the reduce seam (C-GUARD-2) for the event slug (landed #2036) AND
mission_metadata.resolve_mission_identity(C-GUARD-2, IC-05) for themeta.jsonslug, so an unsafe slug becomes the trustedfeature_dir.name; - result: no
mkdir/write outsidederived/; output lands underfeature_dir.name. - Regression note:
progress.pywas already safe post-#2036;views.py/lifecycle.pywere NOT (themeta.jsonfallback bypassed the reduce seam) — IC-05 closes them. The negative test MUST cover themeta.json-slug + empty-event-slug combination.
C-REGRESSION — architectural guard
- A new untrusted-segment join on an audited surface that bypasses the seam fails the
tests/architectural/guard.
Verification
Every contract above is covered by a mutation-killing test: removing the guard makes at least one test fail (SC-004). The symlink-escape case is explicit for C-GUARD-3 / C-SINK-READ.