Hardware-in-the-loop development platform for the Commodore 64.
22 kB
Markdown
at main

Ultimate Tool local service API #

Distribution copy. This is the sanitized public copy shipped in the docs/ directory of the release archives. It is derived from the repository document docs/HTTP_API.md, which is withheld from every archive in its original form. Transformations applied for distribution: this provenance notice; the physical-gate sentence for the source-linked capture was replaced with the same limits stated inline, because the acceptance record it linked to is withheld. No request, response, or schema content was changed, and every relative link below resolves inside both archives.

API schema: 13

The development service exposes one c64dev.toml project at http://127.0.0.1:6464 by default. It serves the dashboard and JSON API from the same origin. It does not listen on the LAN.

Start it with:

cargo run -q -p ultimate-tool -- serve

Use --manifest PATH to select a project contract and --port PORT to change the loopback port.

Safety boundary #

GET /api/v1/session returns a random token that lasts for the current server process. Every POST requires that value in X-Ultimate-Tool-Token, JSON content, and—when sent by a browser—the exact dashboard origin. The service does not enable CORS.

The Ultimate password is read only from C64U_PASSWORD in the server environment. It is never accepted in an API body.

A physical run requires:

  1. POST /api/v1/ultimate/probes with the device host.
  2. Review of the returned product, hostname, ID, firmware, and capabilities.
  3. POST /api/v1/jobs with the expiring probe_id and the exact confirmed reset count.
  4. When the project declares ultimate.reu_image, separately confirm the exact REU image write count. It must equal repeat; otherwise it must be zero.

The probe authorization expires after five minutes and is consumed by one accepted job. The core probes again and pins the identity before mutation.

Endpoints #

Inspection #

  • GET /api/v1/health
  • GET /api/v1/session
  • GET /api/v1/project
  • GET /api/v1/targets
  • GET /api/v1/jobs/active
  • GET /api/v1/jobs/{job_id}
  • GET /api/v1/jobs/{job_id}/events
  • GET /api/v1/runs?limit=50
  • GET /api/v1/runs/compare?baseline=RUN_UUID&candidate=RUN_UUID&context=resident
  • GET /api/v1/runs/checkpoint-comparison?baseline=RUN_UUID&candidate=RUN_UUID
  • GET /api/v1/checkpoint-comparisons?limit=50
  • GET /api/v1/checkpoint-comparisons/{comparison_id}
  • GET /api/v1/runs/{run_id}
  • GET /api/v1/runs/{run_id}/resolve?address=$B000&context=resident
  • GET /api/v1/runs/{run_id}/timeline?cursor=40:120&limit=256&address=$D020-$D021&access=write&context=resident
  • GET /api/v1/runs/{run_id}/crash-report?context=resident
  • GET /api/v1/runs/{run_id}/diagnostic-report
  • GET /api/v1/runs/{run_id}/diagnostic-report/items?section=checkpoints&limit=50&cursor=CURSOR
  • GET /api/v1/runs/{run_id}/evidence/{name}
  • GET /api/v1/riscv/intents/{intent_id}

Run history comes from immutable .ultimate-tool/runs records rather than a database. The evidence endpoint serves only registered files and SHA256SUMS; paths and unregistered names are rejected.

The resolver accepts decimal, $ABCD, and 0xABCD addresses. Its schema-2 result state is resolved, ambiguous, unmapped, or unavailable; all four are successful HTTP responses. The optional context is exact. Invalid addresses and failed analysis-integrity checks return 422 analysis_failed. Each match labels its nullable location as address_callsite, symbol_start, or null. The flat HTTP response uses top-level schema_version = 13 and resolver_schema_version = 2, avoiding duplicate JSON keys. Run comparison returns schema-1 compared, unavailable, or incompatible data with HTTP 200. Invalid comparison evidence returns 422 comparison_failed.

