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
6 changes: 6 additions & 0 deletions crates/openshell-core/src/sandbox_env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ pub const ENDPOINT: &str = "OPENSHELL_ENDPOINT";
/// Unique identifier of the sandbox being supervised.
pub const SANDBOX_ID: &str = "OPENSHELL_SANDBOX_ID";

/// Requested workload image reference, recorded in supervisor OCSF events.
pub const CONTAINER_IMAGE: &str = "OPENSHELL_CONTAINER_IMAGE";

/// Immutable workload image ID resolved by the compute driver at launch.
pub const CONTAINER_IMAGE_UID: &str = "OPENSHELL_CONTAINER_IMAGE_UID";

/// Filesystem path to the UNIX socket used for the in-sandbox SSH server.
pub const SSH_SOCKET_PATH: &str = "OPENSHELL_SSH_SOCKET_PATH";

Expand Down
33 changes: 33 additions & 0 deletions crates/openshell-driver-docker/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5339,6 +5339,37 @@ fn docker_network_trust_supervisor_mount(
})
}

/// Read the image identity pinned at provisioning, including after a restart.
/// Re-resolving the template's tag here could identify a different image.
async fn docker_workload_image_environment(
sandbox: &DriverSandbox,
directory: &Path,
) -> Result<Vec<String>, Status> {
let bytes = tokio::fs::read(directory.join(BOUNDARY_CONFIG_FILE))
.await
.map_err(|error| Status::internal(format!("read Docker image launch identity: {error}")))?;
let bootstrap: BoundaryConfig = serde_json::from_slice(&bytes)
.map_err(|_| Status::internal("decode Docker image launch identity"))?;
let mut environment = Vec::new();
if let Some(image) = sandbox_image(sandbox) {
environment.push(format!(
"{}={image}",
openshell_core::sandbox_env::CONTAINER_IMAGE
));
}
if let Some(uid) = bootstrap
.resource_claims
.get("docker.image_identity")
.filter(|uid| !uid.trim().is_empty())
{
environment.push(format!(
"{}={uid}",
openshell_core::sandbox_env::CONTAINER_IMAGE_UID
));
}
Ok(environment)
}

