Skip to content

Police the endpoints a discovered issuer names, not only the issuer - #810

Merged
jeremy merged 2 commits into
mainfrom
police-oauth-endpoints
Aug 22, 2026
Merged

Police the endpoints a discovered issuer names, not only the issuer#810
jeremy merged 2 commits into
mainfrom
police-oauth-endpoints

Conversation

@jeremy

@jeremy jeremy commented Aug 22, 2026

Copy link
Copy Markdown
Member

Closes #806.

The exposure

#804 judges the advertised issuer's address before the metadata GET. That closes the direct route and leaves the indirect one open: an attacker-controlled issuer on public space passes the policy, is selected, and returns correctly issuer-bound metadata whose token_endpoint and device_authorization_endpoint point wherever it likes. parseAndBindASMetadata checks only that token_endpoint is non-empty and copies it verbatim; RequireSecureEndpoint admits any https host, private space included. The credential POSTs then went out on http.DefaultClientclient_id and device_code from the device flow, the authorization code, client secret, and refresh token from the Exchanger. One public host buys private space, and what arrives is credentials rather than a blind GET.

The decision

Dial-time address enforcement on every device and token endpoint request, on the same shared DefaultIssuerPolicy() client as the issuer hop. Not same-origin, and the reasons are written into SPEC §16 so they are not re-litigated: RFC 8414 §2 does not require endpoints on the issuer origin, RFC 8705 §5 publishes off-origin endpoints by design, and an origin comparison judges a hostname string that is resolved twice — publicly at discovery, privately at the POST, under a rebind. Scheme alone is satisfied by any private host.

The device and exchange functions cannot tell a discovered endpoint from a hand-configured one (PerformDeviceLogin takes a *Config; Exchange/Refresh take a string), so the policy applies to every request on the default client. That is the same uniformity call as #804 made for WithExpectedIssuer, for the same reason; a provenance flag on Config is a marker that a consumer round-tripping the config through storage silently drops.

The change

func WithDevicePolicy(p surfguard.Policy) DeviceOption          // new
func NewExchanger(httpClient *http.Client, opts ...ExchangerOption) *Exchanger  // variadic added
func WithExchangerPolicy(p surfguard.Policy) ExchangerOption    // new

Precedence is one rule across all three surfaces of the package: a client you hand in is yours, enforcement included; otherwise your policy; otherwise the shared default. http.DefaultClient is the spelling of "no policy", so no Without* options were added for these two surfaces — there is nothing they would express that passing a plain client does not.

A refusal is a non-retryable *basecamp.Error (api_error) that also matches errors.Is(err, surfguard.ErrBlocked). In the poll loop it is classified ahead of the timeout and transport cases, so the loop neither backs off and re-dials a target it must stop talking to nor reports it retryable.

The shared-client invariant from #804 extends to the new surfaces: WithDevicePolicy builds its transport once at option construction (PerformDeviceLogin hands the same opts to the auth request and the poll, so a transport per newDeviceConfig would be two pools per login), WithExchangerPolicy builds one per Exchanger, and suppressRedirects keeps copying so the shared default is never mutated.

Demonstrated, not asserted

Seven mutations, each run against the new test file, each caught:

Mutation Tests that fail
device default back to http.DefaultClient PerformDeviceLogin_BlockedEndpointIsNeverContacted, PollDeviceToken_BlockedTokenEndpointTerminates, RefusesLegacyNumericSpelling, DefaultPolicyClientIsSharedAcrossSurfaces, EndpointPolicyIsolation
exchanger default back to http.DefaultClient Exchanger_BlockedTokenEndpointIsNeverContacted, RefusesLegacyNumericSpelling, DefaultPolicyClientIsSharedAcrossSurfaces
poll loop stops classifying ErrBlocked (falls to timeout/transport) PollDeviceToken_BlockedTokenEndpointTerminates
device-auth stops classifying ErrBlocked (falls to transport) PerformDeviceLogin_Blocked…, RefusesLegacyNumericSpelling, EndpointPolicyIsolation
exchange ErrBlocked branch made dead Exchanger_Blocked…, RefusesLegacyNumericSpelling
WithDevicePolicy builds a transport per call DefaultPolicyClientIsSharedAcrossSurfaces
refusal marked Retryable: true all five blocked-endpoint tests

