Skip to content

feat: typed credential output contract (IssueCredentialV2) - #1171

Closed
highb wants to merge 9 commits into
mainfrom
highb/iga-4625/typed-output-contract
Closed

highb wants to merge 9 commits into
mainfrom
highb/iga-4625/typed-output-contract

Conversation

@highb

@highb highb commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

Description

Adds an explicit typed output contract to credential issuance, so a caller can
require that the bytes a connector mints conform to a named SecretPayloadV2
content type — and so an implementation that predates the contract cannot
silently mint legacy bytes under a typed request.

The contract is carried by protocol identity, not by a deployment check:

  • CredentialManagerService.IssueCredentialV2 is a distinct RPC. A runtime
    without it answers Unimplemented before its Issue implementation runs and
    before any provider call. The generated default
    (UnimplementedCredentialManagerServiceServer.IssueCredentialV2) is that
    fence.
  • Task.IssueTypedCredentialTask (arm 124) is a distinct task arm for the
    task-backed transport, with its own handler and its own task type. An older
    task handler does not recognise the arm at all.
  • There is no fallback to the legacy method on Unimplemented, timeout, or
    any other error. A typed issuance that cannot be served is an error.

The builder checks the request against the executing descriptor before
Issue runs: a mismatch is FailedPrecondition (contract drift), an option
that declares nothing is FailedPrecondition (unavailable, not downgraded), and
a malformed pairing — an empty type on the typed path or a nonempty type on the
legacy path — is InvalidArgument. Every refusal happens before the provider is
touched.

Field numbers, verified free and unreserved at this revision:

Message Field Number
CredentialIssueOptionDescriptor output_content_type 11 (1–10 in use)
IssueCredentialRequest output_content_type 6 (1–5 in use)
Task.IssueTypedCredentialTask — 124 (123 is issue_credential; 119 and create_sync_diff are reserved)

output_content_type is not secret_resource_type_id. That field names the
C1 resource type an option mints — the SDK's existing "output type" — and
selects which descriptor handles a request. This one names the encoding of the
credential's value bytes. The proto comments say so on both fields.

Empty is meaningful and is the legacy contract on both sides, so an old caller
talking to a new connector and a new caller talking to an old connector both
keep their existing meaning.

Testing

$ go build ./...
$ go vet ./pkg/connectorbuilder/... ./pkg/tasks/... ./pkg/types/tasks/...
$ go test ./pkg/connectorbuilder/... ./pkg/tasks/... ./pkg/types/tasks/... ./internal/connector/... -count=1
ok  github.com/conductorone/baton-sdk/pkg/connectorbuilder  9.983s
ok  github.com/conductorone/baton-sdk/pkg/tasks             0.006s
ok  github.com/conductorone/baton-sdk/pkg/tasks/c1api       0.077s
ok  github.com/conductorone/baton-sdk/pkg/tasks/local       0.216s
ok  github.com/conductorone/baton-sdk/pkg/types/tasks       0.003s

New tests cover: a matching declaration reaches Issue and is carried on
CredentialIssueInput; a mismatched declaration, an undeclared option, and a
malformed pairing are each refused with the right code; and the legacy path is
unchanged. Each refusal asserts the fake issuer never saw the request, so "refused
before mint" is proven rather than asserted.

The task-type ordinal is pinned by the existing released-ordinal test
(IssueTypedCredentialType = 33).

Notes

  • Source is committed before codegen and generated code separately, so either
    can be rebased cleanly.
  • The C1 side (freeze the type on the winning operation, dispatch through the
    typed method, populate the advisory content type for reveal discovery) is a
    separate PR and is not in this one.
  • No release, image publication, or promotion.

highb and others added 5 commits October 6, 2026 20:55
Add an explicit output content type to the credential option descriptor and
the issue request, a distinct IssueCredentialV2 RPC, and a distinct typed task
arm for the task-backed transport.

The type is a distinct method and a distinct task arm rather than a new field
on the existing ones because protobuf unknown-field tolerance lets an
implementation that predates this contract ignore the field and mint legacy
bytes under a typed request. A runtime without the method, or without the task
arm, refuses before its Issue implementation runs and before any provider call,
so protocol identity supplies the fence instead of a deployment check that a
mutable alias can race.

Field numbers verified free and unreserved at this revision:
- CredentialIssueOptionDescriptor.output_content_type = 11 (1-10 in use)
- IssueCredentialRequest.output_content_type = 6 (1-5 in use)
- Task.IssueTypedCredentialTask = 124 (123 is issue_credential; 119 and
  "create_sync_diff" are reserved)

Generated bindings follow in a separate commit.
Generated from the preceding source commit with `make protogen`. Adds
IssueCredentialRequest.output_content_type, the IssueCredentialV2 RPC, the
descriptor's output_content_type, and the IssueTypedCredentialTask arm.

Generated code is committed separately from the hand-written proto source so a
rebase of either stays clean.
…t type

The SDK's existing vocabulary calls secret_resource_type_id an option's
"output type". The new field names the encoding of the credential's value
bytes, which is a separate axis. Say so where a reader meets both fields, so
the two cannot be conflated when the contract is implemented.
Comment-only regeneration from the preceding source commit.
Serve IssueCredentialV2 through the shared implementation with a typed flag, so
the legacy method keeps its exact behaviour and the typed method cannot be
reached by accident.

The executing descriptor is the authority on what a connector can produce, so a
typed request is checked against it before Issue runs:

- a matching declaration reaches Issue and is carried on CredentialIssueInput
- a mismatched declaration is FailedPrecondition (contract drift)
- an option that declares nothing is FailedPrecondition (unavailable, not
  downgraded)
- an empty type on the typed path, or a nonempty type on the legacy path, is
  InvalidArgument

Every refusal happens before the provider is touched, which the tests assert by
having the fake issuer record its input only inside Issue. There is no fall back
to the legacy method on any error.

Also add the distinct typed task arm and handler, the IssueTypedCredentialType
task type (appended to preserve released ordinals), and the method on the empty
and noop connectors so the generated server's Unimplemented default stays
reachable.

Co-authored-by: c1-squire-dev[bot] <c1-squire-dev[bot]@users.noreply.github.com>
@linear-code

linear-code Bot commented Oct 6, 2026

Copy link
Copy Markdown

IGA-4625

@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

General PR Review: feat: typed credential output contract (IssueCredentialV2)

Blocking Issues: 1 | Suggestions: 3 | Threads Resolved: 0
Criteria: Criteria status: loaded .claude/skills/ci-review.md from trusted base 47f5027e75e5.
Review mode: full
View review run

