Skip to content
Draft
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
1 change: 1 addition & 0 deletions Cargo.lock

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

14 changes: 14 additions & 0 deletions crates/buzz-acp/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,20 @@ The image repository's `buzz-agent/test-checkpoint-storage.py` also stops the
packaged harness during startup while an adapter writes files. It checks that
recovery preserves the final edit and rejected uploads make shutdown fail.

Run the controller lifecycle test against the isolated local kind cluster:

```sh
BUZZ_SANDBOX_TEST_KUBECONFIG=/absolute/kind.kubeconfig \
BUZZ_SANDBOX_TEST_IMAGE=registry.example/uid-guard-fixture@sha256:<digest> \
cargo test -p buzz-backend-kubernetes live_controller_reuses_scopes \
-- --ignored --nocapture
```

The test requires context `kind-remote-agent-check`, the Agent Sandbox controller,
and a loaded fixture image that checks the Pod UID binding before sleeping.
It verifies concurrent reuse, separate threads, owner and relay refusals, and
refusal after the original Pod is replaced. It does not enroll real accounts.

Managed agent sessions may already export harness options. Clear them when
running the package suite: three CLI parsing tests assert the unset defaults,
and inherited values would change the inputs those tests exercise. Running the
Expand Down
1 change: 1 addition & 0 deletions crates/buzz-backend-kubernetes/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ sha2 = { workspace = true }
hex = { workspace = true }
rand = { workspace = true }
chrono = { workspace = true }
uuid = { workspace = true }
http = "1"
http-body-util = "0.1"

Expand Down
23 changes: 23 additions & 0 deletions crates/buzz-backend-kubernetes/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,28 @@ pub fn config_schema() -> serde_json::Value {
"title": "Service account",
"description": "Scheduling/RBAC identity only. No API token is mounted."
},
"sandbox": {
"oneOf": [
{"const": false},
{
"type": "object",
"additionalProperties": false,
"required": ["channel_id", "thread_root"],
"properties": {
"channel_id": {
"type": "string",
"format": "uuid",
"pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
},
"thread_root": {"type": "string", "pattern": "^[0-9a-f]{64}$"}
}
}
],
"x-buzz-ui-type": "boolean",
"default": false,
"title": "Workspace per thread",
"description": "Use Agent Sandbox. Mention the agent in a channel to create or reuse that thread's workspace. Requires an absolute Pod deadline and BUZZ_CHECKPOINT_BUCKET/BUZZ_CHECKPOINT_PREFIX (or BUZZ_SANDBOX_CHECKPOINT=disabled) in the agent environment. Desktop stores a boolean setting and binds the mentioned thread at launch; deploy accepts only {\"channel_id\": \"<UUID>\", \"thread_root\": \"<event ID>\"} and refuses true. false or absent disables Sandbox."
},
Comment thread
tee8z marked this conversation as resolved.
"identity_policy": {
"type": "object",
"description": "Optional approved agent and owner public keys, cryptographically checked before cluster access. Also enables local --check-identity preflight.",
Expand Down Expand Up @@ -806,6 +828,7 @@ mod tests {
"memory_request",
"namespace",
"pod_options",
"sandbox",
"service_account"
]
);
Expand Down
10 changes: 8 additions & 2 deletions crates/buzz-backend-kubernetes/src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,13 @@ const RESPOND_TO_ALLOWLIST: &str = "allowlist";
/// rejects `nobody` on purpose (`managed_agents/types.rs:871-880`), but the
/// harness starts fine with it. This guard exists to cover non-desktop callers,
/// so inheriting a desktop-only narrowing would refuse a launch that works.
const RESPOND_TO_MODES: [&str; 4] = ["owner-only", RESPOND_TO_ALLOWLIST, "anyone", "nobody"];
const RESPOND_TO_MODES: [&str; 5] = [
"owner-only",
"owner",
RESPOND_TO_ALLOWLIST,
"anyone",
"nobody",
];

/// Refuse a respond-to gate the harness will reject at config parse.
///
Expand Down Expand Up @@ -804,7 +810,7 @@ mod tests {
/// for. Without this test, refusing `nobody` would pass everything above.
#[test]
fn every_mode_the_harness_accepts_is_deployable() {
for mode in ["owner-only", "allowlist", "anyone", "nobody"] {
for mode in ["owner-only", "owner", "allowlist", "anyone", "nobody"] {
let agent = payload_json(serde_json::json!({
"respond_to": mode,
"respond_to_allowlist": [pubkey('a')],
Expand Down
13 changes: 12 additions & 1 deletion crates/buzz-backend-kubernetes/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ mod naming;
mod observe;
mod pod;
mod reconcile;
mod sandbox;
mod wire;

use std::io::Read;
Expand Down Expand Up @@ -135,9 +136,10 @@ fn refuse_relay_mesh(raw: &serde_json::Value) -> Option<String> {
/// Run one deploy to a terminal outcome.
async fn deploy_agent(request: &wire::DeployRequest) -> Result<String, String> {
let cfg = config::parse(&request.provider_config)?;
let sandbox = sandbox::Options::parse(&request.provider_config)?;
// Identity before any cluster contact: a malformed nsec is a refusal, not
// a failed connection (§Deploy State Machine step 0).
let identity = identity::check(request, false)?;
let identity = identity::check(request, sandbox.is_some())?;

// One generation for this operation's first attempt; the reconciler mints
// its own per attempt and restamps the correlator to match.
Expand All @@ -150,6 +152,15 @@ async fn deploy_agent(request: &wire::DeployRequest) -> Result<String, String> {
)?;

let client = client::connect(cfg.context.as_deref()).await?;
if let Some(scope) = sandbox {
let owner = request
.agent
.launch
.as_ref()
.and_then(|launch| launch.owner_pubkey.as_deref())
.ok_or("Sandbox requires a verified launch owner")?;
return sandbox::deploy(client, &identity, &cfg, &scope, owner, env).await;
}
let substrate = cluster::Cluster::new(client, &cfg.namespace);
reconcile::deploy(&substrate, &identity, &cfg, env).await
}
Expand Down
Loading
Loading