ProofedMesh/services/testagent-transform
CTO (Paperclip) ba1b88d490
Some checks failed
build-images / signing-service image (push) Failing after 16s
validate / JSON Schema (ajv) (push) Successful in 19s
validate / Go (vet + test) (push) Failing after 4s
validate / Rego (opa fmt + opa test) (push) Successful in 13s
validate / DB (migrate up/down dry-run) (push) Failing after 21s
testagent-transform: Phase-1 trust-chain demo agent (DEV-610)
Minimal CLI agent process that end-to-end demonstrates the Transform step
of the ProofedMesh trust chain:

  1. Fetches an approval attestation from the transparency-log by
     attestation_ref (`sha256:<hex>`).
  2. DSSE-verifies the fetched envelope against a pinned approval-signer
     ed25519 keyring; stops before any signing call on failure.
  3. Builds an in-toto v1 Statement with predicateType
     `basicstack.de/attestations/transform/v1`, operation
     `expense.map_to_sink`, materials[0].attestation_ref = approval_ref,
     and subject[0].digest.sha256 = sha256(canonical(handover_payload)).
  4. Calls signing-service /api/v1/sign/dsse with BOTH identities:
     Authorization: Bearer <pocket-id client_credentials token> AND
     X-Workload-Token: <projected SA token from
     /var/run/secrets/tokens/trust-api>.
  5. Submits the returned DSSE envelope + content_hash to the
     transparency-log /api/v1/log/entries.
  6. POSTs the canonical handover_payload bytes to the dummy-sink
     (schema sink.dummy.v1), carrying the transform content_hash on the
     X-Attestation-Ref header.

Tests use httptest for all four backends:

  - Happy path (verifies the bytes sent to the sink match the signed
    subject digest and that the TLog recorded the same content_hash the
    agent returned).
  - Invalid approval signature -> stops before sign-call; TLog and sink
    untouched.
  - Signing-service returns 403 signing_denied -> surfaced as
    signer.ErrSigningDenied; TLog and sink untouched.
  - Missing Pocket-ID token -> fails before any network call.

Includes PocketID client_credentials TokenSource (OIDC discovery + cache)
and ProjectedSATokenSource (reads kubelet-managed Volume) mirroring
admin-api/approval-service.

Non-scope: no read-side attestations (Phase 2), no K8s/CI image build
wiring (follow-up ticket).

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-25 19:55:02 +00:00
..
cmd/testagent-transform testagent-transform: Phase-1 trust-chain demo agent (DEV-610) 2026-10-25 19:55:02 +00:00
internal testagent-transform: Phase-1 trust-chain demo agent (DEV-610) 2026-10-25 19:55:02 +00:00
Dockerfile testagent-transform: Phase-1 trust-chain demo agent (DEV-610) 2026-10-25 19:55:02 +00:00
go.mod testagent-transform: Phase-1 trust-chain demo agent (DEV-610) 2026-10-25 19:55:02 +00:00
README.md testagent-transform: Phase-1 trust-chain demo agent (DEV-610) 2026-10-25 19:55:02 +00:00

testagent-transform

Phase-1 end-to-end demonstrator of the ProofedMesh trust chain's Transform step (DEV-610, parent DEV-606). It is a one-shot CLI, not an HTTP service: given an approval attestation and a handover_payload, it produces a signed transform attestation, logs it to the transparency-log, and hands the payload to the Phase-1 dummy-sink. The exit code is 0 on success.

