Files
Hanzo Dev 323a68367d
Hanzo CI/CD / cicd (push) Successful in 21s
CI/CD / gate (push) Successful in 22s
CI/CD / containment (push) Successful in 1m46s
CI/CD / image (push) Skipped
CI/CD / rollout (push) Skipped
CI/CD / reach (push) Skipped
CI/CD / fanout (push) Skipped
CI/CD / receipt (push) Skipped
one reader for the IAM address, and the peer beats the public URL
iamurl.go opens by saying the IAM-address policy "existed as three inlined
copies ... so there is exactly one now". There were four, and they had grown
three different fallbacks:

  iamurl.go            IAM_URL, else the public issuer   (the policy)
  auth_apikey.go       IAM_URL, else IAM_INTERNAL_URL, else NOTHING
  apps/account/iam.go  IAM_URL, else a hardcoded cluster address
  apps/platform        re-read IAM_URL to answer a different question

A copy of a policy does not disagree until one is edited. These had already
diverged: auth_apikey read a fifth env name no cloud deployment sets
(IAM_INTERNAL_URL is on admin-guard and chat only), so a single-process deploy
that knew only its issuer resolved "" and API-key auth stayed silently
unconfigured; apps/account reached a cluster address that a non-cluster deploy
does not have.

iamurl.go now answers both questions the estate actually asks, over ONE env
read: IAMBase() for the address, IAMExternal() for whether a separate IAM is
named. They are separate because conflating them IS the bug — a deployment
with only a public issuer resolves a real address while naming no external
IAM, and a caller inferring one from the other reaches for a store that is
not there. TestIAMAddressHasOneReader walks the tree and fails on a second
reader, so this cannot drift back.

Also: pickCompletionsClient preferred the PUBLIC gateway over the in-cluster
peer. `ai` is a plugin of this same binary running as its own process, so that
ordering sent the pod out through Cloudflare and back, minting an OAuth token
to authenticate to its own deployment, to reach code one socket away. The peer
is now first. Ordered, not gated: nothing sets CLOUD_AI_ZAP_ADDR today, so
this is inert until an address is named and the gateway keeps answering.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-03 20:53:13 -07:00

60 lines
2.9 KiB
Go

package cloud
import (
"os"
"strings"
)
// IAMBaseURL resolves the IAM base URL a SERVER-SIDE caller inside the cluster
// should use — the split-horizon policy, stated once.
//
// The public issuer host (e.g. https://hanzo.id) is fronted by Cloudflare, which
// 403s a server-side loopback POST with edge error 1006 — so an in-cluster
// exchange against the public issuer fails and whatever depended on it (the KMS
// login broker, AI M2M minting, per-tenant identity provisioning) silently stays
// down (root-caused 2026-07-04: in-cluster POST to https://hanzo.id/... → 403,
// while http://iam.hanzo.svc/... → 200).
//
// Precedence: the in-cluster IAM service base (IAM_URL — already wired for
// JWKS), then the public issuer as a last resort (single-process / no
// split-horizon deploys). Returns "" only when no IAM is resolvable at all.
// This existed as three inlined copies (ai M2M, the KMS login broker, and the
// per-tenant identity provisioner would have been the fourth); a copy of a
// policy does not disagree until one is edited, so there is exactly one now.
// Endpoint-specific overrides (CLOUD_AI_IAM_TOKEN_URL, CLOUD_KMS_IAM_TOKEN_URL)
// stay with their endpoints — they override a URL, not this policy.
func IAMBaseURL(publicIssuer string) string {
if base := iamService(); base != "" {
return base
}
return strings.TrimRight(strings.TrimSpace(publicIssuer), "/")
}
// IAMIssuer is the PUBLIC identity host this deployment presents (hanzo.id).
// One name for one fact: the issuer stamped into a token and the issuer a
// validator checks are the same string, so they are read in one place.
func IAMIssuer() string { return strings.TrimSpace(os.Getenv("CLOUD_IAM_ISSUER")) }
// IAMBase is IAMBaseURL against this deployment's OWN issuer — what a caller
// with no Config value in hand uses. It exists so "which IAM do I call" has one
// answer whether or not the caller happens to be holding a Config: three callers
// resolved it themselves and grew three different fallbacks (the public issuer,
// nothing at all, and a hardcoded cluster address), which is the disagreement
// this file was written to prevent and did not.
func IAMBase() string { return IAMBaseURL(IAMIssuer()) }
// IAMExternal reports whether this deployment NAMES a separate IAM, as opposed
// to being the IAM itself (the embedded subsystem).
//
// It is a different question from IAMBaseURL — "is there another IAM" versus
// "what is its address" — and it is here because answering it separately is how
// the two disagree: a deployment with only a public issuer resolves a non-empty
// base yet names no external IAM, and a caller that inferred one from the other
// reached for a store that is not there.
func IAMExternal() bool { return iamService() != "" }
// iamService is the in-cluster IAM address, the ONE env read behind all three.
func iamService() string {
return strings.TrimRight(strings.TrimSpace(os.Getenv("IAM_URL")), "/")
}