carlos / platform Public

instances: -kind reaches the door the record shape already had

handleAPIInstanceCreate has read a kind since the route-records design, and
its own comment anticipates the static case — a "kind":"static" body cannot
reach for a hostname outside the app's own domain. The CLI never sent one,
so every record it wrote was an instance.

The console-side test is a PIN, not a red-green cycle: kind:static at an
app's own apex already worked. What it pins is the pair — kind AND an
explicit channel — because the mint depends on both and an omitted channel
falls to stable.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev a93326f8281dfdb7550ffdacf9d51a8b7af00889 parent f51392c
5 files changed, +124 −5
  • apimode.go +3 −2
  • apimode_test.go +47 −2
  • instances_cli.go +9 −1
  • instances_e2e_test.go +15 −0
  • internal/console/apiinstances_test.go +50 −0
diff --git a/apimode.go b/apimode.go
index 661fd08a3..a29d61d04 100644
--- a/apimode.go
+++ b/apimode.go
@@ -2164,14 +2164,15 @@ type instanceProbeReq struct {
// upstream (kind defaults to "instance", which needs no addr at all), and
// probe nil means unprobed — the same "absent, not zero" contract the wire
// carries for every optional field on this door.
-func (s *apiSession) instanceCreate(ctx context.Context, a apiAcct, app, host, region, channel string, addrs []string, probe *instanceProbeReq) (apiInstanceRecord, error) {
+func (s *apiSession) instanceCreate(ctx context.Context, a apiAcct, app, host, region, channel, kind string, addrs []string, probe *instanceProbeReq) (apiInstanceRecord, error) {
in := struct {
Host string `json:"host"`
Region string `json:"region,omitempty"`
Channel string `json:"channel,omitempty"`
+ Kind string `json:"kind,omitempty"`
Addrs []string `json:"addrs,omitempty"`
Probe *instanceProbeReq `json:"probe,omitempty"`
- }{host, region, channel, addrs, probe}
+ }{host, region, channel, kind, addrs, probe}
status, body, err := s.postJSON(ctx, apiPath(a, app, "/instances"), in)
if err != nil {
return apiInstanceRecord{}, err
diff --git a/apimode_test.go b/apimode_test.go
index 941e12fd9..a8eea9d3f 100644
--- a/apimode_test.go
+++ b/apimode_test.go
@@ -771,7 +771,7 @@ func TestInstanceCreateSendsAddrsAndProbe(t *testing.T) {
s := newTestSession(ts.URL)
rec, err := s.instanceCreate(context.Background(), apiAcct{Seg: "bah"}, "app",
- "a.example.com", "", "", []string{"10.0.0.1:8090", "10.0.0.2:8090"},
+ "a.example.com", "", "", "", []string{"10.0.0.1:8090", "10.0.0.2:8090"},
&instanceProbeReq{Path: "/healthz", Status: 200})
if err != nil {
t.Fatalf("instanceCreate: %v", err)
@@ -799,7 +799,7 @@ func TestInstanceCreateSendsAddrsAndProbe(t *testing.T) {
// No addrs/probe given at all: neither key crosses the wire.
gotBody = ""
if _, err := s.instanceCreate(context.Background(), apiAcct{Seg: "bah"}, "app",
- "b.example.com", "", "", nil, nil); err != nil {
+ "b.example.com", "", "", "", nil, nil); err != nil {
t.Fatalf("instanceCreate (no addrs/probe): %v", err)
}
if strings.Contains(gotBody, "addrs") || strings.Contains(gotBody, "probe") {
@@ -3733,3 +3733,48 @@ func TestResolvedAccountExplicitFlagsBypass(t *testing.T) {
t.Fatalf("explicit flags must never consult the endpoint; hits = %d", *hits)
}
}
+
+// TestAPISessionInstanceCreateSendsKind pins the field the static mint
+// depends on: the door has read a "kind" since the route-records design, but
+// this client never sent one, so every record it wrote was an instance.
+// Empty stays absent from the wire — the door's own "empty means instance"
+// contract, not a value this client invents.
+func TestAPISessionInstanceCreateSendsKind(t *testing.T) {
+ for _, tc := range []struct {
+ name string
+ kind string
+ wantKind any
+ }{
+ {"static", "static", "static"},
+ {"empty is omitted", "", nil},
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ var got map[string]any
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if err := json.NewDecoder(r.Body).Decode(&got); err != nil {
+ t.Errorf("decode body: %v", err)
+ }
+ w.WriteHeader(http.StatusCreated)
+ io.WriteString(w, `{"host":"mysite.bab.oncarlos.test","kind":"static","channel":"edge"}`)
+ }))
+ defer ts.Close()
+
+ rec, err := newTestSession(ts.URL).instanceCreate(context.Background(),
+ apiAcct{Seg: "bab"}, "mysite", "mysite.bab.oncarlos.test", "", "edge", tc.kind, nil, nil)
+ if err != nil {
+ t.Fatalf("instanceCreate: %v", err)
+ }
+ if got["kind"] != tc.wantKind {
+ t.Fatalf("wire kind = %#v, want %#v", got["kind"], tc.wantKind)
+ }
+ // The channel is the rail: an omitted one is defaulted to stable
+ // by the door, and no door can change a record's channel after.
+ if got["channel"] != "edge" {
+ t.Fatalf("wire channel = %#v, want an explicit %q", got["channel"], "edge")
+ }
+ if rec.Kind != "static" {
+ t.Fatalf("decoded Kind = %q", rec.Kind)
+ }
+ })
+ }
+}
diff --git a/instances_cli.go b/instances_cli.go
index 951f87137..5e0f145e3 100644
--- a/instances_cli.go
+++ b/instances_cli.go
@@ -203,6 +203,7 @@ func instancesEnable(ctx context.Context, s release.Store, acct, app string, m p
}
const instancesRecordsUsage = `usage: carlos instances create -app <a> -host <h> [-region <r>] [-channel <c>]
+ [-kind instance|service|static]
[-addr <a:p,...>] [-health-path <p>] [-health-status <n>]
carlos instances list -app <a> [-match <substr>] [-health <slug>]
carlos instances delete -app <a> -host <h>
@@ -219,6 +220,11 @@ box's reconcile pass materializes the route. Common flags:
create-only flags:
+ -kind <k> route class: instance (default) | service | static.
+ A static record serves a file tree straight off
+ its channel pointer — no process, no upstream —
+ which is what "carlos deploy -kind static" mints
+ for an app's platform host
-addr <a:p,...> comma-separated TCP upstream set, host:port each
(e.g. 10.0.0.1:8090,10.0.0.2:8090); omitted means
the platform's own default upstream
@@ -282,6 +288,7 @@ func runInstancesCreate(args []string) {
host := fs.String("host", "", "instance host — one DNS label + \".\" + the app's instance domain")
region := fs.String("region", "", "region code for the record (subject to the manifest's allow-list)")
channel := fs.String("channel", "", "release channel the instance follows (default: the manifest's, then stable)")
+ kind := fs.String("kind", "", "route class: instance (default) | service | static — a static record serves a file tree off the channel pointer and has no process")
addr := fs.String("addr", "", "comma-separated TCP upstream set, host:port each (e.g. 10.0.0.1:8090,10.0.0.2:8090); omitted means the platform's own default upstream")
healthPath := fs.String("health-path", "", "states the whole probe alongside -health-status: request path (default \"/\"); honoured for a single -addr too")
healthStatus := fs.Int("health-status", 0, "probe's expected response status (default 200); fields neither flag names take the platform's own defaults")
@@ -301,7 +308,8 @@ func runInstancesCreate(args []string) {
acct := resolvedAccount(sess, acctRaw, acctIDRaw)
app2 := resolvedApp(sess, acct, *app, instancesRecordsUsage)
rec, err := sess.instanceCreate(context.Background(), acct, app2,
- strings.TrimSpace(*host), strings.TrimSpace(*region), strings.TrimSpace(*channel), addrs, probe)
+ strings.TrimSpace(*host), strings.TrimSpace(*region), strings.TrimSpace(*channel),
+ strings.TrimSpace(*kind), addrs, probe)
if err != nil {
log.Fatalf("instances create: %v", err)
}
diff --git a/instances_e2e_test.go b/instances_e2e_test.go
index 12968ff74..19edb52a0 100644
--- a/instances_e2e_test.go
+++ b/instances_e2e_test.go
@@ -119,6 +119,21 @@ func TestE2EInstancesThroughConsole(t *testing.T) {
t.Fatalf("record = %+v, want beta/titogo created by %s", rec, addr)
}
+ // --- create -kind static: the flag reaches the record ------------------
+ // The shape `carlos deploy` mints for a static app's platform host. Here
+ // only to prove the FLAG is wired to the wire field the door has always
+ // read; the door's own acceptance of it is pinned console-side.
+ staticHost := "site." + domain
+ buf.Reset()
+ runInstancesCreate([]string{"-app", "titogo", "-account", "ops", "-host", staticHost, "-kind", "static"})
+ staticRec, err := provision.GetRecord(context.Background(), relStore, release.OpsAccount, "titogo", staticHost)
+ if err != nil {
+ t.Fatalf("record after create -kind static: %v", err)
+ }
+ if staticRec.Kind != "static" {
+ t.Fatalf("record = %+v, want kind static", staticRec)
+ }
+
// --- list ----------------------------------------------------------------
buf.Reset()
runInstancesList([]string{"-app", "titogo", "-account", "ops"})
diff --git a/internal/console/apiinstances_test.go b/internal/console/apiinstances_test.go
index 8da4b3b74..259ee6431 100644
--- a/internal/console/apiinstances_test.go
+++ b/internal/console/apiinstances_test.go
@@ -1978,3 +1978,53 @@ func TestAPIInstanceStoreFailure(t *testing.T) {
t.Fatalf("delete refusal leaks the store's error text: %s", out)
}
}
+
+// TestAPIInstanceCreateStaticAtOwnApex pins the shape `carlos deploy -kind
+// static` mints for an app with no route: a kind:static record at the app's
+// OWN platform apex, carrying an explicit channel.
+//
+// The channel is the half worth a test of its own. An omitted channel is
+// defaulted to the manifest's, then to "stable" — and a fresh app's pipeline
+// entry is "edge", so a mint that let the default stand would write a route
+// that follows a channel nothing ever promotes to, with no door able to
+// change it afterwards (the hello.bdn incident, 2026-08-22).
+func TestAPIInstanceCreateStaticAtOwnApex(t *testing.T) {
+ fx, member := instanceFixture(t, "code-inst-static-apex")
+ fx.srv.cfg.AppsDomain = "oncarlos.test"
+ if status, _, out := rawReq(t, http.MethodPut, enableURL(fx, "bab", "titogo"), member, `{}`); status != http.StatusOK {
+ t.Fatalf("enable = %d %s", status, out)
+ }
+ host := "titogo." + fx.sqid + ".oncarlos.test"
+
+ status, _, out := rawReq(t, http.MethodPost, instancesURL(fx, "bab", "titogo"), member,
+ `{"host":"`+host+`","kind":"static","channel":"edge"}`)
+ if status != http.StatusCreated {
+ t.Fatalf("static apex create = %d %s, want 201", status, out)
+ }
+ var rec provision.Record
+ if err := json.Unmarshal([]byte(out), &rec); err != nil {
+ t.Fatalf("create body %s: %v", out, err)
+ }
+ if rec.Kind != "static" {
+ t.Fatalf("echoed Kind = %q, want static", rec.Kind)
+ }
+ if rec.Channel != "edge" {
+ t.Fatalf("echoed Channel = %q, want the requested edge — an omitted channel falls to stable", rec.Channel)
+ }
+
+ // The bucket copy is what the reconcile pass mints the route from, so it
+ // is the one that has to carry both.
+ stored, err := provision.GetRecord(context.Background(), fx.rel, fx.sqid, "titogo", host)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if stored.Kind != "static" || stored.Channel != "edge" {
+ t.Fatalf("stored record = %+v, want kind static on channel edge", stored)
+ }
+ // A static record with no upstream and no unit is the shape ValidateShape
+ // requires; anything the door let through that carried one would wedge
+ // the host.
+ if len(stored.Addrs) != 0 || stored.Unit != "" {
+ t.Fatalf("stored record = %+v, want no upstream and no unit", stored)
+ }
+}