mirror of
https://github.com/openai/codex.git
synced 2026-09-08 15:50:34 +00:00
ci: add codex exec Linux sandbox smoke
This commit is contained in:
4
.github/workflows/README.md
vendored
4
.github/workflows/README.md
vendored
@@ -13,6 +13,10 @@ The workflows in this directory are split so that pull requests get fast, review
|
||||
- `cargo shear`
|
||||
- `argument-comment-lint` on Linux, macOS, and Windows
|
||||
- `tools/argument-comment-lint` package tests when the lint or its workflow wiring changes
|
||||
- `linux-sandbox-smoke.yml` is an explicit host-policy smoke test for Linux
|
||||
sandboxing. It installs Ubuntu's `bubblewrap` and AppArmor profiles on the
|
||||
runner, then runs `codex exec` once with the default bwrap path and once with
|
||||
`use_legacy_landlock=true`.
|
||||
|
||||
## Post-Merge On `main`
|
||||
|
||||
|
||||
120
.github/workflows/linux-sandbox-smoke.yml
vendored
Normal file
120
.github/workflows/linux-sandbox-smoke.yml
vendored
Normal file
@@ -0,0 +1,120 @@
|
||||
name: Linux sandbox smoke
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/linux-sandbox-smoke.yml"
|
||||
- "codex-rs/**"
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- ".github/workflows/linux-sandbox-smoke.yml"
|
||||
- "codex-rs/**"
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}::${{ github.event.pull_request.number > 0 && format('pr-{0}', github.event.pull_request.number) || github.ref_name }}${{ github.ref_name == 'main' && format('::{0}', github.run_id) || '' }}
|
||||
cancel-in-progress: ${{ github.ref_name != 'main' }}
|
||||
|
||||
jobs:
|
||||
codex_exec:
|
||||
name: codex exec sandbox modes
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
|
||||
|
||||
- name: Install host sandbox dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
sudo apt-get update -y
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||
apparmor \
|
||||
apparmor-profiles \
|
||||
apparmor-utils \
|
||||
bubblewrap \
|
||||
libcap-dev \
|
||||
pkg-config
|
||||
|
||||
- name: Check host sandbox policy
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
bwrap="$(command -v bwrap)"
|
||||
bwrap_real="$(readlink -f "$bwrap")"
|
||||
echo "bwrap=$bwrap_real"
|
||||
if [[ "$bwrap_real" != "/usr/bin/bwrap" ]]; then
|
||||
echo "Expected apt-installed bubblewrap at /usr/bin/bwrap."
|
||||
exit 1
|
||||
fi
|
||||
bwrap --version
|
||||
|
||||
runner_seccomp="$(grep '^Seccomp:' /proc/self/status | tr -s '[:space:]' ' ' | cut -d' ' -f2)"
|
||||
echo "runner.seccomp=$runner_seccomp"
|
||||
if [[ "$runner_seccomp" != "0" ]]; then
|
||||
echo "Linux workers must not run under an extra outer seccomp filter."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -r /sys/module/apparmor/parameters/enabled ]]; then
|
||||
echo "AppArmor module status is unavailable on this Linux worker."
|
||||
exit 1
|
||||
fi
|
||||
apparmor_enabled="$(cat /sys/module/apparmor/parameters/enabled)"
|
||||
echo "apparmor.enabled=$apparmor_enabled"
|
||||
if [[ "$apparmor_enabled" != "Y" ]]; then
|
||||
echo "AppArmor must be enabled for the Linux sandbox smoke test."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! apparmor_userns="$(sysctl -n kernel.apparmor_restrict_unprivileged_userns 2>/dev/null)"; then
|
||||
echo "kernel.apparmor_restrict_unprivileged_userns is unavailable on this Linux worker."
|
||||
exit 1
|
||||
fi
|
||||
echo "kernel.apparmor_restrict_unprivileged_userns=$apparmor_userns"
|
||||
if [[ "$apparmor_userns" == "0" ]]; then
|
||||
echo "AppArmor user namespace restrictions must stay enabled; do not disable them in CI."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if userns_clone="$(sysctl -n kernel.unprivileged_userns_clone 2>/dev/null)"; then
|
||||
echo "kernel.unprivileged_userns_clone=$userns_clone"
|
||||
if [[ "$userns_clone" != "1" ]]; then
|
||||
echo "Linux workers must enable unprivileged user namespaces in the base image."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
profile_source="/usr/share/apparmor/extra-profiles/bwrap-userns-restrict"
|
||||
profile_target="/etc/apparmor.d/bwrap-userns-restrict"
|
||||
if [[ ! -r "$profile_source" ]]; then
|
||||
echo "Ubuntu's bwrap-userns-restrict AppArmor profile is missing."
|
||||
dpkg -L apparmor-profiles | grep bwrap || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sudo ln -sf "$profile_source" "$profile_target"
|
||||
sudo apparmor_parser -r "$profile_target"
|
||||
sudo aa-enforce /usr/bin/bwrap
|
||||
if ! sudo grep -Eq '^bwrap \(enforce\)$' /sys/kernel/security/apparmor/profiles; then
|
||||
echo "Ubuntu's bwrap AppArmor profile is not loaded in enforce mode."
|
||||
sudo grep bwrap /sys/kernel/security/apparmor/profiles || true
|
||||
exit 1
|
||||
fi
|
||||
if ! sudo grep -Eq '^bwrap//.*unpriv_bwrap \(enforce\)$' /sys/kernel/security/apparmor/profiles; then
|
||||
echo "Ubuntu's unprivileged bwrap child profile is not loaded in enforce mode."
|
||||
sudo grep bwrap /sys/kernel/security/apparmor/profiles || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Smoke test codex exec sandbox modes
|
||||
working-directory: codex-rs
|
||||
env:
|
||||
CODEX_LINUX_SANDBOX_SMOKE: 1
|
||||
RUST_BACKTRACE: 1
|
||||
run: cargo test -p codex-exec --test all -- linux_sandbox_smoke --nocapture
|
||||
114
.github/workflows/rust-ci-full.yml
vendored
114
.github/workflows/rust-ci-full.yml
vendored
@@ -569,7 +569,7 @@ jobs:
|
||||
set -euo pipefail
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
sudo apt-get update -y
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends apparmor apparmor-profiles apparmor-utils bubblewrap pkg-config libcap-dev
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends pkg-config libcap-dev
|
||||
fi
|
||||
|
||||
# Some integration tests rely on DotSlash being installed.
|
||||
@@ -645,114 +645,16 @@ jobs:
|
||||
tool: nextest
|
||||
version: 0.9.103
|
||||
|
||||
- name: Check distro bubblewrap sandbox prerequisites (Linux)
|
||||
- name: Enable unprivileged user namespaces (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
bwrap="$(command -v bwrap)"
|
||||
bwrap_real="$(readlink -f "$bwrap")"
|
||||
echo "bwrap=$bwrap_real"
|
||||
if [[ "$bwrap_real" != "/usr/bin/bwrap" ]]; then
|
||||
echo "Expected apt-installed bubblewrap at /usr/bin/bwrap."
|
||||
exit 1
|
||||
# Required for bubblewrap to work on Linux CI runners.
|
||||
sudo sysctl -w kernel.unprivileged_userns_clone=1
|
||||
# Ubuntu 24.04+ can additionally gate unprivileged user namespaces
|
||||
# behind AppArmor.
|
||||
if sudo sysctl -a 2>/dev/null | grep -q '^kernel.apparmor_restrict_unprivileged_userns'; then
|
||||
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
|
||||
fi
|
||||
bwrap --version
|
||||
|
||||
runner_seccomp="$(grep '^Seccomp:' /proc/self/status | tr -s '[:space:]' ' ' | cut -d' ' -f2)"
|
||||
echo "runner.seccomp=$runner_seccomp"
|
||||
if [[ "$runner_seccomp" != "0" ]]; then
|
||||
echo "Linux workers must not run under an extra outer seccomp filter."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -r /sys/module/apparmor/parameters/enabled ]]; then
|
||||
echo "AppArmor module status is unavailable on this Linux worker."
|
||||
exit 1
|
||||
fi
|
||||
apparmor_enabled="$(cat /sys/module/apparmor/parameters/enabled)"
|
||||
echo "apparmor.enabled=$apparmor_enabled"
|
||||
if [[ "$apparmor_enabled" != "Y" ]]; then
|
||||
echo "AppArmor must be enabled for the distro bubblewrap smoke test."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! apparmor_userns="$(sysctl -n kernel.apparmor_restrict_unprivileged_userns 2>/dev/null)"; then
|
||||
echo "kernel.apparmor_restrict_unprivileged_userns is unavailable on this Linux worker."
|
||||
exit 1
|
||||
fi
|
||||
echo "kernel.apparmor_restrict_unprivileged_userns=$apparmor_userns"
|
||||
if [[ "$apparmor_userns" == "0" ]]; then
|
||||
echo "AppArmor user namespace restrictions must stay enabled; do not disable them in CI."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if userns_clone="$(sysctl -n kernel.unprivileged_userns_clone 2>/dev/null)"; then
|
||||
echo "kernel.unprivileged_userns_clone=$userns_clone"
|
||||
if [[ "$userns_clone" != "1" ]]; then
|
||||
echo "Linux workers must enable unprivileged user namespaces in the base image."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
profile_source="/usr/share/apparmor/extra-profiles/bwrap-userns-restrict"
|
||||
profile_target="/etc/apparmor.d/bwrap-userns-restrict"
|
||||
if [[ ! -r "$profile_source" ]]; then
|
||||
echo "Ubuntu's bwrap-userns-restrict AppArmor profile is missing."
|
||||
dpkg -L apparmor-profiles | grep bwrap || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sudo ln -sf "$profile_source" "$profile_target"
|
||||
sudo apparmor_parser -r "$profile_target"
|
||||
sudo aa-enforce /usr/bin/bwrap
|
||||
if ! sudo grep -Eq '^bwrap \(enforce\)$' /sys/kernel/security/apparmor/profiles; then
|
||||
echo "Ubuntu's bwrap AppArmor profile is not loaded in enforce mode."
|
||||
sudo grep bwrap /sys/kernel/security/apparmor/profiles || true
|
||||
exit 1
|
||||
fi
|
||||
if ! sudo grep -Eq '^bwrap//.*unpriv_bwrap \(enforce\)$' /sys/kernel/security/apparmor/profiles; then
|
||||
echo "Ubuntu's unprivileged bwrap child profile is not loaded in enforce mode."
|
||||
sudo grep bwrap /sys/kernel/security/apparmor/profiles || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Smoke test Codex Linux sandbox through distro bubblewrap
|
||||
if: runner.os == 'Linux'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
smoke_file=".codex-bwrap-smoke"
|
||||
rm -f "$smoke_file"
|
||||
trap 'rm -f "$smoke_file"' EXIT
|
||||
|
||||
CODEX_HOME="${RUNNER_TEMP}/codex-bwrap-smoke-home" \
|
||||
cargo run --quiet --target ${{ matrix.target }} --profile ci-test -p codex-cli --bin codex -- \
|
||||
sandbox linux --full-auto -- bash -lc \
|
||||
'
|
||||
set -euo pipefail
|
||||
|
||||
aa_profile="$(cat /proc/self/attr/current)"
|
||||
echo "payload.apparmor=$aa_profile"
|
||||
case "$aa_profile" in
|
||||
*bwrap*unpriv_bwrap*) ;;
|
||||
*)
|
||||
echo "Expected payload to run under the Ubuntu unprivileged bwrap AppArmor profile." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
seccomp_mode="$(grep "^Seccomp:" /proc/self/status | tr -s "[:space:]" " " | cut -d" " -f2)"
|
||||
echo "payload.seccomp=$seccomp_mode"
|
||||
if [[ "$seccomp_mode" != "2" ]]; then
|
||||
echo "Expected Codex to install a seccomp filter in the sandbox payload." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf ok > .codex-bwrap-smoke
|
||||
test "$(cat .codex-bwrap-smoke)" = ok
|
||||
'
|
||||
|
||||
- name: Set up remote test env (Docker)
|
||||
if: ${{ runner.os == 'Linux' && matrix.remote_env == 'true' }}
|
||||
|
||||
150
codex-rs/exec/tests/suite/linux_sandbox_smoke.rs
Normal file
150
codex-rs/exec/tests/suite/linux_sandbox_smoke.rs
Normal file
@@ -0,0 +1,150 @@
|
||||
#![cfg(target_os = "linux")]
|
||||
|
||||
use assert_cmd::prelude::*;
|
||||
use core_test_support::responses;
|
||||
use core_test_support::responses::ev_assistant_message;
|
||||
use core_test_support::responses::ev_completed;
|
||||
use core_test_support::responses::ev_function_call;
|
||||
use core_test_support::responses::ev_response_created;
|
||||
use core_test_support::responses::mount_sse_once;
|
||||
use core_test_support::responses::sse;
|
||||
use core_test_support::test_codex_exec::test_codex_exec;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
|
||||
const SMOKE_ENV: &str = "CODEX_LINUX_SANDBOX_SMOKE";
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||
async fn linux_sandbox_smoke_codex_exec_uses_distro_bwrap() -> anyhow::Result<()> {
|
||||
run_codex_exec_linux_sandbox_smoke(
|
||||
/*use_legacy_landlock*/ false,
|
||||
"call-bwrap-smoke",
|
||||
".codex-bwrap-smoke",
|
||||
BWRAP_PROBE_SCRIPT,
|
||||
"smoke.ok=bwrap",
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||
async fn linux_sandbox_smoke_codex_exec_uses_legacy_landlock() -> anyhow::Result<()> {
|
||||
run_codex_exec_linux_sandbox_smoke(
|
||||
/*use_legacy_landlock*/ true,
|
||||
"call-legacy-landlock-smoke",
|
||||
".codex-legacy-landlock-smoke",
|
||||
LEGACY_LANDLOCK_PROBE_SCRIPT,
|
||||
"smoke.ok=legacy-landlock",
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn run_codex_exec_linux_sandbox_smoke(
|
||||
use_legacy_landlock: bool,
|
||||
call_id: &str,
|
||||
smoke_file: &str,
|
||||
script: &str,
|
||||
expected_marker: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
if std::env::var_os(SMOKE_ENV).is_none() {
|
||||
eprintln!("Skipping Linux sandbox smoke test: set {SMOKE_ENV}=1 to enable.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let test = test_codex_exec();
|
||||
let server = responses::start_mock_server().await;
|
||||
let args = json!({
|
||||
"command": script,
|
||||
"timeout_ms": 10_000_u64,
|
||||
});
|
||||
mount_sse_once(
|
||||
&server,
|
||||
sse(vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_function_call(call_id, "shell_command", &serde_json::to_string(&args)?),
|
||||
ev_completed("resp-1"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
let results_mock = mount_sse_once(
|
||||
&server,
|
||||
sse(vec![
|
||||
ev_assistant_message("msg-1", "done"),
|
||||
ev_completed("resp-2"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut cmd = test.cmd_with_server(&server);
|
||||
cmd.arg("--skip-git-repo-check").arg("--full-auto");
|
||||
if use_legacy_landlock {
|
||||
cmd.arg("-c").arg("use_legacy_landlock=true");
|
||||
}
|
||||
cmd.arg("run linux sandbox smoke").assert().success();
|
||||
|
||||
let output = results_mock
|
||||
.single_request()
|
||||
.function_call_output(call_id)
|
||||
.get("output")
|
||||
.and_then(Value::as_str)
|
||||
.expect("shell command output should be a string")
|
||||
.to_string();
|
||||
assert!(
|
||||
output.contains(expected_marker),
|
||||
"shell command output missing {expected_marker:?}: {output}"
|
||||
);
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(test.cwd_path().join(smoke_file))?,
|
||||
"ok"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const BWRAP_PROBE_SCRIPT: &str = r#"
|
||||
set -euo pipefail
|
||||
|
||||
aa_profile="$(cat /proc/self/attr/current)"
|
||||
echo "payload.apparmor=$aa_profile"
|
||||
case "$aa_profile" in
|
||||
*bwrap*unpriv_bwrap*) ;;
|
||||
*)
|
||||
echo "Expected payload to run under Ubuntu's unprivileged bwrap AppArmor profile." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
seccomp_mode="$(grep '^Seccomp:' /proc/self/status | tr -s '[:space:]' ' ' | cut -d' ' -f2)"
|
||||
echo "payload.seccomp=$seccomp_mode"
|
||||
if [[ "$seccomp_mode" != "2" ]]; then
|
||||
echo "Expected Codex to install a seccomp filter in the sandbox payload." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf ok > .codex-bwrap-smoke
|
||||
test "$(cat .codex-bwrap-smoke)" = ok
|
||||
echo "smoke.ok=bwrap"
|
||||
"#;
|
||||
|
||||
const LEGACY_LANDLOCK_PROBE_SCRIPT: &str = r#"
|
||||
set -euo pipefail
|
||||
|
||||
aa_profile="$(cat /proc/self/attr/current)"
|
||||
echo "payload.apparmor=$aa_profile"
|
||||
case "$aa_profile" in
|
||||
*bwrap*unpriv_bwrap*)
|
||||
echo "Expected legacy Landlock smoke to avoid the bwrap AppArmor profile." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
seccomp_mode="$(grep '^Seccomp:' /proc/self/status | tr -s '[:space:]' ' ' | cut -d' ' -f2)"
|
||||
echo "payload.seccomp=$seccomp_mode"
|
||||
if [[ "$seccomp_mode" != "2" ]]; then
|
||||
echo "Expected Codex to install a seccomp filter in the sandbox payload." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf ok > .codex-legacy-landlock-smoke
|
||||
test "$(cat .codex-legacy-landlock-smoke)" = ok
|
||||
echo "smoke.ok=legacy-landlock"
|
||||
"#;
|
||||
@@ -3,6 +3,7 @@ mod add_dir;
|
||||
mod apply_patch;
|
||||
mod auth_env;
|
||||
mod ephemeral;
|
||||
mod linux_sandbox_smoke;
|
||||
mod mcp_required_exit;
|
||||
mod originator;
|
||||
mod output_schema;
|
||||
|
||||
Reference in New Issue
Block a user