Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[workspace]
members = ["crates/builderlab-auth"]
members = ["crates/blox", "crates/builderlab-auth"]
resolver = "2"

[package]
Expand All @@ -20,6 +20,7 @@ path = "src/bin/bl.rs"

[dependencies]
anyhow = "1"
blox = { path = "crates/blox" }
builderlab-auth = { path = "crates/builderlab-auth", features = ["blocking-client"] }
clap = { version = "4.5", features = ["env"] }
clap_complete = "4.5"
Expand Down
92 changes: 90 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# BuilderLab CLI

`bl` is the command-line interface for BuilderLab. It provides authenticated
access to BuilderLab skills, agents, workspaces, and apps, along with the
`bl tools` command for discovering configured tool extensions.
access to BuilderLab skills, agents, workspaces, apps, and Blox workstations,
along with the `bl tools` command for discovering configured tool extensions.

The CLI is written in Rust and is built from this repository. Run `bl --help`
to see the current command surface:
Expand All @@ -11,6 +11,7 @@ to see the current command surface:
auth Manage BuilderLab marketplace authentication
workspace Manage BuilderLab workspaces
apps Manage apps through Apps Platform
compute Manage Blox workstations
config Get and set bl preferences
skills Manage BuilderLab skills
agents Manage BuilderLab marketplace agents
Expand Down Expand Up @@ -82,6 +83,91 @@ from KGoose for the workstation owner and injects it on the wire. Any other
URL, including an explicit `--base-url` outside that origin, must be an
approved HTTPS ingress and requires `bl auth login`.

## Blox workstations

Use your existing `bl auth login` session to create and inspect Blox workstations:

```bash
bl compute skus
bl compute create my-workstation --multiple 2
bl compute list
bl compute get <name-or-full-id>
bl compute list --include-deleted --limit 20 --offset 20 --json
bl compute exec my-workstation -- uname -a
bl compute suspend my-workstation
bl compute resume my-workstation
bl compute delete my-workstation
```

`bl blox` remains available as a hidden compatibility alias for `bl compute`.

The Blox endpoint defaults to the deployment associated with your BuilderLab
login: Block staging, public staging, or public production. Block production
does not currently deploy Blox. Override the endpoint with
`bl compute --base-url <url> list` or `BL_BLOX_CONTROL_PLANE_URL`. The CLI accepts
only the approved HTTPS ingress origins `blox.test.blockstaging.build`,
`blox.public.builderlabstaging.xyz`, and `blox.public.builderlab.xyz`.

The CLI verifies the same stored session used by the other BuilderLab commands
and sends it as `Authorization: BBIdentity` to Blox ingress. No additional
login or token configuration is required. Redirects are disabled.

`list` returns your workstations, omits connection details, and defaults to 100
results. Use `--limit 0` for all results; a nonzero `--offset` requires a
nonzero limit. `get` forwards your selector to the orchestrator, which resolves
an active canonical name before a numeric ID. Display labels are not selectors;
use the full ID for archived workstations. JSON output retains the generated
protobuf response shape, including string-encoded 64-bit counts. Help and shell
completions work offline.

`create [NAME]` always requests a new workstation. Its default SKU is
`DEFAULT_AMD64`, the SKU available in public BuilderLab staging; `--multiple`
defaults to 1 and must be positive. Use `bl compute skus` to see the codes and
maximum multiples available to your account, and override with `--sku CODE`
when needed. The deployment validates SKU availability and multiples. The CLI
accepts no CPU, memory, repository, or prompt options. It requests no project
repositories; the deployment may still bootstrap your configured home repository.

Creation returns the initial workstation ID and status without waiting for
readiness. Inspect it with `bl compute get <id>`. If the response is lost or cannot
be read, the CLI reports an unknown result and does not retry automatically.
Inspect `bl compute list` or `bl compute get <name>` before trying again to avoid
creating a second workstation. Preserve any `--base-url` override for these checks.

`suspend`, `resume`, and `delete` accept a canonical workstation name or full ID.
The CLI resolves the target once and sends one transition request. It reports
the server's response without waiting for the final state; use `bl compute get <id>`
to inspect progress. Suspension can take several minutes while capturing a
snapshot, then resource teardown may continue after the response. A resume
response does not establish readiness, and deletion may continue asynchronously.

Deletion archives the workstation. Resume after deletion depends on whether the
deployment retained a usable snapshot; use its full ID because archived names
are not resolved. If a transition result is uncertain, inspect that exact ID
using the same Blox endpoint before trying again. Lifecycle requests are never
retried automatically.

`exec <WORKSTATION> -- <COMMAND> [ARG...]` runs an executable with literal
arguments, streams its stdout and stderr, and exits with the remote process's
exit status. Use `--workdir PATH` before `--` to choose a remote directory;
otherwise Blox selects the workstation's configured directory. Shell expansion
and pipelines require an explicit shell, for example:

```bash
bl compute exec my-workstation -- sh -c 'printf hello; uname -a'
```

Execution is noninteractive: stdin and PTY allocation are unsupported. Commands
can run longer than 30 seconds. Interrupting the CLI or losing its connection
may leave the durable command running. The CLI never retries execution; if a
result is uncertain, inspect the command in Blox before starting another copy.

