Relationships
≡ duplicate of #1668#2284 lab lifecycle API: dollar-key guard scans the serialised payload and rejects ordinary prose
Opened by skunk-ape · 9/18/2026
Split out of swamp-club#2279, which covers the client half in the swamp repo. This issue covers the server half.
What happened
routes/api/v1/lab/issues/[number]/lifecycle.ts guards the lifecycle payload against mongo-style operator keys like so:
const payloadStr = JSON.stringify(data.payload);
if (/"\$/.test(payloadStr)) {
return Response.json(
{ error: "payload keys must not start with $" },
{ status: 400 },
);
}The guard scans the serialised body rather than walking the parsed object, so it matches a double-quote character immediately followed by the sigil anywhere in the JSON — including inside string values.
Blast radius
Verified against the exact regex:
| payload shape | current result | correct result |
|---|---|---|
{ $set: { a: 1 } } — operator key |
rejected | rejected |
{ inner: { $gt: 1 } } — nested operator key |
rejected | rejected |
| prose containing a quoted shell variable | rejected | accepted |
| string value beginning with the sigil | rejected | accepted |
| array element beginning with the sigil | rejected | accepted |
| sigil mid-word, e.g. costs 5 per run | accepted | accepted |
The last three rows are ordinary prose and code snippets. Any agent writing triage reasoning, a plan step, or a verification log that quotes a shell command gets its lifecycle entry silently dropped.
Why it matters
The lifecycle record is the audit trail for triage, planning, approval and attestation. Because the swamp-side client treats the post as best-effort (swamp-club#2279), a rejection here is invisible to the caller — the step reports success and the reasoning is lost.
Suggested fix
Walk the parsed object recursively and reject only when an actual key begins with the sigil. Objects and arrays both need traversal. Keep the same 400 and error message so existing clients are unaffected.
Extend tests/routes/lifecycle_api_test.ts with the false-positive cases above so the guard cannot regress to a serialised-body scan.
Closed
No activity in this phase yet.