Skip to content

feat: support 2026-09-01 gateway API - #378

Open
alexbouchardd wants to merge 2 commits into
mainfrom
feat/api-2026-09-01
Open

feat: support 2026-09-01 gateway API#378
alexbouchardd wants to merge 2 commits into
mainfrom
feat/api-2026-09-01

Conversation

@alexbouchardd

Copy link
Copy Markdown
Contributor

Summary

  • update the CLI and cached OpenAPI source to API version 2026-09-01
  • replace project mode/team_mode handling with product/team_product, while retaining local config compatibility
  • send destination rate limits through config.delivery_policy and add delivery-group flags for standalone and inline destinations
  • expose delivery-group filters for events, request events, metrics, and MCP tools
  • update generated reference docs, examples, fixtures, and API-version expectations

Testing

  • env -u GOMODCACHE go test ./...
  • env -u GOMODCACHE go run ./tools/generate-reference --check
  • self-contained guest and login acceptance tests

Credentialed destination and connection acceptance tests were not run because HOOKDECK_CLI_TESTING_API_KEY is not configured in this environment.

@leggetter

leggetter commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

Claude-assisted review - I ran /code-review over the diff, did a manual pass, then checked the findings against the live API and the published 2026-09-01 OpenAPI spec. Flagging the provenance up front: the verification below is real, the prose is generated.

Checklist so we can track what gets picked up. Tick as they land, or push back on any of them.

Edited after posting: the /projects finding is withdrawn once I checked core origin/staging. Details in the struck-through item below.

Blocking

  • /projects doesn't exist on prod. Withdrawn - this is a deploy-order dependency, not a CLI bug. origin/staging in core registers the list route on both paths:

    const path = '/projects';
    // The published CLI lists projects through GET /teams; that alias is kept
    // (and filtered out of the OpenAPI docs) until CLI versions move to /projects.
    const cli_legacy_list_paths = [path, '/teams'];

    So /projects is real and intended, it just isn't on prod yet - running this branch against prod today gives 404 Cannot GET /2026-09-01/projects, while /teams answers on both API versions. The one thing to settle is ordering: this PR can't ship before that core release. Since the /teams alias is deliberately kept for published CLIs, staying on /teams for now would also decouple the two releases, if that's preferable.

    Checked the scope declaration too (scope: 'projects.read') - withScopes passes CLI-authenticated callers through untouched, so that won't bite.

  • --delivery-group will 400 on four of the seven metrics endpoints. It's added in addMetricsCommonFlagsEx (pkg/cmd/metrics.go:86), so every metrics subcommand gets it, but per the spec only /metrics/attempts, /metrics/events and /metrics/queue-depth accept delivery_group. /metrics/requests, /metrics/transformations, /metrics/events-by-issue and /metrics/events-pending-timeseries don't, and their filters are additionalProperties: false, so it's a 400 rather than an ignored field. metrics events --measures pending routes to events-pending-timeseries, so that one breaks too. The skipIssueID mechanism in that file already solves this shape. Same gap in pkg/gateway/mcp/tool_metrics.go:58.

    Re-checked this against core origin/staging rather than the published spec, in case the spec was just stale: resolving each filter schema gives delivery_group on event_filters_schema, attempt_filters_schema and queue_depth_filters_schema only. request_filters_schema, transformation_filters_schema, events_pending_timeseries_filters_schema and events_by_issue_filters_schema don't carry it. So this one survives the deploy.

Worth fixing

  • ListProjects drops the unmarshal error (pkg/hookdeck/projects.go:20). postprocessJsonResponse(res, &projects) returns an error that isn't checked, so a 200 with an unexpected shape gives an empty slice and a nil error - zero projects listed, no failure. checkAndPrintError catches the non-2xx case, so this only bites on a shape mismatch, which is exactly what an endpoint rename risks.

  • No team_mode fallback on ValidateAPIKeyResponse / PollAPIKeyResponse / CIClient. If team_product is ever absent, ProjectProduct, ProjectMode and ProjectType all become "", IsGatewayProject("") is false, and every hookdeck gateway ... command fails with this command requires a Gateway project; current project type is (blank). To be fair to the change: I tested this against prod with project_type, project_mode and project_product stripped from my config, and whoami still printed Project type: Gateway - so the live API does return team_product and this isn't currently broken. Keeping the old field as a fallback through ModeToProduct is cheap insurance rather than a fix.

  • delivery_group is missing ,omitempty (pkg/hookdeck/events.go:18), unlike ResponseStatus two lines below. event list --output json now emits "delivery_group": null on every event, which changes output for anything diffing or schema-checking that JSON.

  • REFERENCE.md:1937 lists the metrics common flags by hand and doesn't mention --delivery-group. generate-reference --check passes because it emits no flag table for those subcommands.

  • gofmt regression. pkg/config/profile.go and pkg/config/profile_credentials_test.go are unformatted on the branch - the new ProjectProduct field breaks the struct alignment. profile.go was clean on main. Nothing in CI catches formatting, which is why it got through.

  • Five copies of the same fallback ladder. Type -> Product -> legacy Mode is open-coded at pkg/cmd/gateway.go:50, pkg/cmd/whoami.go:72, Profile.SaveProfile, Config.setProfileFieldsInViper and Config.constructConfig. One Profile.ResolveProjectType() would cover all five and stop the next caller getting the precedence subtly wrong.

  • Case handling splits down the middle of pkg/config/project_type.go. ProductToProjectType, ProductToLegacyMode and ModeToProduct all lowercase their input; ProjectTypeToProduct and IsGatewayProject are case-sensitive. Sibling functions, opposite contracts, nothing documenting which is which.