Crash reports return schema-2 resolved, ambiguous, unmapped, or unavailable data. Stable unavailable reasons are no_crash_state, site_not_instrumented, and analysis_unavailable. An optional context is exact and recomputes only from the run's checksummed crash-state and build-analysis evidence. Malformed or checksum-invalid registered inputs return 422 crash_analysis_failed. The flat response uses schema_version = 13 and crash_analysis_schema_version = 2. Comparison responses likewise retain API schema 13 and expose comparison_schema_version = 1.

Run bus timeline #

GET /api/v1/runs/{run_id}/timeline?limit=256

Returns bounded pages over one immutable run's checksummed 6510 debug-stream capture. Parameters: cursor (DATAGRAM_ORDINAL:ENTRY_INDEX, default start), limit (default 256, hard maximum 4096), inclusive address range such as $D020-$D021, access (read or write), and an exact analysis context. The flat response carries top-level schema_version = 13 plus timeline_schema_version = 1, mode (bus_only or source_linked), capture_complete, capture_truncated, crosses_gap, next_cursor, and items. Gap and uncertain-datagram rows are always included, even under active filters; source resolution is computed only from the run's own checksummed artifact and analysis. Instruction, call, and return labels are intentionally never produced. Invalid cursors, out-of-range limits, empty contexts, missing captures, and tampered evidence return 422 timeline_failed.

Run diagnostic report #

GET /api/v1/runs/{run_id}/diagnostic-report
GET /api/v1/runs/{run_id}/diagnostic-report/items?section=checkpoints&limit=50

The report summarizes one immutable run: status (finalized or provisional), outcome, target_kind, execution_plane, failure, first_error, expectations (with the recorded test snapshot when present), results (expected-versus-observed terminal and screen verdicts plus a heartbeat and crash summary), artifact, evidence integrity counts, and nullable budget and comparison summaries. A recorded memory capture has a typed checkpoints summary: full eligible proof is verified, while invalid or narrowly recognized legacy captures are unavailable; absent captures are null. Corrupt evidence fails closed. Workflow checkpoints are independently pageable. counts carries the row count of every detail section so a client can size its paging without loading rows. Expectation availability distinguishes recorded values, genuine pre-schema-13 legacy absence, non-test operations, and unavailable modern contracts. Cursor tokens are bounded to 512 bytes. Resolved instrumented crashes retain up to eight verified same-run inline source frames, withheld on ambiguity. The report is assembled from the run record and its verified analysis, crash, budget, and comparison evidence; it never reads the current project manifest, so history stays inspectable after the manifest is changed or deleted. A finished run verifies every registered file first and fails closed with 422 diagnostic_report_failed; an unfinished run returns a provisional summary, where interrupted reports whether the owning service still considers the run active.

The items endpoint returns one bounded page of a section. section is required and is one of checkpoints, budget, comparison_sections, comparison_objects, comparison_symbols, or assertions. limit defaults to 50 and accepts 1 through 100. cursor is the opaque next_cursor from a previous page; it is bound to its run, section, and verified source digest, so cross-run, cross-section, stale, and malformed cursors are rejected. The checkpoints section reports the runner's workflow checkpoints (name/passed/detail/timestamp), not the frozen memory-checkpoint ledger.

Both responses use the flat versioning convention: top-level schema_version = 13 with the inner contract in diagnostic_report_schema_version = 1. An unknown run returns 404 core_error; a corrupt record, tampered evidence, or invalid section, limit, or cursor returns 422 diagnostic_report_failed. Both routes are read-only and need no mutation token. The dashboard script GET /diagnostic-report.js builds these requests and renders the returned data; it performs no diagnostics itself.

Checkpoint comparison #

GET /api/v1/runs/checkpoint-comparison?baseline=RUN_UUID&candidate=RUN_UUID
GET /api/v1/checkpoint-comparisons?limit=50
GET /api/v1/checkpoint-comparisons/{comparison_id}

