mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-20 06:00:06 +08:00
* feat(sandbox): validate configuration before workload activation Closes #3145 Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * fix(sandbox): bound startup failures and preserve activation history Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * fix(sandbox): enforce deadlines on startup RPC attempts Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * test(sandbox): isolate provider auto-create policy fixture Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * test(sandbox): remove unnecessary fixture string delimiters Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * test(sandbox): capture startup logs before ephemeral cleanup Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * fix(sandbox): preserve credential revocation after rebase Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * fix(server): retain provider revision helper for endpoint reports Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * fix(server): import provider object trait in production Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * fix(supervisor): preserve admission across boundary extraction Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * fix(sandbox): use existing boundary discovery imports Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * test(sandbox): distinguish workload and supervisor containers Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * test(server): reconcile admission schema with timestamp migration Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * test(server): reconcile admission with typed deletion schema Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * test(server): reconcile admission with mutation request IDs Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * test(supervisor): adapt local startup fixture to admission state Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * fix(supervisor): deduplicate startup quarantine diagnostics Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * fix(tui): show configuration blockers in sandbox notes Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * feat(sandbox): expire provisioning repair attempts after five minutes Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * fix(supervisor): reconcile admission with provider readiness Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * fix(tui): separate configuration summaries from full diagnostics Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * fix(tui): shorten invalid configuration note Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * test(server): refresh schema inventory after rebase Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> --------- Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com>
195 lines
6.3 KiB
Rust
195 lines
6.3 KiB
Rust
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
#![cfg(feature = "e2e")]
|
|
|
|
//! E2E test: `--provider <type>` auto-creates a provider from local credentials.
|
|
//!
|
|
//! When `--provider claude-code` is passed and no provider named "claude-code" exists,
|
|
//! the CLI should discover `ANTHROPIC_API_KEY` from the local environment,
|
|
//! auto-create a provider, and inject a supervisor-managed placeholder into the
|
|
//! sandbox child process environment.
|
|
//!
|
|
//! The sandbox command (`printenv ANTHROPIC_API_KEY`) verifies that the
|
|
//! placeholder made it all the way through to the sandbox process environment.
|
|
//!
|
|
//! Prerequisites:
|
|
//! - A running openshell gateway (`mise run gateway:docker`)
|
|
//! - The `openshell` binary (built automatically from the workspace)
|
|
|
|
use std::process::Stdio;
|
|
use std::sync::Mutex;
|
|
|
|
use openshell_e2e::harness::binary::openshell_cmd;
|
|
use openshell_e2e::harness::output::{extract_field, strip_ansi};
|
|
|
|
const TEST_API_KEY: &str = "sk-e2e-auto-provider-test-key";
|
|
static CLAUDE_PROVIDER_LOCK: Mutex<()> = Mutex::new(());
|
|
|
|
fn contains_placeholder_for_env_key(output: &str, key: &str) -> bool {
|
|
let legacy = format!("openshell:resolve:env:{key}");
|
|
let revision_prefix = "openshell:resolve:env:v";
|
|
let revision_suffix = format!("_{key}");
|
|
output.split_whitespace().any(|token| {
|
|
token == legacy || (token.starts_with(revision_prefix) && token.ends_with(&revision_suffix))
|
|
})
|
|
}
|
|
|
|
/// Helper: delete a provider by name, ignoring errors.
|
|
async fn delete_provider(name: &str) {
|
|
let mut cmd = openshell_cmd();
|
|
cmd.arg("provider")
|
|
.arg("delete")
|
|
.arg(name)
|
|
.stdout(Stdio::null())
|
|
.stderr(Stdio::null());
|
|
let _ = cmd.status().await;
|
|
}
|
|
|
|
/// Helper: check whether a provider already exists.
|
|
async fn provider_exists(name: &str) -> bool {
|
|
let mut cmd = openshell_cmd();
|
|
cmd.arg("provider")
|
|
.arg("get")
|
|
.arg(name)
|
|
.stdout(Stdio::null())
|
|
.stderr(Stdio::null());
|
|
cmd.status().await.is_ok_and(|status| status.success())
|
|
}
|
|
|
|
/// Helper: delete a sandbox by name, ignoring errors.
|
|
async fn delete_sandbox(name: &str) {
|
|
let mut cmd = openshell_cmd();
|
|
cmd.arg("sandbox")
|
|
.arg("delete")
|
|
.arg(name)
|
|
.stdout(Stdio::null())
|
|
.stderr(Stdio::null());
|
|
let _ = cmd.status().await;
|
|
}
|
|
|
|
/// `--provider claude-code --auto-providers` with `ANTHROPIC_API_KEY` set should
|
|
/// auto-create a "claude-code" provider and inject a placeholder into the sandbox.
|
|
#[tokio::test]
|
|
async fn auto_created_provider_credential_available_in_sandbox() {
|
|
let _provider_lock = CLAUDE_PROVIDER_LOCK
|
|
.lock()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
|
|
if provider_exists("claude-code").await {
|
|
eprintln!("Skipping test: existing provider 'claude-code' would make shared state unsafe");
|
|
return;
|
|
}
|
|
|
|
// Clean up any leftover from a previous run.
|
|
delete_provider("claude-code").await;
|
|
|
|
// This test only reads the injected environment placeholder. Do not inherit
|
|
// the published image's network rules, which may be incompatible with the
|
|
// attached credential provider's startup validation.
|
|
let policy = tempfile::NamedTempFile::new().expect("create provider test policy");
|
|
std::fs::write(
|
|
policy.path(),
|
|
r"version: 1
|
|
filesystem_policy:
|
|
include_workdir: true
|
|
read_only: [/usr, /lib, /etc, /proc]
|
|
read_write: [/sandbox, /tmp, /dev/null]
|
|
landlock:
|
|
compatibility: best_effort
|
|
process:
|
|
run_as_user: sandbox
|
|
run_as_group: sandbox
|
|
network_policies: {}
|
|
",
|
|
)
|
|
.expect("write provider test policy");
|
|
|
|
// Create a sandbox that prints the ANTHROPIC_API_KEY env var.
|
|
// --auto-providers skips the interactive prompt.
|
|
let mut cmd = openshell_cmd();
|
|
cmd.arg("sandbox")
|
|
.arg("create")
|
|
.arg("--detach")
|
|
.arg("--policy")
|
|
.arg(policy.path())
|
|
.arg("--provider")
|
|
.arg("claude-code")
|
|
.arg("--auto-providers")
|
|
.args(["--", "sh", "-c", "exec sleep infinity"])
|
|
.env("ANTHROPIC_API_KEY", TEST_API_KEY)
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped());
|
|
|
|
let output = cmd
|
|
.output()
|
|
.await
|
|
.expect("failed to spawn openshell sandbox create");
|
|
|
|
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
|
|
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
|
let combined = format!("{stdout}{stderr}");
|
|
let clean = strip_ansi(&combined);
|
|
|
|
// Parse sandbox name for cleanup.
|
|
let sandbox_name = extract_field(&combined, "Created sandbox");
|
|
let exec_output = if let Some(ref name) = sandbox_name {
|
|
let mut exec_cmd = openshell_cmd();
|
|
exec_cmd
|
|
.args([
|
|
"sandbox",
|
|
"exec",
|
|
"--name",
|
|
name,
|
|
"--no-tty",
|
|
"--",
|
|
"printenv",
|
|
"ANTHROPIC_API_KEY",
|
|
])
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped());
|
|
Some(exec_cmd.output().await.expect("failed to run sandbox exec"))
|
|
} else {
|
|
None
|
|
};
|
|
|
|
// Always clean up, even if assertions fail.
|
|
if let Some(ref name) = sandbox_name {
|
|
delete_sandbox(name).await;
|
|
}
|
|
delete_provider("claude-code").await;
|
|
|
|
// Now assert.
|
|
assert!(
|
|
output.status.success(),
|
|
"sandbox create should succeed (exit {:?}):\n{clean}",
|
|
output.status.code()
|
|
);
|
|
|
|
let exec_output = exec_output.expect("sandbox name should be present");
|
|
let exec_clean = strip_ansi(&format!(
|
|
"{}{}",
|
|
String::from_utf8_lossy(&exec_output.stdout),
|
|
String::from_utf8_lossy(&exec_output.stderr)
|
|
));
|
|
assert!(
|
|
exec_output.status.success(),
|
|
"sandbox exec should succeed:\n{exec_clean}"
|
|
);
|
|
|
|
assert!(
|
|
clean.contains("Created provider claude-code"),
|
|
"output should confirm provider auto-creation:\n{clean}"
|
|
);
|
|
|
|
assert!(
|
|
contains_placeholder_for_env_key(&exec_clean, "ANTHROPIC_API_KEY"),
|
|
"sandbox should have placeholder ANTHROPIC_API_KEY in its environment:\n{exec_clean}"
|
|
);
|
|
|
|
assert!(
|
|
!exec_clean.contains(TEST_API_KEY),
|
|
"sandbox should not expose the raw ANTHROPIC_API_KEY secret:\n{exec_clean}"
|
|
);
|
|
}
|