Flow

  1. Fetch approval. GET {TRANSFORM_TLOG_URL}/api/v1/log/entries/{approval_ref}. The TLog returns the stored DSSE envelope plus log_index.
  2. Verify. The returned envelope is DSSE-verified against the pinned approval-signer keyring (APPROVAL_SIGNER_KEYS). On failure the agent stops before any signing call — nothing new is attested on top of an untrusted approval.
  3. Build transform statement. in-toto v1 Statement with predicateType = https://basicstack.de/attestations/transform/v1, operation = expense.map_to_sink, materials[0].attestation_ref = approval_ref, and subject[0].digest.sha256 = sha256(canonical(handover_payload)). canonical here is byte-identical to the approval-service's canonical serialisation so a downstream verifier reproduces the same hash.
  4. Sign. POST {TRANSFORM_SIGNING_URL}/api/v1/sign/dsse with BOTH identities set as headers:
    • Authorization: Bearer <pocket-id-access-token> (client_credentials grant against POCKET_ID_ISSUER)
    • X-Workload-Token: <projected-sa-token> (read from TRANSFORM_WORKLOAD_TOKEN_PATH, default /var/run/secrets/tokens/trust-api) The signing-service's structured policy evaluates BOTH identities against the agent registry and OPA; a Pocket-ID bearer alone is not enough. See services/signing-service/internal/httpapi/sign.go.
  5. Log. POST {TRANSFORM_TLOG_URL}/api/v1/log/entries with the envelope + content_hash from step 4.
  6. Handover. POST {TRANSFORM_SINK_URL}/api/v1/handover with the canonical handover_payload bytes. Headers set:
    • X-Payload-Schema: sink.dummy.v1
    • X-Attestation-Ref: <transform content_hash>

Non-scope

  • No read-side attestations (Phase 2).
  • No persistent store; the agent is stateless and idempotent given the same (approval_ref, handover_payload) input (the TLog deduplicates on content_hash).

Configuration

Env var Default Purpose
TRANSFORM_TLOG_URL (required) Base URL of transparency-log.
TRANSFORM_SIGNING_URL (required) Base URL of signing-service.
TRANSFORM_SINK_URL (required) Base URL of dummy-sink.
TRANSFORM_WORKLOAD_TOKEN_PATH /var/run/secrets/tokens/trust-api Projected ServiceAccount token path.
POCKET_ID_ISSUER (required) Pocket-ID OIDC issuer URL.
POCKET_ID_CLIENT_ID (required) OIDC client id for this agent.
POCKET_ID_CLIENT_SECRET (required) OIDC client secret for this agent.
POCKET_ID_AUDIENCE trust-api OIDC token audience.
APPROVAL_SIGNER_KEYS (required) Comma-separated kid=base64(public-ed25519) entries.
TRANSFORM_ACTOR agent-testagent-transform Registry id written into the attestation.
TRANSFORM_OPERATION expense.map_to_sink Transform operation name (must match OPA policy).
TRANSFORM_SINK_TARGET sink:dummy-sink Logical sink identifier.
TRANSFORM_SIGNING_KEY_REF trust-api/agent-testagent-transform-2026-q4 OpenBao Transit key ref for the transform signer.
TRANSFORM_INPUT_SCHEMA / _VERSION expense.canonical / 1.2.0 Input schema name/version carried in the attestation.
TRANSFORM_OUTPUT_SCHEMA / _VERSION sink.db.expense / 1.2.0 Output schema name/version carried in the attestation.
TRANSFORM_VERSION 0.1.0 Agent's own version string, carried in the attestation.
TRANSFORM_SINK_PAYLOAD_SCHEMA sink.dummy.v1 Value sent on the sink's X-Payload-Schema header.

CLI

testagent-transform \
  -workflow     WF-0042 \
  -approval-ref sha256:... \
  -handover     ./handover.json

handover.json must be a JSON object; the agent canonicalises it before hashing + sending to the sink.

Tests

cd services/testagent-transform
go vet ./...
go test ./...

The end-to-end transform tests spin up four httptest servers — approval read, signing-service, transparency-log, dummy-sink — and run the full pipeline against them:

  • Happy path. All four backends respond; the subject digest the agent signs equals the sha256 of the canonical bytes POSTed to the sink; the TLog's recorded content_hash equals the agent's returned TransformRef.
  • Invalid approval signature. The approval mock returns a DSSE envelope whose Ed25519 signature does not verify against the pinned public key. The agent fails with ErrInvalidApproval and never calls the signing-service, TLog, or sink.
  • Signing denied. The signing-service answers 403 signing_denied. The agent surfaces signer.ErrSigningDenied and never calls the TLog or sink.
  • Missing Pocket-ID token. The TokenSource returns "". The signer client rejects before any network traffic.