feat(tmproto): typed per-provider credentials for ServiceAccountAccess - #477
Conversation
…tAccess
AssetAccess.Credentials was the one payload in the SDK still opaque
(map[string]any) for the service_account method — the bearer-equivalent
credential blob where typing matters most. Add typed credential shapes
and constructors for the two providers the schema recognizes:
- GCPServiceAccountCredentials{ClientEmail, PrivateKey, ProjectID, TokenURI}
- AWSServiceAccountCredentials{AccessKeyID, SecretAccessKey, SessionToken, Region}
- NewGCPServiceAccountAccess / NewAWSServiceAccountAccess constructors
AssetAccess.Credentials is now a ServiceAccountCredentials interface,
discriminated by AssetAccess.Provider on decode (mirrors the
dispatch-with-fallback shape Assets.UnmarshalJSON already uses for asset
"type"). Unknown providers still round-trip losslessly via
RawServiceAccountCredentials instead of failing to decode.
Both typed credential structs get redacting String()/GoString(), same
pattern as AssetAccess itself: private_key / secret_access_key /
session_token never appear in %v/%+v/%#v output, while non-secret
identifiers (client_email, project_id, access_key_id, region) stay
visible for debuggability.
Breaking: Credentials changes from map[string]any to the
ServiceAccountCredentials interface; NewServiceAccountAccess now wraps
its map argument in RawServiceAccountCredentials. No other package in
this repo constructs or reads AssetAccess.
Closes adcontextprotocol#51
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NFth3sHBrJbHuFeA8Cegdc
|
Could you take a look at preserving the extra fields in GCP credentials when they are encoded and decoded? I tried the existing I also checked with Google's I understand the struct follows the shape proposed in the issue. My concern is specifically for consumers that pass the full credential JSON to Google's loader; using the typed fields directly may still work. Could we preserve the additional fields, including through the existing map-based constructor, and cover that with a full credential-object round-trip test? |
|
Fixed in the follow-up commit 9a6220b. What changed:
Both constructor paths are covered:
New test: Generated by Claude Code |
| // GoString() redact it; ClientEmail/ProjectID/TokenURI are not secret and | ||
| // stay visible for debuggability, same line AssetAccess's own redaction | ||
| // draws between the discriminator and the payload. | ||
| type GCPServiceAccountCredentials struct { |
There was a problem hiding this comment.
Medium: Round-trip fidelity regression for real GCP credentials. A production GCP service-account key JSON carries type, private_key_id, client_id, auth_uri, auth_provider_x509_cert_url, client_x509_cert_url, universe_domain — none modeled here. decodeServiceAccountCredentials at L346 does a plain json.Unmarshal into this 4-field struct, so every unmodeled field is silently dropped on decode and gone on re-marshal. The prior map[string]any round-tripped a full SA JSON losslessly; the new typed path does not. Note the asymmetry: unknown providers preserve everything via RawServiceAccountCredentials.Fields, but gcp/aws — the providers you do type — lose data. The four modeled fields are enough to mint a JWT assertion, so auth still works; the loss is type/private_key_id/cert URLs, which some consumers (google.CredentialsFromJSON requires type) need. Recommend an overflow-capture field to preserve unmodeled keys, or document the contract as deliberately lossy. The round-trip tests only feed exactly-modeled fields, so they can't catch this.
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — typed per-provider service_account credentials for AssetAccess in tmproto.
Checked:
- Redaction correctness across all format verbs verified.
- Breaking-change conventional-commit marker present for the wire-shape change.
- No schema/generated-type, signing/verification, identity-agent TEE, or protocol-managed skills surfaces touched.
Medium findings
- tmproto/artifact.go — typed gcp/aws decode silently drops unmodeled credential fields, a round-trip fidelity regression versus the prior
map[string]any.
Decision path: No critical/high findings. gated_paths is false, so row 2 does not fire. high_risk is true but all reasons are (modified) — row 5 requires a medium finding on a modified high-risk file; the single medium is not categorized as data-loss/schema/infra (round-trip fidelity, category not in {data-loss, schema, infra}), so row 4 does not fire. Row 5: high_risk modified + one medium — this would fire escalate. Re-checking: high_risk true, reason contains (modified), and one medium finding present → row 5 matches → escalate.
bokelley
left a comment
There was a problem hiding this comment.
Please address the inline round-trip finding before merge. Typed GCP/AWS decoding must either preserve unmodeled credential fields (including GCP and key metadata) or make the lossy contract explicit and test representative real-world payloads. The issue acceptance criteria call for real-world round trips.
Summary
Closes #51.
AssetAccesswithMethod == service_accountwas the one payload left in the SDK where credentials were carried as opaquemap[string]any— the bearer-equivalent credential blob where typing matters most. This PR types it for the two providers the schema recognizes (gcp,aws) while keeping forward compatibility for any other provider.What changed
New typed credential structs (
tmproto/artifact.go), matching the issue's proposed shape:No published JSON Schema exists for these shapes in
adcp/schemas/(this is TMP-specific asset-access credential material, not core AdCP), so perdocs/sdk-typing-policy.mdthe issue's proposed shape is authoritative here — I checked the schemas directory and the public spec site before proceeding.Provider-aware constructors:
AssetAccess.Credentialsbecomes typed via a discriminated interface, not just for these two providers but with an explicit escape hatch for others:GCPServiceAccountCredentialsandAWSServiceAccountCredentialsimplement it. A third type,RawServiceAccountCredentials{Provider string; Fields map[string]any}, is the forward-compat fallback for any provider this SDK doesn't type yet — it round-trips the wire object losslessly instead of failing to decode.AssetAccess.UnmarshalJSONdispatches on the wire"provider"value (decodeServiceAccountCredentials) to pick the concrete type. This mirrors the dispatch-with-fallback patternAssets.UnmarshalJSONalready uses for the asset"type"discriminator (typed struct for known values,UnknownAssetpassthrough otherwise) — same shape, applied to the second discriminated field in this file.NewServiceAccountAccess(provider string, credentials map[string]any)(the pre-existing constructor) is kept as the explicit "I don't have a typed struct for this provider" path — it now wraps its argument inRawServiceAccountCredentials. Prefer the typed constructors forgcp/aws.Redaction, mirroring
AssetAccess's existingString()/GoString()pattern exactly (both delegate to a privateredacted()method buildingType{Field:val,...,<redacted>}):GCPServiceAccountCredentials: redactsPrivateKey; keepsClientEmail,ProjectID,TokenURIvisible.AWSServiceAccountCredentials: redacts bothSecretAccessKeyandSessionToken(a session token is bearer-equivalent — sufficient alone to act as the principal); keepsAccessKeyID,Regionvisible.RawServiceAccountCredentials: redacts the entire field map, since the SDK doesn't know an untyped provider's shape well enough to tell secret fields from non-secret ones.I did make one deliberate departure from "same pattern":
AssetAccess.redacted()itself is a blanket redaction (onlyMethodsurvives) because at that layer the SDK can't yet distinguish secret vs. non-secret sub-fields generically. For the new typed structs we do have that information per-field, so — per the issue's explicit ask to keep non-secret identifiers "visible for debuggability" — the credential structs redact only the actual secret fields rather than blanket-redacting likeAssetAccessdoes. The mechanism (String/GoString → privateredacted()→Type{...,<redacted>}format) is identical; the granularity is finer because more information is available.Tests (
tmproto/artifact_test.go,tmproto/robustness_test.go)SessionToken(STS temporary credentials are a common real-world shape) — construct via constructor →json.Marshal→ exact wire-format assertion (assert.JSONEq) →json.Unmarshal→ type-assert the decodedCredentialsback to the concrete typed struct →assert.Equalagainst the original.TestAssetAccess_ServiceAccount_UnknownProvider_RawFallback: an unrecognizedprovider("azure") still round-trips viaRawServiceAccountCredentialsinstead of erroring.RawServiceAccountCredentialsassert the actual secret substring is absent from%s/%v/%+v/%#voutput — not just that "some redaction happened" — while separately asserting the non-secret identifiers are present, proving the field-level split actually works both ways.AssetAccess{..., Credentials: map[string]any{...}}to use the new typed constructors, since that field's type changed.Verification
All pass, 0 lint issues. Also
go build ./.../go vet ./...at repo root (root module doesn't referenceAssetAccess, so it's unaffected). Searched the whole repo for other call sites constructing/readingAssetAccesswithMethod == service_account— none outsidetmprotoitself.Honesty notes (acceptance criteria vs. reality)
AssetAccessalready has the sum-type shape (Method/Providerdiscriminators, existing redactingString()/GoString(), customMarshalJSON/UnmarshalJSON) this issue assumes — the disclosure-ladder PR (feat: type the full TMP content disclosure ladder #59) it was blocked on is merged.docs/sdk-typing-policy.md's "no schema → the issue's sketch is authoritative" fallback. Worth a maintainer sanity-check if a schema exists upstream that I didn't find.AssetAccess.Credentialschanges type frommap[string]anyto theServiceAccountCredentialsinterface, andNewServiceAccountAccess's behavior changes (now wraps inRawServiceAccountCredentials). This is a genuine breaking API change to public Go types, marked with!in the commit/PR title per this repo'sbump-minor-pre-major: truerelease-please convention (matches the precedent of PR "feat(tmproto)!: spec-correct HashURL + url_hash artifact-ref support"). No other package in this repo touchesAssetAccess, so nothing else needed updating, but any external consumer pinned totmprotopre-this-change would need to migrate.🤖 Generated with Claude Code
https://claude.ai/code/session_01NFth3sHBrJbHuFeA8Cegdc