The first route is a provisional, read-only comparison of two finished runs' frozen memory-checkpoint captures. It takes baseline and candidate run UUIDs (the baseline_id and candidate_id aliases are also accepted). It runs the same core compare_run_checkpoints method the CLI uses and stores nothing, so it is repeatable and never creates a pair record. The flat response carries top-level schema_version = 13 plus checkpoint_comparison_schema_version = 1 and the schema-1 comparison: state (matched, diverged, incompatible, or unavailable), the project/test/semantics identity, both run and target identifiers, artifact and evidence digests, ordered per-row digests, the first recorded difference, and coded reasons. A run that records no capture is a successful unavailable result, not a 404. A checksum-invalid or inconsistent capture returns 422 checkpoint_comparison_failed.

The other two routes read the immutable pair records a checkpoint-matrix job stores under .ultimate-tool/comparisons. The listing returns bounded summaries (comparison_id, created_at, comparison_sha256, both run IDs, and state) newest first with any per-record warnings; limit defaults to 50 and is rejected with 422 invalid_limit outside 1 through 200 rather than silently clamped. The single-record route returns the stored record flat with top-level schema_version = 13 and checkpoint_comparison_record_schema_version = 1, embedding the full schema-1 comparison. A missing or corrupt pair (including one whose checksum manifest no longer matches) returns 422 checkpoint_comparison_failed, never a swallowed 404. All three routes are read-only and need no mutation token.

The specialized VICE-then-Ultimate checkpoint matrix runs through the ordinary matrix job route with an added flag:

{
  "test": "smoke",
  "targets": ["vice", "ultimate"],
  "repeat": 1,
  "compare_checkpoints": true,
  "ultimate_authorization": {
    "probe_id": "PROBE_UUID",
    "confirmed_reset_count": 1,
    "confirmed_reu_write_count": 0
  }
}

compare_checkpoints defaults to false. When true the service requires exactly the two targets vice then ultimate with repeat 1, validated before the probe authorization is consumed and before any build or network access, so an unsupported order (422 invalid_checkpoint_targets) or repeat (422 invalid_checkpoint_repeat) never reaches a target and never burns a single-use permit. The core also requires a test that declares a memory_checkpoint_capture. The matrix builds once, runs VICE first, and starts the Ultimate candidate only after the VICE baseline passed with a valid, verified capture; it always stores exactly one pair record. The job snapshot then carries that stored checkpoint_comparison_id, and a pair whose state is not matched fails the job even when both runs passed. The job still uses the same single-Ultimate-reset authorization contract as an ordinary physical run; this route adds no new credentials, and C64U_PASSWORD stays server-side. GET /api/v1/project labels each test with a memory_checkpoint_capture boolean so a client can tell which tests are eligible.

Read-only hardware probe #

POST /api/v1/ultimate/probes
Content-Type: application/json
X-Ultimate-Tool-Token: SESSION_TOKEN

{"host":"DEVICE_HOST","strict_gate":false}

The response includes probe_id, expires_at, and a normalized target identity.

strict_gate defaults to false. When true the probe issues each read once, with no retry, redirect, or swallowed optional-capability failure, and the stored probe is marked strict. It is a restriction on the read path and adds no authorization.

Prepare and start a debug-stream capture #

Preparation consumes a fresh probe, binds and joins the configured multicast destination on the selected local interface, holds the target lease, and performs no hardware mutation:

POST /api/v1/debug-captures/preparations

{"probe_id":"PROBE_UUID","interface":"RECEIVER_ETHERNET_IP"}

The returned preparation expires after five minutes. Cancel it without device access using DELETE /api/v1/debug-captures/preparations/{preparation_id}. After reviewing the identity, 6510 Only mode, destination, interface, and exact mutation envelope, start the asynchronous job with:

POST /api/v1/debug-captures

{
  "preparation_id": "PREPARATION_UUID",
  "confirmed_stream_start_count": 1,
  "confirmed_stream_stop_count": 1
}

The preparation is single-use. The core rechecks identity and configuration, never retries either mutation, preserves each complete datagram accepted within the hard bounds, and records an ordinary immutable run. One active preparation reserves the service job lane.

