The sink client in DEV-610 was built against the architecture-doc shape
(POST canonical bytes to `/api/v1/handover` with headers) but DEV-611's
dummy-sink speaks a different contract: `POST /api/v1/sink/dummy/deliver`
with a JSON wrapper `{decision_id, payload_hash, payload, approval_ref,
transform_ref}`. The two subtasks therefore never actually talked to each
other.
Reconcile on the dummy-sink's wire format because the sink's extra
fields (decision_id, approval_ref, transform_ref) are mandatory for the
verification-service replay guard and chain checks:
- Switch `sink.Client.Deliver` to take a `DeliverRequest` carrying all
five fields, and parse the handover_ref / log_index out of the sink's
response for the caller.
- Pull `decision_id` from the handover payload's top-level field inside
`transform.Runner.Execute` and plumb it through to the sink, surface
`SinkHandoverRef` on the result.
- Update the mock sink inside `runner_test.go` to accept the new wire
format and assert every wrapper field survives round-trip.
Additionally add `TRANSFORM_DEV_POCKET_ID_TOKEN` /
`TRANSFORM_DEV_WORKLOAD_TOKEN` env vars so the Phase-1 smoke harness can
drive the agent without a Pocket-ID server or a projected SA token
mount; both short-circuit directly to a `StaticToken`.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
|
||
|---|---|---|
| .. | ||
| cmd/testagent-transform | ||
| internal | ||
| Dockerfile | ||
| go.mod | ||
| README.md | ||
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
- Fetch approval.
GET {TRANSFORM_TLOG_URL}/api/v1/log/entries/{approval_ref}. The TLog returns the stored DSSE envelope pluslog_index. - 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. - 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, andsubject[0].digest.sha256 = sha256(canonical(handover_payload)).canonicalhere is byte-identical to the approval-service's canonical serialisation so a downstream verifier reproduces the same hash. - Sign.
POST {TRANSFORM_SIGNING_URL}/api/v1/sign/dssewith BOTH identities set as headers:Authorization: Bearer <pocket-id-access-token>(client_credentialsgrant againstPOCKET_ID_ISSUER)X-Workload-Token: <projected-sa-token>(read fromTRANSFORM_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. Seeservices/signing-service/internal/httpapi/sign.go.
- Log.
POST {TRANSFORM_TLOG_URL}/api/v1/log/entrieswith the envelope +content_hashfrom step 4. - Handover.
POST {TRANSFORM_SINK_URL}/api/v1/sink/dummy/deliverwith the DEV-611 wrapper JSON{decision_id, payload_hash, payload, approval_ref, transform_ref}. Thepayloadfield is the canonical handover_payload the agent already committed to in the transform attestation's subject digest; the sink re-hashes and refuses the delivery unless it matchespayload_hash. Thedecision_idis read from the top-leveldecision_idfield of the handover payload.
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 oncontent_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 |
Informational schema identifier recorded in the handover attestation. |
TRANSFORM_DEV_POCKET_ID_TOKEN |
(unset) | DEV/CI ONLY: static Pocket-ID bearer, bypasses the OIDC client-credentials grant. |
TRANSFORM_DEV_WORKLOAD_TOKEN |
(unset) | DEV/CI ONLY: static projected ServiceAccount token value. |
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
ErrInvalidApprovaland never calls the signing-service, TLog, or sink. - Signing denied. The signing-service answers
403 signing_denied. The agent surfacessigner.ErrSigningDeniedand never calls the TLog or sink. - Missing Pocket-ID token. The TokenSource returns
"". The signer client rejects before any network traffic.