studio_server._persist_receipt() builds a command receipt's filename as
ts + command + payload-hash prefix + status + a monotonic sequence, and says
plainly why:
"every attempt is durable + unique: ts + command + payload-hash prefix +
status + a monotonic suffix, and we loop until the name is free so rapid
identical repeats never overwrite each other."
The payload-hash prefix alone is deliberately NOT the unique part — it is a hash
of the command plus its inputs, so two runs of the same command with the same
inputs share it. The timestamp and the sequence are what separate them.
_short_rcpt() keeps only the payload-hash prefix:
m = re.search(r"_([0-9a-f]{8})_(?:executed|approved|failed|blocked)", rid)
return "rcpt_" + m.group(1) if m else rid
station-v0.98 makes that handle load-bearing in two places at once:
railcall_receipts_list now returns the handle as a row's ONLY identifier and
drops the filename — the row key is "rcpt" when a handle can be derived and
"file" only when one cannot — and adds the note "verify any row via
railcall_receipt_verify with its rcpt/file value".
railcall_receipt_verify resolves a handle by scanning for the fragment and
refuses when more than one execution receipt matches: "ambiguous rcpt handle
(N matches) — pass the full filename from railcall_receipts_list".
So for any command run twice with the same inputs, the listing prints two rows
that are byte-identical, neither can be verified by the value the listing gave,
and the error's suggested remedy — the full filename from that listing — is
exactly what the listing stopped providing.
(v0.98 added disambiguation for the one case it anticipated: a single approval
mints an approved_not_executed receipt alongside the executed one, and the new
filter picks the executed one. That filter only helps when exactly one execution
receipt matches; two separate executions of the same payload still collide.)
Reproduction steps:
- Point RAILCALL_WS at a scratch workspace and create receipts/capoff/.
- Write two command receipts in the shape _persist_receipt() produces — same
command, same payload-hash prefix, different timestamp and sequence, both
executed:
cmd_20260814T090000Z_discord_post_message_7147f18c_executed_0001.json
cmd_20260814T133000Z_discord_post_message_7147f18c_executed_0007.json
This is what posting the same message twice produces: the payload hash is
over the command and its inputs, so it is identical, and only the timestamp
and sequence differ.
- Derive the handle for each: mcp_server._short_rcpt(<filename>).
- Call the listing tool: mcp_server.h_receipts(None, {"limit": 10}).
- Take the value each row printed and verify it:
mcp_server.h_verify(None, {"file": <that value>}).
Expected:
Every row a listing returns can be verified by the identifier that listing
printed — that is what the row's note promises, and receipts are the product's
proof artifact. Two distinct receipts must be distinguishable in a listing.
Actual:
_short_rcpt(r1) = rcpt_7147f18c
_short_rcpt(r2) = rcpt_7147f18c
handles identical: True
railcall_receipts_list rows:
{"rcpt": "rcpt_7147f18c", "schema": "railcall_command_receipt.v0", "signed": true}
{"rcpt": "rcpt_7147f18c", "schema": "railcall_command_receipt.v0", "signed": true}
note: verify any row via railcall_receipt_verify with its rcpt/file value
verify(rcpt_7147f18c) -> ok=False
"ambiguous rcpt handle (2 matches) — pass the full filename from
railcall_receipts_list"
verify(rcpt_7147f18c) -> ok=False (same, for the other row)
The two rows are byte-identical, so a caller cannot tell the receipts apart, let
alone address one of them.
Root cause:
mcp_server.py — _short_rcpt() derives the handle from the payload-hash fragment
only, discarding the timestamp and monotonic sequence that _persist_receipt()
adds specifically to make repeats unique. h_receipts() then substitutes that
handle for the filename in every row it can derive one for, so the unique
identifier is no longer returned anywhere, while h_verify() requires a unique
match to resolve. The same handle is also returned as "rcpt" by
_module_execute_or_pointer() on a successful read, so a repeated read command
produces the same non-addressable handle there too.
Suggested fix:
Keep the handle compact but unique — the monotonic sequence is already in the
filename and is what disambiguates repeats, e.g. rcpt_7147f18c_0007, with
_short_rcpt capturing both groups and h_verify matching on both. Failing that,
h_receipts should always include the full filename alongside the handle
(the token cost is bounded and only paid on listing), so the error message's
remedy exists; and _short_rcpt should return the full id whenever the derived
handle is not unique among the receipts on disk.
Honest scope:
This is a verifiability and addressability defect, not a governance bypass.
Nothing executes that should not, no signature is forged, and the receipts
themselves are intact on disk and still verifiable by full filename for anyone
who can list the directory directly. What breaks is the MCP-facing path: the
listing tool and the verify tool disagree about what identifies a receipt, so
the "hand a receipt to a verifier" flow dead-ends for repeated commands.
The trigger is ordinary rather than adversarial — running the same command with
the same inputs twice. It does not affect receipts whose filename carries no
derivable handle (those still return "file" with the full name), and reads
without a payload hash are unaffected because "nohash" does not match the
handle pattern.
Counter-evidence checked:
- Confirmed the payload-hash prefix is by construction NOT unique: the filename
builder's own comment states the timestamp and monotonic suffix exist so
"rapid identical repeats never overwrite each other".
- Confirmed against the real, unmodified v0.98 mcp_server from a clean tarball
extraction, driving _short_rcpt, h_receipts and h_verify directly.
- Confirmed the v0.98 approved_not_executed disambiguation does NOT cover this:
it selects a single execution receipt, and here there are two.
- Confirmed the listing genuinely drops the filename for these rows (the row key
is "rcpt", not "file"), so this is not a case of the caller ignoring available
data.
- Confirmed the rows are byte-identical, ruling out the reading that some other
field would let a caller disambiguate them.
Distinctness:
_short_rcpt and the compact listing row are new in station-v0.97/v0.98, so no
earlier report can cover them. This is receipt addressability on the MCP
surface, distinct from findings about whether a receipt's contents are verified
(summary/body-hash binding, chain verification) — here the receipts are fine and
cannot be reached. I did not find a community thread about the rcpt_ handle.