Every blocked case asserts the endpoint's handler ran zero times, and every mock serves a usable response, so a policy that failed to block yields a successful login or exchange rather than a differently-worded failure. The poll test's injected clock advances by each sleep, so a loop that wrongly kept polling runs out to expiry with a recognizably wrong reason instead of hanging.

go test ./..., go vet, gofmt -s, golangci-lint clean; the conformance runner still builds; make doc-constants-check and make go-check-drift pass. The 74 existing device tests and 10 exchange tests pass unedited — every one that reaches the wire already injected srv.Client(), and the five that do not all fail before the request.

What this does and does not close

basecamp-cli is not protected by this change. It passes its general-purpose m.httpClient to NewExchanger, WithDeviceHTTPClient, and NewDiscoverer alike, and a caller-supplied client is the caller's — the enforcement seam is the transport's dialer, so there is nothing to layer on top of an arbitrary RoundTripper. That is the #804 WithIssuerHTTPClient contract applied consistently; the alternative (clone an *http.Transport and overwrite its DialContext/Proxy) is "the policy applies sometimes" in another form. The consumer-side fix is one line, &http.Client{Transport: oauth.DefaultIssuerPolicy().RoundTripper()} for these three call sites, and it is recorded in MIGRATING and Appendix F rather than hidden. The SDK default is now safe; the CLI needs its own PR.

Still open, recorded in Appendix F:

  • The other four SDKs. Only Go and Ruby have surfguard, and the Ruby gem is classification-only by design (the caller pins the resolved address), so Ruby's enforcement would be a Net::HTTP#ipaddr= pin under the default Fetcher transport — not reachable from the injected-Faraday lane. TS, Python, and Kotlin have no classification tables at all; the seams (undici connector lookup, a resolving httpcore backend, OkHttp Dns) are named so the follow-ups are specified rather than rediscovered. Same cross-SDK shape as Download hop 2 follows redirects in four SDKs: needs a cross-SDK destination policy, not a Go patch #805.
  • Exchange redirect-following is not uniform: Go's Exchanger, Kotlin's default client, and TS's exchange (no redirect: option) follow redirects, where the device flow suppresses them in all four. Under the policy client each redirect hop's dial is judged, so the address policy holds; a 307 re-POST of the secret to a public Location does not need the policy to be exploitable. Same shape as Download hop 2 follows redirects in four SDKs: needs a cross-SDK destination policy, not a Go patch #805, so noted rather than patched here.
  • AuthManager.refreshLocked posts a refresh token to Credentials.TokenEndpoint on the main API client. That endpoint is consumer-stored; if the consumer stored a discovered one, the same reasoning applies at the consumer's boundary.

API break

NewExchanger gains a variadic parameter. Source-compatible for every direct call; a function value of the old type stops compiling, and apidiff reports it incompatible — hence breaking, as #804 was for NewDiscoverer.


Summary by cubic

Polices the addresses of device-authorization and token endpoints, not just the issuer metadata GET, closing the indirect SSRF path where a public issuer could steer credentials into private space.

  • Device flow (PerformDeviceLogin, RequestDeviceAuthorization, PollDeviceToken) and Exchanger POST via a shared client that enforces oauth.DefaultIssuerPolicy() (same as discovery). Device-flow redirects remain suppressed.
  • Precedence: caller-provided HTTP client is used as-is; else a provided policy; else the shared default. Passing http.DefaultClient restores prior behavior.
  • New options: WithDevicePolicy, WithDeviceHTTPClient, WithExchangerPolicy. NewExchanger now accepts variadic options.
  • Refusals surface as non-retryable api_error matching surfguard.ErrBlocked; the poll loop terminates on the first attempt.
  • Scope: this default enforcement applies to the device-flow and Exchanger POSTs only. AuthManager.refreshLocked posts to a stored TokenEndpoint on the caller’s client and remains outside this default policy. Caller-supplied clients are never wrapped; compose the policy into their transport if desired.

Migration

  • If you used defaults with loopback/private AS endpoints, add WithDevicePolicy(oauth.DefaultIssuerPolicy().AllowLoopback()) or WithExchangerPolicy(...), or supply your own client. Otherwise these requests are now refused.
  • Callers already passing their own client are unchanged and not protected; to enable enforcement, build it with &http.Client{Transport: oauth.DefaultIssuerPolicy().RoundTripper()}.
  • If you used NewExchanger as a function value, update to its new type with ...oauth.ExchangerOption. Direct calls remain source-compatible.