Prepare and start a source-linked capture #

The source-linked flow adds a clean candidate build, artifact snapshot, normalized analysis validation, and a unique probe-symbol requirement on top of the raw receiver preparation, and requires three separate confirmations: one reset with DMA load and launch of the reviewed artifact, one stream start, and one unconditional stop. Preparation performs no hardware mutation:

POST /api/v1/source-linked-captures/preparations

{"probe_id":"PROBE_UUID","interface":"RECEIVER_ETHERNET_IP","test":"timeline-probe"}

Start after reviewing the returned identity, stream configuration, and prerequisites:

POST /api/v1/source-linked-captures

{
  "preparation_id": "PREPARATION_UUID",
  "confirmed_reset_count": 1,
  "confirmed_stream_start_count": 1,
  "confirmed_stream_stop_count": 1
}

All three counts must be exactly one; the counts are validated before the preparation is consumed. Preparations are kind-tagged: a raw start cannot consume a source-linked preparation and vice versa; a mismatch returns 422 capture_preparation_kind_mismatch and leaves the preparation in place. DELETE /api/v1/debug-captures/preparations/{preparation_id} cancels either kind idempotently without device access. The produced run is source-linked and queryable through the timeline endpoint above. The combined operation has passed a physical gate on one reviewed, identity-pinned device through the verified wired Ethernet/IGMPv2 receiver path, where it retained 11,704 datagrams with explicit capture-bound truncation while stable post-stop reads verified terminal pass and QUT1. That is one reviewed device/artifact/network result, not a guarantee for other configurations; the acceptance record itself is withheld from the distribution. The operation requires a wired Ethernet receiver on the device's LAN, the device's multicast debug stream, and three separately confirmed counts (one reset with DMA load and launch, one stream start, one unconditional stop). It performs no retry: an ambiguous failure leaves the capture stopped and is never replayed.

Start a matrix job #

Fake or VICE:

{
  "test": "smoke",
  "targets": ["fake", "vice"],
  "repeat": 1,
  "baseline_run_id": "OPTIONAL_RUN_UUID"
}

With physical hardware:

{
  "test": "smoke",
  "targets": ["vice", "ultimate"],
  "repeat": 1,
  "strict_gate": false,
  "ultimate_authorization": {
    "probe_id": "PROBE_UUID",
    "confirmed_reset_count": 1,
    "confirmed_reu_write_count": 0
  }
}

strict_gate defaults to false and is orthogonal to compare_checkpoints. It is a fail-stop restriction, never an added permission: it keeps the pinned identity, reset count, probe expiry, token, origin, and lease rules unchanged. When true the job fails closed on the first transport, status, body, or parse error and requires a probe issued with strict_gate; an ordinary probe is rejected with 422 probe_strictness_mismatch before the permit is consumed and before any mutation. The bounded supported shapes, the mutation budget (3 + 6 = 9 across the two separately authorized operations), and the one-resume-per-pause-episode rule are documented in CHECKPOINT_COMPARISON.md.

GET /api/v1/project exposes ultimate_reu_image as a project-relative path or null. If it is non-null, a hardware probe must advertise reu_write_dma before a job can consume that probe authorization.

The service returns 202 Accepted with a job_id. One matrix job may run at a time; another submission receives 409 job_busy. Closing the browser does not cancel an active job.

Trusted native preparation and standing authorization #

POST /api/v1/riscv/preparations builds the configured ultimate_riscv project and returns an immutable preparation. It performs no device access.

After a fresh POST /api/v1/ultimate/probes, create an intent:

{
  "preparation_id": "PREPARATION_UUID",
  "probe_id": "PROBE_UUID",
  "authorization": "standing",
  "expires_at": null,
  "maximum_runs": null,
  "confirmed_standing_authorization": true
}

