Skip to content
Merged
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.

9 changes: 7 additions & 2 deletions crates/buzz-backend-kubernetes/src/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,17 @@ pub fn check_managed_defaults(
/// A provider that forwards the request (`buzz-backend-relay`) runs them
/// first, so a request that would be refused never leaves the machine.
pub fn check_sandbox_request(request: &wire::DeployRequest) -> Result<(), wire::ProviderError> {
sandbox::Options::parse(&request.provider_config)?
.ok_or("the session manager starts Sandbox sessions only")?;
check_sandbox_scope(&request.provider_config)?;
identity::check(request, true)?;
Ok(())
}

/// Validate a concrete channel and thread scope before accepting a managed start.
pub fn check_sandbox_scope(config: &serde_json::Value) -> Result<(), String> {
sandbox::Options::parse(config)?.ok_or("the session manager starts Sandbox sessions only")?;
Ok(())
}

impl Target {
/// Overwrite what the managed target decides. A no-op for kubeconfig.
fn confine(&self, request: &mut wire::DeployRequest) -> Result<(), wire::ProviderError> {
Expand Down
25 changes: 25 additions & 0 deletions crates/buzz-backend-kubernetes/src/sandbox_fake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,31 @@ impl State {
self.secrets.remove(*name);
ok(&json!({"apiVersion":"v1","kind":"Status","status":"Success"}))
}
["api", "v1", "namespaces", _, "secrets", name] if method == Method::PUT => {
let Some(prior) = self.secrets.get(*name) else {
return status(404, "NotFound");
};
let mut secret: Secret = serde_json::from_slice(body).unwrap();
if prior["immutable"] == true
|| secret.metadata.resource_version.as_deref()
!= prior["metadata"]["resourceVersion"].as_str()
{
return status(409, "Conflict");
}
secret.metadata.uid = prior["metadata"]["uid"].as_str().map(str::to_owned);
secret.metadata.resource_version = Some(self.next_rv());
secret.data.get_or_insert_with(Default::default).extend(
secret
.string_data
.take()
.unwrap_or_default()
.into_iter()
.map(|(key, value)| (key, ByteString(value.into_bytes()))),
);
let stored = serde_json::to_value(secret).unwrap();
self.secrets.insert((*name).into(), stored.clone());
ok(&stored)
}
["api", "v1", "namespaces", _, "secrets", name] => match self.secrets.get(*name) {
Some(secret) => ok(secret),
None => status(404, "NotFound"),
Expand Down
8 changes: 8 additions & 0 deletions crates/buzz-backend-kubernetes/src/wire.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,8 @@ pub struct ErrorResponse {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ErrorCode {
/// A desktop launch cannot register its relay endpoint for mobile starts.
InvalidLaunchRegistration,
/// The session is a tombstone; only an explicit recovery starts another.
SessionEnded,
/// A stop is draining; the session ends once its Pod is gone.
Expand Down Expand Up @@ -242,6 +244,12 @@ pub enum ErrorCode {
GithubLoginMismatch,
/// The profile could not be written to the developer's secret.
ProfileStoreFailed,
/// No saved launch exists for this owner, community and agent.
LaunchNotRegistered,
/// A saved launch could not be read or durably updated.
LaunchStoreFailed,
/// An owner-start payload is not a signed mention of the requested agent.
InvalidStart,
/// Client-side: the manager did not accept the request within 30 s.
ManagerUnavailable,
/// Client-side: the manager accepted the request but did not finish it
Expand Down
1 change: 1 addition & 0 deletions crates/buzz-sandbox-reaper/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ aws-sdk-secretsmanager = { version = "1", default-features = false, features = [
reqwest = { workspace = true }
toml = "1.0"
zeroize = { workspace = true }
url = { workspace = true }
# Same reason as buzz-backend-kubernetes: the process-level CryptoProvider is
# installed explicitly because the workspace unifies ring and aws-lc-rs.
rustls = { version = "0.23", default-features = false, features = ["ring", "std"] }
Expand Down
1 change: 1 addition & 0 deletions crates/buzz-sandbox-reaper/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ mod config;
mod enroll;
mod evidence;
mod manager;
mod mobile_launch;
mod plan;
mod profile;
mod reconcile;
Expand Down
83 changes: 77 additions & 6 deletions crates/buzz-sandbox-reaper/src/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use sha2::{Digest, Sha256};

use crate::config::{is_dns_label, is_owner, CONFIG_VERSION};
use crate::enroll::{self, Enrollment};
use crate::mobile_launch::{Registration, Start};

/// How long received event ids and finished responses are kept.
pub const REPLAY_WINDOW_SECS: u64 = 600;
Expand Down Expand Up @@ -253,7 +254,13 @@ pub struct Job {
/// What a job runs.
pub enum Work {
/// A provider protocol request, against the owner's namespace.
Provider { request: Request, target: Target },
Provider {
request: Request,
target: Target,
registration: Option<Registration>,
},
/// An owner-authorized start using a registered launch identity.
Start { start: Start, target: Target },
/// Steps 5 and 6 of enrollment: the merge, the GitHub login check and
/// the write.
Enroll {
Expand All @@ -268,8 +275,41 @@ impl Work {
/// Run to the `done` response.
pub async fn run(self, manager: &Manager) -> serde_json::Value {
match self {
Work::Provider { request, target } => {
response_value(&provider::run(request, &target).await)
Work::Provider {
request,
target,
registration,
} => {
let response = response_value(&provider::run(request, &target).await);
if response["ok"] == true {
if let (
Some(registration),
Target::Managed {
client, namespace, ..
},
) = (registration, &target)
{
if let Err(error) = registration.save(client.clone(), namespace).await {
return error;
}
}
}
response
}
Work::Start { start, target } => {
let Target::Managed {
client, namespace, ..
} = &target
else {
return refusal(
ErrorCode::InvalidStart,
"mobile starts require a managed target",
);
};
match start.request(client.clone(), namespace).await {
Ok(request) => response_value(&provider::run(request, &target).await),
Err(error) => error,
}
}
Work::Enroll { developer, profile } => {
manager.enrollment.run(&developer, profile).await
Expand Down Expand Up @@ -444,11 +484,42 @@ impl Manager {
Work::Profile {
developer: developer.clone(),
}
} else if op == Op::Start {
let start = match Start::admit(
opened.request.payload.clone(),
owner,
opened.created_at.as_secs(),
) {
Ok(start) => start,
Err(response) => return answer(response),
};
let target = Target::Managed {
client: self.client.clone(),
namespace: developer.namespace.clone(),
agent: start.agent(),
owner,
defaults: self.defaults.clone(),
env: self.config.launch_env(developer),
};
Work::Start { start, target }
} else {
let registration = if op == Op::Deploy {
match Registration::new(
owner,
opened.signer,
opened.request.payload["agent"].clone(),
) {
Ok(registration) => Some(registration),
Err(response) => return answer(response),
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
} else {
None
};
match self.payload(&opened) {
// 6. The namespace comes from the verified owner, never the request.
Ok(request) => Work::Provider {
request,
registration,
target: Target::Managed {
client: self.client.clone(),
namespace: developer.namespace.clone(),
Expand Down Expand Up @@ -563,7 +634,7 @@ impl Manager {
return Admission::Ignore {
event: event.id,
error,
}
};
}
};
Admission::Done {
Expand All @@ -579,12 +650,12 @@ impl Manager {

/// Steps 2 and 7: the verified owner.
fn owner(&self, opened: &OpenedRequest) -> Result<PublicKey, (ErrorCode, &'static str)> {
if matches!(opened.request.op, Op::Enroll | Op::Profile) {
if matches!(opened.request.op, Op::Enroll | Op::Profile | Op::Start) {
// The profile is owner-level: an agent key's attestation is not enough.
if opened.auth_tag.is_some() {
return Err((
ErrorCode::SignerMismatch,
"enroll and profile must be signed by the owner key itself",
"enroll, profile and start must be signed by the owner key itself",
));
}
return Ok(opened.signer);
Expand Down
1 change: 1 addition & 0 deletions crates/buzz-sandbox-reaper/src/manager_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ async fn an_attested_fresh_deploy_starts_in_the_owners_namespace() {
env,
..
},
..
} = job.work
else {
panic!("not a managed deploy");
Expand Down
Loading
Loading