// The `carlos sidecars` verb family (Task 14, design: sidecar members): a // sidecar is a process the platform runs that serves no requests and that // nothing can dial, so its whole shape follows the instance family's // deliberately, minus the fields a sidecar has no use for. See // docs/superpowers/specs/2026-08-17-sidecar-members-design.md §11. // // - `enable` — the app's opt-in to sidecar provisioning. Two modes, // resolved credential-first exactly as instances enable's is // (instances_cli.go): store mode writes apps///sidecars.json // via provision.PutSidecarManifest with every field under the // operator's hand; API mode asks the console, which lets a member's own // args/placement/channel/idle/max-run/hibernate through untouched while // refusing -pool and -regions — those are operator, store-mode-only // policy fields (apisidecars.go's handleAPISidecarEnable). // - `create` / `ls` / `wake` / `rm` — the CLI front over the console's // sidecar record verbs. API mode ONLY: a record is a console-authorized // write, and there is deliberately NO box-side equivalent — a sidecar's // host is derived under the reserved .sidecar.internal suffix, and // `carlos add` refuses to mint one by hand (its reserved-suffix rail). // - `logs` delegates to the existing `carlos logs -host ` path for the // sidecar's own derived host: logs key on host, not on Kind (design // §9.3), so there is no separate sidecar-log machinery to keep in sync. // // Regions: `-regions` on `enable` takes PLAIN REGION CODES ONLY, never a // fleet placement target. A fleet-placed app's sidecars inherit the app's // own placement (set by `carlos apps place`), which overrides whatever the // manifest's Regions allow-list or a create request says; that allow-list is // only ever consulted for an UNPLACED app (Task 12 review finding). Offering // `-regions ie,fleet/eleven` here would be unsatisfiable and doubly // misleading, so this flag's help text points at `carlos apps place` // instead of pretending to accept fleet syntax. package main import ( "context" "flag" "fmt" "log" "os" "strings" "time" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/release" ) const sidecarsEnableUsage = `usage: carlos sidecars enable -app [flags] Signed-in mode (carlos auth login; no CARLOS_DEPLOYMENT_* env): the console writes the manifest under your own membership. -pool and -regions are operator, store-mode flags and are refused here. Store mode (CARLOS_DEPLOYMENT_BUCKET/CARLOS_DEPLOYMENT_DIR set): every field is the operator's call, and this write REPLACES the whole manifest — the same full-overwrite CAS upsert "carlos instances enable" uses, so a field you omit here resets to its own default, not to whatever a previous enable left it at. flags: -app app whose sidecars this enables (required) -args "argv..." argv the activator execs the app's own binary with, whitespace-split via strings.Fields — shell- word semantics (quoting, escaping) are deliberately NOT supported, same contract as -seed on "carlos instances enable" -placement

"any" (default) or "no-inbound" — no-inbound mints only on boxes declaring CARLOS_INBOUND=none (design §8) -pool

