Skip to content

feat(adk): the operator runbook and starter kit, first cut (ADK item 5; R1, R2, R7) - #464

Merged
LamaSu merged 17 commits into
masterfrom
feat/adk-runbook
Oct 3, 2026
Merged

LamaSu merged 17 commits into
masterfrom
feat/adk-runbook

Conversation

@LamaSu

@LamaSu LamaSu commented Sep 29, 2026 •

Copy link
Copy Markdown
Owner

ADK item 5: the operator runbook and starter kit (first cut)

This covers the operator's R1 ("a complete runbook, device to verified kernel, against the current code"), R2 ("the agent asks only what it can't do or find"), and the pack side of R7 (report every phase). It is a starter kit that three runtimes load: Claude Code (CLAUDE.md), Codex (AGENTS.md) and PCC's hosted agent (item 14). It lives in starter/ until the operator picks its public repo.

What's in it

Path What
starter/runbook/ Ten phases: prerequisites, identify, intake, research, build, register, verify, operate, publish, session. Each has a goal, exact commands, done-when checks, the only questions to ask, and a report call. runbook.json is the machine-readable graph, and its phases are painpoints' contract v1 enum.
starter/runbook/index.json R6: 22 events keyed <phase>.<event> ("don't know", a 4xx on register, the e-stop set, a leaked key…), each with what to do, the runbook section, and what to report.
starter/bin/pcc-report Reports each phase in contract v1. It persists sessionId and seq, redacts on the client side, sends no transcript, builds the session roll-up from local logs, and never blocks the onboarding. Stdlib only.
starter/CLAUDE.md, starter/AGENTS.md The same rules for both harnesses.

The participant-facing starter/README.md is pcc-launch's, in its own PR from master (item 5 is split by file).
| starter/.gitignore | .pcc/, which holds this attempt's secrets (mode 600) and state. |

How it was grounded

Safety lines it holds

  • Gateway: use only the one you were given, and never call production unless told to.
  • Secrets: only in .pcc/ (0600), and never printed. The node's private key is made locally and never sent (provisioning sends publicKey). curl sends the API key from .pcc/auth.header (-H @file), so no command line carries it for ps to show. Provisioning output goes straight to a file, since it includes a private key.
  • Money and safety are never defaulted. The safety envelope is drafted from cited sources and confirmed once by the human.
  • Typed operations only. A job's raw parameters never reach the device (bus #4121).
  • No false "verified". test-job is not used as proof, prove is self-attestation, and a by-hand run is stored unverified until the operating agent's signed evidence arrives (D4a, feat(gateway): operator status derives readiness from canonical rows (ADK D4(a)) #428).

Tests (stdlib unittest, 25)

  • 10 for pcc-report: redaction, seq, the contract body, the roll-up, a real POST to a local server, and an unreachable gateway.
  • 15 guards for the runbook:
    • the phase graph, and every phase reporting;
    • no curl to production;
    • pcc-node[crypto] with a version or a pinned SHA;
    • pcc_job_complete and test-job only as warnings;
    • provisioning written to .pcc/;
    • .pcc/ git-ignored;
    • no key expanded onto a command line, and every authenticated call sends -H @.pcc/… (mutation-checked);
    • every provisioning call sends its own publicKey (mutation-checked);
    • the index: keys match phases, every section is a real heading, and reports use the contract (mutation-checked).

Run: python3 -m unittest discover -s starter/bin -p 'test_*.py'.

Commits after the first cut:

Next: R1 (10-08) exercises it end to end, and the final cut is due 10-15.

🤖 Generated with Claude Code

LamaSu and others added 17 commits September 29, 2026 16:35
…5; R1, R2, R7)

ADK track item 5 and the operator's R1 ("a complete runbook, device to
verified kernel, against the current code") and R2 ("ask only what you
cannot do or find"). This is the starter kit that three runtimes load:
Claude Code (CLAUDE.md), Codex (AGENTS.md) and PCC's hosted agent (item 14).
It lives in starter/ until the operator picks its public repo.

