daybook/issues/005-patch-silently-mishandles-invalid-input.md
peter 07328a3412 Fix issues #002-#006 found by the LLM test harness
- #006 (high, data loss): make_room_for() no longer deletes unkept
  artifacts on a doomed request - checks whether eviction can possibly
  succeed before deleting anything. Also fixed a related latent bug found
  while testing: the function returned evicted Artifact objects after
  calling .delete() on them, so their .id was always None (Django clears
  pk on delete); now returns ids captured beforehand.
- #003: added server-side field-length validation (source/title/
  source_name/source_id/theme) on POST and PATCH, matching the model's
  own max_length limits, which were previously unenforced on the ingest
  path entirely.
- #004: ported daybook_post.py's content-shape validation
  (html_fragment/html_full/deck/markdown) to the server, closing the gap
  for any client that doesn't go through the helper script.
- #005: PATCH now rejects wrong-typed `tags` (previously silently wiped
  existing tags) and rejects immutable fields (content/source/kind/day)
  with 400 instead of silently no-op'ing them.
- #002: deleted the misleading skills/daybook-posting/.env.example (a
  zero-dependency script that never reads .env files) and clarified
  SKILL.md/docs/llm_posting_helper.md that these must be real
  process/shell env vars.

Adds tests/unit/ for storage-layer tests. 46 tests passing (was 25).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 13:38:59 +01:00

3.1 KiB

005 — PATCH /api/artifacts/{id} silently mishandles invalid/immutable input instead of rejecting it

Status: Fixed

Description

ArtifactDetailView.patch (artifacts/views.py) applies whatever recognized keys are present in the request body without validating their type or rejecting fields it can't act on. Two confirmed effects:

1. A wrong-typed tags value silently wipes existing tags (data loss).

normalize_tags() returns [] for anything that isn't a str or list. ArtifactDetailView.patch calls it unconditionally whenever "tags" is a key in the request body, with no type check first.

Reproduced:

# Artifact has tags ["important", "keep-me"]
curl -X PATCH .../api/artifacts/a74de286 -d '{"tags": 12345}' ...
# -> 200, tags silently become []

An agent that sends a malformed tags value (e.g. a stray int from a bad template substitution) gets a 200 and loses the artifact's existing tags with no indication anything went wrong.

2. Immutable fields (content, source, kind, day) are silently no-ops instead of rejected.

docs/api.md's "Not editable yet" table lists these fields and why, but doesn't say what happens if a client sends them anyway. In practice: PATCH with {"content": "...", "source": "...", "kind": "...", "day": "..."} returns 200 with the artifact unchanged — the fields are silently dropped, not rejected. An agent attempting to fix a typo in content via PATCH (a very natural thing to try, since PATCH is the only mutate-after-create endpoint) gets a success response that looks like it worked but did nothing.

Both are instances of the same root cause: patch() doesn't validate before applying, unlike POST's validate_source_fields().

Suggested Fix

  • Type-check tags (must be str or list) before calling normalize_tags; return 400 on other types instead of silently emptying it. Same principle likely applies to metadata (already checked) and could apply to source_prompt/source_name/source_id (currently accept anything via implicit str coercion via Django, but worth a pass).
  • Either make sending an immutable field (content/source/kind/day) in a PATCH body a 400 ("field is not editable"), or explicitly document that unknown/immutable keys are ignored — right now the behavior is undocumented and easy to mistake for success.

Resolution

Went with explicit rejection for both, over silent-ignore-and-document:

  • tags is now type-checked (str or list) before normalize_tags runs; any other type returns 400 and leaves existing tags untouched.
  • Sending content, source, kind, or day in a PATCH body now returns 400 naming the offending field(s), via a new IMMUTABLE_PATCH_FIELDS check at the top of ArtifactDetailView.patch, before any other processing — chosen over documenting the no-op because a 200 that does nothing is easy to mistake for success.

docs/api.md's "Not editable" table now says so explicitly instead of just listing reasons. Tests added: wrong-typed tags is rejected and leaves existing tags intact, and each of the four immutable fields is rejected individually.