Home

BugTape docs

Customer-owned runner verification

How a repaired build is verified.

BugTape can record scoped test evidence signed by an enrolled runner. The standalone Node signer creates that signature. It does not run tests, upload artifacts, contact BugTape, enroll trust, or prove a repair. Run it only inside CI that you control, after that CI has measured the observations it will sign. Do not expose it as a service that signs arbitrary agent assertions.

Required handoff

A human organization admin must enroll an Ed25519 public key and the runner's device/runtime. Keep the private key in the customer's CI secret store. Never send it to BugTape, an agent, or browser code. Runner/device enrollment is human-admin-only; an agent PAT cannot enroll itself. A simulator is not a physical iPhone. Physical iOS requires a device-lab runner and explicit physical-device enrollment.

A human admin/manager must register the application, installation and repaired build. The build record must contain the exact tested artifact SHA-256 and source revision. This registry currently rejects PATs, including CI PATs. Arrange this handoff before an unattended verification run; the signer does not remove that authority boundary. Do not reuse the failed build's ID for changed bytes.

The agent needs a current project work grant and the relevant PAT actions:

WorkPAT action
Read the failure and artifact metadataget_repro_context
Acquire, renew, release or complete its claimclaim_issue
Prepare, submit or promote verificationsubmit_verification
Read a verification receiptread_verification
Read receipt artifact metadatarequest_evidence
Reserve/upload a CI log through the evidence APIcreate_bug plus a current project grant

The last row is separate from submit_verification. Evidence writes also require a current role allowed to write evidence. Preserve the same upload credential across reservation and upload. A capture key has a separate scoped evidence-write path; it cannot perform agent-work operations.

The claim, challenge and submission belong to the same PAT. A separate runner can sign offline and return the signed file to that PAT's owner; the runner does not need the PAT to sign. Another PAT cannot take over the challenge. Keep generation and fencingToken as decimal strings.

Run, store, sign, submit

  1. Read get_repro_context with the exact occurrence ID. Acquire claim_issue and retain its ID, generation and fencingToken. Renew it during long tests. Stop on a stale fence and acquire current authority.
  2. Run the registered repaired build in the enrolled runtime. Record the actual command, steps, expected and observed outcomes, and result. Check the build bytes and source revision; a signature over invented observations does not make them real.
  3. Check GET /v1/evidence/capabilities on your configured API base. Continue only if storage is configured and the required format is admitted. Configuration alone does not prove upload success. Do not assume hosted evidence storage, PNG admission, or physical-device qualification is available.
  4. Redact a real test log and obtain the required consent. Reserve a log/text/plain artifact with its exact byte length and SHA-256. Upload the same bytes, then inspect it until it is ready with the expected hash. A reservation or 202 uploading response is not stored evidence. Report unavailable storage or missing authority as a blocker.
  5. Obtain prepare_verification after the measured test and upload are ready, while the claim remains live. The challenge expires within ten minutes and no later than the claim. Give the trusted runner its exact challenge fields and measured submission.
  6. Create an unsigned submission as described below. Sign it with the public helper. Save the signed file before transport, then submit that file through submit_verification or the receipt HTTP endpoint. The helper performs no HTTP request.
  7. Read get_verification_receipt. Inspect status, blockedReasons, scope and current applicable state. Unsigned assertions remain pending; even a valid signature can yield pending due to scope, build, device, test or artifact checks.
  8. If whole-issue coverage is ready, call promote_verification while the claim is still live. Promotion requires current trusted receipts covering every observed application/runtime and proposed repaired build. Successful promotion completes the claim atomically; do not call complete_claim afterward. Separately, use complete_claim with an applicable receipt for bounded work completion without whole-issue promotion. Completion alone does not verify the issue; later promotion needs a new live claim and fresh applicable evidence. A later failure can invalidate applicability and promotion. Release unfinished claims.

Give each new command a new UUID. For an uncertain retry, resend the identical command and body. In particular, resend the saved signed file: do not regenerate proof IDs, timestamps, signatures, or command IDs for an identical retry. A changed attempt requires a new command and, if the old challenge was consumed, a new challenge.