With `--json`, execution prints JSONL events with `workstation_id`, `command_id`,
and `output` containing the generated protobuf stream response (its `data` bytes
are base64). A final event contains the process's `exit_code`. Local or service
failures retain the normal JSON error envelope on stderr; a remote nonzero exit
does not add an error envelope.

## Local development

Install the repository tools and git hooks once:
Expand Down Expand Up @@ -124,6 +210,7 @@ The most useful configuration options are:
| `BL_SKILLS_PROFILE` | Select a skills configuration profile. |
| `BL_AUTH_STORAGE` | Select authentication storage, including `file` for local testing. |
| `BL_AUTH_STORAGE_FILE` | Path used when file-backed auth storage is selected. |
| `BL_BLOX_CONTROL_PLANE_URL` | Override the approved Blox control-plane ingress. |
| `KGOOSE_BASE_URL` | Override the backend base URL for local development. |
| `BL_KGOOSE_PLAYPEN` | Route backend requests through a named development playpen. |

Expand All @@ -143,6 +230,7 @@ development without modifying keyring state.
- `src/bl/` contains the `bl` command implementations.
- `crates/builderlab-auth/` contains browser login, session storage, workspace,
and organization-routing support.
- `crates/blox/` contains generated orchestrator protocol types and its Connect client.
- `src/lib.rs` wires the CLI runtime and test harness together.
- `tests/bl_e2e.rs` contains the offline and mock-service end-to-end coverage.
- `docker/acceptance/` contains the isolated skills acceptance harness.
Expand Down
24 changes: 24 additions & 0 deletions crates/blox/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[package]
name = "blox"
version = "0.1.0"
edition = "2021"
rust-version = "1.88.0"
license = "Apache-2.0"
publish = false

[dependencies]
anyhow = "1"
pbjson = "0.9"
prost = "0.14"
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
url = "2.5"

[build-dependencies]
pbjson-build = "0.9"
protoc-bin-vendored = "3"
tonic-prost-build = "0.14"

[dev-dependencies]
tiny_http = "0.12"
47 changes: 47 additions & 0 deletions crates/blox/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Blox client

This crate contains Blox orchestrator protocol messages and a synchronous
Connect JSON client. Its client methods list, retrieve, and create workstations,
and discover the caller's available compute SKUs.
Durable process execution uses `execute_process`, followed by
`stream_command_output` to deliver typed stdout/stderr messages and exit codes.
Lifecycle methods request suspension, resumption, and deletion once and return
the server's typed response. Callers must inspect lifecycle status and deletion
success; an accepted request does not imply that asynchronous teardown or startup
has finished.
Callers supply the endpoint and credentials; the crate does not perform login
or select a deployment.

Unary requests time out after 30 seconds, except lifecycle operations, which
allow up to 12 minutes for snapshot creation and provider teardown. Responses
are limited to 2 MiB, and requests do not
follow redirects or retry requests. Error messages redact reflected session
credentials. Creation has no idempotency key: after a transport error or an
invalid response, inspect existing workstations before attempting another create.

Output streaming uses Connect JSON envelopes with a 2 MiB per-envelope limit
and no overall time limit. It requires a successful protocol end-stream message;
transport EOF is an error. Stream callbacks can stop reading without stopping
the remote process. The caller must capture a process exit code before reporting
success, and must not relaunch execution after an uncertain result.

For BuilderLab ingress, `with_session_credential` sends an existing shared
login session using `Authorization: BBIdentity <credential>`. The caller must
select an ingress configured to accept this authentication scheme.

## Protocol source

`protos/xyz/block/blox/orchestrator/v1/orchestrator.proto` is an unchanged copy
of `open/protos/xyz/block/blox/orchestrator/v1/orchestrator.proto` from the Blox
repository (`squareup/blox`) at revision
`57bc2e40b162b43e1d1b2d0b860bd67e0a3ce3e7`.

To sync the protocol, replace that file with the complete source file from the
chosen Blox revision and update this revision note in the same change. Run
`cargo test -p blox` in the Hermit environment to regenerate the Rust messages
and check binary and protobuf JSON encoding. Generated files stay in Cargo's
build output. The `go_package` option is retained as source metadata.

JSON decoding ignores new fields from newer servers. Unknown enum variants
decode to their unspecified default; refresh the copied protocol to recognize
new workstation statuses. Known enum values retain their protobuf names.
27 changes: 27 additions & 0 deletions crates/blox/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
use std::path::PathBuf;

fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=protos");

let protoc = protoc_bin_vendored::protoc_bin_path()?;
std::env::set_var("PROTOC", protoc);

let proto_root = PathBuf::from("protos");
let service_proto = proto_root.join("xyz/block/blox/orchestrator/v1/orchestrator.proto");
let descriptor_path = PathBuf::from(std::env::var("OUT_DIR")?).join("proto_descriptor.bin");

tonic_prost_build::configure()
.build_client(false)
.build_server(false)
.file_descriptor_set_path(&descriptor_path)
.compile_protos(&[service_proto], &[proto_root])?;

let descriptor_set = std::fs::read(descriptor_path)?;
pbjson_build::Builder::new()
.register_descriptors(&descriptor_set)?
.ignore_unknown_fields()
.ignore_unknown_enum_variants()
.build(&[".xyz.block.blox.orchestrator.v1"])?;
Ok(())
}
Loading