async fn spawn_docker_control_process(
docker: &Docker,
sandbox: &DriverSandbox,
Expand All @@ -5365,6 +5396,7 @@ async fn spawn_docker_control_process(
let workspace_root = tokio::fs::read_to_string(directory.join(WORKSPACE_ROOT_FILE))
.await
.map_err(|error| Status::internal(format!("read Docker workspace root: {error}")))?;
let image_environment = docker_workload_image_environment(sandbox, &directory).await?;
let supervisor_name = format!("{}-supervisor", container_name_for_sandbox(sandbox));
let _ = docker
.remove_container(
Expand Down Expand Up @@ -5415,6 +5447,7 @@ async fn spawn_docker_control_process(
openshell_core::telemetry::enabled_env_value()
),
];
environment.extend(image_environment);
environment.extend(supervisor_otlp_environment(config));
if config.guest_tls.is_some() {
environment.push(format!(
Expand Down
15 changes: 15 additions & 0 deletions crates/openshell-driver-docker/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,21 @@ async fn restart_rotates_bootstrap_descriptor_and_provisioning_evidence_together

let bootstrap: BoundaryConfig =
serde_json::from_slice(&fs::read(directory.join(BOUNDARY_CONFIG_FILE)).unwrap()).unwrap();
// A restart keeps the original image identity even if the mutable
// template reference now names another image in the registry.
let sandbox = test_sandbox();
let image_env = docker_workload_image_environment(&sandbox, &directory)
.await
.unwrap();
assert!(image_env.contains(&format!(
"{}=image-test",
openshell_core::sandbox_env::CONTAINER_IMAGE_UID
)));
assert!(image_env.contains(&format!(
"{}={}",
openshell_core::sandbox_env::CONTAINER_IMAGE,
sandbox_image(&sandbox).unwrap()
)));
let descriptor = read_docker_runtime_descriptor(&sandbox_id, &config)
.await
.unwrap()
Expand Down
5 changes: 3 additions & 2 deletions crates/openshell-driver-mxc/src/etw_consumer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1679,6 +1679,7 @@ impl EtwEventProcessor {
sandbox_id: sandbox_id.to_string(),
sandbox_name: sandbox_name.to_string(),
container_image: "mxc/appcontainer".to_string(),
container_image_uid: None,
origin: openshell_ocsf::EventOrigin::Gateway {
name: self.gateway_name.clone(),
},
Expand Down Expand Up @@ -2106,8 +2107,8 @@ mod tests {
assert_eq!(json["device"]["name"], "production");
assert_eq!(json["device"]["uid"], "production");
assert_eq!(json["device"]["type"], "Server");
assert_eq!(json["container"]["name"], "agent-01");
assert_eq!(json["container"]["uid"], "sbx-123");
assert_eq!(json["unmapped"]["sandbox_name"], "agent-01");
assert_eq!(json["unmapped"]["sandbox_id"], "sbx-123");
}

#[test]
Expand Down
31 changes: 30 additions & 1 deletion crates/openshell-driver-podman/src/container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -724,7 +724,10 @@ fn build_env(
openshell_core::sandbox_env::SSH_SOCKET_PATH.into(),
config.ssh_socket_path.clone(),
);
env.insert("OPENSHELL_CONTAINER_IMAGE".into(), image.to_string());
env.insert(
openshell_core::sandbox_env::CONTAINER_IMAGE.into(),
image.to_string(),
);
let main_process = openshell_core::sandbox_env::MainProcessConfig::encode_driver_spec(spec)
.map_err(|error| ComputeDriverError::Precondition(error.to_string()))?;
env.insert(
Expand Down Expand Up @@ -1723,6 +1726,10 @@ pub fn build_isolation_specs(
network_trust_generation(input.network_trust).into(),
);
supervisor.image.clone_from(&input.config.supervisor_image);
supervisor.env.insert(
openshell_core::sandbox_env::CONTAINER_IMAGE_UID.into(),
input.image.id.clone(),
);
supervisor.entrypoint = vec!["/openshell-supervisor".into()];
supervisor.command.extend([
format!(
Expand Down Expand Up @@ -2129,6 +2136,28 @@ mod tests {
vec!["--bootstrap", crate::isolation::BOOTSTRAP_PATH]
);
assert_eq!(specs.supervisor.user, "1000:1001");
assert_eq!(
specs
.supervisor
.env
.get(openshell_core::sandbox_env::CONTAINER_IMAGE_UID)
.map(String::as_str),
Some("sha256:image")
);
assert_eq!(
specs
.supervisor
.env
.get(openshell_core::sandbox_env::CONTAINER_IMAGE)
.map(String::as_str),
Some("image:latest")
);
assert!(
!specs
.workload
.env
.contains_key(openshell_core::sandbox_env::CONTAINER_IMAGE_UID)
);
assert_eq!(specs.supervisor.groups, vec!["2000"]);
assert!(specs.supervisor.cap_add.is_empty());
assert_eq!(specs.workload.netns.nsmode, "none");
Expand Down
2 changes: 1 addition & 1 deletion crates/openshell-ocsf-validate/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Validates OCSF events against JSON Schemas exported by the OCSF schema server.

OCSF validity depends on the profiles an event declares in `metadata.profiles`, and an exported JSON Schema covers one fixed set of profiles. `schemas/v<version>/` therefore holds one schema per event class and profile combination, named `<class>@<profile>+<profile>.json` with profiles sorted. `validate_event` picks the schema from the event's `metadata.version`, `class_uid`, and declared profiles, and fails when none is vendored.

The schemas check required attributes, `at_least_one` constraints, undefined attributes, types, and enum values, including the class and category identifiers. The schemas type `type_uid` only as an integer, so `validate_event` also checks that it equals `class_uid * 100 + activity_id`. The top-level `container` object is accepted although OCSF does not define it at the event level (NVIDIA/OpenShell#4915).
The schemas check required attributes, `at_least_one` constraints, undefined attributes, types, and enum values, including the class and category identifiers. The schemas type `type_uid` only as an integer, so `validate_event` also checks that it equals `class_uid * 100 + activity_id`. Supervisor sandbox identity is validated under `device.container` with the `container` profile; undefined top-level `container` objects are rejected.

## Updating

Expand Down
Loading
Loading