feat(okf): the mapping class gets one expressible form, typed and allowlisted
OKF v0.2 writes its whole trust and provenance layer as mappings, and T2 gave
the mapping class no expressible form. A consumer measured 0 of 53 upstream
concepts through the gate on 0.3.4, 1.0.0 and 1.1.0. That was a contract
collision, not a calibration setting: SPEC.md @ 62432a09 uses flow mappings in
its own 5.1/5.2 examples, and 11 carries a hard MUST for consumers ("MUST treat
a bare `verified` mapping as a one-element list") that presupposes they parse.
Admitted: a flow mapping, as a value or as a block-list item, whose every key is
on a nine-name allowlist and whose every leaf is a plain scalar run through the
unchanged dangerous-value and mapping-construct predicates. The form is safe
because the allowlist inspects every key -- the blanket refusal was the
enforcement, not the point.
Refused, each on its own rule and ground-truthed against PyYAML 6.0.3: a key off
the allowlist, a nested collection, a quoted leaf, a duplicate key, an empty or
unclosed mapping, trailing junk, and {a:b} (which PyYAML reads as the KEY a:b).
A refused mapping still raises rather than degrading into a string, so the 1.1.0
type-confusion defect is not reopened, and the block, dotted and inline-colon
routes still raise.
`resource` is deliberately off the allowlist though SPEC.md 5.1 names it inside
a `sources` entry: it is a pointer rather than a label and the only key T3
exists for, so admitting it would let `executor: { resource: skills/run.md }`
carry an executable-code pointer through in typed clothes -- the door-C route
closed in 1.1.0. It costs nothing today, because the conformant carrier for
sources[].resource is a block sequence of block mappings, which stays refused.
Mapping leaves are scanned like every other frontmatter value (T1). Coverage
matrix 130/130 (new row: the off-allowlist key). No exported surface, detector
behaviour or calibration changed.
This commit is contained in:
parent
7cb4553301
commit
58704834b6
7 changed files with 432 additions and 43 deletions
|
|
@ -596,9 +596,11 @@ _DEGRADED_TO_STRING = [
|
|||
@pytest.mark.parametrize("cid,fm,yaml_reads_as", _DEGRADED_TO_STRING,
|
||||
ids=[c[0] for c in _DEGRADED_TO_STRING])
|
||||
def test_a_mapping_construct_never_degrades_into_a_string(cid, fm, yaml_reads_as):
|
||||
# The mapping *class* has no expressible form through T2 — so a mapping
|
||||
# construct must RAISE, never parse "successfully" into the wrong type. A
|
||||
# consumer reading frontmatter["sources"][0].get("uri") must not be handed a str.
|
||||
# None of these shapes is the one form T2 admits (G3, the allowlisted flow
|
||||
# mapping) — so each must RAISE, never parse "successfully" into the wrong
|
||||
# type. A consumer reading frontmatter["sources"][0].get("uri") must not be
|
||||
# handed a str, and that holds whether the mapping class has no expressible
|
||||
# form or one.
|
||||
with pytest.raises(OKFFrontmatterError):
|
||||
parse_frontmatter(f"---\nid: x\n{fm}---\n\nbody\n")
|
||||
|
||||
|
|
@ -651,13 +653,17 @@ def test_pointer_in_a_degraded_mapping_no_longer_reaches_the_consumer_tree(cid,
|
|||
assert result.disposition is Disposition.FAIL_SECURE, "hole reopened — see LIMITATIONS.md"
|
||||
|
||||
|
||||
def test_every_route_to_a_mapping_fails_on_a_different_rule():
|
||||
# The v0.2 wall is not a choice between two forms where one is better: ALL three
|
||||
# ways to express a mapping fail, each on its own rule, so the mapping *class* has
|
||||
# no expressible form through T2. v0.2's `generated` IS a mapping (`by` required
|
||||
# when present), so it cannot be expressed at all.
|
||||
def test_exactly_one_route_to_a_mapping_is_expressible():
|
||||
# Was: ALL FOUR routes failed, each on its own rule, so the mapping *class* had
|
||||
# no expressible form (and v0.2's `generated` could not be written at all). G3
|
||||
# opens exactly ONE of them - the allowlisted flow form - and the other three
|
||||
# still fail, each on its own rule. That the openable route is the one whose
|
||||
# every key the allowlist inspects is the whole design: block, dotted and inline
|
||||
# give the allowlist nothing to inspect, so they stay shut.
|
||||
assert parse_frontmatter("---\nid: x\ngenerated: { by: x, at: y }\n---\n\nbody\n")[0][
|
||||
"generated"] == {"by": "x", "at": "y"}
|
||||
|
||||
routes = {
|
||||
"flow": "generated: { by: x, at: y }\n",
|
||||
"block": "generated:\n by: x\n",
|
||||
"dotted": "generated.by: x\n",
|
||||
"inline": "generated: by: x\n",
|
||||
|
|
@ -667,11 +673,10 @@ def test_every_route_to_a_mapping_fails_on_a_different_rule():
|
|||
with pytest.raises(OKFFrontmatterError) as exc:
|
||||
parse_frontmatter(f"---\nid: x\n{fm}---\n\nbody\n")
|
||||
errors[name] = str(exc.value)
|
||||
assert "indicator" in errors["flow"]
|
||||
assert "nested mappings" in errors["block"]
|
||||
assert "key" in errors["dotted"]
|
||||
assert "mapping" in errors["inline"]
|
||||
assert len(set(errors.values())) == 4, "routes must fail distinctly, not collapse"
|
||||
assert len(set(errors.values())) == 3, "routes must fail distinctly, not collapse"
|
||||
|
||||
|
||||
_BLOCK_LIST_ITEM_SHAPES = [
|
||||
|
|
@ -698,7 +703,9 @@ def test_two_keys_per_item_is_where_the_block_list_hard_rejects():
|
|||
|
||||
|
||||
@pytest.mark.parametrize("fm", [
|
||||
"generated: { by: x, at: y }\n", "sources: [{ id: a }]\n", "tags: [a, b]\n",
|
||||
# The flow row carries a key OFF the G3 allowlist: the shape is admitted, the
|
||||
# key is not, so this stays a T2 rejection and the door A/B half still holds.
|
||||
"generated: { by: x, tool: y }\n", "sources: [{ id: a }]\n", "tags: [a, b]\n",
|
||||
"generated:\n by: x\n", "generated.by: x\n",
|
||||
"sources:\n - id: a\n resource: file://x\n",
|
||||
])
|
||||
|
|
@ -738,3 +745,161 @@ def test_link_extraction_survives_the_redos_fix():
|
|||
assert extract_link_targets("see [x](./a.md) and [y](/b.md)") == ["./a.md", "/b.md"]
|
||||
assert extract_link_targets("[a b](./c.md)") == ["./c.md"]
|
||||
assert extract_link_targets("text [](./t.md)") == ["./i.png"]
|
||||
|
||||
|
||||
# --- G3: the typed, allowlisted mapping form (2026-08-21) --------------------
|
||||
# Door 1 of three (operator decision, 2026-08-21). The mapping *class* had no
|
||||
# expressible form, and OKF v0.2 writes its whole trust and provenance layer as
|
||||
# mappings — SPEC.md @ 62432a09 §5.2 uses flow form in its own examples, and §11
|
||||
# carries a hard MUST that presupposes they parse ("consumers MUST treat a bare
|
||||
# `verified` mapping as a one-element list"). A consumer measured 0 of 53
|
||||
# upstream concepts through the gate. This admits ONE shape: a flow mapping whose
|
||||
# every key is on the allowlist and whose every leaf is a plain scalar.
|
||||
|
||||
def test_spec_flow_mapping_parses_into_a_typed_mapping():
|
||||
# SPEC.md §5.2, verbatim. This is the red test: it must fail before the form
|
||||
# exists and pass after, with a real dict — never a degraded string.
|
||||
doc = (
|
||||
"---\ntype: table\n"
|
||||
"generated: { by: reference_agent/gemini-2.5-pro, at: 2026-06-20T22:53:05Z }\n"
|
||||
"---\nbody\n"
|
||||
)
|
||||
assert parse_frontmatter(doc)[0]["generated"] == {
|
||||
"by": "reference_agent/gemini-2.5-pro",
|
||||
"at": "2026-06-20T22:53:05Z",
|
||||
}
|
||||
|
||||
|
||||
def test_spec_bare_verified_mapping_parses():
|
||||
# SPEC.md §5.2's bare form, which §11 turns into a hard MUST for consumers
|
||||
# ("MUST treat a bare `verified` mapping as a one-element list") - a rule that
|
||||
# cannot be obeyed by a consumer that cannot parse the mapping.
|
||||
doc = "---\ntype: table\nverified: { by: human:ahormati, at: 2026-06-25T09:00:00Z }\n---\nb\n"
|
||||
assert parse_frontmatter(doc)[0]["verified"] == {
|
||||
"by": "human:ahormati", "at": "2026-06-25T09:00:00Z"}
|
||||
|
||||
|
||||
def test_spec_verified_list_of_flow_mappings_parses():
|
||||
# §5.2's list form. This is the SAME typed form in list position, not the
|
||||
# block-sequence-with-one-key route (`- uri: x`), which stays shut below.
|
||||
doc = (
|
||||
"---\ntype: table\nverified:\n"
|
||||
" - { by: human:ahormati, at: 2026-06-25T09:00:00Z }\n"
|
||||
" - { by: process:finance-nightly, at: 2026-06-26T02:00:00Z }\n"
|
||||
"---\nbody\n"
|
||||
)
|
||||
assert parse_frontmatter(doc)[0]["verified"] == [
|
||||
{"by": "human:ahormati", "at": "2026-06-25T09:00:00Z"},
|
||||
{"by": "process:finance-nightly", "at": "2026-06-26T02:00:00Z"},
|
||||
]
|
||||
|
||||
|
||||
def test_spec_usage_window_parses():
|
||||
doc = "---\ntype: table\nusage_window: { from: 2026-06-01T00:00:00Z, to: 2026-06-30T00:00:00Z }\n---\nb\n"
|
||||
assert parse_frontmatter(doc)[0]["usage_window"] == {
|
||||
"from": "2026-06-01T00:00:00Z", "to": "2026-06-30T00:00:00Z"}
|
||||
|
||||
|
||||
def test_an_unknown_key_inside_a_mapping_is_still_rejected():
|
||||
# The rejection side of the allowlist. Without this test the allowlist could
|
||||
# silently grow to "anything" - or be emptied - and nothing would fail.
|
||||
with pytest.raises(OKFFrontmatterError) as exc:
|
||||
parse_frontmatter("---\nid: x\ngenerated: { by: a, tool: shell }\n---\n\nbody\n")
|
||||
assert "allowlist" in str(exc.value)
|
||||
|
||||
|
||||
def test_the_allowlist_is_not_empty_and_admits_only_the_spec_keys():
|
||||
# Both directions of the same guard: a shrunk allowlist breaks the first
|
||||
# assertion, a widened one the second.
|
||||
for key in ("by", "at", "from", "to", "id", "title", "author", "usage_count",
|
||||
"last_modified"):
|
||||
assert parse_frontmatter(f"---\nid: x\nk: {{ {key}: v }}\n---\n\nb\n")[0]["k"] == {key: "v"}
|
||||
for key in ("resource", "executor", "attester", "runtime", "command", "uri"):
|
||||
with pytest.raises(OKFFrontmatterError):
|
||||
parse_frontmatter(f"---\nid: x\nk: {{ {key}: v }}\n---\n\nb\n")
|
||||
|
||||
|
||||
_FLOW_REJECTED = [
|
||||
# (id, value, what PyYAML 6.0.3 makes of it)
|
||||
("nested mapping", "{ by: { at: x } }", "a nested mapping"),
|
||||
("nested sequence", "{ by: [a, b] }", "a sequence leaf"),
|
||||
("anchor leaf", "{ by: &a x }", "an anchor definition, silently"),
|
||||
("tag leaf", "{ by: !!python/object:os.system x }", "refused outright"),
|
||||
("block scalar leaf", "{ by: | }", "a scanner error"),
|
||||
("nested colon leaf", "{ by: sub: v }", "refused outright"),
|
||||
("no space after colon", "{by:x}", "the KEY 'by:x', not a scalar"),
|
||||
("quoted leaf", "{ title: 'a, b' }", "a scalar - we refuse, deliberately"),
|
||||
("empty mapping", "{}", "an empty mapping"),
|
||||
("empty leaf", "{ by: }", "None"),
|
||||
("unclosed", "{ by: x", "a parse error"),
|
||||
("trailing junk", "{ by: x } more", "a parse error"),
|
||||
("duplicate key", "{ by: a, by: b }", "last-wins, silently"),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("cid,value,yaml_reads_as", _FLOW_REJECTED,
|
||||
ids=[c[0] for c in _FLOW_REJECTED])
|
||||
def test_the_mapping_form_admits_scalar_leaves_on_allowlisted_keys_only(cid, value, yaml_reads_as):
|
||||
with pytest.raises(OKFFrontmatterError):
|
||||
parse_frontmatter(f"---\nid: x\ngenerated: {value}\n---\n\nbody\n")
|
||||
|
||||
|
||||
def test_a_rejected_mapping_never_degrades_into_a_string():
|
||||
# The 1.1.0 defect, re-asserted against the NEW form: a refused mapping must
|
||||
# raise, not arrive as a str a consumer will .get() a key out of.
|
||||
for value in ("{ by: { at: x } }", "{ tool: shell }", "{ by: x"):
|
||||
with pytest.raises(OKFFrontmatterError):
|
||||
parse_frontmatter(f"---\nid: x\ngenerated: {value}\n---\n\nbody\n")
|
||||
|
||||
|
||||
def test_the_admitted_mapping_is_a_dict_not_a_string():
|
||||
value = parse_frontmatter("---\nid: x\ngenerated: { by: a, at: b }\n---\n\nb\n")[0]["generated"]
|
||||
assert isinstance(value, dict), "a typed form that arrives as a str is the 1.1.0 defect"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("cid,fm", [
|
||||
("block sequence, one key", "attester:\n - resource: attesters/sql_equality.py\n"),
|
||||
("inline second colon", "attester: resource: attesters/sql_equality.py\n"),
|
||||
("block mapping", "attester:\n resource: attesters/sql_equality.py\n"),
|
||||
("flow mapping, pointer key", "attester: { resource: attesters/sql_equality.py }\n"),
|
||||
])
|
||||
def test_the_pointer_routes_stay_shut(cid, fm):
|
||||
# G3 is additive: none of the routes that put an executable-code pointer in a
|
||||
# key the https allowlist never inspects is reopened. The fourth row is why
|
||||
# `resource` is off the allowlist - the form would otherwise have carried the
|
||||
# door-C pointer through in typed clothes instead of degraded ones.
|
||||
doc = f"---\nid: x\ntype: Attested Computation\n{fm}---\n\nbody\n"
|
||||
with pytest.raises(OKFFrontmatterError):
|
||||
parse_frontmatter(doc)
|
||||
assert import_bundle({"computations/x.md": doc}).disposition is Disposition.FAIL_SECURE
|
||||
|
||||
|
||||
def test_injection_in_a_mapping_leaf_is_caught_by_the_scan():
|
||||
# T1 is not weakened by the new shape: a mapping leaf is scanned exactly like a
|
||||
# scalar value or a list item. A typed form that parses but is not scanned would
|
||||
# be a hole, not a fix.
|
||||
doc = f"---\ntype: table\ngenerated: {{ by: {_INJECTION} }}\n---\nclean body\n"
|
||||
assert scan_concept(doc).found is True
|
||||
|
||||
|
||||
def test_injection_in_a_listed_mapping_leaf_is_caught_by_the_scan():
|
||||
doc = f"---\ntype: table\nverified:\n - {{ by: {_INJECTION} }}\n---\nclean body\n"
|
||||
assert scan_concept(doc).found is True
|
||||
|
||||
|
||||
def test_a_conformant_v02_trust_layer_now_reaches_the_gate():
|
||||
# The measured consequence: a consumer reported 0 of 53 upstream concepts through
|
||||
# the gate, because every one of them carries §5.2 trust frontmatter.
|
||||
doc = (
|
||||
"---\n"
|
||||
"type: table\n"
|
||||
"title: Users\n"
|
||||
"resource: https://example.com/users\n"
|
||||
"generated: { by: reference_agent/gemini-2.5-pro, at: 2026-06-20T22:53:05Z }\n"
|
||||
"verified: { by: human:ahormati, at: 2026-06-25T09:00:00Z }\n"
|
||||
"usage_window: { from: 2026-06-01T00:00:00Z, to: 2026-06-30T00:00:00Z }\n"
|
||||
"---\nThe users table.\n"
|
||||
)
|
||||
result = import_bundle({"tables/users.md": doc})
|
||||
assert result.disposition is Disposition.WARN
|
||||
assert result.concepts[0].error is None
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue