The full command surface of the official Render CLI (render-oss/cli v2.27.0), enumerated from render --help and graded against a live bex by driving the unmodified CLI through its REST API. Every command, subcommand, and flag is a dedicated checklist item.
Legend: [x] verified working · [~] works with a documented limitation · [ ] not working / not verifiable here · [-] deliberate bex non-goal.
lego/cli/ imports (rather than forks) Render CLI v2.27.0, pinned to a764810a768202704e7206eb7b87a47211fcd98e, and starts its exported command runner after mapping Bex defaults. With no RENDER_* input, it targets https://api.bex.co/v1/ and lets the upstream config library persist its normal YAML schema as ~/.bex/cli.yaml; BEX_HOST, BEX_CLI_CONFIG_{DIR,PATH}, BEX_WORKSPACE, BEX_OUTPUT, and a temporary BEX_ACCESS_TOKEN are supported Bex inputs. The launcher maps a Bex-owned telemetry opt-out (BEX_CLI_DISABLE_ANALYTICS) onto the upstream opt-out and otherwise lets the imported sender report to bex-api (POST /v1/cli-telemetry-events, w5/m92) — usage analytics stay opt-out upstream, but the endpoint is Bex's, never Render's. An explicit RENDER_CLI_DISABLE_ANALYTICS or DO_NOT_TRACK is left untouched. Hermetic launcher tests cover that mapping, the consent default, process-level authenticated REST behavior, and Render-config isolation — including RENDER_CLI_CONFIG_DIR alone never writing $HOME/.bex/cli.yaml (w8/017), and persistent state (installation_id, notice marker) living under $HOME/.bex/state / BEX_CLI_CONFIG_DIR/state rather than ~/.render/state (w8/018). scripts/bex-cli-auth-e2e.sh is the live device-login/refresh/ logout evidence runner; it must be run against a Bex auth environment before a release tag. The launcher overlays Bex help chrome (Use, Short/Long/examples, bex docs, version line) via lego/cli/internal/branding without forking; residual runtime strings (login TUI, ErrLogin, User-Agent prefix, skills state path) remain upstream.
This launcher record does not replace the unmodified server oracle below: that client is the broad command/API compatibility baseline. The command surface is graded at the launcher pin (v2.27.0 after the w6/m142 round-4 re-baseline; prior: v2.26.0 w8/m33, v2.24.0 w2/m79, v2.21.0 live-REST production runs / launcher v2.22.0). Known server-side non-goals are still recorded as [-] in this checklist, including workflows, ephemeral SSH, and ea objects; importing a newer client does not claim those operations as implemented.
-
Image input validation (w8/m49, 2026-10-02): the rebuilt dev-8 API passed 57 final checks across REST/current Bex/unmodified Render 2.27.0/GraphQL/MCP, service creation, Blueprints, and a configured-digest workload. Both CLIs' same-digest
--image … --waitcontrols reached live, with literal workload refs and Service HTTP 200 verified. Refusals preserve deploy history and saved images. Eight cleanup checks passed. The shared older operator was unchanged: different-image override consumption and the new operator diagnosis are not claimed as live coverage; the latter passed current operator envtest. Two earlier unsuccessful harness/CLI attempts are retained. Evidence and limits. -
Environment selector round-trip (w2/m165, 2026-10-02): a rebuilt dev-2 API passed 113 CLI commands across current Bex + unmodified Render v2.27.0 (
a764810a7682) and the actually installed Bexvdev-140f1ef06+ its unmodified Render v2.26.0 (6c0f561f8af9). Discovery returns canonicalevm-*; KV/PG create/get/list accept the discovered ID, name+project controls, and unscoped controls.kv/pgaliases and rawservices --environment-idsfilters passed. REST/GraphQL/MCP agree; legacyenv-*API inputs, SQL membership, and CR labels retain identity. Ten expected refusals cover scope/name controls and the unchanged CLI's old-literal limitation. Twenty datastores, five environments, five projects, and both disposable accounts were removed and absence-verified. This is local selector/metadata acceptance, not a production or datastore-connectivity claim. All 14 datastore command paths share the audited pinned resolver; the remaining mutations and canonical-ID+wrong-project CLI path are source/regression-covered, not separately live-tested. Evidence, identity contract. Oldenv-*literals still look like names to the unchanged CLI: rediscover theevm-*ID. -
Human device-flow op-class pass (2026-08-20): after fixing the login → "you are not allowed to take this action" regression (the
cliauthdevice-grant requested no granular capability, so every authenticated call 403'd at the dispatch scope matrix — see theloginrow), the unmodifiedbexbinary was driven against a locally-run real bex-api (control-plane store on a throwaway Postgres, realapp.bex.coCRDs, a fake Hydra introspecting the platform-marked human device-flow token that now carriesbex.read bex.write bex.sensitive). ~90 invocations were exercised with zero authorization refusals: 60 client-side (--version,help,completion×4,docs, and the full--helptree for every command/subcommand) plus ~30 authenticated across all four op-classes —whoami/workspaces/workspace current;services/postgres/keyvalues/projectslist + get;services create×4 (web/cron/static/--env-var),postgres/keyvalues create;services/postgres update;restart,postgres suspend/resume;deploys list/create,jobs list,services instances;services/postgres delete;environments(correct 404);logs. Residual non-passes were all non-auth environment artifacts (the samenamespaces "tea-…" not foundcontrol-plane provisioning gap of.pm/w2/026.md, KeyValue datastore-namespace resolution in the hand-seeded tenant, interactive confirm prompts). Note:cli-compat.sh verifyauthenticates withclient_credentialstokens, which are capability-exempt, so it structurally cannot exercise this human-token op-class path — only the device-flow login (scripts/bex-cli-auth-e2e.sh, which runsworkspaces+services) does. Guards:TestDeviceGrantScopeCoversEveryOpClass,TestRenderCLITokenClearsEveryOpClassEndToEnd, and the env-gated liveTestRenderCLIBinaryAgainstLiveServer. -
CLI:
render-oss/cliv2.24.0 (fe8a6188119ee1a53dcf3e5c19f6a5302e840c3f), built unmodified from the Go-module pin, driven throughscripts/cli-compat.shagainst dev-2 on 2026-08-19 (w2/m79). Auth/list/env legs passed (login,whoami,workspace current,workspaces,projects, missing-resource not-found shapes, environments). Create legs (services/postgres/keyvalues) failed with harnessnamespaces "tea-…" not found— control-plane tenant-namespace provisioning on this CAPD stack, not a CLI wire regression (filed.pm/w2/026.md). Prior production/dev supplements below remain historical evidence under older pins unless restated. -
Upstream surface delta v2.24.0 → v2.26.0 (w8/m33 round 3, 2026-09-07, help-graded + backend-wire-shape-verified): launcher pin moved v2.24.0 → v2.26.0 (
6c0f561f8af9d4a6cfb88f4d1845ffd18cee181a, modulev1.1.3-0.20260901190744-6c0f561f8af9);bash scripts/bex-cli-validate.sh+cd lego/cli && go build/vet/test ./...green. Graded the v2.24.0→v2.26.0 command/flag diff (upstreamgit log v2.24.0..v2.26.0+ regenerated REST client):- New
postgres create|update --connection-pool=none|pgbouncer(v2.26.0; CLI validates the enum client-side, sendsconnectionPoolin the create/PATCH body, andpostgres getoutput now shows the mode). bex already accepts this on both create and update —internal/postgresfolds Render'sconnectionPoolenum onto its nativepoolerbool viaresolvePoolerand returns the enum on read (the w2/024 alignment, now exercised by the client). New rows added below, graded[x]on wire shape. - New spec-based compute plan names (v2.25.0:
0.1c-256mb…128c-1024gfor Postgres,1gfor Key Value; legacy names stay valid input). The CLI's--planhelp/picker list is documentation-only, not validation, so it passes any value through. bex validates--planagainst its own tier catalog (tiers.Postgres.IDs()), so a suggested spec-based name is a named 400 while bex's own plan names still work — partially closed by w8/011 input aliases for overlapping rungs; larger Render names remain an Intended divergence. blueprints validatenow exits status 1 on an invalid blueprint for every--outputmode (v2.25.0) — a client-side exit-code change; bex'svalid:falsetransport is unchanged (blueprints row updated).- Analytics is now opt-out (v2.26.0) — the launcher disables it by default (see the launcher record above); recorded under Intended divergences.
- Go 1.27 (upstream
85c8c2c): the module andlego/go.workmoved togo 1.27.0; CI/toolchain handled (seeUPSTREAM_RENDER_CLI.md, lego/AGENTS.md).RENDER_CLI_CONFIG_DIRnow recommended in help (launcher already mapsBEX_CLI_CONFIG_DIR). No new top-level commands (only a hiddenanalytics noticesubcommand) and no Bex-native command-name collisions (code/glm/muse/kimi/deepseek/upgrade);RootCmdstill exported,CustomHelpTemplateintact (internal/brandingtests green), andisRootVersionRequestbody unchanged vslego/cli/internal/update. No fresh live dev-N REST run this round (same environment limit as w2/m79's dev-2 create failure,.pm/w2/026.md); grades are help-graded plus backend wire-shape reads.
- New
-
Upstream surface delta v2.26.0 → v2.27.0 (w6/m142 round 4, 2026-09-10, help-graded + client-diff): launcher pin moved v2.26.0 → v2.27.0 (
a764810a768202704e7206eb7b87a47211fcd98e, modulev1.1.3-0.20260909214233-a764810a7682);bash scripts/bex-cli-validate.sh+cd lego/cli && go build/vet/test ./...green. Graded the v2.26.0→v2.27.0 command/client diff:- New
ea sandboxes snapshots {create,get,list}andea sandboxes create --snapshot-id(v2.27.0) — regenerated REST client gainsCreateSandboxSnapshot*/ListSandboxSnapshots/RetrieveSandboxSnapshot/DeleteSandboxSnapshot. Graded[-]: early-access sandbox surface outside the bexeacompat target (same class as round-3 sandbox-exec ops); importing the client does not claim these as implemented. blueprints validatenow reports workflows in a Blueprint (v2.27.0) — client display change; workflows remain a deliberate non-goal (DO_NOT_DO.md); bex's validate transport continues to refuse workflow resources.golang.org/x/cryptosecurity bump (upstream) — transitive via the pin; no launcher behavior change.- Seams re-verified:
RootCmdexported,CustomHelpTemplateintact,isRootVersionRequestbody unchanged, no new top-level commands, no Bex-native name collisions. Service-disk pin-conditional re-check: still nodiskscommand / service disk flag. w8/016 (2026-09-15): also re-diffeasubcommand names, not only presence — the group issandboxes(plural) with nosandboxalias at this pin; four older checklist rows still saidea sandbox <verb>until this date. No fresh livecli-compat.sh verifythis round (environment carve-out as w8/m33 / w2/m79).
- New
-
Upstream surface delta v2.22.0 → v2.24.0 (help-graded): new root
completion(bash/zsh/fish/powershell) —[x]local help/smoke;RENDER_CLI_CONFIG_DIRpreferred overRENDER_CLI_CONFIG_PATH— launcher already mapsBEX_CLI_CONFIG_DIR; Windows archive shipsrender.exe(release note only);kv create|update --persistence-modeand existingpgdisk flags unchanged in contract. No Bex-native command-name collisions (code/glm/muse/kimi/deepseek).isRootVersionRequestbody unchanged vslego/cli/internal/update. -
CLI: (historical)
render-oss/cliv2.21.0, built unmodified from a separate pinned upstream checkout (or a checksum-verified release binary), driven only throughscripts/cli-compat.sh(Hydraclient_credentialstoken exchange per call,RENDER_HOST→ local bex-api). That dev-9 baseline never touched production; the named supplements below are separately scoped. The repository'slego/cli/path is the distinct import-basedbexlauncher, not an upstream checkout. -
Target: the isolated local dev-9 environment (
.pm/w9/dev-9, bex-api at:54090), currentlego/backendHEAD, on 2026-07-18. -
Method: the maintained regression suite
scripts/cli-compat.sh verify(whole-shapecheckFieldsassertions over the core families), the cleanup-safeservices-parity-verifybaseline/configured legs, and a full sweep of the remaining command tree. Each item was graded on the unmodified CLI's exit status and the wire shape read back via-o jsonand raw RESTGET. The exact service flag contract and redacted POST/PATCH captures are incli-services-create-update.md. -
Operator-correctness schema supplement (w2/m60): the installed, unmodified v2.21.0 CLI's
services update --helpandpostgres update --helpwere re-read on 2026-07-19. Service update has no--type; Postgres update has both--planand--disk-size-gb. Focused backend regression sends the CLI's exactPATCH {"diskSizeGB":…}wire shape: growth persists, a lower value returns the named 400 envelope, and the resource remains at its accepted size. Operator/envtest separately proves a plan downgrade preserves that size. This is a command-schema plus API regression supplement, not a new live dev-9/production CLI run; the previously graded create/update/delete baseline remains the live-client evidence. -
Service-disk supplement (w1/m86, 2026-08-23): ADR082 shipped persistent service disks on REST/GraphQL/MCP/dashboard, and the unmodified v2.24.0 CLI's
--helptree was re-read for a matching surface: there is none. Render's own CLI has nodiskscommand and no service-level disk flag —--disk-size-gbexists only onpostgres create|update(the datastore's own storage, already graded above) andservices create|updatehas no disk flag at all. So this feature adds no row to this checklist: there is nothing in the client to grade, and the absence is Render's, not a bex gap. The parity boundary for disks is the REST shape the CLI would call if it ever grows one —/v1/disksin Render's documented request/response shapes, covered by the backend suite. Re-check this bullet when the CLI pin moves: a futurerender diskscommand would make this a real row. w8/m33 re-check (v2.26.0): still nodiskscommand and no service-level disk flag — the v2.24.0→v2.26.0 command tree adds no disk surface, so this stays a non-row. w6/m142 re-check (v2.27.0): still none — v2.26.0→v2.27.0 adds onlyea sandboxes snapshots*, not disks. -
Durable-delete and Key Value credential supplement (w2/m61): the unmodified v2.21.0 CLI delete commands remain plain service/Postgres/Key Value id-or-name operations with no bex-only flag or cleanup handshake. Focused REST/GraphQL/MCP regressions preserve their existing acknowledgement shapes while subsequent reads show
deletinguntil the finalizer has proved required cleanup absent, then NotFound. Key Value connection-info returns conflict rather than a split credential during rollout. This is an operator/backend regression supplement, not a new live production CLI run; the previously graded unmodified-CLI delete rows remain the live-client evidence. -
Service deleting-read supplement (w3/m81): the
services deleteacknowledgement shape is unchanged, but a REST by-idGET /v1/services/{id}and the CLI'sservices -o jsonlist observed during finalization now agrees on absence — a deleted service's by-id read returnsnot found(Render'sGET 404) the instant deletion is accepted, rather than the w2/m61 interimdeleting, so the CLI never sees a service it can no longer act on. This matches Render's own delete convergence (the w5/m49 production run reachedGET 404+ list absence within five minutes) and the service-deleterow below; it is a backend read-contract correction, not a CLI wire change. Postgres/Key Value keep the interimdeletingread (no serving URL to go stale) pending the adjacent parity follow-up. -
OpenAPI request-boundary supplement (w7/m49): the existing unmodified-v2.21.0 request fixtures across services, deploys, Postgres, Key Value, environment groups, projects, custom domains, jobs, registry credentials, and logs run through the authenticated 119-operation Render-contract intersection. Focused boundary tests also replay the accepted CLI-shaped bodies, compatibility defaults, and bex
dryRun/confirmextensions while proving invalid input has no side effect. This is repository regression evidence, not a new live CLI run; the live grades below are unchanged. Bex deliberately rejects undeclared query/body input more strictly than Render's open schemas, as documented in the OpenAPI boundary contract. -
Canonical-route retirement supplement (w1/m55): the official CLI's existing request fixtures already use Render's canonical
/v1/services,/v1/postgres,/v1/key-value, and/v1/registrycredentialsroutes, so their focused backend regressions remain green after bex removed its non-Render public aliases. Dedicated negative tests now require those retired aliases to return404; the internal control-plane listener's unrelated/v1/appsroute remains available only on port8091. This is repository regression evidence, not a new live CLI run; the live grades below are unchanged. -
Nested-image/delete-convergence correction (w5/m49, production accepted 2026-07-21 UTC): production proved that v2.21.0 serializes
image.ownerId:""on image create and update. The strict REST adapter now accepts and authorizes that Render-declared field after OpenAPI validation. First-build deletion now interrupts synchronous build waits, inventories with the required build-namespace RBAC, repairs least-privilege registry authority, proves repository absence before revocation, and quiesces Ingress/cert-manager TLS producers before Secret deletion. The deployed unmodified CLI passed image create/update/delete and immediate repo-build delete to raw GET 404 plus official-CLI list absence inside the five-minute bound. The original stuck fixture and the final acceptance prefix both had zero Kubernetes/build/TLS/credential residue; Zot htpasswd/ACL/config and a builder-authenticated catalog also had zero matches. Sanitized evidence is in the m49 diagnosis. -
Configured service pass: a disposable local OpenBao plus auth-enabled persistent Zot augmented dev-9 for the second service run. It proved CLI env vars, secret files, create/update registry-credential binding, a genuinely private kubelet pull, and native cron commands.
-
Postgres create supplement (w6/m38):
scripts/postgres-create-cli-smoke.shdrove the same unmodified CLI against isolated dev-6 and a real local CNPG cluster on 2026-07-18. It proved both custom names, database-only and user-only independent defaults, the CR intent, generated Secret metadata, andcurrent_database()/current_userinside PostgreSQL. It also proved that unsupported Datadog create/update flags fail with a named 400 and leave no resource behind. -
Postgres psql supplement (w7/m46):
scripts/psql-compat-verify.shdrove Render's checksum-verified, unmodified v2.21.0 release against isolated dev-7, real CNPG, a configuredBEX_DB_DOMAIN, and pg-sni-proxy on 2026-07-18. Both opaque id and exact name executedSELECT 1 AS bex_psql_probe;through realpsql18.4 over the external TLS/SNI route; the caller's public/32passed the CLI's own allow-list gate, the absent-IP negative case was refused before connect, and the disposable database and local credential state were removed. Redacted markers and release hashes are in the psql CLI artifact. -
Production SSH supplement (w6/020):
scripts/ssh-verify.shdrove the checksum-verified, unmodified v2.21.0 release throughhttps://api.bex.coand the publicssh.bex.co:22gateway in a real PTY on 2026-07-18. Service name, service id with the interactive Any instance selection, and complete instance id all reached the asserted running container and runtime environment; the verifier also re-proved the pinned host key, raw any/exact OpenSSH, PTY resize, exit status, restart/redeploy closure, suspended/free/stale/shell-less/unknown/deleted denial, and deleted its disposable key, service, and Scale workspace. The broader viewer/foreign/static/cron denial matrix remains captured in the prior production acceptance. -
Production Key Value CLI supplement (w2/m57):
scripts/cli-compat.sh kv-cli-verifydrove the unmodified v2.21.0 release throughhttps://api.bex.coand the public*.kv.bex.co:6379TLS/SNI edge on 2026-07-18. Under an automated PTY, opaque-idPING/SETand display-nameGET/DELall passed through the CLI's own list/item/connection-info path with an exact source/32; the verifier removed its key and Key Value, then revoked its API key and deleted its workspace. Sanitized results are in the production acceptance evidence. -
Production durable-logs supplement:
scripts/cli-logs-verify.shdrove the unmodified v2.21.0 CLI throughhttps://api.bex.coagainst the deployed Loki + log-shipper on 2026-07-18, closing the store-onlylogsfilter rows the Loki-less dev-9 baseline could only grade503. A disposable busybox web-service fixture planted one JSON error line, one JSON info line, and one plaintext line at boot, then served a200on/and a404on a nonce path through the public edge;render logs -o jsonthen proved every filter with a positive match AND a negative exclusion — theapp/requestsplit clean both ways,--level errorisolating exactly the planted line (criticalan honest empty),--path/--hostmatching only the probe values,--method GETpositive withPOSTempty, and--status-codeexact404/200exclusion both ways plus the4xxclass shorthand. The fixture is deleted on exit; the raw REST/MCP halves of the same durable-store paths remain covered byscripts/logs-verify.sh. -
Base dev-9 does not run (so paths needing them are marked accordingly, distinguishing a real bex gap from an environment limit): build infra (kpack/zot), OpenBao (env-vars/secret-file store), the registry-credential store, Loki (durable log store — but see the production durable-logs supplement above), Prometheus, cert-manager, OpenFGA (authz is allow-all here),
BEX_DB_DOMAIN/BEX_KV_DOMAIN(external datastore connect), and the SSH gateway.
-
(w2/m79) Create-path
namespaces "tea-…" not foundon CAPD dev-2 duringcli-compat.sh verifywith v2.24.0 — auth/list/env passed; service/Postgres/Key Value creates 500'd before any wire-shape assert. Filed.pm/w2/026.md(harness/control-plane, not a CLI pin bug). -
— fixed (w7/m45):keyvalues update --memory-policy/--ip-allow-list/--clear-ip-allow-listare silent no-opsKeyValuePatchnow carriesMaxmemoryPolicy+IPAllowListandhandleUpdateKeyValuedecodes them, so the CLI's update flags mutate the store; GraphQLsetKeyValueMaxmemoryPolicy+ MCPupdate_key_value(w1/m71's fold ofset_key_value_maxmemory_policy/set_key_value_ip_allow_list) bring the programmatic surfaces to parity. Dashboard memory-policy editing after create has also since shipped (cbf49951/d2b1de3e:KeyValueMaxmemoryPolicySectionedit-in-place on the Key Value detail page, w7/007). -
— fixed (w4/052): bex leftservices update --health-check-path(and every otherserviceDetailsfield) is unusable on a Dockerfile-runtime web servicespec.runtimeempty for a Dockerfile build expressed through the default/auto/dockerfilebuilder — the shape a dashboard, Blueprint, or hand-applied App produces — soGET /v1/servicesreturned noserviceDetails.runtime/env. The upstream CLI reads that field to round-trip a partialservices update, so it rejected the empty value client-side (unsupported runtime "") or treated an explicit--runtime dockeras a forbidden switch (cannot switch runtimes via the CLI), stranding such a service onhealthCheckPath: /with no CLI path to repoint it (found fixing a live production deploy whose homepage-SSR health check timed out as its corpus grew, while the app already shipped a bodyless/healthz). The read surfaces now derive the effective runtime (internal/apps.effectiveRuntime), consistently across REST/GraphQL/MCP and the runtime-keyedenvSpecificDetails: a repo build under the default/auto/dockerfile builder reads backdocker, and a prebuilt image (image set, no repo) reads backimage(noenvSpecificDetails— the container is supplied whole viaimagePath), so both round-trip a partialservices update. Abuildpackbuild or static site has no bex runtime and stays empty as before. The upstream--runtime-guard divergence below is unchanged (the guard is correct once a runtime reads back). The w9/m93 milestone hardened this into a class guard so no future read omission can re-strand the CLI:TestServiceReadRoundTripCompleteness(create-matrix readback: runtime present, non-empty, and shape-consistent for every build strategy),TestEffectiveRuntimeIsTotal(the spec→runtime projection is total for runnable types),TestDockerfileServiceRuntimeIsConsistentAcrossSurfaces, plus build-strategy-diverse fixtures inscripts/cli-services-parity-verify.sh(a runtime-less image service and an out-of-band Dockerfile-via-builder service each exercise aservices update --health-check-pathleg — the monoculture--runtime gofixture that hid this can no longer hide its successor). Live on dev-9, 2026-09-08: unmodifiedrender-oss/cliat pinfe8a6188119e(v2.24.0) against local bex-api HEAD9024a9672—image-healthcheckandweb-builder-roundtripboth PASS (GET derivesruntime/envimage/docker; CLI partial update round-trips; fixtures deleted to GET 404). CLI pin left at v2.24.0 (w8/m33 owns the v2.26.0 refresh). Verifier hygiene fixed in the same pass: out-of-band create putsownerIdin the body (not a rejected query param), and allowlist asserts readserviceDetails.ipAllowList(w6/m106 nesting). -
(upstream CLI, not bex) The v2.21.0 SSH instance picker discards an exact-instance selection: both callbacks pass the service id instead of their
instanceIDargument. Any instance therefore works through the picker, and the supported complete-instance-id command argument works for exact targeting; the verifier does not patch the CLI or falsely credit the broken menu path. -
(upstream CLI, not bex)
skills list/skills updatepanic (nil-pointer) in every non-TTY output mode. -
(upstream CLI, not bex)
logspanics (exit 2) with no usable credential in every non-interactive output mode (-o json|yaml|text):cmd/logs.gocallsdeps.LogLoader()insideRunEbefore auth is established, and(*Dependencies).APIConfig(pkg/dependencies/dependencies.go:281) doespanic(err)onDefaultAPIConfig'sErrLogin— so an absent, expired-beyond-refresh, or corruptcli.yamlturnsbex logsinto a raw Go stack trace (panic: run \render login` to authenticate) where every sibling command (services,deploys list,restart,whoami,workspaces,projects) returns a cleanError:and exits 1. No bex request is ever sent; the crash is entirely client-side. Re-check when the CLI pin moves: upstream may fix the panic. (Found live 2026-09-15,/qa-find-bugs-cliround,bexv0.2.1 / pin v2.27.0a764810a7682; filed.pm/w8/015.md`.) -
(upstream CLI + Render's wire contract, not bex)
--secret-filesilently corrupts binary files (w8/029, sweep 41; pin v2.27.0):pkg/service/secretfiles.gosendsContent: string(data), and the API'scontentis a JSON string on every surface, so invalid UTF-8 bytes become U+FFFD before the request leaves the machine (a 1,024-byte random file mounted with a different sha256; a 404 KB text file round-tripped exactly). bex stores what it receives. Documented indocs/bex-cli.mdwith a base64 recipe; the dashboard upload refuses non-UTF-8 files. Pin-conditional: re-check on every pin move.
-
Log text search (w2/m167): the Render CLI accepts comma-separated
--textvalues and the logs API accepts an array. The exact v2.27.0 pin sends repeatedtextkeys; Bex preserves all of them and matches their OR, ANDed with the other supported filters. Bex treats each term as a case-insensitive literal substring, including wildcard/regex characters, while Render documents wildcard/regex text search. Exact empty terms are ignored, duplicates after lowercasing have no additional effect, and spaces remain literal; no effective terms means no text filter. GraphQL/dashboard and datastore convenience searches remain scalar. See the full contract and source limits. -
Blueprint size (w8/m48): bex limits decoded YAML to 512 KiB before parsing. Oversized
blueprints validateuploads receive a named 413 for that limit. Render documents a 10 MB total multipart request bound; bex keeps the smaller pre-decode amplification guard. Request-envelope allowance is separate from the YAML budget. -
--regionis accepted but platform-stamped fromBEX_REGION(local-capdin dev-9,fsn1in production); the submitted hint is not persisted. Both truthful installation values sit outside the CLI's closed Render-region enum, so a bareservices create --fromclone fails client-side and needs an explicit--region. -
--previewsis rejected platform-wide (400 "not supported by this platform"). -
Postgres Datadog forwarding is not implemented. Supplying
--datadog-api-keyor--datadog-siteon create or update returns a named 400; bex never accepts or persists the credential. -
services update --runtimeis rejected inside the unmodified CLI by design (cannot switch runtimes via the CLI) before any bex request. -
Usage analytics go to bex-api, never Render (w5/m92). Upstream analytics stay opt-out-shaped, but the launcher no longer force-disables them: events (command path, duration, exit code, install id, workspace — no args or secrets) POST to bex-api's
POST /v1/cli-telemetry-events, attributed server-side and retained under the audit-retention window.BEX_CLI_DISABLE_ANALYTICS=1(orDO_NOT_TRACK) opts out; an explicitRENDER_CLI_DISABLE_ANALYTICSis left untouched. The upstream one-time notice block still prints once per machine with Render copy — documented in the Bex CLI guide, not reworded. Release attribution (w5/m94): the launcher stampsX-Bex-CLI-Versionon requests to the configured bex host only, and bex-api stores it beside the upstream pin. The request body stays byte-identical to upstream'sCliTelemetryEventPOSTInput— verified field-for-field against the pinned client (18 fields, no additions, no omissions), so an unmodifiedrenderbinary pointed at bex still ingests and simply records the release as unknown. The header is a bex extension: Render has no equivalent, andUser-Agentkeeps naming the upstream release this checklist tracks. Two launcher-owned paths now report themselves because upstream's post-run hook cannot see them — a provider launch (bex code/bex glm/…) emits immediately before it replaces the process, and root--versionemits with completion kindversion, a deliberate divergence from upstream, which treats version as a non-event.bex upgradeneeded nothing: it is an ordinary command on the imported root and already emitted through upstream's hook (measured, not assumed). GraphQL/MCP telemetry twins remain deliberately absent, matching upstream, which exposes telemetry over REST only. -
Render's spec-based compute plan names are not accepted (v2.25.0). The CLI's
--planhelp/picker advertises Render's new0.1c-256mb…128c-1024g(Postgres) and1g(Key Value) names, but that list is documentation-only, not validation — the CLI passes any value through, and bex validates against its own tier catalog (ADR030), returning a named 400 for a name it does not offer. bex's own plan names work; unambiguous aliases onto bex rungs ship in w8/011 (0.1c-256mb→basic-256mb,0.5c-1g→basic-1gb, KV256mb/1g; larger Render names still 400). w1/m170 (2026-10-02) adds the same exact-size rule for service compute plans (0.5c-512mb→starter,1c-2g→standard,2c-4g→pro,4c-8g→pro_plus,4c-16g→pro_max,8c-32g→pro_ultra) on every plan write and in Blueprints; other compute sizes still 400. -
autoDeployTrigger: checksPass(deploy only after CI checks pass) is rejected with a named 400 (w5/m53). bex maps its booleanspec.autoDeployonto Render'sautoDeployTriggercommit/off(and the legacyautoDeployyes/no), accepting either on create/update withautoDeployTriggertaking precedence; it has no GitHub-checks integration, so the checks-gated trigger is a documented non-goal, not a silent no-op.
Regenerate the command tree with
render <subcommand> --help. Re-run the graded baseline withscripts/cli-compat.sh verify. Grouping mirrors the CLI's ownrender --helpsections.
The interactive-only Key Value client has a separate, opt-in full-edge verifier: set the production-equivalent BEX_API_URL, HYDRA_PUBLIC_URL, CLI_KEY_ENV, and the runner's explicit BEX_KV_VERIFY_ALLOW_CIDR, then run scripts/cli-compat.sh kv-cli-verify. The unmodified CLI provisions the source-restricted public fixture through keyvalues create --ip-allow-list, and the verifier always deletes the returned opaque id. It then launches the same v2.21.0 CLI under an automated pseudo-terminal with explicit --output interactive; one-shot Redis arguments after -- let the child and TUI exit without human input. This is deliberately excluded from the dev-9 baseline because it requires public Key Value DNS, TLS, and the SNI proxy. Production acceptance on 2026-07-18 exposed and repaired three real edge defects in sequence: the datastore wildcard was Cloudflare-proxied instead of DNS-only, Hetzner PROXY protocol was absent so exact source allowlists saw the load balancer, and redis-cli needed an explicit --sni hostname. The final opaque-id and display-name legs passed end to end; scripts/datastore-dns-cloudflare.sh and the proxy tests retain the deployed prerequisites.
-
deploys— list, create, and cancel deploys-
deploys list <serviceID>— carries Render's nested{image:{ref,…}}shape -
deploys create <serviceID>— returns a deploy id; accepts the CLI'sclearCachestring enum-
--clear-cache -
--commit <id>— repository-backed non-cron services only; image-backed services receive a named 400 without creating a deploy (w8/m49) -
--image <url>— image-backed services only; valid tags/digests accepted, with distinct grammar/private-address/untrusted-registry errors. Overrides preserve the saved image. Bex permits another trusted repository; Render documents a same-repository restriction (w8/m49). -
--wait
-
-
deploys cancel <serviceID> <deployID>— resolves and cancels an open deploy
-
- [~]
jobs— create and manage one-off jobs. Creation is a deliberate non-goal (.pm/DO_NOT_DO.md; gated by w4/m116/t003, 2026-10-02); the history verbs still serve jobs created before the gate. The earlier[x]create grade rested on a dev-9 response shape only — on production every create was refused by Kubernetes RBAC and came back alreadyfailed(live 2026-09-17/21/26).-
jobs list <serviceID>— lists jobs with status/planId/startCommand (history) - [-]
jobs create <serviceID>— deliberate non-goal: named410before any record is written or Kubernetes Job submitted; the unmodified client printsreceived response code 410 (ONE_OFF_JOBS_UNSUPPORTED): one-off jobs are not supported on bex; …and exits non-zero. GraphQLcreateJob(extensions.code) and MCPcreate_job(code-prefixed error) refuse identically. Not yet re-probed live.- [-]
--start-command <cmd>— refused with the command (above) - [-]
--plan-id <id>— refused with the command (above)
- [-]
-
jobs cancel <serviceID> <jobID>— cancels a pending/running pre-gate job; an already-terminal job is a409
-
-
keyvalues(aliaskv) — manage Render Key Value instances-
keyvalues create— nested owner/options, opaquered-<xid>id, underscoremaxmemoryPolicy; a nonempty CLI--ip-allow-listcreates public intent without a bex-only flag- payment-required failures remain an ordinary Render API error (HTTP 402 with
id/message); the actionable message namescreate_billing_checkout_session, so the unmodified client does not receive an undecodable body -
--name -
--plan <free|starter|standard|pro|pro_plus> - [~]
--region— accepted but echoeslocal-capd(single fixed region) -
--memory-policy <cache|queue|raw policy>—cache→allkeys_lrualias and raw policies round-trip -
--ip-allow-list cidr=…,description=…— CIDR and description round-trip -
--workspace <id|name> - [~]
--project <id|name>— flag parsed; needs an existing project (none creatable via CLI) -
--environment <id|name>— discovered canonical ID and name+project controls passed on dev-2 (w2/m165); legacy API links remain valid
- payment-required failures remain an ordinary Render API error (HTTP 402 with
-
keyvalues list -
keyvalues get <id|name>— after suspend,statusissuspended(RenderdatabaseStatus; w5/061) so text/JSON Status matches hibernation even though the pinned client drops the separatesuspendedfield -
keyvalues update <id|name>— resolves by opaque id; core fields apply. w4/m116 (2026-09-20):--ip-allow-listhad stored the list without publishing the store, so a private-born Key Value stayed unreachable andkv-clikept targeting the in-cluster.svchost (live sweep 2026-09-17). A nonempty allowlist publishes on every entry point. w2/m166 supersedes the sticky-clear part of w4/m116: the pinned v2.27.0 client and official Render docs define--clear-ip-allow-listas disabling external access. Empty-list updates now withdraw publication; omission preserves it, and an explicit Bexpublicvalue wins. The datastore state matrix records legacy and create-default boundaries. The 2026-10-02 live finding proved the old clear admitted fresh authenticated traffic; the milestone acceptance records verification of the repair.-
--name— rename; opaquered-id stays stable -
--plan -
--memory-policy— mutatesmaxmemoryPolicyon read-back (fixed w7/m45) -
--ip-allow-list— replaces the allow-list, enables the external route for matching sources, and returns CIDR + description (write w7/m45; publication w4/m116) -
--clear-ip-allow-list— clears rules and disables new external connections after convergence (array write w7/m45; external-disable semantics w2/m166); internal access remains available
-
-
keyvalues suspend <id|name> -
keyvalues resume <id|name> -
keyvalues delete <id|name>— unchanged CLI contract; durable cleanup remains internal (w2/m61)
-
- [~]
logs— view logs for services and datastores (single command). (Upstream defect at this pin: with no usable credential — absent, expired-beyond-refresh, or corruptcli.yaml— the command panics with a Go stack trace and exits 2 in every non-interactive mode instead of a clean login error; see the Real-gaps bullet above. Re-check when the CLI pin moves. The[x]rows below grade the authenticated paths.)- query mode — resolves a service by name; empty windows return stable cursors (no parse crash)
- [~]
--tail— streams App or standalone build logs over WebSocket, with all text terms preserved; request/pre-deploy/datastore tails and store-only filters remain unsupported -
-r, --resources <ids>— required in non-interactive mode; honored -
--instance <ids>— comma-separated instance IDs; Bex accepts public IDs and legacy raw pod names, and emits public IDs. Bex-onlytype=predeployqueries via REST, GraphQL, and MCP resolve selectors against the authorized service's existing migration pods before reading logs (w2/039). Omitted selectors include all matching migration pods; unknown or foreign selectors match none, with text/time filters and limits still applied. This live source has no history fallback after TTL cleanup and no tail support. The pinned CLI still rejects--type predeploy; shipped migration output remains available underbuildas documented below. -
--start <time> -
--end <time> -
--direction <backward|forward> -
--limit <count> - [~]
--text <values>— comma-separated CLI values become repeated REST terms and match as an OR, with other filters still ANDed; reversing or duplicating terms does not duplicate lines. Case-insensitive literal substrings only; see the text-search divergence above. The same terms apply to supported tails, while label-value discovery does not evaluate line text -
--level <levels>— durable-logs supplement: a planted JSONerrorline is isolated exactly; an unmatched level is an honest empty. (The CLI's own--levelenum has nounknown, so bex's honest plaintext bucket is reachable over REST only — upstream flag shape, not a bex gap.) Dev-9 (no Loki) answers503 -
--type <types>— closed enumapp/request/build(client rejects bex-onlypredeploy);appworks live without the store; durable supplement provedapp/request;buildalso carries pre-deploy Job stdout once shipped (w5/m100) sopre_deploy_failedis diagnosable without a type the CLI cannot send -
--host <hosts>— durable-logs supplement: matches only the probe host; an absent host is an honest empty. Dev-9 (no Loki) answers503 -
--status-code <codes>— durable-logs supplement: exact404/200exclude each other's probe lines, and the4xxclass shorthand matches. Dev-9 (no Loki) answers503 -
--method <methods>— durable-logs supplement: the GET probes match; a never-sent method is an honest empty. Dev-9 (no Loki) answers503 -
--path <paths>— durable-logs supplement: matches only the probe path; an absent path is an honest empty. Dev-9 (no Loki) answers503 -
--task-id <ids>— accepted as a filter -
--task-run-id <ids>— accepted as a filter
-
postgres(aliaspg) — manage Render Postgres databases-
postgres create— id/name/ipAllowList wire shape correct (description persists). With no allowlist flag, the pinned builder omitsipAllowList; REST defaults to public with explicit0.0.0.0/0and::/0rules, so readback no longer reports a blocked empty list (w2/038). Explicit REST[]creates private intent unless Bexpublic:trueoverrides it; the public API rejectsipAllowList:nullandpublic:nullwith HTTP 400. Native GraphQL/MCP create defaults stay private. Create matrix and CLI limits- payment-required failures use the same CLI-decodable 402 envelope as Service and Key Value creates
-
--name - [~]
--plan <free|basic_*|pro_*|accelerated_*>— bex's own tier names round-trip (input takes either spelling; REST answers in Render's API spellingbasic_256mb, not the Blueprintbasic-256mb, since w8/057); v2.25.0's help/picker also advertises Render's spec-based compute plans (0.1c-256mb…128c-1024g), which map onto bex rungs when unambiguous (w8/011); larger names still 400 - [~]
--region <frankfurt|ohio|oregon|singapore|virginia>— accepted; platform-stampedlocal-capd -
--version <int> -
--disk-size-gb <int>— free is fixed at 1 GB; paid storage is capped at 16,384 GB (w8/m50) -
--disk-autoscaling— requires a paid plan; free is a named 400 (w8/m50) -
--connection-pool <none|pgbouncer>— w8/m33 / v2.26.0: CLI validates the enum then sendsconnectionPool; bex folds it onto its nativepoolerbool (resolvePooler), sopgbouncerprovisions the CNPG Pooler on paid plans andnoneleaves it off; free enable returns a named 400 (w8/m50) (wire-shape graded; w2/024 alignment) -
--high-availability— persists to the CR spec (status flips once replicas are ready) -
--read-replica <name>— requires at least 0.5 CPU and 10 GB storage; maximum five; the existingbasic-1gbplan qualifies at 10 GB (w8/m50) -
--database-name <string>— persisted as the immutable physical database name and live-proven through CNPG SQL identity -
--database-user <string>— persisted as the immutable owner role and live-proven through CNPG SQL identity; either custom-name flag may be omitted independently - [-]
--datadog-api-key <string>— deliberate non-goal; named 400, credential never persisted - [-]
--datadog-site <string>— deliberate non-goal; named 400 -
--workspace <id|name> - [~]
--project <id|name>— flag parsed; needs an existing project -
--environment <id|name>— discovered canonical ID and name+project controls passed on dev-2 (w2/m165); legacy API links remain valid
-
postgres list— resolves through the RC3 cursor envelope -
postgres get <id|name>— resolves by name; every field intact;-o textrenders Workspace/Region; v2.26.0's detail output also shows the connection-pool mode, which bex returns as theconnectionPoolenum (pgbouncer/none); after suspend,statusissuspended(w5/061) so text Status stays truthful when the renderer omits thesuspendedfield -
postgres update <id|name>-
--name— rename; opaquedpg-id stays stable -
--plan— rejects unsupported retained replicas/pooler/autoscaling or allocated storage before writes; accepted changes preserve the disk high-water mark (w8/m50) -
--disk-size-gb— grow succeeds; shrink returns a named 400 and leaves intent/state unchanged (w2/m60 exact PATCH regression) -
--disk-autoscaling— paid enable/disable; free enable is refused and legacy free disable remains allowed (w8/m50) -
--connection-pool <none|pgbouncer>— w8/m33 / v2.26.0: PATCH decodesconnectionPoolviaresolvePooler; paid enable and legacy free disable are allowed, free enable is refused (w8/m50) -
--high-availability -
--ip-allow-list cidr=…,description=…— replaces the list and enables external access for matching sources; description returns (w2/m166) -
--clear-ip-allow-list— clears rules and disables primary/pooler/replica external routes after convergence (w2/m166); internal access remains available - [-]
--datadog-api-key <string>— deliberate non-goal; named 400, credential never persisted - [-]
--datadog-site <string>— deliberate non-goal; named 400 - [~]
--project <id|name>— flag parsed; needs an existing project - [~]
--environment <id|name>— shared canonical-ID resolver covered by w2/m165 source/regression checks; update variant not separately live-tested
-
-
postgres suspend <id|name> -
postgres resume <id|name> -
postgres delete <id|name>— unchanged CLI contract; backup purge is terminally observed (w2/m61)
-
-
restart <resourceID>— restarts by typed id (restart <resourceID>is the CLI's only documented form) -
services— list services and datastores; bareserviceslists them-
-e, --environment-ids <ids>— filter list by environment IDs -
--include-previews— accepted list flag -
services create— returns the{service,deployId}record; rawdashboardUrlis…/<type>/<srv-id>(/web/,/cron/,/static/); a cardless paid create uses Render's declared 402errorschema and keeps the checkout instruction inmessage-
--name -
--type—web_service,cron_job,static_siteall accepted -
--runtime— round-trips (build not exercised in dev-9) -
--repo— round-trips (build not exercised) -
--branch -
--image— exact generatedimage.ownerId:""passes the composed server and the deployed production API (w5/m49) -
--plan - [~]
--region— accepted; bex overwrites tolocal-capd(CLI enum rejectslocal-capditself) -
--num-instances -
--build-command— round-trips (build not exercised) -
--start-command -
--pre-deploy-command -
--cron-command— oncron_job, on every runtime. A cron's command is runtime-independent on the wire: the pinned client's cron builder emits it asenvSpecificDetails.startCommandfor all runtimes (pkg/service/create.go buildCronEnvSpecificDetails, no docker branch) and its clone path reads it back the same single way (pkg/service/clone.go:143,328-334→AsNativeEnvironmentDetails().StartCommand). bex therefore accepts either spelling on write (dockerCommandwins when both are sent) and emits both on read for a docker cron —dockerCommandfor the runtime-keyed readers (dashboard, blueprint generation) andstartCommandfor the client that re-sends it. Before w9/m165 the docker path dropped the command on create and projected an emptydockerCommand, soservices update --cron-commandprinted a no-op it had not performed andcreate --fromcloned an empty command; verified live on dev-9 2026-09-21 across create → read → update → clone, and guarded by thedocker-cron-create/docker-cron-update/docker-cron-clonelegs ofscripts/cli-services-parity-verify.sh. An image-runtime cron projected noenvSpecificDetailsat all, socreate --fromcloned it with no command and every run executed the image entrypoint. Since w1/m167 it emitsenvSpecificDetails: {startCommand}, and only that; an image cron with no command, and an image web service, still emit no block.lego/cli/image_cron_clone_contract_test.godrives the pinned clone path against the read bex-api is proven to emit. -
--cron-schedule— oncron_job -
--health-check-path -
--auto-deploy - [-]
--previews—400 "not supported by this platform"(deliberate non-goal) -
--publish-directory— onstatic_site -
--root-directory— on repo services -
--env-var KEY=VALUE— exact configured App readback -
--secret-file NAME:PATH— exact configured OpenBao readback; content never logged -
--registry-credential <cred>— exact credential metadata plus authenticated private-image pull toRunning -
--ip-allow-list cidr=…,description=…— ordered CIDR and description round-trip -
--build-filter-path <path> -
--build-filter-ignored-path <path> -
--maintenance-mode— requires a paid plan (400on free) -
--maintenance-mode-uri <uri> -
--max-shutdown-delay <seconds> -
--environment-id <id> - [~]
--from <serviceID>— clones fine, but needs an explicit--region(CLI re-validates the source'slocal-capd)
-
-
services update <service>- workload type is intentionally not an update flag — current CLI schema matches Render's immutable-type contract; bex Blueprint and CRD admission reject the same transition
-
--name— rename; the shared API rejects controls/line separators, more than 100 Unicode code points after trimming, and registered resource-ID lookalikes (w8/034). The pinned CLI trims before HTTP and cannot express an empty clear; direct REST/GraphQL/MCP preserve the empty-clear contract. The 100-code-point bound is Bex policy; Render's exact limit is unverified. -
--plan - [~]
--runtime— upstream CLI guard; exits before any request (cannot switch runtimes via the CLI) -
--repo -
--branch -
--image— exact generatedimage.ownerId:""PATCH passes locally and against the deployed production API (w5/m49) -
--build-command -
--start-command -
--pre-deploy-command -
--cron-command— exact configured native-cron replacement -
--cron-schedule -
--health-check-path -
--auto-deploy - [-]
--previews—400 "not supported by this platform" -
--publish-directory— onstatic_site -
--root-directory— on repo services (image services correctly400) -
--registry-credential— distinct credential A→B replacement plus authenticated rollout -
--ip-allow-list cidr=…,description=…— ordered CIDR and description replacement -
--build-filter-path <path> -
--build-filter-ignored-path <path> -
--maintenance-mode -
--maintenance-mode-uri <string> -
--max-shutdown-delay <int>
-
services instances <serviceID>— decodes live pod ids; suspended service returns[]; unknown id fails not-found -
services delete <serviceID>— acknowledgement shape is unchanged; production image deletion and immediate first-build deletion reached GET 404 + official-CLI list absence inside five minutes, with zero cluster/Zot residue (w5/m49)
-
- [-]
workflows— Render Workflows (deliberate bex non-goal;GET /v1/workflowsis a200 []stub, everything else404/405/TTY-blocked)- [-]
workflows list— returns empty from the stub (exit 0) - [-]
workflows create—405 Method Not Allowed - [-]
workflows init— client-side scaffolding works, no bex call - [-]
workflows dev— client-side dev server, no bex call - [-]
workflows start/workflows cancel— shortcuts;404/ TTY-blocked - [-]
workflows tasks—tasks list/tasks runs {start,list,show,cancel}all404/ TTY-blocked - [-]
workflows versions—versions list/versions release404
- [-]
-
workspaces— lists the caller's realtea-…workspace
-
login— recognizesRENDER_API_KEY(already-authenticated short-circuit); full browser/device flow verified separately in production. w8/m27 compatibility: the official launcher stays unmodified and platform-marked (bex.co/platform-client). The dispatch-time scope matrix (w8/m27, docs/ADR069-security-review-round14.md) requires every human OAuth token — platform clients included — to carry a granular capability; a token with no granular scope is refused with403, which the CLI surfaces as "you are not allowed to take this action." So bex's own device-flow adapter (internal/cliauth, not a CLI fork) requestsopenid offline_access bex.read bex.write bex.sensitiveon the unmodified CLI's behalf — the exact granular set the Render-CLI Hydra client is provisioned to allow withskip_consent(scripts/auth-bootstrap-client.sh) — so the minted platform-client token satisfies read/write/sensitive/mint op classes.bex.apiremains a compatibility alias only and is never requested.TestDeviceGrantScopeCoversEveryOpClasspins the coverage; do not trim the adapter's requested scope back to identity-only. Do not fork the CLI. -
logout— drives the OAuth revoke path; the credential'sclient_credentialsgrant fails afterward -
whoami— reports the key-minting user's email (Kratos-admin lookup) -
workspace— manage the CLI's active workspace-
workspace current— returns the active tenant -
workspace set <id>— set→persist→read round-trip verified
-
-
kv-cli [id|name]— automated PTY plusscripts/cli-compat.sh kv-cli-verifyproved publicrediss://by opaque id (PING/SET) and display name (GET/DEL) through the unmodified v2.21.0 CLI; see the 2026-07-18 production evidence -
pgcli [id|name]— a bounded pseudo-terminal crosses the real interactive-only guard, resolves one disposable public bex Postgres by opaque id and exact name, preserves passthrough flags, and runsSELECT 1 AS bex_pgcli_probe;through real pgcli 4.5.0 twice; the negative piped guard, redacted shim handoff, SQL markers, and cleanup proof are recorded in the pgcli CLI artifact. w4/m116/t001 (2026-09-20):pgcliconsumes the same external connection string aspsql, so it inherits the samesslmode=verify-fulltrust requirement and the same launcher-side CA provisioning described in thepsqlrow below (pgclireaches libpq through psycopg, which honorsPGSSLROOTCERTidentically). Covered by the launcher's tests; not re-probed live against production in that task -
psql [id|name]— the checksum-verified, unmodified v2.21.0 CLI resolved one disposable public Postgres by opaque id and exact name, passed its client-side public/32allow-list gate, consumed the externalsslmode=requireconnection string through pg-sni-proxy, and returnedSELECT 1 AS bex_psql_probe;through realpsql18.4 twice. Unknown-name, absent-allowlist, endpoint-convergence, credential-redaction, and cleanup behavior are asserted by the live verifier and its hermetic regression (the psql CLI artifact). Re-graded for the verify-full era (w9/063, live probe 2026-09-20): thesslmode=requireevidence above predates w4/m95, which deliberately moved a public database's external strings tosslmode=verify-full(lego/backend/internal/postgres/service.go:759,761,775,789). The pinned CLI handsExternalConnectionStringtopsqlverbatim (pkg/tui/views/psql.go:75,213— bex cannot override the mode without forking the client), so on a machine with no CA installedbex psql <dpg-id> -c 'SELECT 1 AS bex_psql_probe;'fails withroot certificate file … does not exist, andPGSSLROOTCERT=systemfails withcertificate verify failed(the CNPG CA is private, by design). This is intended behavior, not a regression or a gap: the supported path is to install the CA from the dashboard's Connections panel (connection-infoserverCaCertificate) and pointPGSSLROOTCERT=/path/to/<id>-ca.pemat it — with that, the same command returned the probe row1on 2026-09-20 (bex v0.2.1, pin v2.27.0, production). Render needs no counterpart because it presents publicly-trusted certificates; this is a recorded bex extension. Automated by w4/m116/t001 (2026-09-20): requiring every user to install a CA by hand was not an acceptable out-of-box journey, so thebexlauncher now provisions it — the same interception class as the root--versionpath (lego/cli/main.go,lego/cli/internal/pgtrust/). Before delegating apsql/pgcliinvocation that names a database, the launcher readsserverCaCertificatefromGET /v1/postgres/{id}/connection-info, writes it to a private (0600) temporary file, and setsPGSSLROOTCERTfor the child process, removing the file when the session ends. TLS is never downgraded and the connection string is never rewritten; an explicitPGSSLROOTCERTor an existing~/.postgresql/root.crtalways wins, an internal-only database is untouched, and a 503 (CA not provisioned yet) is refused with a message that names the cause instead of psql's opaque certificate error. The behavior is client-side only, so an unmodifiedrenderbinary sees a byte-identical request/response contract and keeps the manual-CA journey above. Proven by the launcher's tests (lego/cli/psql_trust_test.go,lego/cli/internal/pgtrust/*_test.go); not yet re-probed live against production from a CA-less machine.-
-c, --command <SQL>— both id and exact-name selectors returned the asserted probe row through the real non-TTY client path
-
-
ssh [serviceID|serviceName|instanceID]— live-proven through the public production gateway with the unmodified v2.21.0 CLI: service name, service id → Any instance, and complete instance id all reached the asserted running container; the upstream exact-instance picker bug is documented above, while the direct instance-id form proves exact targeting (ADR035)- [-]
-e, --ephemeral— deliberate bex non-goal (flag parses) - [-]
--plan <string>— tied to--ephemeral(non-goal)
- [-]
- [~]
blueprints— manage Blueprints (infrastructure as code)-
blueprints validate <render.yaml>— the unmodified v2.21.0 CLI successfully decoded both a validhello-go/render.yamlresponse and an invalidautoDeployTrigger: checksPassresponse from freshly deployedapi.bex.coon 2026-08-03 (the latter carriedservices[0].autoDeployTriggerplus source line/column). w8/m19 production re-grade (2026-08-16): after the m19 image rollout, the same unmodified CLI and the raw multipart response accepted one custom.yamlfixture containing staticbuildCommand, monorepodockerContext, and private-imageimage.creds.fromRegistryCreds.name; the response wasvalid:true. This closes the CLI validate transport/decoding grade. It does not turn the Blueprint feature's overall partial row into blanket parity: the capability registry still intentionally rejects its documented subset, and the separate live apply legs are not evidence for this CLI command. w8/m33 / v2.25.0:blueprints validatenow exits status 1 on an invalid blueprint for every--outputmode (previously it could exit 0 while printingvalid:false); this is a client-side exit-code change over the samevalid:true|falsebody bex already returns, so the transport grade is unaffected. - Bex Blueprint datastore apply/export (w2/m166): an omitted
ipAllowListon sync preserves rules and public intent;[]disables external access and nonempty rules enable matching sources. Explicit lists have the same effect at create; allowed omission retains the Bex-native private default. Export writes[]for private resources,0.0.0.0/0plus::/0for public/empty resources, and existing rules for restricted public resources. Render YAML cannot retain inactive rules separately from publication, so the first apply can normalize storage without changing effective access; its plan must show that change, followed by a no-op plan. These are Bex apply/export semantics, not additional pinned-CLI commands or a blanket live-parity grade. State matrix and export limits.
-
-
environments <projectID>— decodes the RC15 cursor envelope into real values; unknown project fails not-found -
projects— lists projects in the active workspace
-
docs— unmodifiedrender docsopensrender.com/docs(client-side; no bex call). Thebexlauncher overrides this command to open the Bex CLI guide on GitHub (lego/cli/internal/branding.DocsURL); that is a launcher branding overlay, not a change to the unmodified-CLI grade above. -
completion [bash|zsh|fish|powershell]— w2/m79 / v2.24.0: generates shell autocompletion scripts client-side (no bex call); help + local smoke on the unmodified pin - [~]
ea— early-access surfaces;ea sandboxescreate/list/stop (w3/m32) and exec (w3/m33) all ship on the gVisor substrate,ea objectsstill out of the compatibility target. The pinned CLI's group is plural (Use: "sandboxes", nosandboxalias) —bex ea sandbox …exits 1 withunknown command "sandbox" for "bex ea". Re-diff those names on every pin bump.- [-]
ea objects list— bex/v1/objects→404(--localworks client-side) - [-]
ea objects put—404(--localworks client-side) - [-]
ea objects get—404(--localworks client-side) - [-]
ea objects delete—404(--localworks client-side) -
ea sandboxes create— verified with the real CLI (v2.21.0):POST /v1/sandboxes→201. The CLI sends only--plan(no template flag), so an empty template resolves tobase, and it sendsownerIdin the body (bex reads body-or-query).plan/region/timeoutSecondsand the effective{default:"deny-all"}policy round-trip from reserved runtime metadata. Lifetime (w5/m99): omitted ortimeoutSeconds: 0means the documented default/maximum of 86400 (24h), matching the pinned CLI's--timeouthelp — never an immortal sandbox; the read shape always returns the effective bound.allow-allis a deliberate named 400 (SANDBOX_NETWORK_POLICY_UNSUPPORTED) because bex will not claim an unenforced policy. Runs on the gVisor substrate in the caller's<ws>-sandboxnamespace. ID shape (w9/m94, code landed 2026-09-20): theida create returns is a bex typed id (sbx-<xid>), not the OpenSandbox substrate's UUID. The live hunt of 2026-09-20 caught production returning the bare UUID (c372c97e-…), which the pinned client'sea sandboxes copyarg parser rejects client-side (cmd/sandboxcopy.go:^(sbx-[A-Za-z0-9]+):(.*)$). bex now mints the public id itself and carries it as durable sandbox metadata; every read path (list/get/stop/exec, REST + GraphQL + MCP) reports that id and dual-accepts a pre-w9/m94 substrate id. Pinned bylego/backend/internal/sandbox/publicid_test.goand, on the client side,lego/cli/sandbox_id_contract_test.go(a bex-shaped id parses; a bare UUID is refused before any request). Live-verified 2026-10-02 (w9/m94 t004): releasedbex v0.2.1(pin v2.27.0) against production, workspacebex-canary, human device login:ea sandboxes create --plan=starter --timeout=600 -o json→"id": "sbx-…";copy ./probe <sbx-id>:/tmp/probepasses the client's arg parser and uploads (and the reverse download round-trips the bytes);exec <sbx-id> -- true→ exit 0,-- sh -c 'echo out; echo err >&2; exit 7'→ exit 7;listshows thesbx-id;stop --confirm→ exit 0, thenlistis empty andexec/GETanswer404 SANDBOX_NOT_FOUND. RESTGET /v1/sandboxes/{id}, GraphQLsandbox/sandboxes, and MCPlist_sandboxesall reported the identicalsbx-id; a bare-UUID id answers the same non-enumerating404 SANDBOX_NOT_FOUND, never 500. -
ea sandboxes exec— re-graded and re-verified live 2026-09-15 (w7/m147): with the pinned launcher against production,exec <id> -- sh -c 'echo out; echo err >&2; exit 7'printsout/errand exits 7,-- trueexits 0, an unknown or terminated id exits 1 withreceived response code 404 (SANDBOX_NOT_FOUND): sandbox not found; the raw handshake mints201 {executionId: exe-…, expiresAt (+60 s), method: POST, token, uri}and a replayed, altered-command, cross-sandbox, OAuth-bearer, or bare redeem is refused with 409/403/403/401/401 JSON{message}bodies. Since the v2.24.0 pin (w2/m79) the client runs a two-step handshake, not the singlePOST /execthe earlier evidence below describes:POST /v1/sandboxes/{id}/runs/stream/token?ownerId=with{"command"}→201 {executionId, expiresAt, method, token, uri}, thenPOST <uri>withAuthorization: Bearer <token>,Accept: text/event-stream,{"command"}→200SSE. bex serves both since m147: the mint is a gated write, and the redeem route sits outside the OAuth gate accepting only that single-use, ≤60 s, HMAC connect token bound to workspace + sandbox + execution + command (an OAuth token is refused there; a connect token is refused everywhere else). The SSE terminal events now carry the keys every pin from v2.21.0 to v2.27.0 decodes —event: exit{"exit_code":N}andevent: error{"status":404,"message":"sandbox is no longer running"}— where bex previously emittedexitCodeand{error,code}, so a failing command exited 0 and an error printedstatus 0:; the v2.21.0 proof below only ran exit-0 commands and could not see it. Pinned two ways:lego/cli/sandbox_contract_test.godrives the pinnedpkg/sandbox.Repoagainstlego/cli/testdata/sandbox-exec-contract.json(exit 7 with stdout + stderr; terminated-target message), andlego/backend/internal/sandbox/connect_test.goproves the real mint, redeem, and gateway produce those exact bytes. Legacy single-stepPOST /v1/sandboxes/{id}/execstill streams for ≤ v2.21 clients and MCP. Prior evidence (v2.21.0, w3/m33): verified on prod with the real CLI (w3/m33):POST /v1/sandboxes/{id}/execstreams the command's stdout/stderr + exit as SSE (event: output/exit).render ea sandbox exec <id> -- sh -c 'uname -r'returned4.19.0-gvisorfrom inside the gVisor sandbox (historical command string; at this pin the group is plural —bex ea sandboxes exec— with nosandboxalias). Runs via k8spods/execconfined to the isolated ssh-gateway (bex-api authorizes + reverse-proxies, never gainspods/exec); execd's gRPC API and the CLI's older two-step token flow are both unused. -
ea sandboxes copy— bounded implementation (w7/m150): the pinned client mints withPOST /v1/sandboxes/{id}/files/{operation}/token?ownerId=&path=and no request body, then streams to the returned PUT/GET URI. The API and isolated gateway now serve both operations with owner/admin and fresh permission checks, path-bound single-use tokens backed by the shared nonce store, 256 MiB wire/decoded limits, at most 10,000 archive entries, and a five-minute deadline. Supported paths must be clean absolute paths; links and special archive entries are refused; directory uploads require a new destination, while single files may replace a regular file. Uploads stage before publication; gzip completion distinguishes a successful download from late execution failure. The shared fixture and pinned-client tests cover the actual wire contract; backend tests compose the real mint/redeem and gateway handlers. Live-verified 2026-10-03 (w7/m150 t005, dev-7): the pinned launcher uploaded and downloaded a binary file, an empty file and a nested directory byte-identically through a real sandbox and the in-cluster gateway. The live refusal matrix (traversal, foreign owner, replay, wrong path or method, oversize, missing parent, existing directory, interrupted upload) behaved as documented, and every fixture was removed. See the exact supported subset and failure behavior. The earlier claim that the client rejects escaping symlinks was too broad: it can create one, but refuses subsequent traversal through it. Sandbox groups remain a separate non-goal. - [~]
ea sandboxes create --env-var/--env-file— not supported, refused by name (w9/067). The pinned client sends these as a first-classenvobject (pkg/sandbox/service.go:91) and bex does not plumb environment into a sandbox, so the create answers400 SANDBOX_ENV_UNSUPPORTEDnaming the flags — previously it fell through strict decoding asunknown field "env", which names an internal wire key rather than the unsupported feature. Whether bex should support sandbox env at all is an open scope decision, not a defect.--snapshot-idrefuses the same way (SANDBOX_SNAPSHOTS_UNSUPPORTED), consistent with its[-]grade below. -
ea sandbox-groups list— gap (graded 2026-09-15):GET /v1/sandbox-groups?ownerId=→ bex404. Not implemented; same candidate note. - [-]
ea sandboxes snapshots {create,get,list}— non-goal at this pin (see the v2.27.0 re-baseline bullet above): the snapshot routes are outside the bexeacompat target; help-graded only. -
ea sandboxes list— verified with the real CLI:GET /v1/sandboxes→200, returned as the CLI's cursor-paginated[{sandbox, cursor}]envelope. Results are additionally filtered to the caller's durable owner metadata; workspace admins are the explicit override. -
ea sandboxes stop— verified with the real CLI:POST /v1/sandboxes/{id}/terminate→204; a foreign/ownerless id returns the sameSANDBOX_NOT_FOUND404 as an absent id.
- [-]
- [~]
skills— manage Render agent skills for AI coding tools (client-side; no bex dependency)-
skills install— works;--dry-runreports intended changes without writing -
skills list— upstream CLI panic (nil-pointer) in every non-TTY output mode -
skills update— same upstream non-TTY panic; no--dry-run -
skills remove— works
-
-
help [command]— CLI builtin, no bex call