Written for commit 9eb2ddd. Summary will update on new commits.

Review in cubic

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds dial-time address enforcement to Go OAuth device and token endpoint requests, closing an indirect SSRF path.

Changes:

  • Adds device-flow and exchanger policy options.
  • Classifies blocked endpoints as terminal, non-retryable API errors.
  • Adds security tests and migration documentation.

Tip

If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
SPEC.md Defines endpoint address-policy requirements and SDK status.
MIGRATING.md Documents behavior changes and overrides.
go/README.md Explains default enforcement and configuration.
go/pkg/basecamp/oauth/exchange.go Enforces policy for token exchanges.
go/pkg/basecamp/oauth/endpoint_policy_test.go Tests blocking, precedence, and isolation.
go/pkg/basecamp/oauth/discovery.go Shares policy-client construction and errors.
go/pkg/basecamp/oauth/device.go Enforces policy across device-flow requests.
Suppressed comments (1)

SPEC.md:4128

  • The PR description says the unprotected AuthManager.refreshLocked path is recorded in Appendix F, but this new appendix section ends without mentioning it. Please record that this SDK path uses the caller-owned AuthManager client even when Credentials.TokenEndpoint was copied from discovery, so consumers do not infer that later automatic refreshes receive the new default policy.
already passes its own client — `basecamp-cli` passes its general-purpose
client to all three entry points — sees no change, and is also not protected
by this: the policy lives in the transport, and that consumer owns its
transport.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread SPEC.md Outdated
Comment thread SPEC.md Outdated
jeremy added 2 commits August 22, 2026 00:53
…806)

It leaves the indirect route open: an issuer on public space passes, is
selected, and returns issuer-bound metadata whose token_endpoint and
device_authorization_endpoint point anywhere — and those are where the
client_id, device_code, authorization code, client secret, and refresh
token are POSTed, on http.DefaultClient.

The device flow and the Exchanger now default to the same shared
DefaultIssuerPolicy client as the issuer hop, with the same override
shape: WithDevicePolicy / WithDeviceHTTPClient, WithExchangerPolicy / a
non-nil NewExchanger client. A caller-supplied client stays the caller's,
enforcement included. A refusal is a non-retryable api_error that matches
surfguard.ErrBlocked; in the poll loop it terminates on the first attempt
instead of being read as a timeout and backed off.

Same-origin is recorded as NOT the control (RFC 8414 does not require it,
RFC 8705 publishes off-origin endpoints by design, and a hostname
comparison does not survive a rebind between discovery and the POST).

SPEC §16 gains requirement 6 [Go-first]; Appendix F records the per-SDK
state and the seam each would need; MIGRATING records the tightening.
…ming uniform timeouts

Review on #810: AuthManager.refreshLocked posts a stored refresh token —
which the documented device-login bridge sources from a discovered
Config.TokenEndpoint — on the caller-owned client, outside the new
default policy; and the bounded-timeout claim was false for Go's
doTokenRequest (caller context only) and Kotlin's postTokenRequest (no
HttpTimeout). Requirement 6 now names its boundary and the stored-
credentials residual explicitly, in both §16 and Appendix F.
@jeremy
jeremy force-pushed the police-oauth-endpoints branch from 1114242 to 9eb2ddd Compare August 22, 2026 07:54
@jeremy

jeremy commented Aug 22, 2026

Copy link
Copy Markdown
Member Author

Fixed — the review body's suppressed note on Appendix F is addressed alongside the inline thread: Appendix F now records that AuthManager.refreshLocked runs on the caller-owned client even when Credentials.TokenEndpoint was copied from discovery, so later automatic refreshes do not receive the new default policy. Also rebased onto main (#809 landed); the only conflict was both PRs' MIGRATING entries, kept both, and Appendix F's #805 reference now notes #809 closed the download hop, leaving the exchange path as the remaining redirect-following exception.

@jeremy
jeremy merged commit fa15fc1 into main Aug 22, 2026
45 checks passed
@jeremy
jeremy deleted the police-oauth-endpoints branch August 22, 2026 07:59
jeremy added a commit that referenced this pull request Aug 22, 2026
* origin/main:
  Police the endpoints a discovered issuer names, not only the issuer (#810)
  Refuse redirects on the signed download hop in every SDK (#809)
  Quiet known-noise CodeQL alerts without losing coverage (#807)
  Judge the advertised OAuth issuer's address, not just its spelling (#804)
  Let Go's raw GET retry loop see the Retry-After it already parses (#796)
  Deflake three tests that raced a wall clock, and gate the class that produced two of them (#794)
  SPEC §6: decide which statuses honour Retry-After, and how each loop composes it (#793)
  Pin the conformance runners' fixture reads, and give CI a leg that can see them break (#791)
  Report an anonymous embed the timestamp walk cannot resolve, instead of skipping it (#790)

# Conflicts:
#	go/go.mod
jeremy added a commit that referenced this pull request Aug 22, 2026
* origin/main:
  Police the endpoints a discovered issuer names, not only the issuer (#810)
  Refuse redirects on the signed download hop in every SDK (#809)
  Quiet known-noise CodeQL alerts without losing coverage (#807)
  Judge the advertised OAuth issuer's address, not just its spelling (#804)
jeremy added a commit that referenced this pull request Aug 22, 2026
Main moved by four while this PR was in review: #804, #807, #809, #810.
Merged in; what each needed here, verified against the tree:

- #809 and #810 wrote their own Unreleased entries when they merged (#805,
  #806) and carry `breaking` -- nothing to add.
- #807 is CI-internal -- nothing to add.
- #804 carries `breaking` but had no entry: resource-first discovery's
  second hop now rides the address-policed shared client, refusing
  special-use-space issuers non-retryably and dropping the caller's
  transport for that hop. Entry added beside #806's, with the variadic
  NewDiscoverer compile note and the remedies in policy order.
- The trailer's verification commit moves to fa15fc1 and its in-flight
  list shrinks to what is actually in flight.

Codex round 5's guard finding rides along: the pyproject check now parses
the [project] table (awk section-scoped exact line) instead of matching a
version assignment anywhere in the file. Proven with the literal bypass --
a stale [project].version plus an exact assignment in another table passed
the old whole-file grep and is refused now; restore by copy, diff -q clean.
jeremy added a commit that referenced this pull request Aug 22, 2026
…nt-feed-go-connector

* origin/event-feed-foundations:
  Make the fake transport honour the oversize sentinel it mirrors
  Police the endpoints a discovered issuer names, not only the issuer (#810)
  Refuse redirects on the signed download hop in every SDK (#809)
  Quiet known-noise CodeQL alerts without losing coverage (#807)
  Judge the advertised OAuth issuer's address, not just its spelling (#804)
  Let Go's raw GET retry loop see the Retry-After it already parses (#796)
  Deflake three tests that raced a wall clock, and gate the class that produced two of them (#794)
  SPEC §6: decide which statuses honour Retry-After, and how each loop composes it (#793)
  Pin the conformance runners' fixture reads, and give CI a leg that can see them break (#791)
  Report an anonymous embed the timestamp walk cannot resolve, instead of skipping it (#790)
jeremy added a commit that referenced this pull request Aug 22, 2026
…ent-feed-conformance-driver

* origin/event-feed-go-connector:
  Event feed: verdicts that lost coin flips, and a driver blind to eras
  Fix the filters-clone comment that stated the opposite of the code
  Event feed: a policy reason never echoes what the server wrote
  Event feed: wakes are not verdicts, and three more places the order was the bug
  Make the fake transport honour the oversize sentinel it mirrors
  Police the endpoints a discovered issuer names, not only the issuer (#810)
  Refuse redirects on the signed download hop in every SDK (#809)
  Event feed: the oversize verdict survived the seam but not the sanitizer
  Event feed: close the rendering-cap class, type the oversize rejection
  Event feed: cancellation could not reach the one exit, and the origin was checked after the mangle
  Event feed: own the ledger's bytes, cap the dial rendering, test the store fake
  Quiet known-noise CodeQL alerts without losing coverage (#807)
  Judge the advertised OAuth issuer's address, not just its spelling (#804)
  Let Go's raw GET retry loop see the Retry-After it already parses (#796)
  Deflake three tests that raced a wall clock, and gate the class that produced two of them (#794)
  SPEC §6: decide which statuses honour Retry-After, and how each loop composes it (#793)
  Pin the conformance runners' fixture reads, and give CI a leg that can see them break (#791)
  Report an anonymous embed the timestamp walk cannot resolve, instead of skipping it (#790)
jeremy added a commit that referenced this pull request Aug 22, 2026
* Make the release guide and guards tell the truth before v0.15.0

MIGRATING.md and the release tooling carried nine catalogued defects; this
repairs the seven that live in the tree. The retro-labels on merged PRs and
the Unreleased -> v0.15.0 promotion happen at tag time.

- Re-file the two post-tag entries out of "# v0.14.0" into "# Unreleased":
  the #662 absent-expiry changes and the TimelineEventData pointer retype
  both landed in #703, after the tag. Proof: `git show
  go/v0.14.0:MIGRATING.md` contains neither heading.

- Write the four entries the section was missing: #773 (the merge-safe Go
  reads return a transport failure verbatim -- errors.As and Retryable
  results move), #737 (four TS paginated methods now declare the ListResult
  they always returned), #735 (every generated Swift model has a public
  init -- recorded as NOT a break: no existing initializer changed shape,
  the 35 affected models were previously unconstructible so no consumer
  code exists against them), and the maxPages runtime cap (`1919e77f7`, a
  bare commit label-generated notes cannot list).

- Fix the #650 miscount: `position` is conditional on the wire but was
  modeled before #723, so it is not one of "the seven" -- two of the seven
  new keys are conditional, plus `position`. Derived from the v0.14.0 and
  current Tool schemas and the bc3 partial's own `if`s.

- Refresh the #604 table's Kotlin row to agree with the #750 entry and the
  KDoc it cites: the SerializationException lands in `decodeFailure`, the
  discriminator; `cause` mirrors it and is explicitly not one.

- Rewrite the "# Not in this release" trailer: "Nothing is in flight ...
  merged at 9a819e4" was 53 commits stale. It now names the verification
  commit and the actual in-flight set, and dates the historical record
  below it.

- Close the `make release` guard gap: it grepped seven of the ten files
  scripts/bump-version.sh writes, so a truncated bump could tag with the
  root package.json, typescript/src/client.ts or python/pyproject.toml
  constant stale. All three join the guard. Proven by mutating each file
  and watching `make release` refuse with the new message; restored by
  copy, verified with diff -q.

- Release bodies now say that a change merged without a pull request
  appears only in MIGRATING.md, since generate_release_notes builds from
  merged PRs and structurally cannot list bare commits.

* Absorb the bot round: exact guards, honest scope, the missing Ruby entry

Seven fixes from the Copilot and Codex reviews, all taken:

- The pyproject guard is an exact whole-line match (grep -qxF). The old
  regex left dots unescaped and the end unanchored, so a valid-PEP-440
  "0.15+0" passed it and failed only in the Python release workflow,
  after other SDKs had published -- the exact post-tag failure class
  this PR exists to close.

- typescript/package-lock.json (both SDK-version fields, via jq) and
  ruby/Gemfile.lock join the lockfile guards; bump-version.sh rewrites
  both, and neither was checked. All three new/changed guards proven by
  mutation: each refused with its message and exit 2, restored by copy,
  diff -q clean.

- The trailer no longer claims every count in the guide was measured at
  8fcb39a -- v0.13.0's totals state their own 9a819e4 baseline. The
  claim is scoped to the Unreleased section and the in-flight survey.

- The #735 entry tells the two Swift shapes apart: updateGaugeNeedle was
  callable only as the nil-payload {} that bc3 400s; updateMyPreferences
  was not callable at all (outer requires the unconstructible payload).

- The maxPages entry described sloppy-mode assignment wrong: [[Set]] on
  an inherited getter-only accessor creates no own property -- the
  assignment is silently ignored, not shadowed.

- Three Ruby bare commits (2f21c9d, 3281530, 4785146) were
  consumer-visible -- crashes on mailto:/hostless server-supplied URLs
  became ApiError refusals -- and had entries nowhere. One combined
  entry records the class and the rescue that stops matching.

- The release-body sentence no longer promises MIGRATING.md is a
  complete record of PR-less commits; it states the mechanism (the
  generated notes cannot see them) and points at the guide for
  consumer-visible changes.

* Scope the Ruby entry's no-request claim to the rejected target

Codex round 2: the request that returned the malformed Link or Location
header was necessarily already sent — only the follow-up to the rejected
target is prevented. Saying "before anything is sent" misled anyone
reasoning about hooks or request counts.

* Absorb Codex round 3: getter scope, PATH-spec anchor, test-import claim

- The #773 entry scoped the classification change to the four composites;
  Documents.Get installs markBodyReadFailures itself and Schedules.GetEntry
  delegates to getEntryWithBody, so direct getter callers see it too. The
  entry now names the getters and the composites built on them.

- Both ruby Gemfile.lock guards anchor to the 4-space PATH-spec line with
  grep -qxF; the loose match could be satisfied by the version-bearing
  2-space entry while the PATH spec stayed stale. Proven: mutating only
  the PATH-spec line now refuses with exit 2, and a clean tree passes the
  guard block.

- The #735 entry claimed every Swift test file uses @testable import; the
  generator-only test files import BasecampGenerator plain. Narrowed to
  every test file that imports the SDK module.

* Read the JSON versions as fields, and check both Gemfile.lock records

Codex round 4 and Copilot round 2 converged on the same hole: grep -qF
over package.json matches a "version" string anywhere in the document,
so a stale top-level version passed while any nested metadata field
carried the requested one. Proven literally: a crafted package.json with
top-level 0.13.9 and a nested 0.14.0 satisfied the old grep and is
refused by the new jq field read. typescript/package.json gets the same
treatment -- same shape, same class.

Copilot also wanted both version-bearing Gemfile.lock records checked,
not just the PATH spec: a lockfile whose CHECKSUMS entry lags the PATH
spec would pass the anchored guard and fail only post-tag. Both files
now check both exact lines; a CHECKSUMS-only staleness is refused with
its own message, proven by mutation with restore-by-copy.

* Absorb the four main merges and Codex round 5

Main moved by four while this PR was in review: #804, #807, #809, #810.
Merged in; what each needed here, verified against the tree:

- #809 and #810 wrote their own Unreleased entries when they merged (#805,
  #806) and carry `breaking` -- nothing to add.
- #807 is CI-internal -- nothing to add.
- #804 carries `breaking` but had no entry: resource-first discovery's
  second hop now rides the address-policed shared client, refusing
  special-use-space issuers non-retryably and dropping the caller's
  transport for that hop. Entry added beside #806's, with the variadic
  NewDiscoverer compile note and the remedies in policy order.
- The trailer's verification commit moves to fa15fc1 and its in-flight
  list shrinks to what is actually in flight.

Codex round 5's guard finding rides along: the pyproject check now parses
the [project] table (awk section-scoped exact line) instead of matching a
version assignment anywhere in the file. Proven with the literal bypass --
a stale [project].version plus an exact assignment in another table passed
the old whole-file grep and is refused now; restore by copy, diff -q clean.

* Guard the heading promotion, and fix two entry misstatements

Copilot round 3 caught the release procedure's last unguarded step: the
"# Unreleased" -> "# v(VERSION)" promotion was a hand edit nothing
enforced, so a tag could ship with its notes still filed as unreleased.
scripts/promote-migrating.sh now does the rewrite (exact-line, idempotent,
refusing the both-headings and neither-heading states), bump-version.sh
calls it as step 11, and make release guards both directions: the
promoted heading must exist and "# Unreleased" must not. Proven: release
refuses on today's tree; the script promotes a scratch copy correctly,
is idempotent, and errors on both degenerate states.

Codex round 6's two rides along:

- The TS client guard is an exact whole-line match including the
  semicolon, so a comment carrying the assignment text cannot satisfy it
  while the real constant lags.

- The #804 entry's remedy list dropped the address-class split in
  transcription: AllowLoopback re-admits loopback and nothing else, and
  Allow does not pierce the IANA tables -- for RFC 1918 the policy must
  be built without them, which is the implementation's own documented
  spelling. The entry now says so, and the #735 entry stops claiming
  UpdateGaugeNeedleRequest has a required member (its member is optional;
  the outer init exists because request models always got one).

* Watch promote-migrating.sh with the gate that watches its caller

Copilot: delegating the promotion to a new script moved release-bump
behavior out of the sensitive-change gate's sight -- bump-version.sh is
listed in extra-patterns and the new script was not. It is now.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking Breaking change to public API go

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Discovered OAuth endpoints are not address-policed: a public issuer can steer credential POSTs into private space

2 participants