Evidence reservation

The reservation body is:

{
  "requestId": "<new reservation UUID>",
  "kind": "log",
  "contentType": "text/plain",
  "sizeBytes": 123,
  "sha256": "<64 lowercase hex characters for the redacted log bytes>",
  "consent": true,
  "redactionStatus": "client_redacted",
  "applicationId": "<registered application UUID>",
  "buildId": "<registered repaired build UUID>",
  "installationId": "<registered installation UUID>"
}

Replace the placeholders and byte count with measured values. Consent and redaction declarations must be true in fact. The reservation returns { artifact }; upload to that artifact ID with the declared Content-Type and raw bytes, then inspect its state. Current default log admission is at most 1MiB when storage is configured; use the capability response for the deployment's policy.

Normally omit occurrenceId from repaired-build evidence reservations. Supplying the failed occurrence binds the artifact to that occurrence's old build and can conflict with repaired-build IDs. The verification submission's failedOccurrenceId supplies the failure relationship. Do not create a synthetic failure to attach a successful test log.

Unsigned submission

All fields below are required. Unknown fields are rejected. Do not put signature in the unsigned input; the helper adds it. The HTTP/MCP submission includes the returned signature.

FieldContract
commandId, projectId, issueId, claimIdUUID strings. Use a new commandId for this submission, distinct from challenge creation.
generation, fencingTokenPositive decimal strings, 1–19 digits. Never convert them to JavaScript numbers.
failedOccurrenceIdUUID of the real captured failure in this issue/application.
applicationId, buildId, installationId, deviceIdRegistered UUIDs for the actual tested scope.
buildSha256Exact tested build hash, 64 lowercase hex characters.
sourceRevisionExact registered source revision, 1–200 characters.
runtimebrowser, server, ios_physical, or ios_simulator.
osVersionExact enrolled device OS version, 1–100 characters.
testCommandActual command description, 1–2000 characters. The signer never executes it.
steps1–100 objects with description, expected, observed (each 1–1000 characters), and result (passed, failed, skipped). Preserve array order.
resultOverall passed, failed, or inconclusive. Failed/skipped observations must not be changed to passed for signing.
artifacts1–32 unique {artifactId, sha256} objects. IDs are stored artifact UUIDs; hashes are 64 lowercase hex characters.
proofThe six attestation fields below, without signature.

The attestation fields are runnerId, fresh proofId, challengeId, nonce, issuedAt, and expiresAt. The first three are UUIDs; challengeId is the challenge response's id. Copy its nonce exactly (32–128 characters). Use explicit-zone ISO timestamps, preferably UTC with seconds and milliseconds. Preserve their exact strings when signing and sending.

Choose issuedAt after challenge creation. The server permits at most 30 seconds of issuance clock skew around its challenge creation/current time. expiresAt must be after issuance, no more than ten minutes later, and no later than the challenge expiry. The helper checks timestamp syntax and the ten-minute interval; it cannot check server time, nonce validity, challenge ownership, expiry on the server, or whether the challenge was consumed. A synchronized runner clock and a current challenge remain required.

The helper accepts only plain JSON-shaped inputs and does not coerce or normalize values. It rejects invalid calendar dates, nonstandard timestamp offsets, and unknown fields. All submission scalar fields are strings: numeric generation/fencing values are rejected even if they look correct. No private project package is needed.

Exact signature bytes

This is the version 1 Ed25519 protocol. It is not a JWT, a signature of the nonce alone, or a signature of ordinary pretty-printed JSON.

  1. Let submission be the validated input with its entire proof property removed.
  2. Canonicalize submission and SHA-256 its UTF-8 bytes. Encode that hash as 64 lowercase hex characters.
  3. Let attestation contain the six proof fields, excluding signature.
  4. Form { schemaVersion: 1, purpose: "bugtape.verification", ...attestation, submissionHash }.
  5. Canonicalize that object. Sign its UTF-8 bytes directly with Ed25519 (Node sign(null, bytes, privateKey)), without a newline or a separate signature digest.
  6. Encode the 64-byte signature as unpadded base64url, exactly 86 characters. Add it as proof.signature and send the unchanged submission.

