Component: workbench/primitives/receipt_summary.py (rendered by Studio - Runs)
Station: station-v1.5.6, built 2026-08-23T17:43:49Z, engine c83aae31
What happens
Open a command receipt in Runs. The plain-language panel makes exactly one statement about
external contact: "Nothing left this machine - no external system was written to." It prints
that sentence on every command receipt - correct for the ones that touched nothing, wrong for
the ones whose signed body carries external_api_touched: true. The panel then closes with
"Signed AND verified against this install's key - the summary above is derived from the sealed,
signature-checked record." The strongest trust claim in the product is attached to a sentence
the sealed record contradicts.
Why
effects() derives the "Touched:" list by walking a per-node array:
primitives/receipt_summary.py:113-114
for n in (receipt or {}).get("node_receipts") or []:
if not isinstance(n, dict) or not n.get("external_api_touched"):
Command receipts have no node_receipts array. approval_airlock.make_receipt() builds the
entire railcall_command_receipt.v1 body at approval_airlock.py:428-462 and there is no node
array in it - external_api_touched is a single top-level bool at line 445. So effects()
returns [] for every command receipt by construction, and summarize() takes the else branch:
primitives/receipt_summary.py:158-161
if eff:
lines.append("Touched: " + "; ".join(e["sentence"] for e in eff))
else:
lines.append("Nothing left this machine - no external system was written to.")
Nothing gates the summary to workflow receipts on the way in. _handle_receipts_read attaches_summary to whatever read_receipt() returned, command receipts included
(routes/dispatch_reads_final.py:340-342), and the Studio detail panel renders _summary.lines
unconditionally (studio/scripts/views/receipts.js:442-447). Command receipts are a first-class
row in that tab by design - receipts_index() lists them first and even carries the same field
into the row as external (routes/receipts.py:97-130, field at line 122).
The file's own rule, receipt_summary.py:13-16: "Say only what the receipt says... a summary
that drifts from its receipt is worse than no summary." Line 161 drifts from the receipt it is
summarizing. The comment at lines 190-195 records a prior community report against this same
function's proof line, fixed by verifying instead of asserting; this is its sibling on the
effects line, which that pass did not reach.
Reproduce
Path A - stock UI:
- Configure any integration whose provider is not
railcall(I usedfreelancer). - Studio - Sends, pick a command from that provider, Fire it.
- Approve it in the airlock (method
ui_click). A read-classified command skips the airlock
and executes directly via commands.py:324-339 - same result: external_api_touched: true,
no node_receipts, same sentence. Note that a2d3bf upgrades every command on an
egress-capable module to write_requires_approval (commands.py:304-320), which is why my
own read commands carry an approval block.
- The station writes
.railcall_workspace/receipts/cmd_<ts>_<cmd>_executed_NNNN.jsonwith
"external_api_touched": true.
- Studio - Runs. The header stat "External sends" counts that row
(views/receipts.js:172,178).
- Click the row. The summary panel reads "Nothing left this machine - no external system was
written to."
- Click "Copy JSON" on that same panel. The sealed body says
"external_api_touched": true.
Path B - deterministic, no credentials, no network. Against any receipt from step 4:
cd ~/.railcall/station/workbench
python3 -c "
import sys, json; sys.path.insert(0,'.')
from primitives import receipt_summary as R
d = json.load(open('<path to the cmd_*_executed_*.json>'))
print('sealed external_api_touched =', d['external_api_touched'])
print(R.as_text(R.summarize(d)))
"
Evidence
Station station-v1.5.6. 265 command receipts in~/.railcall/station/.railcall_workspace/receipts/. Of those, 0 carry a node_receipts key and
0 carry an effects key; 255 carry external_api_touched: false and 10 carryexternal_api_touched: true. All 10 are signed (ed25519, key_id c49d817b993aacb5) and all 10
are result_status: executed.
Taking cmd_20260825T193821Z_get_bids_31099499_executed_0021.json. Sealed record:
"resolved_command_id": "get_bids", "provider": "freelancer",
"mode": "write_requires_approval", "approval": {"method": "ui_click", ...},
"external_api_touched": true, "result_status": "executed",
"output": {"count": 5, "low": 15.0, "high": 20.0, "avg": 16.67, "bids": [...]},
"integrity_hash": "sha256:a37acea9c06a61219e6959c1a59fb53cecac0e53fdfb09aa337b00aff23ae40a"
The output block holds five live bid records that came back from freelancer.com. Running the
station's own summarize() on that file (Path B above) prints:
This workflow finished at 19:38 UTC
Origin could not be determined
. Nothing left this machine - no external system was written to.
Signed AND verified against this install's key - the summary above is derived from the sealed,
signature-checked record.
summary["effects"] is []. So the same screen states both claims: the Runs header counts
"External sends 10" from external_api_touched, and each of those 10 rows opens onto a sentence
saying nothing left the machine, under a line asserting the summary is derived from the verified
seal.
Impact
The operator reading Runs is misinformed about the one fact the airlock exists to establish:
whether this action reached a third party. The signature is not the problem - it covers the
record correctly, and the record is right. The summary is derived from that record and inverts it.
It is worse in the detail panel than the numbers suggest, because that panel renders no other
statement on the subject. It shows integrity, signature, key_id, invoker, outcome, error
(views/receipts.js:452-461), plus the routing and incremental blocks. external_api_touched
is not rendered anywhere in it. The false sentence is the only claim about external contact an
operator sees there; the true value is reachable only through Copy JSON or the header count.
On the read case, verified here: all 10 of my receipts are reads (search_projects,get_project, get_bids). For a read the second clause is defensible - nothing was written to
the provider. The first clause is not: an authenticated HTTPS request carrying query parameters
went to freelancer.com. That clause alone is the defect.
On the write case, code-derived and flagged as such below: those 10 receipts already went through
the approved-write branch (routes/commands.py:538-548, mode: write_requires_approval,approval.method: ui_click). That branch computes
ext = (cmd.get("provider") != "railcall") and not (
isinstance(output, dict) and output.get("dry_run"))
There is no read/write discriminator in that expression, and make_receipt emits nonode_receipts for either. A Slack post, a Stripe charge or a Sheets append fired from Sends
therefore produces the identical receipt shape and the identical sentence, where both clauses
are false: the operator is told nothing was written, on a signed record of a write that
succeeded.
Suggested fix
Smallest change that respects the design: gate the else branch on the top-level field rather
than inventing a new sentence, so the existing wording keeps serving the 255 receipts where it
is correct.
if eff:
lines.append("Touched: " + "; ".join(e["sentence"] for e in eff))
elif r.get("external_api_touched"):
lines.append("Touched: " + _label_provider(r.get("provider")) + " - "
+ _label_verb(r.get("resolved_command_id"), r.get("provider"))
+ " (single command; this receipt carries no per-node detail)")
else:
lines.append("Nothing left this machine - no external system was written to.")
_label_provider and _label_verb already handle this shape - list_unscheduled maps to
"records read", so read verbs are in the vocabulary; get_bids would render as
"Freelancer - get bids". Exact wording is yours. The invariant worth holding is narrower than
the wording: never print the "nothing left" sentence while the sealed record saysexternal_api_touched: true. Under-claiming ("this receipt records external contact") is safe
by the doctrine already written into lines 190-195; over-claiming the absence of egress is not.
The same guard covers any future receipt family that carries the top-level bool without a node
array, which is what made this reachable in the first place.
What I did not verify
- I did not execute a write command against a live provider on this station. The write paragraph
is derived from routes/commands.py:538-548 and approval_airlock.make_receipt, not observed.
Every receipt I observed is a read.
- Verified on station-v1.5.6. station-v1.5.7 shipped 2026-08-24; the delta I catalogued was
entirely Teams governance (team_approval.py, routes/team.py, team_mesh.py,
team_policy.py, team_jobs.py, routes/relay.py, studio_server.py, new
primitives/team_rules.py and primitives/team_blind.py) and did not include
receipt_summary.py. I could not diff against a v1.5.7 checkout - none exists on this machine.
If the file moved after v1.5.7, this may already be gone.
- No test in the install references
receipt_summary. Grepping the whole tree, the only
references are the module itself, routes/dispatch_reads_final.py, and the import smoke test in
scripts/publish-release.sh:85. So I have no evidence either way about intent - only that
nothing pins the current behaviour.
Adjacent, same function, lower priority and not the point of this report: the headline on a
command receipt reads "This workflow finished at 19:38 UTC". Command receipts carry noworkflow_id and no outcome, so summarize() falls back to "This workflow" (line 134) and
"finished" (line 135).