Skip to content

ISS-12714: Print write operation ids from scripts - #345

Merged
shafty023 merged 2 commits into
mainfrom
feat/iss-12714-print-operation-id
Oct 26, 2026
Merged

shafty023 merged 2 commits into
mainfrom
feat/iss-12714-print-operation-id

Conversation

@shafty023

@shafty023 shafty023 commented Oct 26, 2026 •

Copy link
Copy Markdown
Collaborator

Purpose

ISS-12714 (child of PRD-800, "Writes made outside the Closedloop MCP"). A write made from a shell or script is tied to a session only when its operation id appears in that call's recorded output. The server side shipped in symphony-alpha ISS-12552 (#9058): API and MCP write responses carry an optional operationReceipt ({ operationId: "clop_<uuid>", outcome }), present only while the org's attribution-evidence-policy flag is on. This makes the two script-side write paths in this repo print that id.

What changed

platform 1.1.6: upload-artifact

  • upload_artifact.py adds a top-level operationReceipt field to its JSON result for the document it creates (create-document) or versions (create-document-version). It reads the receipt from the MCP body root, or from data for an enveloped body, the same lookup symphony-alpha's file-bug.mjs and stack-rank-apply.mjs use, and passes it through as those scripts do.
  • The field is placed ahead of response, so a transcript that keeps only the head of a long result (Claude Code's persisted-output preview) still holds the id.
  • SKILL.md step 5a reports the operation id when the output has one.

vibe 1.0.83: cl-policy direct-API fallback

  • The fallback is instructions, not code. New step 6: for a POST/PUT/PATCH/DELETE, keep the response in a variable, print it (or the usual jq filter of it) on stdout, then print (.data? // .).operationReceipt.operationId on stderr in the same shell call. Steps 7 to 9 are renumbered; nothing references them by number.
  • Version pin moved from the 1.0.82 release test to the new test, following the previous releases (main shipped 1.0.82 while this was open).

Output compatibility

  • upload_artifact.py: the field is additive and omitted when there is no receipt, or when it is malformed (no string operationId). Without a receipt, the keys and their order are exactly what they were. Stdout is still one JSON document.
  • cl-policy: stdout is byte-identical to the response or the caller's filter; the id goes to stderr. Claude Code records stderr in the shell tool result, which is what symphony-alpha's matcher reads (measured below); Codex is expected to as well and was not measured. With no receipt, a list response, or an error envelope, nothing extra is printed and the exit status is 0.

Tests

  • plugins/platform/skills/upload-artifact/scripts/test_upload_artifact.py (new): receipt present for create and version (root and data shapes), the id in the matcher's clop_<uuid> form, field order, receipt absent or malformed (keys unchanged), and main() printing one parseable JSON document through a stubbed MCP session. With the script change reverted, the receipt-present cases fail and the absent cases pass.
  • plugins/vibe/tools/python/test_cl_policy_operation_receipt_contract.py (new): runs the SKILL.md snippet verbatim in bash (minus its curl assignment) against REST, delete and root-shaped receipts, and against no-receipt, list, malformed and error responses, asserting stdout unchanged, stderr equal to the id or empty, and exit 0. Swapping the snippet's redirections makes the present cases fail.
  • Matcher check: fed both scripts' output to symphony-alpha's own readShellOperationIds (packages/lib/harness/tool-result-facts.ts, identical to origin/main adba5b00e6) via tsx; it returned the clop_ id for create, version and the cl-policy snippet, and [] with no receipt.

AC2: verified end to end on a local stack

Run on a local seeded symphony-alpha stack (origin/main 177d0deafe; own Postgres, API and MCP on localhost; seed e2e profile API key generated for the run and deleted after; no production endpoint or credential used), with attribution-evidence-policy forced on for the seeded org through the existing preview flag snapshot (feature_flag_snapshots), no code edits.

  • upload_artifact.py (this branch) printed operationReceipt.operationId for a create and a version; the cl-policy step 6 snippet left stdout unchanged and printed its clop_ id on stderr. All exited 0.
  • A Claude Code transcript whose three Bash results hold that output (stdout, then stderr) was imported by Desktop's own collector and synced through its real sync lanes with the API key. Desktop sent 3 shell claims.
  • list-artifact-links on the local MCP then showed the session tied to both documents by those receipts (sessionEvidence.operations: creation and revision on one, creation on the other). Each receipt's principal is the session owner and its time falls inside its shell call's window.
  • Negatives: with the ids removed from the output, no receipt-bound evidence; with the flag off, no evidence stored at all.
  • Stderr: in 400 recent local Claude Code transcripts, 4,690 of 4,706 Bash results that wrote to both streams record both in the tool_result text, stdout first, so the cl-policy stderr line reaches symphony-alpha's matcher.

Commands run

uv run pytest plugins/
uv run ruff check .
uv run pyright
(cd tools/plugin-manifests && npm ci && npm run typecheck && npm test)
git diff --check

Results: ruff and pyright clean; plugin-manifests 6/6; the vibe node tests that read manifests or skill text (97/97); pytest plugins/ 2502 passed, 3 skipped, and 4 failed. All 4 failures are in plugins/code/tools/python/test_install_script.py, where install.sh hit the test's 20 second subprocess timeout on this loaded machine (the whole file times out locally, even run alone). This branch does not touch install.sh or the code plugin. Platform (9) and vibe (443) pass on their own.

- upload_artifact.py adds the server's operationReceipt to its JSON
  result for each document it creates or versions, ahead of response,
  and omits it when the response has none
- cl-policy's direct-API fallback prints each write response's clop_
  operation id on stderr in the same shell call, leaving stdout as it was
- Tests for receipt present, absent and malformed, and for the unchanged
  output shapes; the cl-policy test runs the documented snippet
- Bump platform to 1.1.6 and vibe to 1.0.82, with changelog entries

Testing: pytest plugins/, ruff, pyright, plugin-manifests tests; both
outputs checked against symphony-alpha's readShellOperationIds

Risks: None identified; the new field and stderr line are additive
main shipped vibe 1.0.82, so this branch's cl-policy change moves to vibe 1.0.83, with the manifest version pin moved to its test.
@shafty023
shafty023 merged commit 7245e81 into main Oct 26, 2026
9 of 10 checks passed
shafty023 added a commit that referenced this pull request Oct 26, 2026
#345 took 1.0.83 on main. Retire its 1.0.83 version pin and pin this
release at 1.0.84.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant