refactor: rename sandbox-daemon to sandbox-agent

This commit is contained in:
Nathan Flurry 2026-01-25 02:30:12 -08:00
parent f92ecd9b9a
commit a49ea094f3
41 changed files with 808 additions and 134 deletions

View file

@ -29,6 +29,13 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install tsx
run: npm install -g tsx
- name: Resolve version - name: Resolve version
id: vars id: vars
run: | run: |
@ -44,7 +51,7 @@ jobs:
- name: Determine latest - name: Determine latest
id: latest id: latest
run: | run: |
node scripts/release/main.js --version "${{ steps.vars.outputs.version }}" --print-latest --output "$GITHUB_OUTPUT" ./scripts/release/main.ts --version "${{ steps.vars.outputs.version }}" --print-latest --output "$GITHUB_OUTPUT"
binaries: binaries:
name: "Build & Upload Binaries" name: "Build & Upload Binaries"
@ -97,11 +104,11 @@ jobs:
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_RELEASES_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_RELEASES_SECRET_ACCESS_KEY }}
run: | run: |
VERSION="${{ needs.setup.outputs.version }}" VERSION="${{ needs.setup.outputs.version }}"
BINARY_NAME="sandbox-daemon-${{ matrix.target }}${{ matrix.binary_ext }}" BINARY_NAME="sandbox-agent-${{ matrix.target }}${{ matrix.binary_ext }}"
aws s3 cp \ aws s3 cp \
"dist/${BINARY_NAME}" \ "dist/${BINARY_NAME}" \
"s3://rivet-releases/sandbox-daemon/${VERSION}/${BINARY_NAME}" \ "s3://rivet-releases/sandbox-agent/${VERSION}/${BINARY_NAME}" \
--region auto \ --region auto \
--endpoint-url https://2a94c6a0ced8d35ea63cddc86c2681e7.r2.cloudflarestorage.com \ --endpoint-url https://2a94c6a0ced8d35ea63cddc86c2681e7.r2.cloudflarestorage.com \
--checksum-algorithm CRC32 --checksum-algorithm CRC32
@ -109,7 +116,7 @@ jobs:
if [ "${{ needs.setup.outputs.latest }}" = "true" ]; then if [ "${{ needs.setup.outputs.latest }}" = "true" ]; then
aws s3 cp \ aws s3 cp \
"dist/${BINARY_NAME}" \ "dist/${BINARY_NAME}" \
"s3://rivet-releases/sandbox-daemon/latest/${BINARY_NAME}" \ "s3://rivet-releases/sandbox-agent/latest/${BINARY_NAME}" \
--region auto \ --region auto \
--endpoint-url https://2a94c6a0ced8d35ea63cddc86c2681e7.r2.cloudflarestorage.com \ --endpoint-url https://2a94c6a0ced8d35ea63cddc86c2681e7.r2.cloudflarestorage.com \
--checksum-algorithm CRC32 --checksum-algorithm CRC32
@ -128,6 +135,9 @@ jobs:
with: with:
node-version: 20 node-version: 20
- name: Install tsx
run: npm install -g tsx
- name: Install AWS CLI - name: Install AWS CLI
run: | run: |
sudo apt-get update sudo apt-get update
@ -149,4 +159,4 @@ jobs:
LATEST_FLAG="--no-latest" LATEST_FLAG="--no-latest"
fi fi
node scripts/release/main.js --version "$VERSION" $LATEST_FLAG --upload-typescript --upload-install ./scripts/release/main.ts --version "$VERSION" $LATEST_FLAG --upload-typescript --upload-install

1
.gitignore vendored
View file

@ -7,7 +7,6 @@ build/
target/ target/
# Package manager # Package manager
pnpm-lock.yaml
package-lock.json package-lock.json
yarn.lock yarn.lock

View file

@ -1,3 +1,9 @@
[workspace] [workspace]
resolver = "2" resolver = "2"
members = ["engine/packages/*"] members = ["engine/packages/*"]
[workspace.package]
version = "0.1.0"
edition = "2021"
authors = ["Sandbox Agent Contributors"]
license = "Apache-2.0"

190
LICENSE Normal file
View file

@ -0,0 +1,190 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2026 Sandbox Agent Contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View file

@ -1,4 +1,4 @@
# Sandbox Daemon # Sandbox Agent SDK
Run inside sandboxes to provide support Run inside sandboxes to provide support
@ -81,9 +81,9 @@ TODO
This project aims to solve 3 problems with agents: This project aims to solve 3 problems with agents:
- **Universal Agent API**: Claude Code, Codex, Amp, and OpenCode all have put a lot of work in to the agent scaffold. Each have respective pros and cons and need to be easy to be swapped between. - **Universal Coding Agent API**: Claude Code, Codex, Amp, and OpenCode all have put a lot of work in to the agent scaffold. Each have respective pros and cons and need to be easy to be swapped between.
- **Agent Transcript**: Maintaining agent transcripts is difficult since the agent manages its own sessions. This provides a simpler way to read and retrieve agent transcripts in your system.
- **Agents In Sandboxes**: There are many complications with running agents inside of sandbox providers. This lets you run a simple curl command to spawn an HTTP server for using any agent from within the sandbox. - **Agents In Sandboxes**: There are many complications with running agents inside of sandbox providers. This lets you run a simple curl command to spawn an HTTP server for using any agent from within the sandbox.
- **Agent Transcript**: Maintaining agent transcripts is difficult since the agent manages its own sessions. This provides a simpler way to read and retrieve agent transcripts in your system.
Features out of scope: Features out of scope:

View file

@ -8,25 +8,25 @@ case $TARGET in
echo "Building for Linux x86_64 musl" echo "Building for Linux x86_64 musl"
DOCKERFILE="linux-x86_64.Dockerfile" DOCKERFILE="linux-x86_64.Dockerfile"
TARGET_STAGE="builder" TARGET_STAGE="builder"
BINARY="sandbox-daemon-$TARGET" BINARY="sandbox-agent-$TARGET"
;; ;;
x86_64-pc-windows-gnu) x86_64-pc-windows-gnu)
echo "Building for Windows x86_64" echo "Building for Windows x86_64"
DOCKERFILE="windows.Dockerfile" DOCKERFILE="windows.Dockerfile"
TARGET_STAGE="" TARGET_STAGE=""
BINARY="sandbox-daemon-$TARGET.exe" BINARY="sandbox-agent-$TARGET.exe"
;; ;;
x86_64-apple-darwin) x86_64-apple-darwin)
echo "Building for macOS x86_64" echo "Building for macOS x86_64"
DOCKERFILE="macos-x86_64.Dockerfile" DOCKERFILE="macos-x86_64.Dockerfile"
TARGET_STAGE="x86_64-builder" TARGET_STAGE="x86_64-builder"
BINARY="sandbox-daemon-$TARGET" BINARY="sandbox-agent-$TARGET"
;; ;;
aarch64-apple-darwin) aarch64-apple-darwin)
echo "Building for macOS aarch64" echo "Building for macOS aarch64"
DOCKERFILE="macos-aarch64.Dockerfile" DOCKERFILE="macos-aarch64.Dockerfile"
TARGET_STAGE="aarch64-builder" TARGET_STAGE="aarch64-builder"
BINARY="sandbox-daemon-$TARGET" BINARY="sandbox-agent-$TARGET"
;; ;;
*) *)
echo "Unsupported target: $TARGET" echo "Unsupported target: $TARGET"
@ -36,12 +36,12 @@ case $TARGET in
DOCKER_BUILDKIT=1 DOCKER_BUILDKIT=1
if [ -n "$TARGET_STAGE" ]; then if [ -n "$TARGET_STAGE" ]; then
docker build --target "$TARGET_STAGE" -f "docker/release/$DOCKERFILE" -t "sandbox-daemon-builder-$TARGET" . docker build --target "$TARGET_STAGE" -f "docker/release/$DOCKERFILE" -t "sandbox-agent-builder-$TARGET" .
else else
docker build -f "docker/release/$DOCKERFILE" -t "sandbox-daemon-builder-$TARGET" . docker build -f "docker/release/$DOCKERFILE" -t "sandbox-agent-builder-$TARGET" .
fi fi
CONTAINER_ID=$(docker create "sandbox-daemon-builder-$TARGET") CONTAINER_ID=$(docker create "sandbox-agent-builder-$TARGET")
mkdir -p dist mkdir -p dist
docker cp "$CONTAINER_ID:/artifacts/$BINARY" "dist/" docker cp "$CONTAINER_ID:/artifacts/$BINARY" "dist/"

View file

@ -20,8 +20,8 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/usr/local/cargo/git \ --mount=type=cache,target=/usr/local/cargo/git \
--mount=type=cache,target=/build/target \ --mount=type=cache,target=/build/target \
RUSTFLAGS="-C target-feature=+crt-static" \ RUSTFLAGS="-C target-feature=+crt-static" \
cargo build -p sandbox-daemon-core --release --target x86_64-unknown-linux-musl && \ cargo build -p sandbox-agent-core --release --target x86_64-unknown-linux-musl && \
mkdir -p /artifacts && \ mkdir -p /artifacts && \
cp target/x86_64-unknown-linux-musl/release/sandbox-daemon /artifacts/sandbox-daemon-x86_64-unknown-linux-musl cp target/x86_64-unknown-linux-musl/release/sandbox-agent /artifacts/sandbox-agent-x86_64-unknown-linux-musl
CMD ["ls", "-la", "/artifacts"] CMD ["ls", "-la", "/artifacts"]

View file

@ -55,8 +55,8 @@ COPY . .
RUN --mount=type=cache,target=/usr/local/cargo/registry \ RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/usr/local/cargo/git \ --mount=type=cache,target=/usr/local/cargo/git \
--mount=type=cache,target=/build/target \ --mount=type=cache,target=/build/target \
cargo build -p sandbox-daemon-core --release --target aarch64-apple-darwin && \ cargo build -p sandbox-agent-core --release --target aarch64-apple-darwin && \
mkdir -p /artifacts && \ mkdir -p /artifacts && \
cp target/aarch64-apple-darwin/release/sandbox-daemon /artifacts/sandbox-daemon-aarch64-apple-darwin cp target/aarch64-apple-darwin/release/sandbox-agent /artifacts/sandbox-agent-aarch64-apple-darwin
CMD ["ls", "-la", "/artifacts"] CMD ["ls", "-la", "/artifacts"]

View file

@ -55,8 +55,8 @@ COPY . .
RUN --mount=type=cache,target=/usr/local/cargo/registry \ RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/usr/local/cargo/git \ --mount=type=cache,target=/usr/local/cargo/git \
--mount=type=cache,target=/build/target \ --mount=type=cache,target=/build/target \
cargo build -p sandbox-daemon-core --release --target x86_64-apple-darwin && \ cargo build -p sandbox-agent-core --release --target x86_64-apple-darwin && \
mkdir -p /artifacts && \ mkdir -p /artifacts && \
cp target/x86_64-apple-darwin/release/sandbox-daemon /artifacts/sandbox-daemon-x86_64-apple-darwin cp target/x86_64-apple-darwin/release/sandbox-agent /artifacts/sandbox-agent-x86_64-apple-darwin
CMD ["ls", "-la", "/artifacts"] CMD ["ls", "-la", "/artifacts"]

View file

@ -42,8 +42,8 @@ COPY . .
RUN --mount=type=cache,target=/usr/local/cargo/registry \ RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/usr/local/cargo/git \ --mount=type=cache,target=/usr/local/cargo/git \
--mount=type=cache,target=/build/target \ --mount=type=cache,target=/build/target \
cargo build -p sandbox-daemon-core --release --target x86_64-pc-windows-gnu && \ cargo build -p sandbox-agent-core --release --target x86_64-pc-windows-gnu && \
mkdir -p /artifacts && \ mkdir -p /artifacts && \
cp target/x86_64-pc-windows-gnu/release/sandbox-daemon.exe /artifacts/sandbox-daemon-x86_64-pc-windows-gnu.exe cp target/x86_64-pc-windows-gnu/release/sandbox-agent.exe /artifacts/sandbox-agent-x86_64-pc-windows-gnu.exe
CMD ["ls", "-la", "/artifacts"] CMD ["ls", "-la", "/artifacts"]

View file

@ -1,7 +1,9 @@
[package] [package]
name = "sandbox-daemon-agent-credentials" name = "sandbox-agent-agent-credentials"
version = "0.1.0" version.workspace = true
edition = "2021" edition.workspace = true
authors.workspace = true
license.workspace = true
[dependencies] [dependencies]
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }

View file

@ -1,13 +1,15 @@
[package] [package]
name = "sandbox-daemon-agent-management" name = "sandbox-agent-agent-management"
version = "0.1.0" version.workspace = true
edition = "2021" edition.workspace = true
authors.workspace = true
license.workspace = true
[dependencies] [dependencies]
thiserror = "1.0" thiserror = "1.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
sandbox-daemon-agent-credentials = { path = "../agent-credentials" } sandbox-agent-agent-credentials = { path = "../agent-credentials" }
reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"] } reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"] }
flate2 = "1.0" flate2 = "1.0"
tar = "0.4" tar = "0.4"

View file

@ -1 +1 @@
pub use sandbox_daemon_agent_credentials::*; pub use sandbox_agent_agent_credentials::*;

View file

@ -1,7 +1,9 @@
[package] [package]
name = "sandbox-daemon-agent-schema" name = "sandbox-agent-agent-schema"
version = "0.1.0" version.workspace = true
edition = "2021" edition.workspace = true
authors.workspace = true
license.workspace = true
[dependencies] [dependencies]
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }

View file

@ -1,4 +1,5 @@
use std::fs; use std::fs;
use std::io::{self, Write};
use std::path::Path; use std::path::Path;
fn main() { fn main() {
@ -16,10 +17,16 @@ fn main() {
let schema_path = schema_dir.join(file); let schema_path = schema_dir.join(file);
// Tell cargo to rerun if schema changes // Tell cargo to rerun if schema changes
println!("cargo:rerun-if-changed={}", schema_path.display()); emit_stdout(&format!(
"cargo:rerun-if-changed={}",
schema_path.display()
));
if !schema_path.exists() { if !schema_path.exists() {
eprintln!("Warning: Schema file not found: {}", schema_path.display()); emit_stdout(&format!(
"cargo:warning=Schema file not found: {}",
schema_path.display()
));
// Write empty module // Write empty module
let out_path = Path::new(&out_dir).join(format!("{}.rs", name)); let out_path = Path::new(&out_dir).join(format!("{}.rs", name));
fs::write(&out_path, "// Schema not found\n").unwrap(); fs::write(&out_path, "// Schema not found\n").unwrap();
@ -49,6 +56,16 @@ fn main() {
fs::write(&out_path, formatted) fs::write(&out_path, formatted)
.unwrap_or_else(|e| panic!("Failed to write {}: {}", out_path.display(), e)); .unwrap_or_else(|e| panic!("Failed to write {}: {}", out_path.display(), e));
println!("cargo:warning=Generated {} types from {}", name, file); emit_stdout(&format!(
"cargo:warning=Generated {} types from {}",
name, file
));
} }
} }
fn emit_stdout(message: &str) {
let mut out = io::stdout();
let _ = out.write_all(message.as_bytes());
let _ = out.write_all(b"\n");
let _ = out.flush();
}

View file

@ -1,7 +1,9 @@
[package] [package]
name = "sandbox-daemon-error" name = "sandbox-agent-error"
version = "0.1.0" version.workspace = true
edition = "2021" edition.workspace = true
authors.workspace = true
license.workspace = true
[dependencies] [dependencies]
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }

View file

@ -24,18 +24,18 @@ pub enum ErrorType {
impl ErrorType { impl ErrorType {
pub fn as_urn(&self) -> &'static str { pub fn as_urn(&self) -> &'static str {
match self { match self {
Self::InvalidRequest => "urn:sandbox-daemon:error:invalid_request", Self::InvalidRequest => "urn:sandbox-agent:error:invalid_request",
Self::UnsupportedAgent => "urn:sandbox-daemon:error:unsupported_agent", Self::UnsupportedAgent => "urn:sandbox-agent:error:unsupported_agent",
Self::AgentNotInstalled => "urn:sandbox-daemon:error:agent_not_installed", Self::AgentNotInstalled => "urn:sandbox-agent:error:agent_not_installed",
Self::InstallFailed => "urn:sandbox-daemon:error:install_failed", Self::InstallFailed => "urn:sandbox-agent:error:install_failed",
Self::AgentProcessExited => "urn:sandbox-daemon:error:agent_process_exited", Self::AgentProcessExited => "urn:sandbox-agent:error:agent_process_exited",
Self::TokenInvalid => "urn:sandbox-daemon:error:token_invalid", Self::TokenInvalid => "urn:sandbox-agent:error:token_invalid",
Self::PermissionDenied => "urn:sandbox-daemon:error:permission_denied", Self::PermissionDenied => "urn:sandbox-agent:error:permission_denied",
Self::SessionNotFound => "urn:sandbox-daemon:error:session_not_found", Self::SessionNotFound => "urn:sandbox-agent:error:session_not_found",
Self::SessionAlreadyExists => "urn:sandbox-daemon:error:session_already_exists", Self::SessionAlreadyExists => "urn:sandbox-agent:error:session_already_exists",
Self::ModeNotSupported => "urn:sandbox-daemon:error:mode_not_supported", Self::ModeNotSupported => "urn:sandbox-agent:error:mode_not_supported",
Self::StreamError => "urn:sandbox-daemon:error:stream_error", Self::StreamError => "urn:sandbox-agent:error:stream_error",
Self::Timeout => "urn:sandbox-daemon:error:timeout", Self::Timeout => "urn:sandbox-agent:error:timeout",
} }
} }

View file

@ -1,12 +1,17 @@
[package] [package]
name = "sandbox-daemon-openapi-gen" name = "sandbox-agent-openapi-gen"
version = "0.1.0" version.workspace = true
edition = "2021" edition.workspace = true
authors.workspace = true
license.workspace = true
build = "build.rs" build = "build.rs"
[dependencies] [dependencies]
tracing = "0.1"
tracing-logfmt = "0.3"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
[build-dependencies] [build-dependencies]
sandbox-daemon-core = { path = "../sandbox-daemon" } sandbox-agent-core = { path = "../sandbox-agent" }
serde_json = "1.0" serde_json = "1.0"
utoipa = "4.2" utoipa = "4.2"

View file

@ -1,12 +1,13 @@
use std::fs; use std::fs;
use std::io::{self, Write};
use std::path::Path; use std::path::Path;
use sandbox_daemon_core::router::ApiDoc; use sandbox_agent_core::router::ApiDoc;
use utoipa::OpenApi; use utoipa::OpenApi;
fn main() { fn main() {
println!("cargo:rerun-if-changed=../sandbox-daemon/src/router.rs"); emit_stdout("cargo:rerun-if-changed=../sandbox-agent/src/router.rs");
println!("cargo:rerun-if-changed=../sandbox-daemon/src/lib.rs"); emit_stdout("cargo:rerun-if-changed=../sandbox-agent/src/lib.rs");
let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR not set"); let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR not set");
let out_path = Path::new(&out_dir).join("openapi.json"); let out_path = Path::new(&out_dir).join("openapi.json");
@ -16,5 +17,15 @@ fn main() {
.expect("Failed to serialize OpenAPI spec"); .expect("Failed to serialize OpenAPI spec");
fs::write(&out_path, json).expect("Failed to write OpenAPI spec"); fs::write(&out_path, json).expect("Failed to write OpenAPI spec");
println!("cargo:warning=Generated OpenAPI spec at {}", out_path.display()); emit_stdout(&format!(
"cargo:warning=Generated OpenAPI spec at {}",
out_path.display()
));
}
fn emit_stdout(message: &str) {
let mut out = io::stdout();
let _ = out.write_all(message.as_bytes());
let _ = out.write_all(b"\n");
let _ = out.flush();
} }

View file

@ -1,8 +1,12 @@
use std::env; use std::env;
use std::fs; use std::fs;
use std::io::Write;
use std::path::PathBuf; use std::path::PathBuf;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
fn main() { fn main() {
init_logging();
let mut out: Option<PathBuf> = None; let mut out: Option<PathBuf> = None;
let mut stdout = false; let mut stdout = false;
let mut args = env::args().skip(1).peekable(); let mut args = env::args().skip(1).peekable();
@ -26,15 +30,30 @@ fn main() {
} }
} }
let schema = sandbox_daemon_openapi_gen::OPENAPI_JSON; let schema = sandbox_agent_openapi_gen::OPENAPI_JSON;
if stdout { if stdout {
println!("{schema}"); write_stdout(schema);
return; return;
} }
let out = out.unwrap_or_else(|| PathBuf::from("openapi.json")); let out = out.unwrap_or_else(|| PathBuf::from("openapi.json"));
if let Err(err) = fs::write(&out, schema) { if let Err(err) = fs::write(&out, schema) {
eprintln!("failed to write {}: {err}", out.display()); tracing::error!(path = %out.display(), error = %err, "failed to write openapi schema");
std::process::exit(1); std::process::exit(1);
} }
} }
fn init_logging() {
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
tracing_subscriber::registry()
.with(filter)
.with(tracing_logfmt::builder().layer().with_writer(std::io::stderr))
.init();
}
fn write_stdout(text: &str) {
let mut out = std::io::stdout();
let _ = out.write_all(text.as_bytes());
let _ = out.write_all(b"\n");
let _ = out.flush();
}

View file

@ -1,10 +1,12 @@
[package] [package]
name = "sandbox-daemon-core" name = "sandbox-agent-core"
version = "0.1.0" version.workspace = true
edition = "2021" edition.workspace = true
authors.workspace = true
license.workspace = true
[[bin]] [[bin]]
name = "sandbox-daemon" name = "sandbox-agent"
path = "src/main.rs" path = "src/main.rs"
[dependencies] [dependencies]
@ -14,10 +16,10 @@ serde_json = "1.0"
axum = "0.7" axum = "0.7"
clap = { version = "4.5", features = ["derive"] } clap = { version = "4.5", features = ["derive"] }
futures = "0.3" futures = "0.3"
sandbox-daemon-error = { path = "../error" } sandbox-agent-error = { path = "../error" }
sandbox-daemon-agent-management = { path = "../agent-management" } sandbox-agent-agent-management = { path = "../agent-management" }
sandbox-daemon-agent-credentials = { path = "../agent-credentials" } sandbox-agent-agent-credentials = { path = "../agent-credentials" }
sandbox-daemon-universal-agent-schema = { path = "../universal-agent-schema" } sandbox-agent-universal-agent-schema = { path = "../universal-agent-schema" }
reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls", "stream"] } reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls", "stream"] }
dirs = "5.0" dirs = "5.0"
time = { version = "0.3", features = ["parsing", "formatting"] } time = { version = "0.3", features = ["parsing", "formatting"] }
@ -26,6 +28,9 @@ tokio-stream = { version = "0.1", features = ["sync"] }
tower-http = { version = "0.5", features = ["cors"] } tower-http = { version = "0.5", features = ["cors"] }
utoipa = { version = "4.2", features = ["axum_extras"] } utoipa = { version = "4.2", features = ["axum_extras"] }
schemars = "0.8" schemars = "0.8"
tracing = "0.1"
tracing-logfmt = "0.3"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
[dev-dependencies] [dev-dependencies]
tempfile = "3.10" tempfile = "3.10"

View file

@ -0,0 +1 @@
pub use sandbox_agent_agent_credentials::*;

View file

