Files
cleveragents-core/tools/controller/_json_safe.py
T
drew 0bc734c020 style: ruff format the controller-state-machine branch (288 files)
Applies `ruff format` to the accumulated formatting debt on this branch.
Formatting-only — no behavioral changes. Required for CI/lint's format
gate (`nox -s format -- --check`), which the branch was failing on 288
tracked files that drifted from ruff's canonical style.

In-progress WIP files are intentionally excluded so this commit stays a
clean formatting-only diff.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 00:09:17 -04:00

72 lines
2.6 KiB
Python

"""Safe JSON encoding helper.
Phase 1k++ refinement (N5/R4): we used to pass ``default=str`` to
``json.dumps`` so nested datetime/Decimal fields didn't crash the
write AFTER the agent had done its real work. But ``default=str`` is
too permissive — a worker that accidentally emits a ``set`` or a
custom class gets silently stringified to ``"<MyObj at 0x...>"``,
masking a real output bug.
This module ships a restricted encoder that:
- Encodes ``datetime`` and ``date`` as ISO-8601 strings.
- Encodes ``Decimal`` as its string repr (preserves precision).
- Encodes ``UUID`` as its canonical string form.
- Encodes ``Path`` as its string form.
- Raises ``TypeError`` for anything else — including ``set`` /
``frozenset`` (round 3 R4: silent set→list coercion contradicted
the "raise loudly" design intent because JSON has no native set
and a reader doing ``parsed["tags"]`` would get a list, losing
set algebra. Callers that genuinely want a JSON array should call
``sorted(list(my_set))`` explicitly).
Scope (round-3 R8): this encoder is used at the boundaries where
WORKER-ORIGINATED payloads are persisted — i.e., the runner's
output-write and the scheduler's input_payload write. Other
``json.dumps`` call sites in the controller (event-row payloads,
discovery markers, etc.) serialize fixed-shape dicts of native types
and don't need restriction.
"""
from __future__ import annotations
import json
from datetime import date, datetime
from decimal import Decimal
from pathlib import Path
from typing import Any
from uuid import UUID
# Public list of types this encoder coerces. Anything else → TypeError.
_SAFE_TYPES = (datetime, date, Decimal, UUID, Path)
def _restricted_default(value: Any) -> Any:
if isinstance(value, datetime):
return value.isoformat()
if isinstance(value, date):
return value.isoformat()
if isinstance(value, Decimal):
return str(value)
if isinstance(value, UUID):
return str(value)
if isinstance(value, Path):
return str(value)
raise TypeError(
f"Object of type {type(value).__name__} is not JSON-serializable "
f"and not in the controller's safe-type allowlist "
f"({', '.join(t.__name__ for t in _SAFE_TYPES)}). "
f"To emit a set as a JSON array, call sorted(list(...)) "
f"explicitly at the producer."
)
def safe_json_dumps(obj: Any, **kwargs: Any) -> str:
"""json.dumps with a restricted ``default`` that handles the
allowlist + raises for anything else.
Extra kwargs are forwarded to ``json.dumps``."""
return json.dumps(obj, default=_restricted_default, **kwargs)
__all__ = ["safe_json_dumps"]