- #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>
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 bestrorlist) before callingnormalize_tags; return400on other types instead of silently emptying it. Same principle likely applies tometadata(already checked) and could apply tosource_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 aPATCHbody a400("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:
tagsis now type-checked (strorlist) beforenormalize_tagsruns; any other type returns400and leaves existing tags untouched.- Sending
content,source,kind, ordayin aPATCHbody now returns400naming the offending field(s), via a newIMMUTABLE_PATCH_FIELDScheck at the top ofArtifactDetailView.patch, before any other processing — chosen over documenting the no-op because a200that 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.