@ -4,23 +4,24 @@ use std::path::PathBuf;
use clap::{Args, Parser, Subcommand}; use clap::{Args, Parser, Subcommand};
use reqwest::blocking::Client as HttpClient; use reqwest::blocking::Client as HttpClient;
use reqwest::Method; use reqwest::Method;
use sandbox_daemon_agent_management::agents::AgentManager; use sandbox_agent_agent_management::agents::AgentManager;
use sandbox_daemon_core::router::{ use sandbox_agent_core::router::{
AgentInstallRequest, AppState, AuthConfig, CreateSessionRequest, MessageRequest, AgentInstallRequest, AppState, AuthConfig, CreateSessionRequest, MessageRequest,
PermissionReply, PermissionReplyRequest, QuestionReplyRequest, PermissionReply, PermissionReplyRequest, QuestionReplyRequest,
}; };
use sandbox_daemon_core::router::{AgentListResponse, AgentModesResponse, CreateSessionResponse, EventsResponse}; use sandbox_agent_core::router::{AgentListResponse, AgentModesResponse, CreateSessionResponse, EventsResponse};
use sandbox_daemon_core::router::build_router; use sandbox_agent_core::router::build_router;
use serde::Serialize; use serde::Serialize;
use serde_json::Value; use serde_json::Value;
use thiserror::Error; use thiserror::Error;
use tower_http::cors::{Any, CorsLayer}; use tower_http::cors::{Any, CorsLayer};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
const API_PREFIX: &str = "/v1"; const API_PREFIX: &str = "/v1";
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(name = "sandbox-daemon")] #[command(name = "sandbox-agent")]
#[command(about = "Sandbox daemon for managing coding agents", version)] #[command(about = "Sandbox agent for managing coding agents", version)]
struct Cli { struct Cli {
#[command(subcommand)] #[command(subcommand)]
command: Option<Command>, command: Option<Command>,
@ -215,6 +216,7 @@ enum CliError {
} }
fn main() { fn main() {
init_logging();
let cli = Cli::parse(); let cli = Cli::parse();
let result = match &cli.command { let result = match &cli.command {
@ -223,11 +225,19 @@ fn main() {
}; };
if let Err(err) = result { if let Err(err) = result {
eprintln!("{err}"); tracing::error!(error = %err, "sandbox-agent failed");
std::process::exit(1); std::process::exit(1);
} }
} }
fn init_logging() {
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
tracing_subscriber::registry()
.with(filter)
.with(tracing_logfmt::builder().layer().with_writer(std::io::stderr))
.init();
}
fn run_server(cli: &Cli) -> Result<(), CliError> { fn run_server(cli: &Cli) -> Result<(), CliError> {
let auth = if cli.no_token { let auth = if cli.no_token {
AuthConfig::disabled() AuthConfig::disabled()
@ -262,8 +272,8 @@ fn run_server(cli: &Cli) -> Result<(), CliError> {
fn default_install_dir() -> PathBuf { fn default_install_dir() -> PathBuf {
dirs::data_dir() dirs::data_dir()
.map(|dir| dir.join("sandbox-daemon").join("bin")) .map(|dir| dir.join("sandbox-agent").join("bin"))
.unwrap_or_else(|| PathBuf::from(".").join(".sandbox-daemon").join("bin")) .unwrap_or_else(|| PathBuf::from(".").join(".sandbox-agent").join("bin"))
} }
fn run_client(command: &Command, cli: &Cli) -> Result<(), CliError> { fn run_client(command: &Command, cli: &Cli) -> Result<(), CliError> {
@ -502,7 +512,7 @@ fn print_json_response<T: serde::de::DeserializeOwned + Serialize>(
let parsed: T = serde_json::from_str(&text)?; let parsed: T = serde_json::from_str(&text)?;
let pretty = serde_json::to_string_pretty(&parsed)?; let pretty = serde_json::to_string_pretty(&parsed)?;
println!("{pretty}"); write_stdout_line(&pretty)?;
Ok(()) Ok(())
} }
@ -515,8 +525,7 @@ fn print_text_response(response: reqwest::blocking::Response) -> Result<(), CliE
return Err(CliError::HttpStatus(status)); return Err(CliError::HttpStatus(status));
} }
print!("{text}"); write_stdout(&text)?;
std::io::stdout().flush()?;
Ok(()) Ok(())
} }
@ -533,9 +542,32 @@ fn print_empty_response(response: reqwest::blocking::Response) -> Result<(), Cli
fn print_error_body(text: &str) -> Result<(), CliError> { fn print_error_body(text: &str) -> Result<(), CliError> {
if let Ok(json) = serde_json::from_str::<Value>(text) { if let Ok(json) = serde_json::from_str::<Value>(text) {
let pretty = serde_json::to_string_pretty(&json)?; let pretty = serde_json::to_string_pretty(&json)?;
eprintln!("{pretty}"); write_stderr_line(&pretty)?;
} else { } else {
eprintln!("{text}"); write_stderr_line(text)?;
} }
Ok(()) Ok(())
} }
fn write_stdout(text: &str) -> Result<(), CliError> {
let mut out = std::io::stdout();
out.write_all(text.as_bytes())?;
out.flush()?;
Ok(())
}
fn write_stdout_line(text: &str) -> Result<(), CliError> {
let mut out = std::io::stdout();
out.write_all(text.as_bytes())?;
out.write_all(b"\n")?;
out.flush()?;
Ok(())
}
fn write_stderr_line(text: &str) -> Result<(), CliError> {
let mut out = std::io::stderr();
out.write_all(text.as_bytes())?;
out.write_all(b"\n")?;
out.flush()?;
Ok(())
}

View file

@ -16,8 +16,8 @@ use axum::Json;
use axum::Router; use axum::Router;
use futures::{stream, StreamExt}; use futures::{stream, StreamExt};
use reqwest::Client; use reqwest::Client;
use sandbox_daemon_error::{AgentError, ErrorType, ProblemDetails, SandboxError}; use sandbox_agent_error::{AgentError, ErrorType, ProblemDetails, SandboxError};
use sandbox_daemon_universal_agent_schema::{ use sandbox_agent_universal_agent_schema::{
convert_amp, convert_claude, convert_codex, convert_opencode, AttachmentSource, CrashInfo, convert_amp, convert_claude, convert_codex, convert_opencode, AttachmentSource, CrashInfo,
EventConversion, PermissionRequest, PermissionToolRef, QuestionInfo, QuestionOption, EventConversion, PermissionRequest, PermissionToolRef, QuestionInfo, QuestionOption,
QuestionRequest, QuestionToolRef, Started, UniversalEvent, UniversalEventData, QuestionRequest, QuestionToolRef, Started, UniversalEvent, UniversalEventData,
@ -31,10 +31,10 @@ use tokio_stream::wrappers::BroadcastStream;
use tokio::time::sleep; use tokio::time::sleep;
use utoipa::{OpenApi, ToSchema}; use utoipa::{OpenApi, ToSchema};
use sandbox_daemon_agent_management::agents::{ use sandbox_agent_agent_management::agents::{
AgentError as ManagerError, AgentId, AgentManager, InstallOptions, SpawnOptions, StreamingSpawn, AgentError as ManagerError, AgentId, AgentManager, InstallOptions, SpawnOptions, StreamingSpawn,
}; };
use sandbox_daemon_agent_management::credentials::{ use sandbox_agent_agent_management::credentials::{
extract_all_credentials, CredentialExtractionOptions, ExtractedCredentials, extract_all_credentials, CredentialExtractionOptions, ExtractedCredentials,
}; };

View file

@ -1,9 +1,9 @@
use std::collections::HashMap; use std::collections::HashMap;
use sandbox_daemon_agent_management::agents::{ use sandbox_agent_agent_management::agents::{
AgentError, AgentId, AgentManager, InstallOptions, SpawnOptions, AgentError, AgentId, AgentManager, InstallOptions, SpawnOptions,
}; };
use sandbox_daemon_agent_management::credentials::{ use sandbox_agent_agent_management::credentials::{
extract_all_credentials, CredentialExtractionOptions, extract_all_credentials, CredentialExtractionOptions,
}; };

View file

@ -1 +0,0 @@
pub use sandbox_daemon_agent_credentials::*;

View file

@ -1,10 +1,12 @@
[package] [package]
name = "sandbox-daemon-universal-agent-schema" name = "sandbox-agent-universal-agent-schema"
version = "0.1.0" version.workspace = true
edition = "2021" edition.workspace = true
authors.workspace = true
license.workspace = true
[dependencies] [dependencies]
sandbox-daemon-agent-schema = { path = "../agent-schema" } sandbox-agent-agent-schema = { path = "../agent-schema" }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
schemars = "0.8" schemars = "0.8"

View file

@ -4,7 +4,7 @@ use schemars::JsonSchema;
use thiserror::Error; use thiserror::Error;
use utoipa::ToSchema; use utoipa::ToSchema;
pub use sandbox_daemon_agent_schema::{amp, claude, codex, opencode}; pub use sandbox_agent_agent_schema::{amp, claude, codex, opencode};
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)] #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]

View file

@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sandbox Daemon Console</title> <title>Sandbox Agent Console</title>
<style> <style>
:root { :root {
color-scheme: dark; color-scheme: dark;

View file

@ -1,15 +1,16 @@
{ {
"name": "@sandbox-daemon/web", "name": "@sandbox-agent/web",
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",
"license": "Apache-2.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "pnpm --filter @sandbox-agent/typescript-sdk build && vite build",
"preview": "vite preview" "preview": "vite preview"
}, },
"devDependencies": { "devDependencies": {
"@sandbox-daemon/typescript-sdk": "workspace:*", "@sandbox-agent/typescript-sdk": "workspace:*",
"@types/react": "^18.3.3", "@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1", "@vitejs/plugin-react": "^4.3.1",

View file

@ -582,7 +582,7 @@ export default function App() {
<header className="app-header"> <header className="app-header">
<div className="brand"> <div className="brand">
<span className="brand-mark" /> <span className="brand-mark" />
Sandbox Daemon Console Sandbox Agent Console
</div> </div>
<div className="inline-row"> <div className="inline-row">
<span className={`status-pill ${connected ? "success" : "warning"}`}> <span className={`status-pill ${connected ? "success" : "warning"}`}>
@ -602,11 +602,11 @@ export default function App() {
<section className="connect-hero reveal"> <section className="connect-hero reveal">
<div className="hero-title">Bring the agent fleet online.</div> <div className="hero-title">Bring the agent fleet online.</div>
<div className="hero-subtitle"> <div className="hero-subtitle">
Point this console at a running sandbox-daemon, then manage sessions, messages, and approvals in Point this console at a running sandbox-agent, then manage sessions, messages, and approvals in
one place. one place.
</div> </div>
<div className="callout mono"> <div className="callout mono">
sandbox-daemon --host 0.0.0.0 --port 8787 --token &lt;token&gt; --cors-allowed-origin sandbox-agent --host 0.0.0.0 --port 8787 --token &lt;token&gt; --cors-allowed-origin
http://localhost:5173 --cors-allowed-methods GET,POST --cors-allowed-headers Authorization,x-sandbox-token http://localhost:5173 --cors-allowed-methods GET,POST --cors-allowed-headers Authorization,x-sandbox-token
</div> </div>
<div className="tag-list"> <div className="tag-list">

42
justfile Normal file
View file

@ -0,0 +1,42 @@
set dotenv-load := true
# Build a single target via Docker
release-build target="x86_64-unknown-linux-musl":
./docker/release/build.sh {{target}}
# Build all release binaries
release-build-all:
./docker/release/build.sh x86_64-unknown-linux-musl
./docker/release/build.sh x86_64-pc-windows-gnu
./docker/release/build.sh x86_64-apple-darwin
./docker/release/build.sh aarch64-apple-darwin
# Upload binaries from dist/ (requires AWS creds + aws cli)
release-upload-binaries version latest="auto":
{{~ if latest == "auto" ~}}
npx tsx scripts/release/main.ts --version {{version}} --upload-binaries
{{~ else if latest == "true" ~}}
npx tsx scripts/release/main.ts --version {{version}} --latest --upload-binaries
{{~ else if latest == "false" ~}}
npx tsx scripts/release/main.ts --version {{version}} --no-latest --upload-binaries
{{~ else ~}}
@echo "latest must be auto|true|false" && exit 1
{{~ endif ~}}
# Upload TypeScript artifacts + install.sh
release-upload-artifacts version latest="auto":
{{~ if latest == "auto" ~}}
npx tsx scripts/release/main.ts --version {{version}} --upload-typescript --upload-install
{{~ else if latest == "true" ~}}
npx tsx scripts/release/main.ts --version {{version}} --latest --upload-typescript --upload-install
{{~ else if latest == "false" ~}}
npx tsx scripts/release/main.ts --version {{version}} --no-latest --upload-typescript --upload-install
{{~ else ~}}
@echo "latest must be auto|true|false" && exit 1
{{~ endif ~}}
# Full local release test: build all, then upload binaries + artifacts
release-test version latest="auto":
just release-build-all
just release-upload-binaries {{version}} {{latest}}
just release-upload-artifacts {{version}} {{latest}}

View file

@ -1,6 +1,7 @@
{ {
"name": "sandbox-daemon-workspace", "name": "sandbox-agent-workspace",
"private": true, "private": true,
"license": "Apache-2.0",
"packageManager": "pnpm@9.15.0", "packageManager": "pnpm@9.15.0",
"scripts": { "scripts": {
"build": "turbo run build", "build": "turbo run build",

View file

@ -2,6 +2,7 @@
"name": "agent-schemas", "name": "agent-schemas",
"version": "1.0.0", "version": "1.0.0",
"type": "module", "type": "module",
"license": "Apache-2.0",
"scripts": { "scripts": {
"extract": "tsx src/index.ts", "extract": "tsx src/index.ts",
"extract:opencode": "tsx src/index.ts --agent=opencode", "extract:opencode": "tsx src/index.ts --agent=opencode",

View file

@ -90,7 +90,7 @@ export function createNormalizedSchema(
): NormalizedSchema { ): NormalizedSchema {
return { return {
$schema: "http://json-schema.org/draft-07/schema#", $schema: "http://json-schema.org/draft-07/schema#",
$id: `https://sandbox-daemon/schemas/${id}.json`, $id: `https://sandbox-agent/schemas/${id}.json`,
title, title,
definitions, definitions,
}; };

324
scripts/release/main.ts Executable file
View file

@ -0,0 +1,324 @@
#!/usr/bin/env tsx
import fs from "node:fs";
import path from "node:path";
import { spawnSync, execFileSync } from "node:child_process";
const ENDPOINT_URL =
"https://2a94c6a0ced8d35ea63cddc86c2681e7.r2.cloudflarestorage.com";
const BUCKET = "rivet-releases";
const PREFIX = "sandbox-agent";
const BINARY_FILES = [
"sandbox-agent-x86_64-unknown-linux-musl",
"sandbox-agent-x86_64-pc-windows-gnu.exe",
"sandbox-agent-x86_64-apple-darwin",
"sandbox-agent-aarch64-apple-darwin",
];
function parseArgs(argv: string[]) {
const args = new Map<string, string>();
const flags = new Set<string>();
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
if (!arg.startsWith("--")) continue;
if (arg.includes("=")) {
const [key, value] = arg.split("=");
args.set(key, value ?? "");
continue;
}
const next = argv[i + 1];
if (next && !next.startsWith("--")) {
args.set(arg, next);
i += 1;
} else {
flags.add(arg);
}
}
return { args, flags };
}
function run(cmd: string, cmdArgs: string[], options: Record<string, any> = {}) {
const result = spawnSync(cmd, cmdArgs, { stdio: "inherit", ...options });
if (result.status !== 0) {
process.exit(result.status ?? 1);
}
}
function runCapture(
cmd: string,
cmdArgs: string[],
options: Record<string, any> = {},
) {
const result = spawnSync(cmd, cmdArgs, {
stdio: ["ignore", "pipe", "pipe"],
encoding: "utf8",
...options,
});
if (result.status !== 0) {
const stderr = result.stderr ? String(result.stderr).trim() : "";
throw new Error(`${cmd} failed: ${stderr}`);
}
return (result.stdout || "").toString().trim();
}
interface ParsedSemver {
major: number;
minor: number;
patch: number;
prerelease: string[];
}
function parseSemver(version: string): ParsedSemver {
const match = version.match(
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z.-]+))?(?:\+([0-9A-Za-z.-]+))?$/,
);
if (!match) {
throw new Error(`Invalid semantic version: ${version}`);
}
return {
major: Number(match[1]),
minor: Number(match[2]),
patch: Number(match[3]),
prerelease: match[4] ? match[4].split(".") : [],
};
}
function compareSemver(a: ParsedSemver, b: ParsedSemver) {
if (a.major !== b.major) return a.major - b.major;
if (a.minor !== b.minor) return a.minor - b.minor;
return a.patch - b.patch;
}
function isStable(version: string) {
return parseSemver(version).prerelease.length === 0;
}
function getAllGitVersions() {
try {
execFileSync("git", ["fetch", "--tags", "--force", "--quiet"], {
stdio: "ignore",
});
} catch {
// best-effort
}
const output = runCapture("git", ["tag", "-l", "v*"]);
if (!output) return [];
return output
.split("\n")
.map((tag) => tag.replace(/^v/, ""))
.filter((tag) => {
try {
parseSemver(tag);
return true;
} catch {
return false;
}
})
.sort((a, b) => compareSemver(parseSemver(b), parseSemver(a)));
}
function getLatestStableVersion() {
const versions = getAllGitVersions();
const stable = versions.filter((version) => isStable(version));
return stable[0] || null;
}
function shouldTagAsLatest(version: string) {
const parsed = parseSemver(version);
if (parsed.prerelease.length > 0) {
return false;
}
const latestStable = getLatestStableVersion();
if (!latestStable) {
return true;
}
return compareSemver(parsed, parseSemver(latestStable)) > 0;
}
function getAwsEnv() {
const accessKey =
process.env.AWS_ACCESS_KEY_ID || process.env.R2_RELEASES_ACCESS_KEY_ID;
const secretKey =
process.env.AWS_SECRET_ACCESS_KEY ||
process.env.R2_RELEASES_SECRET_ACCESS_KEY;
if (!accessKey || !secretKey) {
throw new Error("Missing AWS credentials for releases bucket");
}
return {
AWS_ACCESS_KEY_ID: accessKey,
AWS_SECRET_ACCESS_KEY: secretKey,
AWS_DEFAULT_REGION: "auto",
};
}
function uploadDir(localPath: string, remotePath: string) {
const env = { ...process.env, ...getAwsEnv() };
run(
"aws",
[
"s3",
"cp",
localPath,
`s3://${BUCKET}/${remotePath}`,
"--recursive",
"--checksum-algorithm",
"CRC32",
"--endpoint-url",
ENDPOINT_URL,
],
{ env },
);
}
function uploadFile(localPath: string, remotePath: string) {
const env = { ...process.env, ...getAwsEnv() };
run(
"aws",
[
"s3",
"cp",
localPath,
`s3://${BUCKET}/${remotePath}`,
"--checksum-algorithm",
"CRC32",
"--endpoint-url",
ENDPOINT_URL,
],
{ env },
);
}
function uploadContent(content: string, remotePath: string) {
const env = { ...process.env, ...getAwsEnv() };
const result = spawnSync(
"aws",
[
"s3",
"cp",
"-",
`s3://${BUCKET}/${remotePath}`,
"--endpoint-url",
ENDPOINT_URL,
],
{
env,
input: content,
stdio: ["pipe", "inherit", "inherit"],
},
);
if (result.status !== 0) {
process.exit(result.status ?? 1);
}
}
function buildTypescript(rootDir: string) {
const sdkDir = path.join(rootDir, "sdks", "typescript");
if (!fs.existsSync(sdkDir)) {
throw new Error(`TypeScript SDK not found at ${sdkDir}`);
}
run("npm", ["install"], { cwd: sdkDir });
run("npm", ["run", "build"], { cwd: sdkDir });
return path.join(sdkDir, "dist");
}
function uploadTypescriptArtifacts(rootDir: string, version: string, latest: boolean) {
console.log("==> Building TypeScript SDK");
const distPath = buildTypescript(rootDir);
console.log("==> Uploading TypeScript artifacts");
uploadDir(distPath, `${PREFIX}/${version}/typescript/`);
if (latest) {
uploadDir(distPath, `${PREFIX}/latest/typescript/`);
}
}
function uploadInstallScript(rootDir: string, version: string, latest: boolean) {
const installPath = path.join(
rootDir,
"scripts",
"release",
"static",
"install.sh",
);
let installContent = fs.readFileSync(installPath, "utf8");
const uploadForVersion = (versionValue: string, remoteVersion: string) => {
const content = installContent.replace(/__VERSION__/g, versionValue);
uploadContent(content, `${PREFIX}/${remoteVersion}/install.sh`);
};
uploadForVersion(version, version);
if (latest) {
uploadForVersion("latest", "latest");
}
}
function uploadBinaries(rootDir: string, version: string, latest: boolean) {
const distDir = path.join(rootDir, "dist");
if (!fs.existsSync(distDir)) {
throw new Error(`dist directory not found at ${distDir}`);
}
for (const fileName of BINARY_FILES) {
const localPath = path.join(distDir, fileName);
if (!fs.existsSync(localPath)) {
throw new Error(`Missing binary: ${localPath}`);
}
uploadFile(localPath, `${PREFIX}/${version}/${fileName}`);
if (latest) {
uploadFile(localPath, `${PREFIX}/latest/${fileName}`);
}
}
}
function main() {
const { args, flags } = parseArgs(process.argv.slice(2));
const versionArg = args.get("--version");
if (!versionArg) {
console.error("--version is required");
process.exit(1);
}
const version = versionArg.replace(/^v/, "");
parseSemver(version);
let latest: boolean;
if (flags.has("--latest")) {
latest = true;
} else if (flags.has("--no-latest")) {
latest = false;
} else {
latest = shouldTagAsLatest(version);
}
const outputPath = args.get("--output");
if (flags.has("--print-latest")) {
if (outputPath) {
fs.appendFileSync(outputPath, `latest=${latest}\n`);
} else {
process.stdout.write(latest ? "true" : "false");
}
}
if (flags.has("--upload-typescript")) {
uploadTypescriptArtifacts(process.cwd(), version, latest);
}
if (flags.has("--upload-install")) {
uploadInstallScript(process.cwd(), version, latest);
}
if (flags.has("--upload-binaries")) {
uploadBinaries(process.cwd(), version, latest);
}
}
main();

View file

@ -7,28 +7,28 @@
# shellcheck enable=require-variable-braces # shellcheck enable=require-variable-braces
set -eu set -eu
WORK_DIR="/tmp/sandbox_daemon_install" WORK_DIR="/tmp/sandbox_agent_install"
rm -rf "$WORK_DIR" rm -rf "$WORK_DIR"
mkdir -p "$WORK_DIR" mkdir -p "$WORK_DIR"
cd "$WORK_DIR" cd "$WORK_DIR"
SANDBOX_DAEMON_VERSION="${SANDBOX_DAEMON_VERSION:-__VERSION__}" SANDBOX_AGENT_VERSION="${SANDBOX_AGENT_VERSION:-__VERSION__}"
SANDBOX_DAEMON_BASE_URL="${SANDBOX_DAEMON_BASE_URL:-https://releases.rivet.dev}" SANDBOX_AGENT_BASE_URL="${SANDBOX_AGENT_BASE_URL:-https://releases.rivet.dev}"
UNAME="$(uname -s)" UNAME="$(uname -s)"
ARCH="$(uname -m)" ARCH="$(uname -m)"
if [ "$(printf '%s' "$UNAME" | cut -c 1-6)" = "Darwin" ]; then if [ "$(printf '%s' "$UNAME" | cut -c 1-6)" = "Darwin" ]; then
if [ "$ARCH" = "x86_64" ]; then if [ "$ARCH" = "x86_64" ]; then
FILE_NAME="sandbox-daemon-x86_64-apple-darwin" FILE_NAME="sandbox-agent-x86_64-apple-darwin"
elif [ "$ARCH" = "arm64" ]; then elif [ "$ARCH" = "arm64" ]; then
FILE_NAME="sandbox-daemon-aarch64-apple-darwin" FILE_NAME="sandbox-agent-aarch64-apple-darwin"
else else
echo "Unknown arch $ARCH" 1>&2 echo "Unknown arch $ARCH" 1>&2
exit 1 exit 1
fi fi
elif [ "$(printf '%s' "$UNAME" | cut -c 1-5)" = "Linux" ]; then elif [ "$(printf '%s' "$UNAME" | cut -c 1-5)" = "Linux" ]; then
if [ "$ARCH" = "x86_64" ]; then if [ "$ARCH" = "x86_64" ]; then
FILE_NAME="sandbox-daemon-x86_64-unknown-linux-musl" FILE_NAME="sandbox-agent-x86_64-unknown-linux-musl"
else else
echo "Unsupported Linux arch $ARCH" 1>&2 echo "Unsupported Linux arch $ARCH" 1>&2
exit 1 exit 1
@ -44,7 +44,7 @@ if [ -z "$BIN_DIR" ]; then
fi fi
set -u set -u
INSTALL_PATH="$BIN_DIR/sandbox-daemon" INSTALL_PATH="$BIN_DIR/sandbox-agent"
if [ ! -d "$BIN_DIR" ]; then if [ ! -d "$BIN_DIR" ]; then
CHECK_DIR="$BIN_DIR" CHECK_DIR="$BIN_DIR"
@ -61,18 +61,18 @@ if [ ! -d "$BIN_DIR" ]; then
fi fi
fi fi
URL="$SANDBOX_DAEMON_BASE_URL/sandbox-daemon/${SANDBOX_DAEMON_VERSION}/${FILE_NAME}" URL="$SANDBOX_AGENT_BASE_URL/sandbox-agent/${SANDBOX_AGENT_VERSION}/${FILE_NAME}"
echo "> Downloading $URL" echo "> Downloading $URL"
curl -fsSL "$URL" -o sandbox-daemon curl -fsSL "$URL" -o sandbox-agent
chmod +x sandbox-daemon chmod +x sandbox-agent
if [ ! -w "$BIN_DIR" ]; then if [ ! -w "$BIN_DIR" ]; then
echo "> Installing sandbox-daemon to $INSTALL_PATH (requires sudo)" echo "> Installing sandbox-agent to $INSTALL_PATH (requires sudo)"
sudo mv ./sandbox-daemon "$INSTALL_PATH" sudo mv ./sandbox-agent "$INSTALL_PATH"
else else
echo "> Installing sandbox-daemon to $INSTALL_PATH" echo "> Installing sandbox-agent to $INSTALL_PATH"
mv ./sandbox-daemon "$INSTALL_PATH" mv ./sandbox-agent "$INSTALL_PATH"
fi fi
case ":$PATH:" in case ":$PATH:" in
@ -84,4 +84,4 @@ case ":$PATH:" in
;; ;;
esac esac
echo "sandbox-daemon installed successfully." echo "sandbox-agent installed successfully."

View file

@ -1,13 +1,14 @@
{ {
"name": "sandbox-daemon-typescript", "name": "@sandbox-agent/typescript-sdk",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"license": "Apache-2.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"generate:openapi": "cargo run -p sandbox-daemon-openapi-gen -- --out src/generated/openapi.json", "generate:openapi": "cargo check -p sandbox-agent-openapi-gen && cargo run -p sandbox-agent-openapi-gen -- --out src/generated/openapi.json",
"generate:types": "openapi-typescript src/generated/openapi.json -o src/generated/openapi.ts", "generate:types": "openapi-typescript src/generated/openapi.json -o src/generated/openapi.ts",
"generate": "npm run generate:openapi && npm run generate:types", "generate": "pnpm run generate:openapi && pnpm run generate:types",
"build": "tsc -p tsconfig.json" "build": "pnpm run generate && tsc -p tsconfig.json"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.0.0", "@types/node": "^22.0.0",

View file

@ -50,14 +50,14 @@ we need a standard thiserror for error responses. return errors as RFC 7807 Prob
it's ran with a token like this using clap: it's ran with a token like this using clap:
sandbox-daemon --token <token> --host xxxx --port xxxx sandbox-agent --token <token> --host xxxx --port xxxx
(you can specify --no-token too) (you can specify --no-token too)
(also add cors flags to the cli to configure cors, default to no cors) (also add cors flags to the cli to configure cors, default to no cors)
also expose a CLI endpoint for every http endpoint we have (specify this in claude.md to keep this to date) so we can do: also expose a CLI endpoint for every http endpoint we have (specify this in claude.md to keep this to date) so we can do:
sandbox-daemon sessions get-messages --endpoint xxxx --token xxxx sandbox-agent sessions get-messages --endpoint xxxx --token xxxx
### http api ### http api
@ -154,7 +154,7 @@ type AgentError = { tokenError: ... } | { processExisted: ... } | { installFaile
### error taxonomy ### error taxonomy
All error responses use RFC 7807 Problem Details and map to a Rust `thiserror` enum. Canonical `type` values should be stable strings (e.g. `urn:sandbox-daemon:error:agent_not_installed`). All error responses use RFC 7807 Problem Details and map to a Rust `thiserror` enum. Canonical `type` values should be stable strings (e.g. `urn:sandbox-agent:error:agent_not_installed`).
Required error types: Required error types:
@ -439,7 +439,7 @@ this machine is already authenticated with codex & claude & opencode (for codex)
in frontend/packages/web/ build a vite + react app that: in frontend/packages/web/ build a vite + react app that:
- connect screen: prompts the user to provide an endpoint & optional token - connect screen: prompts the user to provide an endpoint & optional token
- shows instructions on how to run the sandbox-daemon (including cors) - shows instructions on how to run the sandbox-agent (including cors)
- if gets error or cors error, instruct the user to ensure they have cors flags enabled - if gets error or cors error, instruct the user to ensure they have cors flags enabled
- agent screen: provides a full agent ui covering all of the features. also includes a log of all http requests in the ui with a copy button for the curl command - agent screen: provides a full agent ui covering all of the features. also includes a log of all http requests in the ui with a copy button for the curl command

View file

@ -44,8 +44,8 @@
- [x] Parse JSONL output for subprocess agents and extract session/result metadata - [x] Parse JSONL output for subprocess agents and extract session/result metadata
- [x] Map permissionMode to agent CLI flags (Claude/Codex/Amp) - [x] Map permissionMode to agent CLI flags (Claude/Codex/Amp)
- [x] Implement session resume flags for Claude/OpenCode/Amp (Codex unsupported) - [x] Implement session resume flags for Claude/OpenCode/Amp (Codex unsupported)
- [x] Replace sandbox-daemon core agent modules with new agent-management crate (delete originals) - [x] Replace sandbox-agent core agent modules with new agent-management crate (delete originals)
- [x] Stabilize agent-management crate API and fix build issues (sandbox-daemon currently wired to WIP crate) - [x] Stabilize agent-management crate API and fix build issues (sandbox-agent currently wired to WIP crate)
- [x] Implement OpenCode shared server lifecycle (`opencode serve`, health, restart) - [x] Implement OpenCode shared server lifecycle (`opencode serve`, health, restart)
- [x] Implement OpenCode HTTP session APIs + SSE event stream integration - [x] Implement OpenCode HTTP session APIs + SSE event stream integration
- [x] Implement JSONL parsing for subprocess agents and map to `UniversalEvent` - [x] Implement JSONL parsing for subprocess agents and map to `UniversalEvent`
@ -71,7 +71,7 @@
## Frontend (frontend/packages/web) ## Frontend (frontend/packages/web)
- [x] Build Vite + React app with connect screen (endpoint + optional token) - [x] Build Vite + React app with connect screen (endpoint + optional token)
- [x] Add instructions to run sandbox-daemon (including CORS) - [x] Add instructions to run sandbox-agent (including CORS)
- [x] Implement full agent UI covering all features - [x] Implement full agent UI covering all features
- [x] Add HTTP request log with copyable curl command - [x] Add HTTP request log with copyable curl command
@ -93,6 +93,6 @@
--- ---
- implement release pipeline - [x] implement release pipeline
- implement e2b example - implement e2b example
- implement typescript "start locally" by pulling form server using version - implement typescript "start locally" by pulling form server using version