the app set is a property of the binary, not of a values file
Hanzo CI/CD / cicd (push) Canceled after 0s
CI/CD / gate (push) Canceled after 0s
CI/CD / containment (push) Canceled after 0s
CI/CD / image (push) Canceled after 0s
CI/CD / rollout (push) Canceled after 0s
CI/CD / reach (push) Canceled after 0s
CI/CD / fanout (push) Canceled after 0s
CI/CD / receipt (push) Canceled after 0s
Hanzo CI/CD / cicd (push) Canceled after 0s
CI/CD / gate (push) Canceled after 0s
CI/CD / containment (push) Canceled after 0s
CI/CD / image (push) Canceled after 0s
CI/CD / rollout (push) Canceled after 0s
CI/CD / reach (push) Canceled after 0s
CI/CD / fanout (push) Canceled after 0s
CI/CD / receipt (push) Canceled after 0s
CLOUD_ENABLE / --enable named the subsystems to mount. The binary already knows: manifest.Apps is the host's set, and a plugin IS its app (Listen sets cfg.Enable from what plugin/<app>/main.go was built as — that stays; it is the binary stating itself, not a deployment restating it). A second source of truth can only add disagreement, and it did. Both devnet outages on 2026-08-02 were this list: one named "plans", an app the manifest does not have, and one omitted "kms", the credential broker every other child pulls its data-plane key from — so every child failed closed at its first store open. Production has never set it. Removing the input removes the failure class and five branches with it: the broker precondition, the unknown-name guard, and three `on != nil` selections that only existed to police a list nobody should have been writing. Empty already meant all, which is what production runs. Values move in the same change — devnet and testnet drop the list, so no deployment is left naming a variable the binary no longer reads. Docs and cloud-probe.sh follow; the probe had been STRIPPING the variable, so it already agreed. TestTheAppSetIsNotNamedTwice replaces TestAnAllowlistWithoutTheBrokerIsRefused — that test policed the list, and the guard is now that no source states the set again. Also fixes a pre-existing red on main, unrelated to this: apps/catalog's cloud.Request call site was never added to allowedRequestUses, so the escape hatch ratchet failed on clean origin/main. It is a legitimate use (the published corpus reads as PublicOrg for everyone, so the tenant is re-pointed while the caller's authority travels whole) and is now recorded with that reason. Co-authored-by: Hanzo Dev <dev@hanzo.ai>
This commit is contained in:
+14
-56
@@ -37,7 +37,6 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
@@ -61,8 +60,6 @@ func main() {
|
||||
|
||||
listen := flag.String("listen", getenv("CLOUD_LISTEN", ":8080"), "HTTP listen address")
|
||||
zapAddr := flag.String("zap", getenv("CLOUD_ZAP_LISTEN", ":9653"), "ZAP-RPC listen address")
|
||||
enable := flag.String("enable", os.Getenv("CLOUD_ENABLE"), "comma-separated subsystems to mount; empty mounts all")
|
||||
|
||||
// The operator flags helm and universe pass to the ENTRYPOINT. The host
|
||||
// consumes none of them itself — it is a router; it opens no store and
|
||||
// validates no token — but the per-app CHILDREN read them from the environment
|
||||
@@ -83,7 +80,7 @@ func main() {
|
||||
"CLOUD_IAM_ISSUER": *iamIssuer,
|
||||
})
|
||||
|
||||
if err := run(*listen, *zapAddr, *enable); err != nil {
|
||||
if err := run(*listen, *zapAddr); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "cloud:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -102,7 +99,7 @@ func forward(kv map[string]string) {
|
||||
}
|
||||
}
|
||||
|
||||
func run(addr, zapAddr, enable string) error {
|
||||
func run(addr, zapAddr string) error {
|
||||
// THE FLEET'S ONE AGENT DOOR is served BY THIS HOST, at POST /v1/mcp, and
|
||||
// zip's is switched off so that exactly one handler holds the address.
|
||||
//
|
||||
@@ -126,7 +123,6 @@ func run(addr, zapAddr, enable string) error {
|
||||
// host's own environment — both BEFORE the first Load spawns an eager child.
|
||||
secret, rootKey := stampAndScrub()
|
||||
|
||||
on := enabled(enable)
|
||||
// absent is what this host tried to mount and could not: name → why. Written
|
||||
// only by the loops below, read only by the health route registered after
|
||||
// them, so it is frozen before anything serves and needs no lock.
|
||||
@@ -138,29 +134,20 @@ func run(addr, zapAddr, enable string) error {
|
||||
// because the broker-first split would put the fleet's document in an order
|
||||
// that is not the fleet's.
|
||||
//
|
||||
// It is the enable list applied, coresident apps included: an app that mounts
|
||||
// as middleware on a sibling's router still SERVES its routes, so it belongs in
|
||||
// the document even though the host claims no prefix for it.
|
||||
// Coresident apps included: an app that mounts as middleware on a sibling's
|
||||
// router still SERVES its routes, so it belongs in the document even though
|
||||
// the host claims no prefix for it.
|
||||
composed := make([]string, 0, len(manifest.Apps))
|
||||
for _, a := range manifest.Apps {
|
||||
if on == nil || on[a.Name] {
|
||||
composed = append(composed, a.Name)
|
||||
}
|
||||
composed = append(composed, a.Name)
|
||||
}
|
||||
|
||||
// The broker is a precondition, not a selection. Every child this host spawns
|
||||
// carries a CREDZ_TOKEN, and credz refuses to fall back to a dev key once a
|
||||
// token is present — so a child that cannot reach the broker resolves Unkeyed
|
||||
// and fails closed at its FIRST store open, in every build. An allowlist that
|
||||
// omits it therefore does not produce a smaller deployment; it produces one
|
||||
// where no child can open a store, and the symptom is every eager child exiting
|
||||
// 1 with "CLOUD_KMS_MASTER_KEY_REF is required" while the loop below silently
|
||||
// mounts no broker at all. Refused for the same reason a name the manifest does
|
||||
// not list is refused: the alternative is a deployment that cannot work and does
|
||||
// not say so.
|
||||
if on != nil && !on[launch.Broker] {
|
||||
return fmt.Errorf("--enable omits %q, the credential broker every other child pulls its data-plane key from; add it or drop --enable", launch.Broker)
|
||||
}
|
||||
// THE MANIFEST IS THE APP SET. The host mounts what it was built with; there
|
||||
// is no second list to disagree with it. The broker below is ordered first
|
||||
// because every other child pulls its data-plane key from it, not because it
|
||||
// was selected — a deployment that omitted it produced children that all
|
||||
// failed at their first store open, which is why naming the set twice was
|
||||
// never a smaller deployment, only a broken one.
|
||||
|
||||
// THE BROKER FIRST, and eagerly. Every other app pulls its data-plane key and
|
||||
// its scoped credentials from it, so an app that starts before it has nothing
|
||||
@@ -174,28 +161,21 @@ func run(addr, zapAddr, enable string) error {
|
||||
// a mount MEANS is one function (mount), so the failure policy cannot drift
|
||||
// between them.
|
||||
for _, a := range manifest.Apps {
|
||||
if a.Name != launch.Broker || (on != nil && !on[a.Name]) {
|
||||
if a.Name != launch.Broker {
|
||||
continue
|
||||
}
|
||||
if err := mount(app, a, true, secret, rootKey, absent); err != nil {
|
||||
return err
|
||||
}
|
||||
delete(on, a.Name)
|
||||
}
|
||||
|
||||
for _, a := range manifest.Apps {
|
||||
if a.Name == launch.Broker || (on != nil && !on[a.Name]) {
|
||||
if a.Name == launch.Broker {
|
||||
continue
|
||||
}
|
||||
if err := mount(app, a, a.Eager, secret, rootKey, absent); err != nil {
|
||||
return err
|
||||
}
|
||||
delete(on, a.Name)
|
||||
}
|
||||
// A name that matched nothing is a typo, and the symptom of tolerating one is
|
||||
// a subsystem that is simply absent from a deployment with no error anywhere.
|
||||
if len(on) > 0 {
|
||||
return fmt.Errorf("--enable names %v, which the manifest does not list — run `make generate` if the app is new, else fix the name", keys(on))
|
||||
}
|
||||
|
||||
// Liveness belongs to the HOST, not to any app: it must answer while every
|
||||
@@ -618,28 +598,6 @@ func childEnv(app, secret, rootKey string) []string {
|
||||
|
||||
// enabled parses the subsystem allowlist. nil means every app, which is the
|
||||
// default and the shape a full deployment runs.
|
||||
func enabled(list string) map[string]bool {
|
||||
list = strings.TrimSpace(list)
|
||||
if list == "" {
|
||||
return nil
|
||||
}
|
||||
on := map[string]bool{}
|
||||
for _, n := range strings.Split(list, ",") {
|
||||
if n = strings.TrimSpace(n); n != "" {
|
||||
on[n] = true
|
||||
}
|
||||
}
|
||||
return on
|
||||
}
|
||||
|
||||
func keys(m map[string]bool) []string {
|
||||
out := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
out = append(out, k)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
func getenv(key, fallback string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
|
||||
+40
-36
@@ -6,10 +6,10 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hanzoai/cloud/credz/launch"
|
||||
"github.com/hanzoai/cloud/manifest"
|
||||
"github.com/hanzoai/cloud/webui"
|
||||
"github.com/zap-proto/zip"
|
||||
@@ -258,44 +258,48 @@ func TestRequiredAbortsByName(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestAnAllowlistWithoutTheBrokerIsRefused pins the OTHER half of the outage —
|
||||
// why the child had no key at all.
|
||||
// THE MANIFEST IS THE APP SET, and nothing states it a second time.
|
||||
//
|
||||
// A child launched by this host always carries a CREDZ_TOKEN, and credz refuses
|
||||
// to fall back to a dev key once a token is present, so a deployment with no
|
||||
// broker is one where EVERY child resolves Unkeyed and dies at its first store
|
||||
// open. Reproduced exactly, with the real binaries:
|
||||
// This replaces a test that policed an --enable allowlist: it had to refuse a
|
||||
// list omitting the credential broker (every child then failed closed at its
|
||||
// first store open) and a list naming an app the manifest does not have. Both
|
||||
// failures were real — they took devnet down twice on 2026-08-02 — and both
|
||||
// were only possible because the app set was written down twice, once in the
|
||||
// binary and once in a values file. Production never set the list at all.
|
||||
//
|
||||
// [pubsub] data-plane encryption posture: no usable key → store opens fail closed
|
||||
// [pubsub] audit: open audit store: … cek: CLOUD_KMS_MASTER_KEY_REF is required
|
||||
// cloud: zip: Add service 0: zip: Load(pubsub): exited before listening: exit status 1
|
||||
//
|
||||
// The host used to mount zero brokers and say nothing, so the only evidence was a
|
||||
// child complaining about an environment variable the host deliberately scrubs.
|
||||
// An allowlist that omits the broker is a misconfiguration, refused for the same
|
||||
// reason a name the manifest does not list is refused.
|
||||
func TestAnAllowlistWithoutTheBrokerIsRefused(t *testing.T) {
|
||||
t.Setenv(launch.RootEnv, devKey)
|
||||
|
||||
// run returns from the guard BEFORE it loads anything or listens, which is the
|
||||
// only reason it is callable from a test at all.
|
||||
err := run(":0", ":0", "pubsub") // no "kms"
|
||||
if err == nil {
|
||||
t.Fatal("an --enable list without the credential broker was accepted: every child would fail closed at its first store open")
|
||||
}
|
||||
if !strings.Contains(err.Error(), launch.Broker) {
|
||||
t.Errorf("refusal = %q, want it to name %q so the fix is obvious", err, launch.Broker)
|
||||
}
|
||||
|
||||
// The negative controls are the two lists that must NOT trip it: the broker
|
||||
// named explicitly, and the empty list production runs (nil = every app, which
|
||||
// includes the broker). Asserted on the guard's own predicate rather than by
|
||||
// calling run, because a run that gets past the guard mounts 112 children and
|
||||
// blocks in Listen.
|
||||
for _, list := range []string{"kms,pubsub", ""} {
|
||||
if on := enabled(list); on != nil && !on[launch.Broker] {
|
||||
t.Errorf("--enable %q would be refused as missing %q", list, launch.Broker)
|
||||
// So the guard is now structural: no source states the set again.
|
||||
func TestTheAppSetIsNotNamedTwice(t *testing.T) {
|
||||
var offenders []string
|
||||
err := filepath.WalkDir("..", func(path string, d os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() {
|
||||
if path != ".." && strings.HasPrefix(d.Name(), ".") {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
|
||||
return nil
|
||||
}
|
||||
b, rerr := os.ReadFile(path)
|
||||
if rerr != nil {
|
||||
return rerr
|
||||
}
|
||||
if strings.Contains(string(b), "CLOUD_ENABLE") || strings.Contains(string(b), `"enable"`) {
|
||||
offenders = append(offenders, path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("walk: %v", err)
|
||||
}
|
||||
if len(offenders) > 0 {
|
||||
t.Errorf("these name the app set a second time: %v\n"+
|
||||
"the binary already knows which apps it was built with (manifest.Apps), and a\n"+
|
||||
"plugin knows which one it IS (Listen sets cfg.Enable) — a deployment must not restate it",
|
||||
offenders)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -461,12 +461,17 @@ func LoadConfig() *Config {
|
||||
ControlPlaneQuorum: getenvInt("CONTROL_PLANE_QUORUM", 0),
|
||||
}
|
||||
|
||||
enableCSV := getenv("CLOUD_ENABLE", "")
|
||||
// Bind the CLI overrides once (see flagsOnce). A later call keeps its env-derived
|
||||
// cfg unchanged — tests set env via t.Setenv, never argv — so guarding the
|
||||
// registration loses nothing while making LoadConfig re-entrant.
|
||||
// THE BINARY KNOWS WHICH APP IT IS; a deployment does not restate it.
|
||||
//
|
||||
// cfg.Enable is set by Listen from the app the plugin was built as — one
|
||||
// process per app, and plugin/<app>/main.go names it. CLOUD_ENABLE/--enable
|
||||
// used to state the same set a SECOND time, from the values file, and the
|
||||
// only thing a second source of truth can add is disagreement: both devnet
|
||||
// outages on 2026-08-02 were this list naming an app that does not exist
|
||||
// ("plans") and omitting one that every other child needs ("kms"). Neither
|
||||
// is representable now. Empty still means all, which is what production has
|
||||
// always run.
|
||||
flagsOnce.Do(func() {
|
||||
flag.StringVar(&enableCSV, "enable", enableCSV, "comma-separated subsystem list (empty=all)")
|
||||
flag.StringVar(&cfg.Brand, "brand", cfg.Brand, "white-label brand")
|
||||
flag.StringVar(&cfg.Domain, "domain", cfg.Domain, "primary domain")
|
||||
flag.StringVar(&cfg.IAMIssuer, "iam-issuer", cfg.IAMIssuer, "JWKS issuer")
|
||||
@@ -476,14 +481,6 @@ func LoadConfig() *Config {
|
||||
flag.Parse()
|
||||
})
|
||||
|
||||
if enableCSV != "" {
|
||||
for _, name := range strings.Split(enableCSV, ",") {
|
||||
if s := strings.TrimSpace(name); s != "" {
|
||||
cfg.Enable = append(cfg.Enable, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// White-label by brand (HIP-0111): when the operator does not pin
|
||||
// CLOUD_IAM_ISSUER / --iam-issuer, derive the canonical OIDC issuer from the
|
||||
// brand so a lux deployment validates against lux.id, zoo against zoo.id,
|
||||
|
||||
@@ -207,6 +207,10 @@ var allowedRequestUses = map[string]string{
|
||||
"TestTheLedgerSelectorStaysOnTheURLForBodyWrites (apps/books/wire_test.go) is that measurement " +
|
||||
"and goes red the day it moves. ONE function, which every body-carrying op asks, reading " +
|
||||
"through the same sandboxQuery the untyped handlers beside them use; LIVE off the HTTP path.",
|
||||
"apps/catalog/catalog.go": "browse — the published corpus is read as PublicOrg by everyone, " +
|
||||
"signed in or not, so the tenant is re-pointed for the index Ask while the caller's authority " +
|
||||
"travels whole. cloud.As needs the request to do that; cloud.For alone drops the caller and the " +
|
||||
"public browse 500s with \"index: no org on the call\".",
|
||||
"apps/books/ask.go": "narrateAsk — the payer for the ONE grounded completion an Ask narrates with. " +
|
||||
"The bill lands on principal.Ledger, the SELECTED billing org, which a SuperAdmin masquerade " +
|
||||
"moves off the effective org — so principal.OrgFrom would charge the org being INSPECTED for a " +
|
||||
|
||||
Reference in New Issue
Block a user