pool stamped into every record at create (store mode only; default: "", the shared pool) -channel release channel stamped into every record at create (store-mode default "stable"; signed-in mode: omitted = preserve what the manifest holds) -regions allow-list for a record's region, PLAIN REGION CODES ONLY (store mode only); empty (default) = any shape-valid region. Consulted only for an UNPLACED app — a fleet-placed app's sidecars inherit the app's own placement instead, set with "carlos apps place", which overrides this list (and anything a create request says) entirely. There is no fleet syntax here. -idle idle timeout before a hibernating sidecar sleeps (default 5m; a Go duration — 5m, 1h — sent to the wire as whole seconds, never a duration string) -max-run max run duration before the sweeper stops a sidecar regardless of idle (default 1h; same duration-to-seconds conversion as -idle) -no-hibernate this app's sidecars never hibernate (default: they do) -account account (sqid or name) the app belongs to (default "ops") -account-id account sqid, matched as a sqid ONLY — supersedes -account -console console to enable through (signed-in mode only; default: the only logged-in one) Writing this manifest is the app's opt-in to sidecar provisioning: absence means the feature does not exist for the app, and the API refuses creates until this is set (design §3.1, §11). ` func runSidecarsEnable(args []string) { fs := flag.NewFlagSet("sidecars enable", flag.ExitOnError) app := fs.String("app", "", "app whose sidecars this enables") argsFlag := fs.String("args", "", "argv the activator execs the app's own binary with, whitespace-split (strings.Fields)") placement := fs.String("placement", "", `"any" (default) or "no-inbound"`) pool := fs.String("pool", "", "pool stamped into every record at create (store mode only)") channel := fs.String("channel", "", "release channel stamped into every record at create") regions := fs.String("regions", "", "comma-separated region allow-list, plain region codes only (store mode only); empty = any shape-valid region — see carlos apps place for fleet placement") idle := fs.String("idle", "", "idle timeout before a hibernating sidecar sleeps (default 5m; Go duration)") maxRun := fs.String("max-run", "", "max run duration before the sweeper stops a sidecar (default 1h; Go duration)") noHibernate := fs.Bool("no-hibernate", false, "this app's sidecars never hibernate (default: they do)") consoleFlag := consoleOption(fs, "console to enable through (signed-in mode only; default: the only logged-in one)") acctRaw, acctIDRaw, acctOf := accountFlags(fs) fs.Parse(args) if *app != "" && !release.ValidApp(*app) { log.Fatalf("carlos sidecars enable: %q is not a valid app name", *app) } set := map[string]bool{} fs.Visit(func(f *flag.Flag) { set[f.Name] = true }) sess, err := resolveMode(*consoleFlag) if err != nil { log.Fatal(err) } if sess != nil { // Signed-in mode. -pool/-regions are refused rather than ignored — // a flag that silently did nothing would read as policy applied. for _, f := range []string{"pool", "regions"} { if set[f] { log.Fatalf("carlos sidecars enable: -%s is an operator, store-mode flag. Set CARLOS_DEPLOYMENT_BUCKET/CARLOS_DEPLOYMENT_DIR to enable in store mode instead", f) } } var channelReq *string if set["channel"] { channelReq = channel } var argsReq *[]string if set["args"] { // parseArgv("") is an empty (non-nil after this) argv: an // explicit -args "" clears the manifest's args, matching // -channel's explicit-vs-absent split. argv := parseArgv(*argsFlag) if argv == nil { argv = []string{} } argsReq = &argv } var placementReq *string if set["placement"] { placementReq = placement } var idleReq *int if set["idle"] { secs, err := parseDurationSeconds(*idle) if err != nil { log.Fatalf("carlos sidecars enable: -idle: %v", err) } idleReq = &secs } var maxRunReq *int if set["max-run"] { secs, err := parseDurationSeconds(*maxRun) if err != nil { log.Fatalf("carlos sidecars enable: -max-run: %v", err) } maxRunReq = &secs } var hibernateReq *bool if *noHibernate { no := false hibernateReq = &no } printUsing(stdout, sess) acct := resolvedAccount(sess, acctRaw, acctIDRaw) app2 := resolvedApp(sess, acct, *app, sidecarsEnableUsage) m, err := sess.sidecarsEnable(context.Background(), acct, app2, channelReq, argsReq, placementReq, idleReq, maxRunReq, hibernateReq) if err != nil { log.Fatalf("sidecars enable: %v", err) } fmt.Fprintf(stdout, "%s for %s (channel %s, placement %s)\n", colorFor(stdout).ok("enabled sidecars"), m.App, m.Channel, m.Placement) mutedf(stdout, " first sidecar: carlos sidecars create -app %s ", m.App) return } // Store mode — the operator path: every field this manifest carries is // set fresh on every call (PutSidecarManifest is a full-overwrite CAS // upsert, not a patch), exactly as instances enable's own store-mode // write is. if *app == "" { log.Fatal(sidecarsEnableUsage) } if *channel == "" && !set["channel"] { *channel = "stable" } if !provision.ValidPool(*pool) { log.Fatalf("carlos sidecars enable: %q is not a valid pool", *pool) } if !release.ValidChannel(*channel) { log.Fatalf("carlos sidecars enable: %q is not a valid channel", *channel) } switch *placement { case "", provision.PlacementAny, provision.PlacementNoInbound: default: log.Fatalf(`carlos sidecars enable: -placement %q: want "any" or "no-inbound"`, *placement) } var regionList []string if *regions != "" { for _, r := range strings.Split(*regions, ",") { r = strings.TrimSpace(r) if r == "" || !provision.ValidRegion(r) { log.Fatalf("carlos sidecars enable: %q is not a valid region — -regions takes plain region codes only; for fleet placement use `carlos apps place`", r) } regionList = append(regionList, r) } } var idleSecs int if set["idle"] { var err error idleSecs, err = parseDurationSeconds(*idle) if err != nil { log.Fatalf("carlos sidecars enable: -idle: %v", err) } } var maxRunSecs int if set["max-run"] { var err error maxRunSecs, err = parseDurationSeconds(*maxRun) if err != nil { log.Fatalf("carlos sidecars enable: -max-run: %v", err) } } m := provision.SidecarManifest{ Schema: 1, App: *app, Pool: *pool, Channel: *channel, Placement: *placement, Regions: regionList, Args: parseArgv(*argsFlag), IdleSeconds: idleSecs, MaxRunSeconds: maxRunSecs, } if *noHibernate { no := false m.Hibernate = &no } if err := sidecarsEnable(context.Background(), openStore(), acctOf(), *app, m); err != nil { log.Fatal(err) } fmt.Printf("%s for %s (channel %s)\n", colorFor(os.Stdout).ok("enabled sidecars"), *app, *channel) } // parseArgv splits -args's raw value on whitespace (strings.Fields) into // the manifest's Args argv — instances enable's -seed contract // (instances_cli.go's parseSeed), carried over unchanged: quoting and // escaping are deliberately NOT supported, and anything richer belongs in // the app's own argv parsing. func parseArgv(raw string) []string { return strings.Fields(raw) } // parseDurationSeconds parses a Go duration flag value (e.g. "5m", "1h") // into whole seconds for the wire, which carries idle/max-run timeouts as // plain integer seconds, never a duration string (design §4.1, // provision.SidecarManifest.IdleSeconds/MaxRunSeconds). A negative duration // is refused here rather than reaching the console — or, in store mode, the // bucket — as a nonsense wire value. func parseDurationSeconds(raw string) (int, error) { d, err := time.ParseDuration(raw) if err != nil { return 0, fmt.Errorf("%q is not a valid duration (e.g. 5m, 1h): %w", raw, err) } if d < 0 { return 0, fmt.Errorf("%q must not be negative", raw) } return int(d / time.Second), nil } // sidecarsEnable is the core store-mode `carlos sidecars enable` write — // separated from flag parsing/validation above so a test can call it // directly against a release.Store, instancesEnable's own precedent // (instances_cli.go). func sidecarsEnable(ctx context.Context, s release.Store, acct, app string, m provision.SidecarManifest) error { return provision.PutSidecarManifest(ctx, s, acct, app, m) } const sidecarsRecordsUsage = `usage: carlos sidecars create -app [-region ] [-channel ] carlos sidecars ls -app [-long] carlos sidecars wake -app carlos sidecars rm -app carlos sidecars logs -app Signed-in only (carlos auth login): sidecar records are console-authorized writes — the console checks the name and region against the app's manifest (and a fleet-placed app's own placement, which overrides both) and a box's sidecar reconcile pass turns the record into a running process. Common flags: -account account (sqid or name; default "ops") -account-id account sqid, matched as a sqid ONLY -console console to act through (default: the only logged-in one) create-only flags: -region region code for the record, subject to the manifest's allow-list — or overridden entirely by the app's own placement, when it has one (carlos apps place) -channel release channel the sidecar follows (default: the manifest's, then "stable") ls-only flags: -long print the synthetic host (...sidecar.internal) instead of the short name There is no set-upstreams for a sidecar: it has no upstream set, and nothing dials it. ` // requireSidecarsAPISession resolves signed-in mode for the sidecar record // verbs, refusing direct mode by NAME — sidecars' own advice, never // instances' (Task 12 review finding, binding on this task): `carlos add` // explicitly refuses to mint a sidecar host (its reserved-suffix rail), so // a refusal here that pointed at that verb would send an operator // somewhere that cannot help them. There is no box-side equivalent for a // sidecar record at all — this family is console-authorized only. func requireSidecarsAPISession(verb, consoleFlag string) *apiSession { sess, err := resolveMode(consoleFlag) if err != nil { log.Fatal(err) } if sess == nil { log.Fatalf("carlos sidecars %s works through a console, and this shell is in direct bucket mode\n"+ "(CARLOS_DEPLOYMENT_BUCKET/CARLOS_DEPLOYMENT_DIR is set). Sidecar records are console-authorized\n"+ "writes with no direct-bucket equivalent; unset the store env and run: carlos auth login -console .\n"+ "There is no box-side way to mint a sidecar host by hand — carlos add refuses the reserved suffix", verb) } return sess } func runSidecarsCreate(args []string) { fs := flag.NewFlagSet("sidecars create", flag.ExitOnError) app := fs.String("app", "", "app the sidecar belongs to") region := fs.String("region", "", "region code for the record (subject to the manifest's allow-list, or the app's own placement)") channel := fs.String("channel", "", "release channel the sidecar follows (default: the manifest's, then stable)") consoleFlag := consoleOption(fs, "console to create through (default: the only logged-in one)") acctRaw := fs.String("account", "", "account (sqid or name) the app belongs to (default: your only account, or the configured/chosen one)") acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes -account") fs.Parse(args) if fs.NArg() != 1 || strings.TrimSpace(fs.Arg(0)) == "" { log.Fatal(sidecarsRecordsUsage) } name := strings.TrimSpace(fs.Arg(0)) sess := requireSidecarsAPISession("create", *consoleFlag) printUsing(stdout, sess) acct := resolvedAccount(sess, acctRaw, acctIDRaw) app2 := resolvedApp(sess, acct, *app, sidecarsRecordsUsage) rec, err := sess.sidecarCreate(context.Background(), acct, app2, name, strings.TrimSpace(*region), strings.TrimSpace(*channel)) if err != nil { log.Fatalf("sidecars create: %v", err) } fmt.Fprintf(stdout, "%s %s (channel %s)\n", colorFor(stdout).ok("created sidecar"), rec.Name, rec.Channel) // Deliberately NOT "it comes live": the default sidecar hibernates, so the // reconcile pass mints its row and then nothing runs. Saying otherwise // sends a member off to read logs for a process that was never started. mutedf(stdout, " host: %s — its row lands on the box on the next reconcile pass", rec.Host) mutedf(stdout, " a hibernating sidecar (the default) runs nothing until it is woken: carlos sidecars wake %s -app %s", rec.Name, app2) mutedf(stdout, " an always-on one (hibernate: false in the app's sidecar settings) starts as soon as the row lands") } // sidecarVersionCell renders `ls`'s VERSION column: the build the agent // last started for the sidecar, or the literal "unknown" when it never has // — the honesty rule this branch was built around (design §9.2, binding on // this task): a stamp that overclaims is worse than an absent one, so this // must NEVER render the record's own Channel (the desired version) as a // stand-in for a version nobody has confirmed is actually running. func sidecarVersionCell(running string) string { if running == "" { return "unknown" } return running } // sidecarPlacementCell renders a record's placement, defaulting the same // way provision.SidecarManifest.PlacementOrDefault does server-side: empty // means "any". func sidecarPlacementCell(placement string) string { if placement == "" { return provision.PlacementAny } return placement } func runSidecarsList(args []string) { fs := flag.NewFlagSet("sidecars ls", flag.ExitOnError) app := fs.String("app", "", "app whose sidecar records to list") long := fs.Bool("long", false, "print the synthetic host instead of the short name") consoleFlag := consoleOption(fs, "console to list through (default: the only logged-in one)") acctRaw := fs.String("account", "", "account (sqid or name) the app belongs to (default: your only account, or the configured/chosen one)") acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes -account") fs.Parse(args) sess := requireSidecarsAPISession("ls", *consoleFlag) acct := resolvedAccount(sess, acctRaw, acctIDRaw) app2 := resolvedApp(sess, acct, *app, sidecarsRecordsUsage) recs, truncated, err := sess.sidecarList(context.Background(), acct, app2) if err != nil { log.Fatalf("sidecars ls: %v", err) } w := newTable(stdout) idCol := "NAME" if *long { idCol = "HOST" } fmt.Fprintln(w, idCol+"\tREGION\tCHANNEL\tPOOL\tPLACEMENT\tVERSION\tCREATED\tBY") for _, r := range recs { id := r.Name if *long { id = r.Host } fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", id, r.Region, r.Channel, r.Pool, sidecarPlacementCell(r.Placement), sidecarVersionCell(r.RunningVersion), r.CreatedAt, r.CreatedBy) } w.Flush() // The console caps one answer at sidecarListMax (500, apisidecars.go) // and says so honestly via `truncated` — this line is the same honesty // rule as VERSION's "unknown": a list that quietly stops at a prefix // and says nothing is exactly the overclaim the rest of this branch // refuses to make, and it is not hypothetical for the motivating // customer (Eleven: a bot per enrolled member, so exceeding 500 is // growth, not misuse). No pagination here — a cursor is a product // decision, not this fix's job — just the fact that more exist. if truncated { mutedf(stdout, "(showing the first %d sidecars — more exist)", len(recs)) } } func runSidecarsWake(args []string) { fs := flag.NewFlagSet("sidecars wake", flag.ExitOnError) app := fs.String("app", "", "app the sidecar belongs to") consoleFlag := consoleOption(fs, "console to wake through (default: the only logged-in one)") acctRaw := fs.String("account", "", "account (sqid or name) the app belongs to (default: your only account, or the configured/chosen one)") acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes -account") fs.Parse(args) if fs.NArg() != 1 || strings.TrimSpace(fs.Arg(0)) == "" { log.Fatal(sidecarsRecordsUsage) } name := strings.TrimSpace(fs.Arg(0)) sess := requireSidecarsAPISession("wake", *consoleFlag) printUsing(stdout, sess) acct := resolvedAccount(sess, acctRaw, acctIDRaw) app2 := resolvedApp(sess, acct, *app, sidecarsRecordsUsage) res, err := sess.sidecarWake(context.Background(), acct, app2, name) if err != nil { log.Fatalf("sidecars wake: %v", err) } fmt.Fprintf(stdout, "%s %s\n", colorFor(stdout).ok("requested wake for"), name) mutedf(stdout, " host: %s — the box notices within seconds of the next serial move", res.Host) // The console cannot confirm the wake landed: a box reports its state on // the 5-minute status snapshot, and a running version only appears for a // sidecar that calls /v1/ready, which an app is never required to do. So // this names where the answer actually is rather than implying the write // itself was the confirmation. mutedf(stdout, " watch it come up: carlos sidecars logs %s -app %s", name, app2) } func runSidecarsRm(args []string) { fs := flag.NewFlagSet("sidecars rm", flag.ExitOnError) app := fs.String("app", "", "app the sidecar belongs to") consoleFlag := consoleOption(fs, "console to delete through (default: the only logged-in one)") acctRaw := fs.String("account", "", "account (sqid or name) the app belongs to (default: your only account, or the configured/chosen one)") acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes -account") fs.Parse(args) if fs.NArg() != 1 || strings.TrimSpace(fs.Arg(0)) == "" { log.Fatal(sidecarsRecordsUsage) } name := strings.TrimSpace(fs.Arg(0)) sess := requireSidecarsAPISession("rm", *consoleFlag) printUsing(stdout, sess) acct := resolvedAccount(sess, acctRaw, acctIDRaw) app2 := resolvedApp(sess, acct, *app, sidecarsRecordsUsage) if err := sess.sidecarDelete(context.Background(), acct, app2, name); err != nil { log.Fatalf("sidecars rm: %v", err) } fmt.Fprintf(stdout, "%s %s — its process stops on the next reconcile pass; parked data stays\n", colorFor(stdout).ok("deleted sidecar"), name) } // runSidecarsLogs delegates to the existing `carlos logs` path for the // sidecar's own derived host: logs key on host, not on Kind (design §9.3), // so there is no separate sidecar-log machinery to keep in sync. The name // is resolved to its host through sidecarGet rather than computed locally, // so this client never has to re-derive provision.SidecarHost's shape. func runSidecarsLogs(args []string) { fs := flag.NewFlagSet("sidecars logs", flag.ExitOnError) app := fs.String("app", "", "app the sidecar belongs to") consoleFlag := consoleOption(fs, "console to read through (default: the only logged-in one)") acctRaw := fs.String("account", "", "account (sqid or name) the app belongs to (default: your only account, or the configured/chosen one)") acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes -account") fs.Parse(args) if fs.NArg() != 1 || strings.TrimSpace(fs.Arg(0)) == "" { log.Fatal(sidecarsRecordsUsage) } name := strings.TrimSpace(fs.Arg(0)) sess := requireSidecarsAPISession("logs", *consoleFlag) acct := resolvedAccount(sess, acctRaw, acctIDRaw) app2 := resolvedApp(sess, acct, *app, sidecarsRecordsUsage) rec, err := sess.sidecarGet(context.Background(), acct, app2, name) if err != nil { log.Fatalf("sidecars logs: %v", err) } logsArgs := []string{"-app", app2, "-host", rec.Host} if *consoleFlag != "" { logsArgs = append(logsArgs, "-console", *consoleFlag) } if *acctRaw != "" { logsArgs = append(logsArgs, "-account", *acctRaw) } if *acctIDRaw != "" { logsArgs = append(logsArgs, "-account-id", *acctIDRaw) } runLogs(logsArgs) } // runSidecars dispatches carlos sidecars' subcommand words BEFORE any flag // parsing — runInstances' own contract (cli.go): a subcommand word is not // a flag. Unlike instances, every word here is required; there is no bare // box-local listing form for a sidecar (it has no route to list). func runSidecars(args []string) { if len(args) == 0 { log.Fatal(sidecarsEnableUsage) } switch args[0] { case "enable": runSidecarsEnable(args[1:]) case "create": runSidecarsCreate(args[1:]) case "ls": runSidecarsList(args[1:]) case "wake": runSidecarsWake(args[1:]) case "rm": runSidecarsRm(args[1:]) case "logs": runSidecarsLogs(args[1:]) default: log.Fatalf("carlos sidecars: unknown subcommand %q\n%s", args[0], sidecarsEnableUsage) } }