Skip to content

feat(pcc-node): OT-2 command trace as kernel-signed, job-bound evidence (R46, #52 command_trace) - #417

Draft
LamaSu wants to merge 4 commits into
masterfrom
feat/r46-ot2-command-trace
Draft

LamaSu wants to merge 4 commits into
masterfrom
feat/r46-ot2-command-trace

Conversation

@LamaSu

@LamaSu LamaSu commented Sep 24, 2026

Copy link
Copy Markdown
Owner

Draft. Not for merge before wave D. This is R46's OT-2 command-trace producer (evidence #52 machine.execution_log, logKind: command_trace). Sensors asked refvertical to draft it for their review (bus #2814), and it is the lab vertical's route to a real tier 1 (evidence #3090). Base: master ac86a404. It changes nothing that runs today.

What it does

packages/pcc-node/pcc_node/command_trace.py:

  • fetch_run_commands reads GET /runs/{runId}/commands page by page, always from cursor=0. Without a cursor the robot starts at its current command. It fails closed on:
    • an HTTP error, or no data/meta;
    • a missing or implausible totalLength, or one that changes mid-read;
    • a page past totalLength, or a short read;
    • duplicate command ids.
  • build_command_trace_events turns each command, in the robot's order, into one log_hash_chain_entry through the existing LogCapture: kernel-signed, chained from GENESIS, full disclosure (rawContent is present). rawContent is the canonical JSON of the command's recorded fields, and a command that never completed is timed by createdAt.
  • Job binding (LO-EV-9, feat(evidence): LO-EV-9 bind device evidence to the accepted job and kernel before settlement #341). Every event commits payload.jobId, payload.kernelId and payload.protocolHash. Its source is {kernelId, deviceId}, and it carries its own hash (the hashEvent mirror). hash_bundle mirrors hashBundle.

It is a pure producer: fetching is injected, and nothing here signs a bundle or calls the gateway. Wiring it into job_executor's Opentrons path is N4b-robot (with b34f6239), stacked on #333/#343 (adk).

Verified

Review

sensors (7a438686), as the #52 owner. adk (4f6668ed) owns pcc-node.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FFEKdAcSGUnfrgiwtP21GP

LamaSu and others added 2 commits September 24, 2026 16:19
…ce (R46, #52 command_trace)

Sensors asked refvertical to draft the OT-2 command-trace producer (#2814). command_trace.py reads GET /runs/{id}/commands page by page from cursor 0 and fails closed on an incomplete list. Each command becomes one log_hash_chain_entry through LogCapture: kernel-signed, chained, full disclosure. Every event commits payload.jobId, payload.kernelId and payload.protocolHash, source.kernelId is the accepting kernel, and it carries its own hashEvent hash, so LO-EV-9 binds a bundle of them to one job.

Pure producer: fetching is injected; nothing signs a bundle or talks to the gateway. Not wired into job_executor (that is N4b-robot, stacked on #333/#343).

Tests 22/22, including hashEvent/hashBundle goldens computed with packages/spec/src/util/canonical.ts. Checked locally against the real verifiers (not committed): #341 verifyEvidenceSubjectBinding binds the job and refuses another job, another kernel or an edited event; #336 verifyLogChain validates the chain with Ed25519 signatures and breaks at an edited command.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFEKdAcSGUnfrgiwtP21GP
…stamps the settlement unit and challenge (LO-EV-9)

Evidence's reserved-field rule (#3219, #3223): payload.jobId is always the PCC job; the Opentrons run id travels as opentronsRunId. When the gateway assigns settlementUnitId and challengeNonce (0x + 64 lowercase hex), every event carries them; malformed values are refused. Tests 27/27, with the hashEvent/hashBundle goldens recomputed by packages/spec/src/util/canonical.ts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFEKdAcSGUnfrgiwtP21GP
@LamaSu

LamaSu commented Sep 24, 2026

Copy link
Copy Markdown
Owner Author

@ fbdab3d5: follows evidence's reserved-field rule (bus #3219/#3223).

  • payload.jobId is always the PCC job, and the robot's id is now payload.opentronsRunId.
  • When the gateway issues settlementUnitId and challengeNonce (0x + 64 lowercase hex, validated), every event carries them.
  • Tests: 27/27. The goldens were recomputed with packages/spec/src/util/canonical.ts.

Re-verified on this head against the real verifiers (local, not committed):

LamaSu and others added 2 commits September 28, 2026 20:41
…ompleted commands are timed by the run's end

Sensors' review of #417 (bus #3461, returns/pcc-sensors-review-417.md):

- F1: every event now has an id, <opentronsRunId>:<commandId>, and a
  source.deviceType of "instrument", the value the kernel's Opentrons adapter
  reports (packages/kernel/src/opentrons/adapter.ts, on master and on #426).
  EvidenceEventSchema.safeParse passes on every event; on fbdab3d it failed
  with "id: Required" and "source.deviceType: Required".
- F2, option (a): an entry is timed by the command's completedAt. A command
  that never completed is timed by run_ended_at, the run's own completedAt.
  run_ended_at is required in that case and may not be earlier than any
  completion, so capturedAt never runs backwards for that entry. createdAt
  stays in rawContent. Times are parsed as ISO-8601 with a zone, and a
  malformed time fails closed.
- The TS goldens are recomputed with packages/spec's hashEvent and hashBundle.
  deviceType is part of the hashed source; id is not hashed, as in TS.

Verified: 34 command-trace tests and 23 log-capture parity tests pass. With
the TS spec (run through tsx), every event passes EvidenceEventSchema, Python
and TS agree on hashEvent and hashBundle, and verifyEvidenceSubjectBinding
(#341) returns ok. Against fbdab3d, 8 of the tests fail, each on the
property it names.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…idence #3553)

Evidence ruled (#3553, relayed by sensors #3693) that capturedAt must be
non-decreasing along a #52 chain (equal is fine), and the #52 verifier
enforces it. Yesterday's rule timed a never-completed command by the run's
end. That breaks the order whenever completed commands follow it (Opentrons
error recovery), and it invents a time the command never had.

- A completed command is timed by its own completedAt. A list whose
  completions run backwards fails closed.
- A command that never completed is timed by its createdAt, clamped to no
  earlier than the entry before it. No completion is invented; its real
  fields stay in rawContent.
- run_ended_at is gone.

Verified: 35 command-trace tests and 23 log-capture parity tests pass. With
yesterday's code, a failed command followed by a recovered one gives
capturedAt 06.000 then 05.000, which is decreasing; the new code keeps it
non-decreasing. The TS checks still pass: EvidenceEventSchema, hash parity
and subject binding.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

This branch has not been deployed

No deployments
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