Summary
Add a brev links command family to expose and manage secure links (public HTTP/HTTPS port exposure) for a workspace — the CLI is currently the only place this can't be done. Today secure links are UI-only: the environments UI renders them as an "HTTP services" / "Share a Service" table on the environment and registered-node pages, but brev has no way to list, create, or modify them.
Background
Dev-plane exposes the full secure-link surface on both EnvironmentService and ExternalNodeService as Connect-RPC, and the CLI's pinned buf.build proto already contains every one of these RPCs and the Port message — no server or proto work is needed:
| RPC |
Effect |
OpenHTTPPort(env_id, port_number, custom_hostname, http_protocol, authorized_emails, allow_public_unauthenticated) |
Expose a port as a public link → returns Port (idempotent; AlreadyExists on hostname conflict) |
GetNetworkInfo(env_id) |
List ports incl. hostname, port_number, server_port, authorized_emails, allowed_sources, allow_public_unauthenticated, http_protocol |
SetPortTarget(port_id, port_number) |
Retarget which server port a link forwards to |
SetPortAllowedSources(port_id, cidr_blocks) |
IP/CIDR allowlist |
SetHTTPPortProtocol(port_id, http_protocol) |
Toggle HTTP ⇄ HTTPS |
SetHTTPPortAccess(port_id, authorized_emails, allow_public_unauthenticated) |
Email allowlist + public/no-auth toggle |
ClosePort(port_id) |
Remove a link |
The hostname returned by OpenHTTPPort is the full public URL (<subdomain>.<skybridge-endpoint>, e.g. myapp-r7zb490ph.apps.run.brev.nvidia.com) — the CLI should print it verbatim, never reconstruct it. Authz is enforced server-side per RPC (operation:environment:ports:maintain); the CLI only needs the user's existing token.
The CLI already has all the plumbing: register.NewEnvironmentServiceClient / register.NewNodeServiceClient (Bearer-token Connect clients), GetNetworkInfo is already exercised in pkg/cmd/refresh/sshaccess.go, and name→ID resolution exists via GetWorkspaceByNameOrID. This is a pure client-side feature.
Proposed interface
Noun-root parent command, mirroring the brev org {create,ls,set} precedent and the proposed brev launchable family (#391):
brev links ls <workspace> # list secure links (URLs, ports, access)
brev links open <workspace> [port] # expose a port as a secure link
brev links close <workspace> <id|hostname> # remove a link
brev links set-target <workspace> <id> <port>
brev links set-access <workspace> <id> --emails a@x,b@y --public|--private
brev links set-sources <workspace> <id> --cidr 0.0.0.0/0
brev links set-protocol <workspace> <id> --https|--http
brev links open flags:
--hostname NAME # custom subdomain prefix (default: derived from port + env id, matching the UI)
--https # expose as HTTPS (default)
--http # expose as plain HTTP
--public # allow unauthenticated public access
--emails a@x,b@y # authorized emails (default: current user)
Feature set (acceptance criteria):
Debate — how this feels alongside the existing CLI
Noun parent vs. flat verbs. The CLI is mostly verb-first (start, stop, create, open, exec) but already uses noun parents where an operation has a real surface: org {set,ls,create}, ls orgs, and the pending launchable {create,deploy} (#391). Secure links have 6+ operations, so a flat set of top-level verbs (expose, unexpose, link-target, …) would add ~6 commands to an already ~40-command brev help and read as noise. brev links {ls,open,close,set-*} keeps the top level clean and groups by intent. This is the strongest argument for the parent.
links vs secure-links vs expose. "Secure links" is the product term (the authz operations are literally operation:environment:secureLinks:*), so secure-links is the most explicit option but verbose in daily use. links is shorter, reads naturally (brev links ls my-ws), and the Short help text can carry the full term. Flat expose/unexpose convey only the create side and leave listing (links?) and modification without a home — rejected. Recommendation: links root, with secure-links noted as the explicit alias if maintainers prefer product fidelity.
open collision. brev open already means "open an editor on the instance." Inside brev links, open = "open a new link" — which actually matches the RPC (OpenHTTPPort) and reads correctly in context, but the collision is worth a deliberate call. Alternative: brev links create (also fine, just diverges from the RPC name). Open question for maintainers.
Relationship to port-forward. port-forward is a local, private SSH tunnel; a secure link is a public, HTTPS endpoint. Both belong in the "Instance Access" section of brev help (alongside copy, exec, open, shell, grant-ssh). To keep the boundary obvious, help text should cross-reference: e.g. port-forward "private/local", links open "public/shared". Without this, users will reasonably try port-forward to publish a service and be confused.
Composability (the PRD direction). brev links ls --json should emit structured rows so brev links ls my-ws --json | jq '.[] | select(.protocol=="HTTPS")' works, and brev links open my-ws 8080 | xargs open should yield a bare URL when stdout is piped (pipe detection is already a stated design principle). Optionally support stdin passthrough so brev ls | brev links ls lists links across many workspaces — consistent with brev stop/brev delete consuming instance names from stdin.
Implementation notes (low risk, all client-side)
- New package
pkg/cmd/links/ following the portforward/grantssh layout: per-command Store interface, completions.GetAllWorkspaceNameCompletionHandler, cmderrors.TransformToValidationError, breverrors.WrapAndTrace, terminal colors via t.Vprintf.
- Annotate root
links with map[string]string{"access": ""} so it lands in the "Instance Access" help section; subcommands annotated so they don't appear at top level (the orgsubcommand pattern).
- Reuse
register.NewEnvironmentServiceClient(store, config.GlobalConfig.GetBrevPublicAPIURL()) and the GetNetworkInfo call site in refresh/sshaccess.go as the reference implementation.
- Hostname composition: replicate
buildSecureLinkCustomHostname from the environments UI (name-or-port + -<envID> suffix) so CLI-created links look identical to UI-created ones.
Summary
Add a
brev linkscommand family to expose and manage secure links (public HTTP/HTTPS port exposure) for a workspace — the CLI is currently the only place this can't be done. Today secure links are UI-only: the environments UI renders them as an "HTTP services" / "Share a Service" table on the environment and registered-node pages, butbrevhas no way to list, create, or modify them.Background
Dev-plane exposes the full secure-link surface on both
EnvironmentServiceandExternalNodeServiceas Connect-RPC, and the CLI's pinned buf.build proto already contains every one of these RPCs and thePortmessage — no server or proto work is needed:OpenHTTPPort(env_id, port_number, custom_hostname, http_protocol, authorized_emails, allow_public_unauthenticated)Port(idempotent;AlreadyExistson hostname conflict)GetNetworkInfo(env_id)hostname,port_number,server_port,authorized_emails,allowed_sources,allow_public_unauthenticated,http_protocolSetPortTarget(port_id, port_number)SetPortAllowedSources(port_id, cidr_blocks)SetHTTPPortProtocol(port_id, http_protocol)SetHTTPPortAccess(port_id, authorized_emails, allow_public_unauthenticated)ClosePort(port_id)The hostname returned by
OpenHTTPPortis the full public URL (<subdomain>.<skybridge-endpoint>, e.g.myapp-r7zb490ph.apps.run.brev.nvidia.com) — the CLI should print it verbatim, never reconstruct it. Authz is enforced server-side per RPC (operation:environment:ports:maintain); the CLI only needs the user's existing token.The CLI already has all the plumbing:
register.NewEnvironmentServiceClient/register.NewNodeServiceClient(Bearer-token Connect clients),GetNetworkInfois already exercised inpkg/cmd/refresh/sshaccess.go, and name→ID resolution exists viaGetWorkspaceByNameOrID. This is a pure client-side feature.Proposed interface
Noun-root parent command, mirroring the
brev org {create,ls,set}precedent and the proposedbrev launchablefamily (#391):brev links openflags:Feature set (acceptance criteria):
brev links ls <workspace>lists links with URL, public port, target port, protocol, and access (emails / public / CIDRs) — reusing theGetNetworkInfopattern already inrefresh/sshaccess.gobrev links openprints the returned public URL; idempotent re-open returns the existing linkbrev links close/set-*map 1:1 to the RPCs above--jsonoutput forlsand a bare-URL piped output foropen(per the composable-CLI PRD)--node NAME(resolves viahelpers.ResolveNodeByName, thegrant-ssh/revoke-sshpattern) — dev-plane'sExternalNodeServiceexposes the same RPCsDebate — how this feels alongside the existing CLI
Noun parent vs. flat verbs. The CLI is mostly verb-first (
start,stop,create,open,exec) but already uses noun parents where an operation has a real surface:org {set,ls,create},ls orgs, and the pendinglaunchable {create,deploy}(#391). Secure links have 6+ operations, so a flat set of top-level verbs (expose,unexpose,link-target, …) would add ~6 commands to an already ~40-commandbrev helpand read as noise.brev links {ls,open,close,set-*}keeps the top level clean and groups by intent. This is the strongest argument for the parent.linksvssecure-linksvsexpose. "Secure links" is the product term (the authz operations are literallyoperation:environment:secureLinks:*), sosecure-linksis the most explicit option but verbose in daily use.linksis shorter, reads naturally (brev links ls my-ws), and theShorthelp text can carry the full term. Flatexpose/unexposeconvey only the create side and leave listing (links?) and modification without a home — rejected. Recommendation:linksroot, withsecure-linksnoted as the explicit alias if maintainers prefer product fidelity.opencollision.brev openalready means "open an editor on the instance." Insidebrev links,open= "open a new link" — which actually matches the RPC (OpenHTTPPort) and reads correctly in context, but the collision is worth a deliberate call. Alternative:brev links create(also fine, just diverges from the RPC name). Open question for maintainers.Relationship to
port-forward.port-forwardis a local, private SSH tunnel; a secure link is a public, HTTPS endpoint. Both belong in the "Instance Access" section ofbrev help(alongsidecopy,exec,open,shell,grant-ssh). To keep the boundary obvious, help text should cross-reference: e.g.port-forward"private/local",links open"public/shared". Without this, users will reasonably tryport-forwardto publish a service and be confused.Composability (the PRD direction).
brev links ls --jsonshould emit structured rows sobrev links ls my-ws --json | jq '.[] | select(.protocol=="HTTPS")'works, andbrev links open my-ws 8080 | xargs openshould yield a bare URL when stdout is piped (pipe detection is already a stated design principle). Optionally support stdin passthrough sobrev ls | brev links lslists links across many workspaces — consistent withbrev stop/brev deleteconsuming instance names from stdin.Implementation notes (low risk, all client-side)
pkg/cmd/links/following theportforward/grantsshlayout: per-commandStoreinterface,completions.GetAllWorkspaceNameCompletionHandler,cmderrors.TransformToValidationError,breverrors.WrapAndTrace, terminal colors viat.Vprintf.linkswithmap[string]string{"access": ""}so it lands in the "Instance Access" help section; subcommands annotated so they don't appear at top level (theorgsubcommandpattern).register.NewEnvironmentServiceClient(store, config.GlobalConfig.GetBrevPublicAPIURL())and theGetNetworkInfocall site inrefresh/sshaccess.goas the reference implementation.buildSecureLinkCustomHostnamefrom the environments UI (name-or-port +-<envID>suffix) so CLI-created links look identical to UI-created ones.