Tests

  • The upgrade path has no test. pkg/config/config_test.go isn't touched, but constructConfig gained the back-fill that every existing user hits on first run after upgrading (ProjectProduct derived from a legacy project_mode). Nothing asserts project_mode = "inbound" yields event_gateway, and the existing "use project" test still passes only a legacy mode and never checks project_product was written.

  • TestProductMappings covers about a third of the matrix. Seven flat assertions in a file where every neighbour is table-driven. Missing: ProductToProjectType("") (the wipe case above), ProjectTypeToProduct for Console/Outpost/unknown, ProductToLegacyMode for console/outpost/empty, ModeToProduct for inbound/console/outpost/unknown, anything case-related, and a Type -> Product -> Type round trip that would pin the deliberate outbound-to-inbound flattening currently described only in a comment.

  • No acceptance test asserts project_product reaches disk. product shows up in the ATs only inside three mock responses. Nothing checks the field is persisted to config.toml after login or project use, which is the one end-to-end guarantee the rename needs.

Not blocking

  • 99 hardcoded 2026-09-01 literals across test files, 87 of them in pkg/gateway/mcp/server_test.go. APIPathPrefix says "Change in one place when the API version is updated" and 23 test sites already use it. Mechanical, but the next bump repeats all of this churn.

  • buildDeliveryPolicy (pkg/cmd/destination_common.go:81) runs regardless of destination type, and DestinationTypeConfigCLI in the spec has no delivery_policy and is additionalProperties: false. So --destination-type CLI plus the new delivery-group flags gives an opaque API 400 instead of a client-side error. Same shape as the pre-existing rate_limit behaviour, so this widens an existing surface rather than adding a new break.

Open question on naming

Not blocking, and I may be missing context from the API side. product reads odd to me next to a display type of Gateway / Console / Outpost, and the CLI now carries three vocabularies at once: product on the wire, legacy mode in config, and type in output. The public spec documents no project resource at all, so product is internal naming rather than something the public API commits to. Is product the settled term API-side, or is this the moment to align on one word? --output json still emits type, so nothing user-facing changes either way today.


Verified while reviewing: go build ./..., go test ./... and generate-reference --check all pass on the branch, and the delivery-policy payload the CLI builds (rate, period, groups.{key,rate,rate_period,overrides}) matches DestinationDeliveryPolicy in the spec exactly, including the second|minute|hour group-period enum.

I can put the non-blocking fixes and the test gaps into a PR against this branch if that's easier than folding them in yourself.

@leggetter

Copy link
Copy Markdown
Collaborator

Follow-up PR with the fixes from the review above: #379 (targets this branch, so merge / cherry-pick / close as you prefer).

Covers everything on the checklist except the /projects deploy-ordering question, which is yours to call. Ticking the items it addresses:

  • --delivery-group restricted to the endpoints that accept it, plus client-side errors on the two metrics events routes that can't use it
  • ListProjects unmarshal error propagated
  • team_mode fallback
  • ,omitempty on delivery_group
  • REFERENCE.md
  • gofmt on the two regressed files
  • five fallback ladders collapsed into Profile.ResolveProjectType()
  • case handling aligned
  • upgrade-path test, mapping matrix, acceptance assertion on project_product

Left alone: the 99 hardcoded version literals in tests, and the CLI-destination delivery-policy validation - both judgement calls that are yours rather than mine.

leggetter added a commit that referenced this pull request Sep 10, 2026
core `origin/staging` now names the field `type` on GET /projects and
`team_type` on the CLI auth endpoints, with the same event_gateway | console |
outpost values. The CLI followed the wire rename and the internal vocabulary
with it, so it speaks the same word as the API it calls.

That word was already taken. The CLI used ProjectType for the display label -
"Gateway", "Console", "Outpost" - so the two meanings had to be separated:

  ProjectType   event_gateway | console | outpost   what the API calls type
  TypeLabel()   Gateway | Console | Outpost         derived at print time

The label is presentation and is no longer stored. project_type on disk now
holds the API value, project_product is gone entirely (it only ever existed on
this unreleased branch, so nothing has written it), and project_mode is still
written for older CLIs reading the same file.

NormalizeProjectType is the single door every value goes through. It accepts an
API type, a display label written by an older CLI, or a legacy mode, and returns
the API type - so the three vocabularies converge in one place instead of at
each call site.

The auth structs read team_type, then team_product, then team_mode. Prod
currently serves team_product while staging serves team_type, so without that
chain a CLI shipping ahead of the deploy would blank the project type and fail
every gateway command. Verified against prod, which still serves the old field:
whoami resolves Gateway from a config stripped of all type information.

Two things deliberately unchanged, both user-facing: `--output json` still emits
gateway | outpost | console, and the `--type` filter still accepts them. The
API type is not used there - changing it would break anyone parsing that output.

Tests caught two regressions worth naming: the gateway error message printed the
wire value ("current project type is outpost") instead of the label the user was
shown, and saving local config dropped an unrecognized mode instead of carrying
it through. Both fixed.

Refs #378

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BnrKWZQASV7bFJ4oGWwmo9
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.

2 participants