Canonicalization sorts object keys recursively using JavaScript string comparison (a < b), not locale order. It retains array order. Keys and scalar values use JSON.stringify spelling, with no extra whitespace. There is no Unicode normalization. Object properties whose value is undefined are omitted by the server canonicalizer; accepted submissions have no such missing fields. JSON null is null. The protocol's schemaVersion is the number 1; the submission's generation and fence remain strings. Non-finite numbers are not valid JSON and the public helper rejects them.

The server validates the complete signed schema first. That schema currently performs no coercion or normalization of verification strings or array order. Client-supplied object key order therefore does not matter; changing a string, timestamp spelling, array order, command ID or evidence hash after signing changes the message.

Offline command

Save the public sign-verification.mjs file in the trusted runner workspace. Node 18 or later and its standard library are sufficient. Create unsigned.json from that runner's measured observations and the live challenge, then run:

node sign-verification.mjs unsigned.json /run/secrets/bugtape-runner.pem signed.json

The key file must contain the customer runner's Ed25519 private key. Input is bounded to 1MiB and key material to 16KiB. The command creates a new signed.json with mode0600 using exclusive creation; it refuses to overwrite an existing file or follow an existing output symlink. It prints no key, nonce or signature. Treat the output as sensitive and send it through the original PAT's authorized transport. Keep the exact file for uncertain retries.

An existing file causes a nonzero exit. A partial write also exits nonzero; do not submit a failed command's output. The command does not print parser excerpts or input values on failure. Successful signing is not a receipt, test pass, hosted-storage check, device attestation or issue promotion.

For an existing trusted Node runner, import signVerification(unsigned, privateKeyPem) to get the signed object, or signFiles(inputFile, keyFile, newOutputFile) for bounded file I/O. verificationMessage(unsigned) returns the exact unsigned proof message for cross-implementation testing. These functions make no HTTP requests and do not execute testCommand.

Existing API routes

Use the configured API base for your deployment. Agent-work routes require the scoped PAT, not the browser capture key or a substituted human JWT.

OperationRoute
Acquire/read claimPOST / GET /v1/agent-work/claims
Renew/release/completePOST /v1/agent-work/claims/{renew,release,complete}
ChallengePOST /v1/agent-work/verification/challenges
Submit signed JSONPOST /v1/agent-work/verification/receipts
Read receiptGET /v1/agent-work/verification/receipts/:receiptId?projectId=…&issueId=…
Receipt artifact metadataGET /v1/agent-work/verification/receipts/:receiptId/evidence?projectId=…&issueId=…
Promote coveragePOST /v1/agent-work/verification/promote
Human admin enrollmentPOST /v1/agent-work/verification/{runners,devices}
Human admin enrollment listGET /v1/agent-work/verification/enrollment?projectId=…
Human admin revokePOST /v1/agent-work/verification/{runners,devices}/revoke
Human manager/admin repaired buildPOST /v1/applications/projects/:projectId/:applicationId/builds
Evidence capabilityGET /v1/evidence/capabilities
Reserve artifactPOST /v1/evidence/projects/:projectId/artifacts
Upload raw bytesPUT /v1/evidence/projects/:projectId/artifacts/:artifactId/content
Inspect artifactGET /v1/evidence/projects/:projectId/artifacts/:artifactId

Runner enrollment requires commandId, projectId, name, publicKeyPem, trustLevel (ci or device_lab), and allowedRuntimes. Device enrollment requires commandId, projectId, name, runnerId, applicationId, installationId, runtime, deviceIdentifierSha256 and osVersion; physical iOS additionally requires physicalDeviceConfirmed. Send public keys only. Build creation requires requestId, sourceRevision and artifactSha256, with project/application scope in the path.

Claim commands carry projectId, issueId and commandId. Commands against a claim also carry claimId, generation and fencingToken; completion adds receiptId. GET claim uses projectId/issueId query parameters. A fresh challenge returns id, nonce, projectId, issueId, claimId, generation, fencingToken and expiresAt. Follow the current MCP schemas for the full promotion target/receipt contract. See MCP tools, Agents, and API reference.