Review Summary

This PR adds three new proto fields. output_content_type goes on CredentialIssueOptionDescriptor (field 11) and on IssueCredentialRequest (field 6). It also adds a new IssueCredentialV2 RPC and a new Task.IssueTypedCredentialTask task (oneof arm 124, task type ordinal 33). In the builder, IssueCredential and IssueCredentialV2 now share one issueCredential(…, typed) function. On the typed path it rejects an empty, undeclared, or mismatched content type before Issue runs, and on the legacy path it rejects a nonempty one. I scanned the full PR diff for security and correctness, including the generated pb files. The proto changes only add fields, the new field numbers and arm 124 are free, and the regenerated Go code matches the proto source.

Repo criteria applied, with risk triage:

  • Silence: yes. A typed task that falls back to the legacy RPC would mint well-formed but wrong bytes without any error.
  • Durability: yes. This adds new wire fields, a new RPC, and a new task arm.
  • Version-pair dependence: yes. Safety depends on an old runtime answering Unimplemented.
  • Consumer distance: high. The consumers are the C1 platform and downstream connectors.
  • Consequence: rung 5, because issuing a credential is an external side effect.

Verdict: HIGH. The PR does include a permutation table: the builder tests cover all five match/mismatch/undeclared/malformed combinations and assert that Issue is never reached when a request is refused. What's missing is a cross-version or old-runtime test (an old handler or the Unimplemented default refusing typed issuance) and any test of the task-backed transport. Before merge, I recommend the full pass-set review described in docs/BUG_CATCHING.md §6.

Coverage: I reviewed every file in the PR. I did not run builds or tests; the compile break below comes from reading the code.

Security Issues

None found.

Correctness Issues

  • New — internal/connector/noop_provisioner.go (registered at internal/connector/connector.go:517): adding the RPC adds IssueCredentialV2 to CredentialManagerServiceServer, and that interface has no mustEmbedUnimplemented… requirement. noopProvisioner doesn't implement the new method, so RegisterCredentialManagerServiceServer(s, noop) won't compile. internal/connector is the gRPC server wrapper every connector process links, so this breaks all of them. The file isn't in the diff. That conflicts with the PR's claim that go build ./... and ./internal/connector/... tests pass. (Confidence: high)

Suggestions

  • New — pkg/tasks/c1api/issue_typed_credential.go:40: nothing tests the new task handler or the manager's dispatch to it. If the handler called the legacy RPC or dropped OutputContentType, it would silently mint legacy bytes under a typed task and no test would fail. (Confidence: high)

  • New — pkg/sdk/version.go is unchanged. Two exported interfaces gain a method:

    • CredentialManagerServiceServer: a downstream server that doesn't embed Unimplemented… stops compiling.
    • CredentialManagerServiceClient, reached through types.ConnectorClient: a hand-written fake or wrapper client that implements the interface directly stops compiling.

    Per the repo criteria, call this out in the PR and bump the minor version. (Confidence: medium)

  • New — pkg/connectorbuilder/credentials.go:199: typed issuance uses the same span name (builder.IssueCredential) and metric task type (IssueCredentialType) as legacy issuance, so dashboards can't tell typed refusals or failures apart from legacy ones. (Confidence: medium, observability only)

Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Correctness Issues

In `internal/connector/noop_provisioner.go`:
- Around line 37-39: The new IssueCredentialV2 RPC added a method to
  v2.CredentialManagerServiceServer, which has no mustEmbedUnimplemented requirement.
  noopProvisioner does not implement it, so RegisterCredentialManagerServiceServer(s, noop)
  in internal/connector/connector.go:517 fails to compile. Add:
  func (n *noopProvisioner) IssueCredentialV2(ctx context.Context, request *v2.IssueCredentialRequest) (*v2.IssueCredentialResponse, error) {
      return nil, status.Error(codes.FailedPrecondition, "provisioning is not enabled")
  }
  Then run go build ./... and go test ./internal/connector/... to confirm.

## Suggestions

In `pkg/tasks/c1api/issue_typed_credential.go`:
- Around line 33-57: Add a test modeled on issue_credential_test.go. Use a fake ConnectorClient
  that records which method was called. Assert that IssueCredentialV2 (not IssueCredential) is
  called with OutputContentType, RequestId = task id, and ExpiresAt forwarded. Assert that a task
  missing output_content_type, identity, options, or encryption configs finishes non-retryable
  without calling the client. Also cover manager.go dispatch for IssueTypedCredentialType.

In `pkg/sdk/version.go`:
- Bump the 0.x minor version and add a migration note to the PR. CredentialManagerServiceServer
  and CredentialManagerServiceClient (and so types.ConnectorClient) gained IssueCredentialV2,
  which breaks downstream implementations that don't embed the Unimplemented server or that
  implement the client interface directly.

In `pkg/connectorbuilder/credentials.go`:
- Around line 193-199: When typed is true, use a distinct span name (e.g. builder.IssueCredentialV2)
  and/or the IssueTypedCredentialType task type for metrics, so typed and legacy issuance can be
  told apart.

// answers Unimplemented before its Issue implementation runs and before any
// provider call, so the method's existence is the fence. Callers must never
// fall back to IssueCredential on Unimplemented or any other error.
rpc IssueCredentialV2(IssueCredentialRequest) returns (IssueCredentialResponse);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Bug: Adding this RPC adds IssueCredentialV2 to CredentialManagerServiceServer, which has no mustEmbedUnimplemented… requirement. internal/connector/noop_provisioner.go doesn't implement it, so connectorV2.RegisterCredentialManagerServiceServer(s, noop) at internal/connector/connector.go:517 won't compile, and that breaks every binary that imports internal/connector. Add func (n *noopProvisioner) IssueCredentialV2(...) returning FailedPrecondition, "provisioning is not enabled". That also keeps the provisioning-disabled path refusing typed issuance the same way it refuses the legacy RPC. (Confidence: high)

return h.helpers.FinishTask(ctx, nil, nil, errors.Join(errors.New("malformed issue typed credential task"), ErrTaskNonRetryable))
}