- runbook/:
  - 10 phases, from prerequisites to session. Each has a goal, exact
    commands, done-when checks, the only questions to ask, and a report
    call.
  - runbook.json is the same graph in machine-readable form. Its phases are
    painpoints' contract v1 enum, in order.
  - Every command follows the path rehearsal R0 proved on a master gateway,
    and each route was checked in master's code. Each known gap says "on the
    current code" with a workaround, and "coming" with the change that closes
    it (#390, #450, #454, #428, item 12, R8, kits, economics).
- The R2 asking rule and the 9 class-C intake questions, each with its "why".
  Money and safety are never defaulted.
- Secrets live only in .pcc/ (mode 600, git-ignored). They are read with
  $(cat …) and never printed; provisioning output goes straight to a file.
- The node's two-call finish (evidence, then status completed). Never
  pcc_job_complete after it; never /api/setup/test-job as proof.
- Typed operations only: "never let a job's raw parameters reach the
  device", because pcc-node's generic adapter is a passthrough (bus #4121).
- bin/pcc-report: stdlib Python that reports each phase in painpoints'
  contract v1. It uses a persisted sessionId and seq, redacts on the client
  side, never sends a transcript, builds the session roll-up from local
  phase logs, and never blocks the onboarding.

Tests (stdlib unittest, 17): 8 for pcc-report (redaction, seq, contract
body, roll-up, a real POST to a local server, an unreachable gateway), and
9 guards for the runbook (the phase graph, each phase reports, no curl to
production, pcc-node[crypto] with a version or a pinned SHA,
pcc_job_complete and test-job only as warnings, provisioning written to
.pcc/, .pcc/ git-ignored).

agent: pcc-adk (4f6668ed)

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

The fixture pcc_live_AbCdEf0123456789 in test_pcc_report.py is synthetic, but
it matched the review router's secret pattern. It is now concatenated at
runtime, the same way agent-pack-truth.test.ts builds its fake PEM marker.
All 17 starter tests still pass.

agent: pcc-adk (4f6668ed)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
On master, /api/feedback stores each report's summary and detail and drops the
attempt fields (phase, outcome, sessionId, seq) until painpoints' #458 merges.
Phase 0 now says so, and asks for summaries that stand on their own.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The operating loop polls GET /api/operator/jobs, so the channel that fills the
operator status's slot 3 should give the gateway nothing to send to. The file
transport is not dispatched on master, and once it is, its path would name a
file on the gateway's own disk, not the operator's. "manual" says what happens.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every authenticated curl in the runbook expanded the key into argv
(-H "Authorization: Bearer $(cat .pcc/api-key)"), where any user of the
machine can read it from ps or /proc/<pid>/cmdline. Phase 0 now writes
.pcc/auth.header (0600) next to .pcc/api-key, and every call uses
curl -H @.pcc/auth.header; the test buyer's key goes to .pcc/buyer.header.
The signing-key step reads its inputs from .pcc/ instead of the environment.

A new guard fails on any "Bearer $" or KEY="$(" line, and on any
authenticated $BASE curl without -H @.pcc/ (mutation-checked both ways).
18 tests pass.

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

The provisioning response carries a server-minted Ed25519 private key as hex,
as base64 PKCS#8 and in a secret-named JSON field. Hex was caught; the base64
form was not, by this client or by the gateway's redactSecrets. pcc-report now
also removes:
- a PEM private-key block, whole or cut off;
- the value of any secret-named JSON field (private_key*, *secret*, api_key,
  password, mnemonic, seed);
- a run of 40+ base64 characters that mixes upper case, lower case and digits
  (paths and prose are left alone; tested).
The pcc_ key pattern now also allows _ and -, like the gateway's.

Fixtures are built at runtime. Each new rule is mutation-checked. 20 tests pass.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
At the release commit, load_or_create_keys without pynacl writes an HMAC
placeholder pair to .pcc/node-keys.json, and later runs load that file even
after pynacl is installed, so register_signing_key keeps refusing. Phase 5
now says: install the crypto extra, delete the never-registered key file,
run the step again.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Item 5 is split by file so both lanes' PRs come from master without
conflicting (launch #4024, #4165): launch writes the participant-facing
starter/README.md (its draft is item5-copy/README.draft.md); adk keeps the
runbook, pcc-report, CLAUDE.md, AGENTS.md and .gitignore. The agent entry
points already start at runbook/README.md.

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

Steward #3964 made rehearsal R0's P9 a runbook rule: agents send their own
publicKey at provisioning, so they never receive a private key. Without one,
POST /api/auth/provision mints an Ed25519 pair and returns the private half
(hex and base64 PKCS#8) in the response.

- Phase 0 step 3 (new) makes the node key with pcc-node's load_or_create_keys
  in .pcc/node-keys.json, refusing up front if pynacl is missing, and the
  operator's provisioning call (step 4) sends its public half.
- Phase 5 registers that same key as the kernel's signing key; the recovery
  for a placeholder key now re-runs phase 0 step 3.
- The test buyer sends a throwaway public key (a buyer signs nothing).
- A guard requires publicKey in every provisioning call (mutation-checked on
  both calls). 21 tests pass.

Dry-run of both provisioning commands against a stub gateway, with pcc-node
at the release commit 81a0994: valid JSON bodies carrying a 64-hex key;
.pcc/ 0700 and every file in it 0600.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
starter/runbook/index.json maps 22 events, keyed <phase>.<event>, to what
to do, the runbook section that covers it, and the phase and outcome to
report when the event ends a phase. Examples: the human says "don't know",
a register call answers 4xx, the node key was made without pynacl, the
emergency stop is set, a secret leaked (provision a new key, then revoke
the old one with DELETE /api/auth/keys/<id>). runbook.json points at it,
and the runbook README says when to look an event up.

The keys are the draft launch has (#4228); launch's guidance text goes at
guidance/<key>.md, read if present.

4 new guards: keys match their phase, every section is a real heading
(outside code fences), reports use the contract's phases and outcomes, and
runbook.json links the index. Each is mutation-checked. 25 tests pass.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The runbook's pre-publication install pointed at release/pcc-node-0.1.1's
81a0994, which still turned a polled job's fields into device commands
(an Opentrons robot playing a job's Python, HTTP passthrough, OctoPrint
select+print) and skipped the gateway's TLS check (verdict 68b, 3 CRITICAL).
The release branch now carries #442's round-2 fixes: 1e9308a (407
pcc-node tests; the release gate passes on its wheel and sdist).

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

Verdict 115b on #464. 16 new guards in starter/bin failed at 932c0ae.
Now 41 tests pass:

- F2: "completed" is sent only after an evidence receipt that says
  "stored": true for this jobId. The relay answers 200 with stored:false
  for an unknown job or a storage failure. New index event:
  verify.evidence-not-stored.
- F3: nothing calls the result verified. It is a registered kernel
  exercised by one real job, whose evidence stays unverified until the
  operating agent signs it (README, AGENTS, CLAUDE, phases 5, 6, 9, graph).
- F4: the capability is priced from .pcc/intake.json, not a literal 25.
  register-device lists no capabilities, so no zero-priced duplicate.
  The payout wallet is asked in phase 0 and sent at provisioning, where
  it becomes the operator id. The buyer test stops (verify.price-mismatch)
  unless the quote equals the human's price. The license rate is never
  offered or written as a default. The publish text follows economics
  (#4807, #492).
- F5: the e-stop drill says what it shows (the gateway refuses new jobs
  for a stopped kernel; the device's own stop works) and what it doesn't.
  pcc-node 0.1.1 does not read the flag.
- F6: the envelope digest is re-checked against envelope.confirmed.json
  before any run (phases 6 and 7).
- F7: the kernel is fetched and checked before its signing key is
  registered.
- F8, F9: runbook.json matches the phases. Register no longer needs
  pcc-node start, verify asks for the drill, and target.default is null.
- F10, F11: pcc-report never raises on a malformed gateway address or
  unwritable state, and repairs a permissive .pcc to 0700. Phase 0
  chmods .pcc too.
- The kernel's text address goes in physicalAddress.

Also for 0.1.1: pcc-node needs https or http to 127.0.0.1/[::1]. Don't
run pcc-node start beside the loop, because its heartbeat says
acceptingJobs: false. The install pin moves to release r2 @dcc44db9.
F1 (operator-route ownership) is the gateway's WP-C; until it carries it,
onboard only on a private gateway.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ed, and the stop reads clear only from a stored policy (verdict 115c)

Each finding was reproduced at cfc5605 first. In bin/test_runbook.py,
TestRound3's five checks failed on the old text; the two new pcc-report
tests raised on the old script.

- F1: phase 6's test job is created with POST /api/jobs/submit, which makes a
  queued job and nothing else: no quote, no escrow, no payment. The runbook
  checks that its settlement shows no session and no escrow before running
  it. submit-from-discovery is named only in a warning, and the late,
  non-enforcing price check is gone with it (index event verify.price-mismatch
  becomes verify.money-attached).
- F2: no wallet is asked for or sent. Phase 0 tells the human that PCC cannot
  yet pay a wallet they name, so the runbook runs test jobs only. Phase 7's
  new rule 0 refuses any job whose settlement shows a negotiation session or
  an escrow (payout_destination_unbound). Paid operation waits for a payout
  destination the human confirms and checks as the actual recipient.
- F3: the final status is computed by one script from the device's last run
  answer and the evidence receipt: completed only for a succeeded run whose
  receipt says stored: true for this jobId with a non-empty bundleId. The
  status call posts that file; no command types "completed".
- F4: the stop reads clear only for a stored policy (updatedAt present, no
  source "default") whose emergencyStop is false. The drill now checks the
  read gives STOPPED, then CLEAR.
- F5: the provisioning body is built from .pcc/operator.json (written with
  the agent's file tool) into a private file, sent with --data-binary @file,
  and deleted. No personal value is on a command line.
- F6: pcc-report treats attempt state that is not an object as absent, and
  the session roll-up skips lines that are not complete phase objects.

The old test that asserted walletAddress was sent and the quote was checked
encoded the premises this verdict disproved, so it is replaced by TestRound3.
The provisioning public-key test now also accepts a body file built just
before the call. starter tests: 47 passed.

agent: pcc-adk (4f6668ed)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…teway cannot run its test job (verdict 115d)

Both CRITICAL findings were reproduced first.

- C1: phase 6's first device run came before its first policy read. A trace
  on the real gateway: with the kernel's stored policy at emergencyStop true,
  POST /api/jobs/submit still answered 200 queued, and the operator poll
  listed the job. So the runbook's own check is the only guard.
  Phase 6 now starts with the stop: how to read it, and the drill. The drill's
  stop and resume leave the stored policy the later checks need. The stop is
  read again before submitting, and immediately before the run (section 3
  step 5). Phase 7's rule 5 reads it once more right before every run. The
  drill no longer claims the gateway refuses a job for a stopped kernel,
  because the current gateway queues it.
- C2: a trace on the real gateway: POST /api/jobs/submit for the gateway's own
  local kernel answered {"deviceId":"dev_fdm_001","status":"accepted"},
  started at once on a local device. An external kernel's job answered
  {"deviceId":null,"status":"queued"}.
  Before submitting, the runbook now requires GET /api/setup/detect to list
  only mock devices the gateway runs itself (or none), so a local dispatch
  cannot actuate anything real. After submitting, it requires status queued
  with no deviceId, or it stops. The text no longer says the route "creates a
  queued job and nothing else". New index events: verify.gateway-runs-it and
  verify.stop-not-clear.

TestRound4's three checks fail on the 7b155b2 runbook files and pass now.
The round-3 test's section reference follows the renumbering. Starter tests:
50 passed.

agent: pcc-adk (4f6668ed)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… one without running it (verdict 115e)

Phase 6 no longer submits a test job. The gateway's setup report lists only
its configured devices, so no runbook check before a submission can show
that the submission will only queue. Phase 6 now ends with the drill and
reports the test job blocked until the gateway offers a server-enforced
queue-only submission. Section 3 stays as the by-hand run for that day.

Phase 7 reports operate blocked: on the current gateway it has no job it
may run (paid jobs wait for payout binding, test jobs for that submission).

The drill-failure event no longer treats the gateway queueing a job for a
stopped kernel, which the runbook documents as current behaviour, as a
failure. Phase 7 points at the envelope check where it now is (phase 6
section 3 step 3).

Tests: TestRound5 (3 tests); the round 3 and round 4 submission checks now
assert that no command anywhere creates a job. Each new or changed check
fails at 487ab59.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…d the test job waiting (verdict 115e)

The README, AGENTS.md, CLAUDE.md, the phase table, intake's payout note and
runbook.json's title still said the kernel ends exercised by one test job.
Phase 6 no longer runs one on the current gateway, so they now say what it
ends with: registered, its emergency stop drilled, its test job waiting for
a queue-only submission, and no job that carries money.

Test: TestRound5.test_no_summary_says_a_test_job_ran fails at 487ab59.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rrent gateway (verdict 115e)

Phase 9's example still reported "session ok" with "one test run". With
verify and operate ending blocked, the session is blocked too, and ok only
once a test job has run and the loop has a job it may run.

Test: the no-summary check now also refuses "one test run" and
"session ok"; it fails at 487ab59.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@LamaSu
LamaSu marked this pull request as ready for review October 3, 2026 07:18
@LamaSu
LamaSu merged commit e33b49e into master Oct 3, 2026
8 checks passed
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