deeper-bucket-sync / app /frontmatter.py
cmpatino's picture
cmpatino HF Staff
Upload folder using huggingface_hub
4879fc7 verified
Raw
History Blame Contribute Delete
6.16 kB
from __future__ import annotations
import datetime
import io
from typing import Any
import yaml
from app.config import Settings
from app.errors import InvalidFrontmatter
_DELIM = "---"
def parse(text: str) -> tuple[dict[str, Any], str]:
if not text.startswith(_DELIM):
return {}, text
rest = text[len(_DELIM):].lstrip("\n")
end = rest.find(f"\n{_DELIM}")
if end == -1:
return {}, text
fm_text = rest[:end]
body = rest[end + len(_DELIM) + 1 :]
if body.startswith("\n"):
body = body[1:]
try:
data = yaml.safe_load(fm_text) or {}
except yaml.YAMLError as e:
raise InvalidFrontmatter(f"could not parse YAML frontmatter: {e}")
if not isinstance(data, dict):
raise InvalidFrontmatter("frontmatter must be a mapping")
return data, body
def serialise(fm: dict[str, Any], body: str) -> str:
buf = io.StringIO()
buf.write(_DELIM)
buf.write("\n")
yaml.safe_dump(fm, buf, sort_keys=False, default_flow_style=False, allow_unicode=True)
buf.write(_DELIM)
buf.write("\n")
if body:
if not body.startswith("\n"):
buf.write("\n")
buf.write(body)
if not body.endswith("\n"):
buf.write("\n")
return buf.getvalue()
def merge(client_fm: dict[str, Any], server_fm: dict[str, Any]) -> dict[str, Any]:
"""Server-stamped fields always win; client fields fill in the rest."""
merged = dict(client_fm)
merged.update(server_fm)
return merged
# Exactly the frontmatter keys the system itself writes onto a message: `type`
# and `refs` come from the client, `agent`/`timestamp`/`via` are server-stamped
# over whatever arrived, and `broadcast`/`channel` are server-owned (the routes
# reject them from clients with a more specific error before we get here — they
# are listed so this set stays an honest inventory of what a message file can
# contain, and so re-posting a message the API itself served still round-trips).
#
# The allowlist exists because message frontmatter is author-controlled and ends
# up inside the very JSON a watcher parses. In eq2 the client scanned responses
# for `"filename":"..."` anywhere, so one post carrying a
# `filename: 99999999-…zzz.md` key could pin every watcher's cursor past all
# future mail, permanently. The client-side fix is to read only the top-level
# server-computed `cursor` (WATCH_DESIGN.md §4.4); this is the other half, and
# it is the half that holds even against a client that gets it wrong: no
# response-shaped name (`filename`, `cursor`, `next`, `watch`) can ever appear
# in serialised frontmatter.
#
# The key allowlist alone is not enough: `yaml.safe_load` happily turns a
# mapping-valued key (e.g. `type: {cursor: 99999999-…zzz.md}`) into a nested
# dict, and that dict's *keys* serialise as raw JSON object keys — untouched by
# the JSON-string-escaping that makes ordinary string values safe. So every
# frontmatter value must also be a YAML scalar (`refs` is the one exception,
# where a list of scalars is the client-facing shape); rejecting non-scalar
# values is what actually makes the "no response-shaped name can ever appear
# in serialised frontmatter" invariant hold, rather than just holding for
# top-level keys.
MESSAGE_FRONTMATTER_KEYS = frozenset(
{"type", "refs", "agent", "timestamp", "via", "broadcast", "channel"}
)
# yaml.safe_load's scalar result types (it also produces datetime.date/
# datetime.datetime for bare-looking dates and timestamps, not just str).
_SCALAR_TYPES = (str, int, float, bool, type(None), datetime.date, datetime.datetime)
def _is_scalar(value: Any) -> bool:
return isinstance(value, _SCALAR_TYPES)
def validate_message_frontmatter(fm: dict[str, Any]) -> None:
"""Reject client-supplied message frontmatter outside the allowlist, naming
the offending key (WATCH_DESIGN.md §5.5); also reject any value that is not
a YAML scalar (`refs` may be a list of scalars) — see the module comment
above for why non-scalar values are the other half of the vulnerability."""
for key, value in fm.items():
if key not in MESSAGE_FRONTMATTER_KEYS:
raise InvalidFrontmatter(
f"frontmatter key {key!r} is not allowed on a message; allowed "
f"keys: {', '.join(sorted(MESSAGE_FRONTMATTER_KEYS))} — put "
"anything else in the body"
)
values = value if key == "refs" and isinstance(value, (list, tuple)) else (value,)
if not all(_is_scalar(v) for v in values):
raise InvalidFrontmatter(
f"frontmatter value for {key!r} must be a scalar; lists are "
"allowed only for 'refs' and only of scalars"
)
ALLOWED_RESULT_STATUS = {"agent-run", "negative"}
def validate_result_frontmatter(settings: Settings, fm: dict[str, Any]) -> None:
"""Validate against the challenge's configured result schema.
The score field must be a positive number; `status` (when required) must
be agent-run|negative; every other required field must be a non-empty
string (or at least present, for non-string values).
"""
for field in settings.required_result_field_list:
if field not in fm:
raise InvalidFrontmatter(f"result frontmatter missing required field: {field}")
score_val = fm[settings.score_field]
if isinstance(score_val, bool) or not isinstance(score_val, (int, float)) or score_val <= 0:
raise InvalidFrontmatter(
f"`{settings.score_field}` must be a positive number ({settings.score_unit})"
)
if "status" in settings.required_result_field_list:
if fm["status"] not in ALLOWED_RESULT_STATUS:
raise InvalidFrontmatter(
f"`status` must be one of {sorted(ALLOWED_RESULT_STATUS)}, got {fm['status']!r}"
)
for field in settings.required_result_field_list:
if field in (settings.score_field, "status"):
continue
val = fm[field]
if isinstance(val, str) and not val.strip():
raise InvalidFrontmatter(f"`{field}` must be a non-empty string")