| 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 |
|
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| MESSAGE_FRONTMATTER_KEYS = frozenset( |
| {"type", "refs", "agent", "timestamp", "via", "broadcast", "channel"} |
| ) |
|
|
| |
| |
| _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") |
|
|