Submit this to POST /api/v1/riscv/intents. One-shot mode does not require the standing confirmation. GET /api/v1/riscv/intents/{intent_id} exposes the exact UE2 hash, target snapshot, bounds, use count, and state. POST /api/v1/riscv/intents/{intent_id}/revoke with {"reason":"..."} is permanent and audit-recorded.

POST /api/v1/riscv/runs accepts:

{"intent_id":"INTENT_UUID","device":"DEVICE_HOST"}

It returns an asynchronous job. The core freshly probes the device and requires equality with the intent's identity and firmware snapshot before any mutation. Each run uses a fresh display ID and UTR2 token. Exact stock recovery and a tool-owned post-recovery C64 health check are required; failure of either suspends the standing intent. The core also latches authorization immediately before a first FTP creation and the final Run Update write, so ambiguous acceptance leaves the intent non-active instead of permitting a later retry. The host validator and intent do not sandbox native code.

If a native run already proved exact stock recovery but could not start its C64 health check, POST /api/v1/riscv/health-checks accepts:

{
  "intent_id": "INTENT_UUID",
  "after_run_id": "NATIVE_RUN_UUID",
  "device": "DEVICE_HOST"
}

This is a separate asynchronous job. It performs no FTP creation or Run Update. The core requires an Ultimate RISC-V parent run from the same standing intent, exact stock recovery, no prior linked health-check run, and the exact intent-bound target/firmware snapshot. The intent is suspended while the C64 reset/run and coordinated startup write are pending and reactivated only after observable pass.

Progress stream #

GET /api/v1/jobs/{job_id}/events is a server-sent event stream. Each progress event has an integer SSE ID and a JSON body:

{
  "schema_version": 13,
  "sequence": 7,
  "job_id": "JOB_UUID",
  "progress": {
    "schema_version": 5,
    "timestamp": "2026-08-03T00:00:00Z",
    "kind": "diagnostic",
    "run_id": "RUN_UUID",
    "target": "vice",
    "detail": "ready",
    "diagnostic": {}
  }
}

The server replays its bounded per-job buffer after Last-Event-ID. When the requested sequence is older than the retained buffer, it sends resync_required; clients then reload the job and run resources.

Progress is live coordination data and lasts only for the server process. Completed run evidence remains durable.

Run summaries and details expose the versioned structured failure record and the run-level heartbeat summary. Failures include a stable code, layer, stage, detection time, last event, and heartbeat state. first_error remains present for compatibility with older consumers. Schema-9 run records may also contain a schema-2 crash_state with capture-entry registers, an optional exact site, and metadata for the registered raw context, bounded stack, zero-page, and selected-global evidence files. They also contain supporting_artifacts; schema-5 screen observation records use screen_match with empty events/mailbox observations and no supported heartbeat. Schema 7 added nullable analysis; schema 8 added budget, comparison, and crash_analysis metadata for checksummed normalized evidence. Older records load unavailable fields as null. Schema 9 analysis metadata adds line_range_count beside source/section/contribution/symbol counts and line availability. Budget metadata includes verdict, result counts, and minimum headroom. Comparison metadata includes state, baseline, delta counts, and warnings. Schema 8 adds nullable crash_analysis metadata for registered crash-analysis.json; schemas 1–7 load it as null. Project test descriptions expose observation and make expected_terminal nullable for screen tests. Run schema 11 adds native preparation, launch-intent, invocation-display, and post-recovery C64 health-check linkage while retaining compatibility with historical native smoke records. Run schema 12 adds the ultimate_debug_stream execution plane, nullable debug_capture metadata, and a nullable build so capture runs do not invent build commands. Historical object-valued build records load unchanged. Run schema 13 records the selected test's expectation snapshot that the diagnostic report compares against; historical records load with a null snapshot and report expectations as not_recorded_legacy.

Errors #

API errors use one envelope:

{
  "schema_version": 13,
  "error": {
    "code": "hardware_authorization_required",
    "message": "Ultimate runs require a fresh probe and reset confirmation"
  }
}

Configuration and authorization failures do not silently fall back to another target or weaker identity check.