Summary
Add a cwsandbox join-token command that creates a Tower join token via the ATC API. This replaces the manual curl workflow documented in the Sandbox Tower Helm chart installation guide, giving operators a single command to generate tokens for new Tower deployments.
Context: Join tokens are currently created by manually calling the ATC REST API (POST /v1beta1/towers/tokens). Brandon identified the need for a CLI helper in [Slack discussion] after manually distributing tokens to staging towers. This is a natural fit for the cwsandbox CLI.
Blocked by: #10 / PR #60 (base CLI with ls and exec commands)
Motivation
When deploying the Sandbox Tower Helm chart to a new cluster, operators need a join token. Today this requires:
- Constructing a
curl POST request with the correct endpoint, headers, and JSON body
- Extracting the token from the JSON response
- Figuring out the right
kubectl and helm commands to apply it
A CLI command makes this workflow self-documenting and reduces mistakes during Tower onboarding.
Proposed UX
cwsandbox join-token \
--tower-id my-cluster-east \
--tower-group-id production \
--ttl 3600 \
--description "Join token for my-cluster-east"
Default output prints the token value and the follow-up commands needed to use it:
Join token created successfully.
Token: <token-value>
Token ID: a1b2c3d4-...
Tower: my-cluster-east
Group: production
Expires: 2026-02-28T15:02:21Z
To use this token, run:
kubectl create namespace sandbox-system
kubectl create secret generic sandbox-tower-join-token \
--namespace sandbox-system \
--from-literal=token='<token-value>'
Then install the Helm chart:
helm install sandbox-tower coreweave/sandbox-tower \
--namespace sandbox-system \
-f sandbox-tower-values.yaml
With --json flag, output the raw API response for scripting:
{
"token": "...",
"tokenId": "a1b2c3d4-...",
"towerId": "my-cluster-east",
"towerGroupId": "production",
"expiresAt": "2026-02-28T15:02:21Z",
"organizationId": "org-..."
}
Flags
| Flag |
Required |
Default |
Description |
--tower-id |
Yes |
- |
Unique identifier for the Tower (e.g. cluster name). Must be unique per org. |
--tower-group-id |
No |
"default" |
Group identifier for organizing related Towers |
--ttl |
No |
3600 (1h) |
Token TTL in seconds |
--description |
No |
Auto-generated from tower-id |
Human-readable description |
--label |
No |
- |
Key=value label, repeatable (e.g. --label env=prod --label team=infra) |
--json |
No |
false |
Output raw JSON response |
API Reference
Source: aviato/proto/coreweave/aviato/v1beta1/tower_join.proto
POST /v1beta1/towers/tokens - Create
Request:
tower_id string REQUIRED, must be unique per org
tower_group_id string optional, defaults to "default"
ttl_seconds int32 optional, defaults to 3600 (1 hour)
description string optional
labels map<string, string> optional
Response:
token string the join token (single-use, treat as secret)
token_id string UUID for tracking
tower_id string
tower_group_id string
expires_at Timestamp
organization_id string
Behavior:
- One active token per tower per org. Creating a new token auto-revokes the previous active token for that tower.
- Token is single-use: marked "used" after the Tower exchanges it for mTLS certs via
POST /v1beta1/towers/join.
Token lifecycle
States: active -> used | revoked | expired
active: newly created, not yet exchanged
used: Tower exchanged the token for mTLS certs
revoked: explicitly revoked by user
expired: TTL elapsed (evaluated lazily at query time)
Implementation Notes
- This is an HTTP REST call, not gRPC. The existing SDK uses gRPC for sandbox operations, but the join token endpoint is REST. Use
httpx (already a dependency) or requests.
- Authentication should reuse the existing
CWSANDBOX_API_KEY path from _auth.py. W&B auth is not relevant for this admin operation.
- The command is an admin/operator action, distinct from the sandbox lifecycle commands (
ls, exec). Bare cwsandbox join-token is sufficient for now; if list/revoke are added later, make bare invocation an alias for join-token create so existing usage doesn't break.
- The token value is a secret: consider warning if outputting to a terminal.
Future Work
cwsandbox join-token list - list tokens with status filtering (GET /v1beta1/towers/tokens)
cwsandbox join-token revoke <token-id> - revoke active tokens (DELETE /v1beta1/towers/tokens/{token_id})
The API already supports both operations.
Summary
Add a
cwsandbox join-tokencommand that creates a Tower join token via the ATC API. This replaces the manualcurlworkflow documented in the Sandbox Tower Helm chart installation guide, giving operators a single command to generate tokens for new Tower deployments.Context: Join tokens are currently created by manually calling the ATC REST API (
POST /v1beta1/towers/tokens). Brandon identified the need for a CLI helper in [Slack discussion] after manually distributing tokens to staging towers. This is a natural fit for thecwsandboxCLI.Blocked by: #10 / PR #60 (base CLI with
lsandexeccommands)Motivation
When deploying the Sandbox Tower Helm chart to a new cluster, operators need a join token. Today this requires:
curlPOST request with the correct endpoint, headers, and JSON bodykubectlandhelmcommands to apply itA CLI command makes this workflow self-documenting and reduces mistakes during Tower onboarding.
Proposed UX
cwsandbox join-token \ --tower-id my-cluster-east \ --tower-group-id production \ --ttl 3600 \ --description "Join token for my-cluster-east"Default output prints the token value and the follow-up commands needed to use it:
With
--jsonflag, output the raw API response for scripting:{ "token": "...", "tokenId": "a1b2c3d4-...", "towerId": "my-cluster-east", "towerGroupId": "production", "expiresAt": "2026-02-28T15:02:21Z", "organizationId": "org-..." }Flags
--tower-id--tower-group-id"default"--ttl3600(1h)--description--label--label env=prod --label team=infra)--jsonfalseAPI Reference
Source:
aviato/proto/coreweave/aviato/v1beta1/tower_join.protoPOST /v1beta1/towers/tokens- CreateRequest:
Response:
Behavior:
POST /v1beta1/towers/join.Token lifecycle
States:
active->used|revoked|expiredactive: newly created, not yet exchangedused: Tower exchanged the token for mTLS certsrevoked: explicitly revoked by userexpired: TTL elapsed (evaluated lazily at query time)Implementation Notes
httpx(already a dependency) orrequests.CWSANDBOX_API_KEYpath from_auth.py. W&B auth is not relevant for this admin operation.ls,exec). Barecwsandbox join-tokenis sufficient for now; if list/revoke are added later, make bare invocation an alias forjoin-token createso existing usage doesn't break.Future Work
cwsandbox join-token list- list tokens with status filtering (GET /v1beta1/towers/tokens)cwsandbox join-token revoke <token-id>- revoke active tokens (DELETE /v1beta1/towers/tokens/{token_id})The API already supports both operations.