resp, err := h.helpers.ConnectorClient().IssueCredentialV2(ctx, v2.IssueCredentialRequest_builder{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: Nothing tests this handler or the IssueTypedCredentialType dispatch in manager.go. Here are two regressions that would ship without a test catching them: the handler calls IssueCredential instead of IssueCredentialV2, or OutputContentType gets dropped from the built request. Either one silently mints legacy bytes under a typed task. Mirror issue_credential_test.go with a fake client that asserts the V2 method was called and the content type was forwarded, plus a case for the malformed task. (Confidence: high)

A failure after Issue returned means the provider object already exists, but
three post-Issue paths returned an error with no handle for it: cardinality
validation, output validation, and encryption. The caller could neither revoke
the object nor tell whether retrying was safe.

Carry the provider identity in the error and say plainly that issuance must not
be retried. This does not add a discard point; it makes the existing one
recoverable, and it preserves the ambiguity contract the task handler already
relies on when it marks issuance errors non-retryable.

The test mints a real object and then returns undeliverable output, and asserts
the handle appears in the error.
@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

General PR Review: feat: typed credential output contract (IssueCredentialV2)

Blocking Issues: 1 | Suggestions: 3 | Threads Resolved: 0
Criteria: Criteria status: loaded .claude/skills/ci-review.md from trusted base 47f5027e75e5.
Review mode: full
View review run

Review Summary

This PR adds a typed issuance path:

  • a new CredentialManagerService.IssueCredentialV2 RPC;
  • output_content_type fields on CredentialIssueOptionDescriptor (field 11) and IssueCredentialRequest (field 6);
  • a new Task.IssueTypedCredentialTask arm (124) and IssueTypedCredentialType (ordinal 33), with a handler and dispatch.

The builder refuses mismatched, undeclared, and malformed requests before Issue runs. The head commit also changes all post-Issue failures, including on the legacy path, to an Internal error that names the minted provider handle and says not to retry. The PR description doesn't mention this.

I scanned the full diff for security and correctness. I checked the generated pb output against the proto sources, and the field numbers and oneof arm are additive. I applied the repo criteria: exported API and interface stability, proto/wire rules, version signalling, and risk triage.

Risk triage:

  • Silence: no. The known break fails loudly at compile time.
  • Durability: yes. It adds new wire fields, a new RPC, and a new task arm.
  • Uncontrolled dimensions: yes. Safety depends on an old runtime answering Unimplemented or ignoring arm 124, which is a version-pair dependence.
  • Consumer distance: yes. The callers are the C1 platform and downstream connectors.

Verdict: HIGH (version-pair). The risk is in the multi-artifact class. The instrument that would cover it is a two-artifact harness: a new caller against an old server and an old task manager, asserting that the old side refuses before Issue. The PR doesn't contain one, so escalate per docs/BUG_CATCHING.md §6.

Security Issues

None found.

Correctness Issues

  • Prior — still present (open thread on proto/c1/connector/v2/resource.proto:121): internal/connector/noop_provisioner.go still has no IssueCredentialV2 method.
    • CredentialManagerServiceServer now requires that method (pb/c1/connector/v2/resource_grpc.pb.go:646), and noopProvisioner doesn't embed Unimplemented….
    • So connectorV2.RegisterCredentialManagerServiceServer(s, noop) at internal/connector/connector.go:517 doesn't compile.
    • That breaks the gRPC server wrapper every connector links. It also contradicts the PR's go build ./... claim. (Confidence: high)

Suggestions

  • Prior — still present (open thread on pkg/tasks/c1api/issue_typed_credential.go:40): nothing tests issueTypedCredentialTaskHandler or the IssueTypedCredentialType dispatch in manager.go. The only test reference is the ordinal pin. If the handler called the legacy RPC or dropped OutputContentType, it would mint legacy bytes under a typed task and no test would fail. The existing issue_credential_test.go fake-client pattern would carry over directly. (Confidence: high)

  • New: pkg/sdk/version.go is still v0.36.0. Two exported generated interfaces gain a method:

    • CredentialManagerServiceServer: a downstream server that doesn't embed Unimplemented… stops compiling.
    • CredentialManagerServiceClient, reached through types.ConnectorClient: a hand-written client that implements the interface directly stops compiling.

    The criteria ask for a 0.x minor bump or a migration note for breaks like these. (Confidence: medium)

  • New: in pkg/connectorbuilder/credentials.go:197, typed issuance uses the same span name (builder.IssueCredential) and metric task type as legacy issuance. Dashboards can't tell typed refusals or failures apart from legacy ones during rollout. (Confidence: medium, observability only)

Coverage notes

I reviewed the generated pb/** files for consistency with the proto changes, not line by line. I didn't run builds or tests because this is a read-only review; the noop-provisioner break comes from reading the code.

Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Correctness Issues

In `internal/connector/noop_provisioner.go`:
- Around line 37-39: noopProvisioner is registered as a CredentialManagerServiceServer (internal/connector/connector.go:517), and that interface now requires IssueCredentialV2. Add `func (n *noopProvisioner) IssueCredentialV2(ctx context.Context, request *v2.IssueCredentialRequest) (*v2.IssueCredentialResponse, error)` that returns `status.Error(codes.FailedPrecondition, "provisioning is not enabled")`, matching IssueCredential, so the package compiles again.

## Suggestions

In `pkg/tasks/c1api/issue_typed_credential.go`:
- Around line 40-50: Add issue_typed_credential_test.go modeled on issue_credential_test.go. Use a fake client that embeds types.ConnectorClient and records IssueCredentialV2. Assert that IssueCredentialV2 (not IssueCredential) is called, that OutputContentType and RequestId are forwarded, that a malformed task (missing output_content_type) is non-retryable and never reaches the client, and that a client error is non-retryable. Also test that c1ApiTaskManager.Process dispatches IssueTypedCredentialType to this handler.

In `pkg/sdk/version.go`:
- Around line 3: CredentialManagerServiceServer and CredentialManagerServiceClient each gained a method, which breaks implementations that don't embed Unimplemented or wrap the generated client. Bump the minor version, or add a migration note to the PR describing the break.

In `pkg/connectorbuilder/credentials.go`:
- Around line 197: Use a distinct span name (e.g. "builder.IssueCredentialV2") and/or a distinct metric task type when typed is true, so typed failures can be told apart from legacy ones.

…k routing

The generated Unimplemented default only proves what a default does when the
method is registered. An executor built before the contract never registers it,
so the downgrade regression serves a real gRPC descriptor with the typed method
filtered out and asserts the typed call is Unimplemented with the legacy Issue
entrypoint never reached. The legacy route on the same old executor still works,
and an executor that registers the method serves the typed call without touching
the legacy one.

On the task side, the typed arm resolves to its own task type, the typed handler
dispatches the typed method, and the legacy handler cannot service a typed task
at all: it reads a nil IssueCredential and refuses as non-retryable with no call.

Also pin that an upgraded SDK leaves legacy issuance alone: an untyped request
still reaches Issue with an empty contract and keeps the connector's own name
and raw bytes on the encrypted result.
@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor
Superseded — see the current review report for commit 63ee3ca99760

General PR Review: feat: typed credential output contract (IssueCredentialV2)

Blocking Issues: 1 | Suggestions: 2 | Threads Resolved: 0
Criteria: Criteria status: loaded .claude/skills/ci-review.md from trusted base 47f5027e75e5.
Review mode: full
View review run

Review Summary

This PR adds three things:

  • a distinct CredentialManagerService.IssueCredentialV2 RPC;
  • output_content_type fields (descriptor field 11, request field 6);
  • a distinct Task.IssueTypedCredentialTask arm (124, ordinal 33), with its own handler and dispatch.

Before Issue runs, the builder checks the requested type against the executing descriptor: an empty type on the typed path or a nonempty type on the legacy path is InvalidArgument, and a missing or mismatched declaration is FailedPrecondition.

I scanned the full PR diff for security and correctness. Proto field numbers are additive and the generated code is regenerated with them.

I applied the repo criteria.

Risk triage:

  • Silence: yes. A wrong typed mint is well-formed bytes.
  • Durability: yes. Wire types and the task arm are durable.
  • Uncontrolled dimensions: yes. Safety depends on which version is on each side: an old runtime has to answer Unimplemented or ignore arm 124.
  • Consumer distance: yes. The consumers are the C1 platform and downstream connectors.

Verdict: HIGH (version-pair). The PR does contain the right instrument for this risk class: credential_typed_transport_test.go runs a two-sided harness with an old executor and old task routing. Extra review beyond CI is still recommended. Coverage gap: I didn't review the generated pb files line by line beyond checking that they match the proto sources.

Security Issues

None found.

Correctness Issues

  • Prior — still present (open thread on proto/c1/connector/v2/resource.proto:121): internal/connector/noop_provisioner.go still has no IssueCredentialV2 method and doesn't embed Unimplemented…. CredentialManagerServiceServer now requires the method (pb/c1/connector/v2/resource_grpc.pb.go:646), so connectorV2.RegisterCredentialManagerServiceServer(s, noop) at internal/connector/connector.go:517 fails to compile. internal/connector is the gRPC server wrapper every connector links, and the failure contradicts the PR's go build ./... claim. (Confidence: high)

Suggestions

  • New: pkg/sdk/version.go is still v0.36.0, but two exported generated interfaces gain a method:

    • CredentialManagerServiceServer: a server that doesn't embed Unimplemented… stops compiling.
    • CredentialManagerServiceClient, reached via types.ConnectorClient: a hand-written fake or wrapper client stops compiling. pkg/sdk/empty_connector.go already needed this update.

    Per the criteria, a minor bump plus a migration note in the PR would signal this. (Confidence: medium)

  • New: pkg/connectorbuilder/credentials.go:194-198: typed issuance uses the same span name (builder.IssueCredential) and metric task type (IssueCredentialType) as legacy issuance. During rollout, dashboards can't tell typed refusals (FailedPrecondition for drift) apart from legacy failures. (Confidence: medium, observability only)

Resolved prior findings

  • pkg/tasks/c1api/issue_typed_credential.go:40 (handler untested) is fixed. pkg/tasks/c1api/issue_typed_credential_test.go:43 covers three cases:

    • the handler calls IssueCredentialV2 with OutputContentType and never the legacy RPC;
    • an empty type is refused before any call;
    • the legacy handler refuses a typed task.

    The test also checks that tasks.GetType resolves the typed arm to IssueTypedCredentialType.

Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Correctness Issues

In `internal/connector/noop_provisioner.go`:
- Around line 37-39: noopProvisioner is registered as a CredentialManagerServiceServer (internal/connector/connector.go:517), and that interface now requires IssueCredentialV2. Add `func (n *noopProvisioner) IssueCredentialV2(ctx context.Context, request *v2.IssueCredentialRequest) (*v2.IssueCredentialResponse, error)` returning the same `status.Error(codes.FailedPrecondition, "provisioning is not enabled")` as IssueCredential, so the package compiles again. Then run `go build ./...`.

## Suggestions

In `pkg/sdk/version.go`:
- Around line 3: CredentialManagerServiceServer and CredentialManagerServiceClient (exported, implemented downstream) each gained a method. Bump the minor version (for example v0.37.0) and add a migration note to the PR telling downstream implementers to add IssueCredentialV2 or embed UnimplementedCredentialManagerServiceServer.

In `pkg/connectorbuilder/credentials.go`:
- Around line 194-198: typed issuance shares the span name "builder.IssueCredential" and metric task type IssueCredentialType with legacy issuance. Use a distinct span name (for example "builder.IssueCredentialV2") and/or tasks.IssueTypedCredentialType when typed is true, so typed failures can be told apart during rollout.

@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor
Superseded — see the current review report for commit 9addbb8e6442

General PR Review: feat: typed credential output contract (IssueCredentialV2)

Blocking Issues: 1 | Suggestions: 2 | Threads Resolved: 0
Criteria: Criteria status: loaded .claude/skills/ci-review.md from trusted base 47f5027e75e5.
Review mode: full
View review run

Review Summary

This PR adds three things:

  • a distinct CredentialManagerService.IssueCredentialV2 RPC;
  • output_content_type fields (descriptor field 11, request field 6);
  • a distinct Task.IssueTypedCredentialTask arm (124, ordinal 33), with its own handler and dispatch.

Before Issue runs, the builder checks the requested type against the executing descriptor: an empty type on the typed path or a nonempty type on the legacy path is InvalidArgument, and a missing or mismatched declaration is FailedPrecondition.

I scanned the full PR diff for security and correctness. Proto field numbers are additive and the generated code is regenerated with them.

I applied the repo criteria.

Risk triage:

  • Silence: yes. A wrong typed mint is well-formed bytes.
  • Durability: yes. Wire types and the task arm are durable.
  • Uncontrolled dimensions: yes. Safety depends on which version is on each side: an old runtime has to answer Unimplemented or ignore arm 124.
  • Consumer distance: yes. The consumers are the C1 platform and downstream connectors.

Verdict: HIGH (version-pair). The PR does contain the right instrument for this risk class: credential_typed_transport_test.go runs a two-sided harness with an old executor and old task routing. Extra review beyond CI is still recommended. Coverage gap: I didn't review the generated pb files line by line beyond checking that they match the proto sources.

Security Issues

None found.

Correctness Issues

  • Prior — still present (open thread on proto/c1/connector/v2/resource.proto:121): internal/connector/noop_provisioner.go still has no IssueCredentialV2 method and doesn't embed Unimplemented…. CredentialManagerServiceServer now requires the method (pb/c1/connector/v2/resource_grpc.pb.go:646), so connectorV2.RegisterCredentialManagerServiceServer(s, noop) at internal/connector/connector.go:517 fails to compile. internal/connector is the gRPC server wrapper every connector links, and the failure contradicts the PR's go build ./... claim. (Confidence: high)

Suggestions

  • New: pkg/sdk/version.go is still v0.36.0, but two exported generated interfaces gain a method:

    • CredentialManagerServiceServer: a server that doesn't embed Unimplemented… stops compiling.
    • CredentialManagerServiceClient, reached via types.ConnectorClient: a hand-written fake or wrapper client stops compiling. pkg/sdk/empty_connector.go already needed this update.

    Per the criteria, a minor bump plus a migration note in the PR would signal this. (Confidence: medium)

  • New: pkg/connectorbuilder/credentials.go:194-198: typed issuance uses the same span name (builder.IssueCredential) and metric task type (IssueCredentialType) as legacy issuance. During rollout, dashboards can't tell typed refusals (FailedPrecondition for drift) apart from legacy failures. (Confidence: medium, observability only)

Resolved prior findings

  • pkg/tasks/c1api/issue_typed_credential.go:40 (handler untested) is fixed. pkg/tasks/c1api/issue_typed_credential_test.go:43 covers three cases:

    • the handler calls IssueCredentialV2 with OutputContentType and never the legacy RPC;
    • an empty type is refused before any call;
    • the legacy handler refuses a typed task.

    The test also checks that tasks.GetType resolves the typed arm to IssueTypedCredentialType.

Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Correctness Issues

In `internal/connector/noop_provisioner.go`:
- Around line 37-39: noopProvisioner is registered as a CredentialManagerServiceServer (internal/connector/connector.go:517), and that interface now requires IssueCredentialV2. Add `func (n *noopProvisioner) IssueCredentialV2(ctx context.Context, request *v2.IssueCredentialRequest) (*v2.IssueCredentialResponse, error)` returning the same `status.Error(codes.FailedPrecondition, "provisioning is not enabled")` as IssueCredential, so the package compiles again. Then run `go build ./...`.

## Suggestions

In `pkg/sdk/version.go`:
- Around line 3: CredentialManagerServiceServer and CredentialManagerServiceClient (exported, implemented downstream) each gained a method. Bump the minor version (for example v0.37.0) and add a migration note to the PR telling downstream implementers to add IssueCredentialV2 or embed UnimplementedCredentialManagerServiceServer.

In `pkg/connectorbuilder/credentials.go`:
- Around line 194-198: typed issuance shares the span name "builder.IssueCredential" and metric task type IssueCredentialType with legacy issuance. Use a distinct span name (for example "builder.IssueCredentialV2") and/or tasks.IssueTypedCredentialType when typed is true, so typed failures can be told apart during rollout.

Reviewed commit: 63ee3ca99760

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking issues found — see the full review report

…rose

A concatenated resource id in an error message is not a recovery contract: a
caller would have to parse English to find the object it has to clean up. Attach
the sanitized ResourceId as a structured gRPC status detail instead, alongside
an ErrorInfo naming the reason and marking retry forbidden and cleanup required.
No credential material and no profile values go into the details.

The message no longer claims a live object exists. A failure after Issue means
the outcome is unresolved and an object may exist; when no identity came back
the status carries no detail, which callers must read as unresolved cleanup
rather than proof that nothing was created.

Prove the detail survives the path a real failure takes: the builder's status
error is joined with the task's non-retryable marker, and the joined error still
exposes the ResourceId through status.FromError. If joining had dropped it, C1
would silently lose the handle again.
@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor
Superseded — see the current review report for commit 9addbb8e6442

General PR Review: feat: typed credential output contract (IssueCredentialV2)

Blocking Issues: 2 | Suggestions: 2 | Threads Resolved: 0
Criteria: Criteria status: loaded .claude/skills/ci-review.md from trusted base 47f5027e75e5.
Review mode: incremental since 63ee3ca
View review run

Review Summary

The PR adds the IssueCredentialV2 RPC, output_content_type fields (descriptor field 11, request field 6), and the IssueTypedCredentialTask arm (124 / ordinal 33).

The new commit (9addbb8) changes how credentialIssuedButUndeliverable reports a minted credential. The provider handle is no longer written into the error message. It is now attached as structured gRPC status details: an ErrorInfo (reason, cause, retry/cleanup flags) and the ResourceId. The commit adds tests that these details survive errors.Join with ErrTaskNonRetryable. errdetails is already vendored and listed in go.mod, so there is no dependency change.

I scanned the full PR diff for security and correctness.

Trusted criteria, triage:

  • Silence: yes. A swallowed status turns a failed mint into a nil-error success.
  • Durability: yes. The proto/wire fields and the task arm persist.
  • Uncontrolled dimensions: yes. Safety depends on which SDK version is on each side.
  • Consumer distance: high. The C1 platform interprets the result.

Verdict: HIGH. Review-blind classes: error-path and version-pair. Instruments in the PR: refusal tests that assert the issuer never saw the request, plus handler and transport tests. There is no cross-version harness. A full pass-set review per docs/BUG_CATCHING.md §6 is recommended before merge.

Other criteria checks:

  • Proto field numbers are additive.
  • pkg/sdk/version.go was not bumped, even though two generated interfaces gained a method (raised earlier in this review's summaries).

Security Issues

None found.

Correctness Issues

  • Prior — still present (open thread on proto/c1/connector/v2/resource.proto:121): internal/connector/noop_provisioner.go still has no IssueCredentialV2 method and doesn't embed Unimplemented…. CredentialManagerServiceServer requires the method (pb/c1/connector/v2/resource_grpc.pb.go:629), so RegisterCredentialManagerServiceServer(s, noop) at internal/connector/connector.go:517 does not compile. That breaks the gRPC server wrapper every connector links. (Confidence: high)
  • New — pkg/connectorbuilder/credentials.go:347-359: st, _ = st.WithDetails(...) discards the error. On failure, grpc returns (nil, err) (vendored internal/status/status.go:134-147), and a nil *Status has Code() == OK, so st.Err() returns nil. The failure happens when anypb.New rejects invalid UTF-8, either in cause.Error() (provider/encryption error text) or in a connector-supplied ResourceId. In that case issueCredential returns (nil, nil) after a mint, and the undeliverable credential is silently treated as success. (Confidence: high)

Suggestions

  • New — pkg/connectorbuilder/credentials.go:346: the cause now lives only in ErrorInfo metadata. err.Error() and the logs show only the generic message, so operators lose the reason delivery failed. (Confidence: medium)
  • Prior (earlier summaries) — still present — pkg/sdk/version.go is still v0.36.0, but CredentialManagerServiceServer and CredentialManagerServiceClient each gained a method. Downstream servers without the embed, and hand-written clients, will stop compiling.

Resolved prior findings

  • pkg/tasks/c1api/issue_typed_credential.go:40 (handler untested) — fixed. pkg/tasks/c1api/issue_typed_credential_test.go (TestIssueTypedCredentialTaskHandler) covers dispatch to IssueCredentialV2 with OutputContentType and confirms the legacy RPC is never called.
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Correctness Issues

In `internal/connector/noop_provisioner.go`:
- Around line 37-39: noopProvisioner is registered as a CredentialManagerServiceServer (internal/connector/connector.go:517), and that interface now requires IssueCredentialV2. Add `func (n *noopProvisioner) IssueCredentialV2(ctx context.Context, request *v2.IssueCredentialRequest) (*v2.IssueCredentialResponse, error)` returning `status.Error(codes.FailedPrecondition, "provisioning is not enabled")`, then run `go build ./...`.

In `pkg/connectorbuilder/credentials.go`:
- Around line 347-359: `st, _ = st.WithDetails(...)` sets st to nil when WithDetails fails (e.g. invalid UTF-8 in cause.Error() or in the ResourceId), and nil.Err() returns nil, so the function returns a nil error after a mint. Only replace st when WithDetails succeeds: `if s2, err := st.WithDetails(...); err == nil { st = s2 }`. Do this for both the ErrorInfo and the ResourceId. Optionally sanitize cause with strings.ToValidUTF8. Add a test with an invalid-UTF-8 cause that asserts a non-nil codes.Internal error.

## Suggestions

In `pkg/connectorbuilder/credentials.go`:
- Around line 346: the cause is no longer in the status message, so logs lose it. Log `cause` with ctxzap/zap before returning, or include a sanitized cause in the message.

In `pkg/sdk/version.go`:
- Bump the minor version, since the generated CredentialManagerServiceServer/Client interfaces gained a method.

Comment on lines +347 to +359
st, _ = st.WithDetails(&errdetails.ErrorInfo{
Reason: "CREDENTIAL_ISSUED_BUT_UNDELIVERABLE",
Domain: "baton-sdk/credentialbuilder",
Metadata: map[string]string{
"cause": cause.Error(),
"retry": "forbidden",
"cleanup": "required",
},
})
if output != nil && output.Secret != nil && output.Secret.GetId() != nil {
st, _ = st.WithDetails(output.Secret.GetId())
}
return st.Err()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Bug: The WithDetails error is discarded, but on failure it returns (nil, err). A nil *Status has Code() == OK, so st.Err() returns nil. The case that triggers this: a proto3 string with invalid UTF-8, such as a provider error text in cause.Error() or a connector-supplied ResourceId. anypb.New then fails, and issueCredential returns (nil, nil) after a mint, so the failure is silently treated as success. Keep the base status when attaching fails, e.g. if withInfo, err := st.WithDetails(...); err == nil { st = withInfo }, and do the same for the ResourceId. (Confidence: high)

// that nothing was created.
func credentialIssuedButUndeliverable(output *CredentialIssueOutput, cause error) error {
st := status.New(codes.Internal,
"credential was minted but cannot be delivered; the provider outcome is unresolved and issuance must not be retried")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: The underlying cause used to be in the status message and now appears only in ErrorInfo.Metadata["cause"]. err.Error() prints only the fixed message, so task-failure logs and anything else that logs the error no longer show why delivery failed (e.g. an encryption or cardinality failure). Either log cause here with zap before returning, or append a sanitized cause to the message. (Confidence: medium, observability)

@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor
Superseded — see the current review report for commit a566a566e177

General PR Review: feat: typed credential output contract (IssueCredentialV2)

Blocking Issues: 2 | Suggestions: 2 | Threads Resolved: 0
Criteria: Criteria status: loaded .claude/skills/ci-review.md from trusted base 47f5027e75e5.
Review mode: incremental since 63ee3ca
View review run

Review Summary

The PR adds the IssueCredentialV2 RPC, output_content_type fields (descriptor field 11, request field 6), and the IssueTypedCredentialTask arm (124 / ordinal 33).

The new commit (9addbb8) changes how credentialIssuedButUndeliverable reports a minted credential. The provider handle is no longer written into the error message. It is now attached as structured gRPC status details: an ErrorInfo (reason, cause, retry/cleanup flags) and the ResourceId. The commit adds tests that these details survive errors.Join with ErrTaskNonRetryable. errdetails is already vendored and listed in go.mod, so there is no dependency change.

I scanned the full PR diff for security and correctness.

Trusted criteria, triage:

  • Silence: yes. A swallowed status turns a failed mint into a nil-error success.
  • Durability: yes. The proto/wire fields and the task arm persist.
  • Uncontrolled dimensions: yes. Safety depends on which SDK version is on each side.
  • Consumer distance: high. The C1 platform interprets the result.

Verdict: HIGH. Review-blind classes: error-path and version-pair. Instruments in the PR: refusal tests that assert the issuer never saw the request, plus handler and transport tests. There is no cross-version harness. A full pass-set review per docs/BUG_CATCHING.md §6 is recommended before merge.

Other criteria checks:

  • Proto field numbers are additive.
  • pkg/sdk/version.go was not bumped, even though two generated interfaces gained a method (raised earlier in this review's summaries).

Security Issues

None found.

Correctness Issues

  • Prior — still present (open thread on proto/c1/connector/v2/resource.proto:121): internal/connector/noop_provisioner.go still has no IssueCredentialV2 method and doesn't embed Unimplemented…. CredentialManagerServiceServer requires the method (pb/c1/connector/v2/resource_grpc.pb.go:629), so RegisterCredentialManagerServiceServer(s, noop) at internal/connector/connector.go:517 does not compile. That breaks the gRPC server wrapper every connector links. (Confidence: high)
  • New — pkg/connectorbuilder/credentials.go:347-359: st, _ = st.WithDetails(...) discards the error. On failure, grpc returns (nil, err) (vendored internal/status/status.go:134-147), and a nil *Status has Code() == OK, so st.Err() returns nil. The failure happens when anypb.New rejects invalid UTF-8, either in cause.Error() (provider/encryption error text) or in a connector-supplied ResourceId. In that case issueCredential returns (nil, nil) after a mint, and the undeliverable credential is silently treated as success. (Confidence: high)

Suggestions

  • New — pkg/connectorbuilder/credentials.go:346: the cause now lives only in ErrorInfo metadata. err.Error() and the logs show only the generic message, so operators lose the reason delivery failed. (Confidence: medium)
  • Prior (earlier summaries) — still present — pkg/sdk/version.go is still v0.36.0, but CredentialManagerServiceServer and CredentialManagerServiceClient each gained a method. Downstream servers without the embed, and hand-written clients, will stop compiling.

Resolved prior findings

  • pkg/tasks/c1api/issue_typed_credential.go:40 (handler untested) — fixed. pkg/tasks/c1api/issue_typed_credential_test.go (TestIssueTypedCredentialTaskHandler) covers dispatch to IssueCredentialV2 with OutputContentType and confirms the legacy RPC is never called.
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Correctness Issues

In `internal/connector/noop_provisioner.go`:
- Around line 37-39: noopProvisioner is registered as a CredentialManagerServiceServer (internal/connector/connector.go:517), and that interface now requires IssueCredentialV2. Add `func (n *noopProvisioner) IssueCredentialV2(ctx context.Context, request *v2.IssueCredentialRequest) (*v2.IssueCredentialResponse, error)` returning `status.Error(codes.FailedPrecondition, "provisioning is not enabled")`, then run `go build ./...`.

In `pkg/connectorbuilder/credentials.go`:
- Around line 347-359: `st, _ = st.WithDetails(...)` sets st to nil when WithDetails fails (e.g. invalid UTF-8 in cause.Error() or in the ResourceId), and nil.Err() returns nil, so the function returns a nil error after a mint. Only replace st when WithDetails succeeds: `if s2, err := st.WithDetails(...); err == nil { st = s2 }`. Do this for both the ErrorInfo and the ResourceId. Optionally sanitize cause with strings.ToValidUTF8. Add a test with an invalid-UTF-8 cause that asserts a non-nil codes.Internal error.

## Suggestions

In `pkg/connectorbuilder/credentials.go`:
- Around line 346: the cause is no longer in the status message, so logs lose it. Log `cause` with ctxzap/zap before returning, or include a sanitized cause in the message.

In `pkg/sdk/version.go`:
- Bump the minor version, since the generated CredentialManagerServiceServer/Client interfaces gained a method.

Reviewed commit: 9addbb8e6442

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking issues found — see the full review report

The noop provisioner is a registered CredentialManagerService, so the new
method has to exist on it. It answers FailedPrecondition like the legacy
IssueCredential, because provisioning is not enabled either way.
@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor
Superseded — see the current review report for commit a566a566e177

General PR Review: feat: typed credential output contract (IssueCredentialV2)

Blocking Issues: 1 | Suggestions: 3 | Threads Resolved: 0
Criteria: Criteria status: loaded .claude/skills/ci-review.md from trusted base 47f5027e75e5.
Review mode: incremental since 9addbb8
View review run

Review Summary

The PR adds a separate IssueCredentialV2 RPC, output_content_type fields (descriptor field 11, request field 6), and a new task arm, IssueTypedCredentialTask (124, ordinal 33), with its own handler. The builder checks the requested type against the descriptor that will run the request, and refuses any mismatch before Issue is called. The only new commit since the last review adds IssueCredentialV2 to noopProvisioner, and it returns FailedPrecondition, the same as the legacy method. I scanned the full PR diff again for security and correctness problems. I also checked every other in-repo implementer of CredentialManagerServiceServer/Client (lambda_server_test.go, syncer_test.go, session_stats_integration_test.go, both harnesses). Each one embeds the interface or the Unimplemented… type, so all of them still compile.

I applied the trusted criteria:

  • Triage: HIGH. The change is durable: it adds proto wire fields and a task arm. Its safety also depends on the version pair: an old runtime has to answer Unimplemented or ignore arm 124.
  • Instruments: this is a schedule/version-pair risk. The PR covers it with the transport test (credential_typed_transport_test.go), which checks that an old runtime answers Unimplemented before it mints anything and that the client doesn't fall back to the legacy method.
  • Wire compatibility: the new field numbers are additive, and the generated pb output is checked in with the proto changes.
  • Exported API: the two generated interfaces gained a method; see the version suggestion below.

Security Issues

None found.

Correctness Issues

  • Prior — still present (open thread): pkg/connectorbuilder/credentials.go:347-359
    • st, _ = st.WithDetails(...) throws away the error. When WithDetails fails, grpc returns (nil, err).
    • A nil *Status reports Code() == OK, so st.Err() returns nil.
    • The failure is reachable: anypb rejects invalid UTF-8, which can appear in cause.Error() or in a ResourceId supplied by the connector.
    • When it fails, issueCredential returns (nil, nil) after the credential was minted, so a credential that can't be delivered is silently treated as success. (Confidence: high)

Suggestions

  • Prior — still present (open thread): pkg/connectorbuilder/credentials.go:346: the underlying cause now appears only in ErrorInfo.Metadata["cause"]. err.Error() and the logs show only the generic message, so operators can't see why delivery failed. (Confidence: medium)
  • Prior (earlier summaries) — still present: pkg/sdk/version.go is still v0.36.0, but CredentialManagerServiceServer and CredentialManagerServiceClient each gained a method. Downstream servers that don't embed Unimplemented…, and hand-written clients, will stop compiling. The criteria ask for a minor bump and a migration note. (Confidence: high)
  • Prior (earlier summaries) — still present: pkg/connectorbuilder/credentials.go:194-199: typed issuance uses the same span name (builder.IssueCredential) and metric task type (IssueCredentialType) as legacy issuance. Dashboards can't tell typed refusals and failures apart from legacy ones during rollout. (Confidence: medium, observability only)

Resolved prior findings

  • proto/c1/connector/v2/resource.proto:121 (noop provisioner missing IssueCredentialV2): fixed. internal/connector/noop_provisioner.go:41-43 now implements the method and returns FailedPrecondition.
  • pkg/tasks/c1api/issue_typed_credential.go:40 (handler untested): fixed. pkg/tasks/c1api/issue_typed_credential_test.go checks that the handler calls IssueCredentialV2 with OutputContentType set and never calls the legacy RPC.
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Correctness Issues

In `pkg/connectorbuilder/credentials.go`:
- Around line 347-359: `st, _ = st.WithDetails(...)` sets st to nil when WithDetails fails (for example, invalid UTF-8 in cause.Error() or in the ResourceId), and nil.Err() returns nil, so the function returns a nil error after a credential was minted. Replace st only when WithDetails succeeds: `if s2, derr := st.WithDetails(...); derr == nil { st = s2 }`. Do this for both the ErrorInfo detail and the ResourceId detail. Optionally sanitize the cause with strings.ToValidUTF8. Add a test with an invalid-UTF-8 cause that asserts the error is non-nil and has code codes.Internal.

## Suggestions

In `pkg/connectorbuilder/credentials.go`:
- Around line 346: the cause is no longer in the status message, so logs lose it. Log `cause` with ctxzap (zap.Error(cause)) before returning, or include a sanitized cause in the status message.
- Around line 194-199: give typed issuance its own span name (for example "builder.IssueCredentialV2") and metric task type (tasks.IssueTypedCredentialType) when typed is true, so typed failures can be told apart from legacy ones.

In `pkg/sdk/version.go`:
- Around line 3: CredentialManagerServiceServer and CredentialManagerServiceClient each gained a method. Bump the minor version (for example to v0.37.0) and add a migration note to the PR for downstream code that implements these interfaces without embedding them.

@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

General PR Review: feat: typed credential output contract (IssueCredentialV2)

Blocking Issues: 1 | Suggestions: 3 | Threads Resolved: 0
Criteria: Criteria status: loaded .claude/skills/ci-review.md from trusted base 47f5027e75e5.
Review mode: incremental since 9addbb8
View review run

Review Summary

The PR adds a separate IssueCredentialV2 RPC, output_content_type fields (descriptor field 11, request field 6), and a new task arm, IssueTypedCredentialTask (124, ordinal 33), with its own handler. The builder checks the requested type against the descriptor that will run the request, and refuses any mismatch before Issue is called. The only new commit since the last review adds IssueCredentialV2 to noopProvisioner, and it returns FailedPrecondition, the same as the legacy method. I scanned the full PR diff again for security and correctness problems. I also checked every other in-repo implementer of CredentialManagerServiceServer/Client (lambda_server_test.go, syncer_test.go, session_stats_integration_test.go, both harnesses). Each one embeds the interface or the Unimplemented… type, so all of them still compile.

I applied the trusted criteria:

  • Triage: HIGH. The change is durable: it adds proto wire fields and a task arm. Its safety also depends on the version pair: an old runtime has to answer Unimplemented or ignore arm 124.
  • Instruments: this is a schedule/version-pair risk. The PR covers it with the transport test (credential_typed_transport_test.go), which checks that an old runtime answers Unimplemented before it mints anything and that the client doesn't fall back to the legacy method.
  • Wire compatibility: the new field numbers are additive, and the generated pb output is checked in with the proto changes.
  • Exported API: the two generated interfaces gained a method; see the version suggestion below.

Security Issues

None found.

Correctness Issues

  • Prior — still present (open thread): pkg/connectorbuilder/credentials.go:347-359
    • st, _ = st.WithDetails(...) throws away the error. When WithDetails fails, grpc returns (nil, err).
    • A nil *Status reports Code() == OK, so st.Err() returns nil.
    • The failure is reachable: anypb rejects invalid UTF-8, which can appear in cause.Error() or in a ResourceId supplied by the connector.
    • When it fails, issueCredential returns (nil, nil) after the credential was minted, so a credential that can't be delivered is silently treated as success. (Confidence: high)

Suggestions

  • Prior — still present (open thread): pkg/connectorbuilder/credentials.go:346: the underlying cause now appears only in ErrorInfo.Metadata["cause"]. err.Error() and the logs show only the generic message, so operators can't see why delivery failed. (Confidence: medium)
  • Prior (earlier summaries) — still present: pkg/sdk/version.go is still v0.36.0, but CredentialManagerServiceServer and CredentialManagerServiceClient each gained a method. Downstream servers that don't embed Unimplemented…, and hand-written clients, will stop compiling. The criteria ask for a minor bump and a migration note. (Confidence: high)
  • Prior (earlier summaries) — still present: pkg/connectorbuilder/credentials.go:194-199: typed issuance uses the same span name (builder.IssueCredential) and metric task type (IssueCredentialType) as legacy issuance. Dashboards can't tell typed refusals and failures apart from legacy ones during rollout. (Confidence: medium, observability only)

Resolved prior findings

  • proto/c1/connector/v2/resource.proto:121 (noop provisioner missing IssueCredentialV2): fixed. internal/connector/noop_provisioner.go:41-43 now implements the method and returns FailedPrecondition.
  • pkg/tasks/c1api/issue_typed_credential.go:40 (handler untested): fixed. pkg/tasks/c1api/issue_typed_credential_test.go checks that the handler calls IssueCredentialV2 with OutputContentType set and never calls the legacy RPC.
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Correctness Issues

In `pkg/connectorbuilder/credentials.go`:
- Around line 347-359: `st, _ = st.WithDetails(...)` sets st to nil when WithDetails fails (for example, invalid UTF-8 in cause.Error() or in the ResourceId), and nil.Err() returns nil, so the function returns a nil error after a credential was minted. Replace st only when WithDetails succeeds: `if s2, derr := st.WithDetails(...); derr == nil { st = s2 }`. Do this for both the ErrorInfo detail and the ResourceId detail. Optionally sanitize the cause with strings.ToValidUTF8. Add a test with an invalid-UTF-8 cause that asserts the error is non-nil and has code codes.Internal.

## Suggestions

In `pkg/connectorbuilder/credentials.go`:
- Around line 346: the cause is no longer in the status message, so logs lose it. Log `cause` with ctxzap (zap.Error(cause)) before returning, or include a sanitized cause in the status message.
- Around line 194-199: give typed issuance its own span name (for example "builder.IssueCredentialV2") and metric task type (tasks.IssueTypedCredentialType) when typed is true, so typed failures can be told apart from legacy ones.

In `pkg/sdk/version.go`:
- Around line 3: CredentialManagerServiceServer and CredentialManagerServiceClient each gained a method. Bump the minor version (for example to v0.37.0) and add a migration note to the PR for downstream code that implements these interfaces without embedding them.

Reviewed commit: a566a566e177

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking issues found — see the full review report

highb commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

Squire (claude-opus-5-5): Closing as superseded. This PR uses request field numbers 5 and 6 for output_content_type, and #1195 uses the same numbers for request_data, so the two can't both merge. The output contract is moving to a single smaller PR on top of #1213, which carries the type in Field.content_type. I'll link that PR here once it's open.

@highb highb closed this Nov 3, 2026

highb commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

Squire (claude-opus-5-5): The replacement is #1218.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant