carlos / platform Public

appconfig: additive exec_delivery manifest field, issue #18's opt-in

Manifest.ExecDelivery ("" | "file") is the app-level opt-in a TCP-addr
exec route's app uses to ask SyncExec for the same /data/<host>.config.env
delivery a socket-exec route already gets. Schema stays 1 — the field is
additive, so an older binary must still parse a manifest a newer one
wrote; ValidExecDelivery is the single source of truth for "known value"
that both the set-time refusal and (in a following commit) SyncExec's
sync-time ignore-with-a-log share.

SetExecDelivery writes/clears it via the same CAS read-modify-write path
as SetEnv/SetSecret/Unset, but — unlike apply's applyAt generalization —
has no environment-scoped variant at all: it always targets Key(acct,
app), never EnvironmentKey(...). A named environment's manifest is never
a valid place for this field (the reader is the app's own supervisor, one
program per app, not per-environment — the "config-that-lies" failure
mode the issue's design comment calls out).
Paul Campbell d0876a095a683102ad2ff50265801dc837657d1d parent b1bb191
3 files changed, +177 −0
  • internal/appconfig/appconfig.go +39 −0
  • internal/appconfig/store.go +49 −0
  • internal/appconfig/store_test.go +89 −0
diff --git a/internal/appconfig/appconfig.go b/internal/appconfig/appconfig.go
index ca4b4e729..846396278 100644
--- a/internal/appconfig/appconfig.go
+++ b/internal/appconfig/appconfig.go
@@ -22,6 +22,45 @@ type Manifest struct {
Schema int `json:"schema"` // 1
App string `json:"app"`
Vars map[string]Var `json:"vars"`
+ // ExecDelivery is the app's opt-in for issue #18's third SyncExec
+ // eligibility case: a TCP-addr exec route (Backing == "", Socket == "",
+ // Addr != "") whose process carlos does not spawn or own — an
+ // app-supervised instance family (titogo's router fleet is the
+ // motivating case). "" (the zero value, omitted from JSON) means not
+ // opted in; ExecDeliveryFile ("file") means SyncExec writes this app's
+ // routes' merged config to <dataDir>/<host>.config.env exactly as it
+ // does for socket-exec routes — but NEVER bounces them, since carlos
+ // does not own the process (see syncexec.go).
+ //
+ // Additive to schema 1 (schema is not bumped for this field), so any
+ // other value is refused at SET time (SetExecDelivery, ValidExecDelivery)
+ // and, since an older binary must still be able to parse a manifest a
+ // newer one wrote, ignored — loudly, once per pass — at SYNC time
+ // (see SyncExec's checkOptedIn) rather than causing a hard failure.
+ //
+ // APP-LEVEL ONLY: read from the default bundle (config.json) alone.
+ // SetExecDelivery has no environment-scoped variant, unlike apply's
+ // applyAt generalization — a named environment's manifest is never a
+ // valid place for this field, and LoadMerged's merged result never
+ // carries it forward. The reader is the app's own supervisor, one
+ // program per app, not per-route or per-environment: per-route or
+ // per-environment opt-in would let half a fleet read config the other
+ // half silently ignores, the "config-that-lies" failure mode (issue
+ // #18 design comment, 2026-08-03).
+ ExecDelivery string `json:"exec_delivery,omitempty"`
+}
+
+// ExecDeliveryFile is the only non-empty value ValidExecDelivery accepts
+// today for Manifest.ExecDelivery — see its doc comment.
+const ExecDeliveryFile = "file"
+
+// ValidExecDelivery reports whether v is a value this binary understands
+// for Manifest.ExecDelivery: "" (not opted in) or ExecDeliveryFile. Shared
+// by the set-time refusal (SetExecDelivery, appconfig_cli.go's
+// runEnvExecDelivery) and SyncExec's sync-time ignore-with-a-log, so the
+// two can never define "valid" differently.
+func ValidExecDelivery(v string) bool {
+ return v == "" || v == ExecDeliveryFile
}
// Var is one config var or secret. Kind selects which of Value/Envelope is
diff --git a/internal/appconfig/store.go b/internal/appconfig/store.go
index 33883ec37..19d23a570 100644
--- a/internal/appconfig/store.go
+++ b/internal/appconfig/store.go
@@ -214,3 +214,52 @@ func SetSecret(ctx context.Context, s release.Store, acct, app string, sealed ma
func Unset(ctx context.Context, s release.Store, acct, app string, keys []string) (int, error) {
return apply(ctx, s, acct, app, DefaultBundle, nil, keys)
}
+
+// SetExecDelivery writes (`file`) or clears (`""`) the default bundle's
+// exec_delivery field — issue #18's TCP-addr exec route opt-in (see
+// Manifest.ExecDelivery) — via the same CAS read-modify-write path
+// SetEnv/SetSecret/Unset use. value must be ValidExecDelivery; anything
+// else is refused here, before it ever reaches the bucket, mirroring
+// applyAt's var-name/var-value validation.
+//
+// APP-LEVEL ONLY, structurally: unlike apply's applyAt generalization,
+// there is no environment-scoped variant of this function — it always
+// writes Key(acct, app), never EnvironmentKey(acct, app, name). A named
+// environment's manifest is never a valid place for this field (Manifest.
+// ExecDelivery's doc comment has the "config-that-lies" rationale); the
+// CLI's -environment flag is refused before it can reach here at all
+// (appconfig_cli.go's runEnvExecDelivery).
+//
+// Turning the field off (value == "") against an app with no default
+// bundle at all is refused, not silently turned into a create — the same
+// "unset-only against a nonexistent object" refusal applyAt documents, for
+// the same reason: minting an empty bundle just to hold an absent flag
+// would satisfy `carlos route -environment`'s existence rail and Sync's
+// "configured, then fully unset" state for nothing the operator asked for.
+func SetExecDelivery(ctx context.Context, s release.Store, acct, app, value string) error {
+ if !release.ValidAccount(acct) {
+ return fmt.Errorf("config: invalid account %q", acct)
+ }
+ if !release.ValidApp(app) {
+ return fmt.Errorf("config: invalid app %q", app)
+ }
+ if !ValidExecDelivery(value) {
+ return fmt.Errorf("config: invalid exec_delivery %q — want %q or \"\" (off)", value, ExecDeliveryFile)
+ }
+ key := Key(acct, app)
+ m, etag, found, err := getForWriteAt(ctx, s, key, app)
+ if err != nil {
+ return err
+ }
+ if !found && value == "" {
+ return fmt.Errorf("config: %s has no default bundle — nothing to turn off", app)
+ }
+ m.ExecDelivery = value
+ m.Schema, m.App = 1, app
+ raw, err := json.MarshalIndent(m, "", " ")
+ if err != nil {
+ return err
+ }
+ _, err = s.Put(ctx, key, raw, etag)
+ return err
+}
diff --git a/internal/appconfig/store_test.go b/internal/appconfig/store_test.go
index 186af2492..629911dc1 100644
--- a/internal/appconfig/store_test.go
+++ b/internal/appconfig/store_test.go
@@ -195,6 +195,92 @@ func TestSetEnvRejectsUnrepresentableValue(t *testing.T) {
}
}
+func TestSetExecDeliveryRoundTrips(t *testing.T) {
+ s := release.NewDirStore(t.TempDir())
+ ctx := context.Background()
+ if err := SetEnv(ctx, s, "bab", "titogo", map[string]string{"K": "1"}); err != nil {
+ t.Fatalf("SetEnv: %v", err)
+ }
+ if err := SetExecDelivery(ctx, s, "bab", "titogo", ExecDeliveryFile); err != nil {
+ t.Fatalf("SetExecDelivery(file): %v", err)
+ }
+ m, err := Get(ctx, s, "bab", "titogo")
+ if err != nil {
+ t.Fatalf("Get: %v", err)
+ }
+ if m.ExecDelivery != ExecDeliveryFile {
+ t.Fatalf("ExecDelivery = %q, want %q", m.ExecDelivery, ExecDeliveryFile)
+ }
+ // The var set earlier survives — SetExecDelivery is a field write, not
+ // a bundle replace.
+ if m.Vars["K"].Value != "1" {
+ t.Fatalf("Vars[K] = %+v, want value 1 (SetExecDelivery must not clobber existing vars)", m.Vars["K"])
+ }
+ if err := SetExecDelivery(ctx, s, "bab", "titogo", ""); err != nil {
+ t.Fatalf("SetExecDelivery(off): %v", err)
+ }
+ m, err = Get(ctx, s, "bab", "titogo")
+ if err != nil {
+ t.Fatalf("Get: %v", err)
+ }
+ if m.ExecDelivery != "" {
+ t.Fatalf("ExecDelivery after off = %q, want empty", m.ExecDelivery)
+ }
+}
+
+func TestSetExecDeliveryRejectsUnknownValue(t *testing.T) {
+ s := release.NewDirStore(t.TempDir())
+ ctx := context.Background()
+ if err := SetEnv(ctx, s, "bab", "titogo", map[string]string{"K": "1"}); err != nil {
+ t.Fatalf("SetEnv: %v", err)
+ }
+ for _, bad := range []string{"grpc", "FILE", "true", "yes"} {
+ if err := SetExecDelivery(ctx, s, "bab", "titogo", bad); err == nil {
+ t.Fatalf("SetExecDelivery(%q) succeeded, want a refusal", bad)
+ }
+ }
+ // The refusal must not have touched the bucket object at all.
+ m, err := Get(ctx, s, "bab", "titogo")
+ if err != nil {
+ t.Fatalf("Get: %v", err)
+ }
+ if m.ExecDelivery != "" {
+ t.Fatalf("ExecDelivery = %q after a refused set, want untouched (empty)", m.ExecDelivery)
+ }
+}
+
+func TestSetExecDeliveryOffOnNeverConfiguredAppIsRefused(t *testing.T) {
+ // Mirrors TestUnsetOnNeverConfiguredAppErrorsAndWritesNothing: turning
+ // the field off must not mint an empty default bundle just to hold an
+ // absent flag.
+ s := release.NewDirStore(t.TempDir())
+ ctx := context.Background()
+ if err := SetExecDelivery(ctx, s, "bab", "titogo", ""); err == nil {
+ t.Fatal("SetExecDelivery(off) on a never-configured app succeeded, want a refusal")
+ }
+ if _, found, gerr := getIfExists(ctx, s, "bab", "titogo"); gerr != nil || found {
+ t.Fatalf("SetExecDelivery(off) created a manifest object (found = %v, err = %v)", found, gerr)
+ }
+}
+
+func TestSetExecDeliveryOnNeverConfiguredAppCreatesTheBundle(t *testing.T) {
+ // The "file" direction DOES create a bundle on first use — same as
+ // SetEnv/SetSecret already do — since "file" is genuine content to
+ // store, not an unset-only call against nothing.
+ s := release.NewDirStore(t.TempDir())
+ ctx := context.Background()
+ if err := SetExecDelivery(ctx, s, "bab", "titogo", ExecDeliveryFile); err != nil {
+ t.Fatalf("SetExecDelivery(file): %v", err)
+ }
+ m, err := Get(ctx, s, "bab", "titogo")
+ if err != nil {
+ t.Fatalf("Get: %v", err)
+ }
+ if m.ExecDelivery != ExecDeliveryFile {
+ t.Fatalf("ExecDelivery = %q, want %q", m.ExecDelivery, ExecDeliveryFile)
+ }
+}
+
func TestNewerSchemaIsRefusedNotDowngraded(t *testing.T) {
// json.Unmarshal drops fields this binary doesn't know; acting on a
// newer manifest would silently discard them — and a write-back would
@@ -211,6 +297,9 @@ func TestNewerSchemaIsRefusedNotDowngraded(t *testing.T) {
if err := SetEnv(ctx, s, "bab", "keymail", map[string]string{"A": "1"}); err == nil {
t.Fatal("SetEnv wrote over a schema-2 manifest, want a refusal")
}
+ if err := SetExecDelivery(ctx, s, "bab", "keymail", ExecDeliveryFile); err == nil {
+ t.Fatal("SetExecDelivery wrote over a schema-2 manifest, want a refusal")
+ }
got, _, err := s.Get(ctx, Key("bab", "keymail"))
if err != nil {
t.Fatalf("Get raw: %v", err)