From 6144a7beb92e32e1fd41c798aec7aaf6d9ff0b29 Mon Sep 17 00:00:00 2001 From: alangou Date: Tue, 6 Oct 2026 08:51:43 +0000 Subject: [PATCH 01/10] ci(security): use gpt-6.1-sol with high reasoning effort (#4203) Signed-off-by: Adrien Langou --- .github/workflows/codex-security.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codex-security.yml b/.github/workflows/codex-security.yml index 348817ba89..5da0994698 100644 --- a/.github/workflows/codex-security.yml +++ b/.github/workflows/codex-security.yml @@ -76,9 +76,9 @@ concurrency: env: CODEX_SECURITY_MAX_CONCURRENT_THREADS: "8" - CODEX_SECURITY_REASONING_EFFORT: medium + CODEX_SECURITY_REASONING_EFFORT: high NVIDIA_INFERENCE_BASE_URL: https://inference-api.nvidia.com/v1 - NVIDIA_INFERENCE_MODEL: openai/openai/gpt-5.6-sol + NVIDIA_INFERENCE_MODEL: openai/openai/gpt-6.1-sol jobs: analyze: From b7afc156137e7300c8fb266261c9f18a1a497ea9 Mon Sep 17 00:00:00 2001 From: alangou Date: Tue, 6 Oct 2026 12:36:20 +0000 Subject: [PATCH 02/10] fix(deps): upgrade pageant to address Dependabot alert 38 (#4127) Signed-off-by: Adrien Langou --- Cargo.lock | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3a34f0cefa..0e89a5a20a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5369,18 +5369,19 @@ dependencies = [ [[package]] name = "pageant" -version = "0.2.0" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b537f975f6d8dcf48db368d7ec209d583b015713b5df0f5d92d2631e4ff5595" +checksum = "6d8eab09a361a4afe0b1668be978cd97e4f052e927a92b0b608cf902965d49ce" dependencies = [ + "base16ct 1.0.0", "byteorder", "bytes", "delegate", "futures", "log", - "rand 0.8.6", - "sha2 0.10.9", - "thiserror 1.0.69", + "rand 0.10.2", + "sha2 0.11.0", + "thiserror 2.0.20", "tokio", "windows", "windows-strings", From 4a0208fa695d8881f4210e8a89d94af6d895c267 Mon Sep 17 00:00:00 2001 From: Evan Lezar Date: Tue, 6 Oct 2026 12:45:02 +0000 Subject: [PATCH 03/10] fix(ci): stop running e2e in merge queues (#4232) * fix(ci): stop running e2e in merge queues Signed-off-by: Evan Lezar * fix(ci): build e2e artifacts only for selected tests Signed-off-by: Evan Lezar --------- Signed-off-by: Evan Lezar --- .github/workflows/branch-e2e.yml | 15 ++------------- .github/workflows/required-ci-gates.yml | 7 +++++-- CI.md | 17 +++++++++-------- 3 files changed, 16 insertions(+), 23 deletions(-) diff --git a/.github/workflows/branch-e2e.yml b/.github/workflows/branch-e2e.yml index 85097eea56..d46f3fb31f 100644 --- a/.github/workflows/branch-e2e.yml +++ b/.github/workflows/branch-e2e.yml @@ -1,8 +1,6 @@ name: Branch E2E Checks on: - merge_group: - types: [checks_requested] push: branches: - "pull-request/[0-9]+" @@ -50,15 +48,6 @@ jobs: run_kubernetes_ha_e2e="$(jq -r 'index("test:e2e-kubernetes") != null' <<< "$LABELS_JSON")" run_kubernetes_credential_drivers_e2e="$(jq -r 'index("test:e2e-kubernetes") != null' <<< "$LABELS_JSON")" ;; - merge_group) - # Merge groups have no PR labels. When GPU E2E is required as documented - # in CI.md, skipping it leaves the gate pending until the queue times out - # and ejects the PR. HA stays off until stable. - run_core_e2e=true - run_gpu_e2e=true - run_kubernetes_ha_e2e=false - run_kubernetes_credential_drivers_e2e=false - ;; *) run_core_e2e=true run_gpu_e2e=true @@ -82,7 +71,7 @@ jobs: version: needs: [pr_metadata] - if: needs.pr_metadata.outputs.should_run == 'true' + if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_any_e2e == 'true' permissions: contents: read runs-on: ubuntu-latest @@ -228,7 +217,7 @@ jobs: build-rpm: name: Build RPM packages needs: [pr_metadata, version, build-binaries] - if: needs.pr_metadata.outputs.should_run == 'true' + if: needs.pr_metadata.outputs.run_integration == 'true' permissions: actions: read contents: read diff --git a/.github/workflows/required-ci-gates.yml b/.github/workflows/required-ci-gates.yml index b5b521a8f3..efd7f7ebab 100644 --- a/.github/workflows/required-ci-gates.yml +++ b/.github/workflows/required-ci-gates.yml @@ -276,6 +276,9 @@ jobs: resolve_context evaluate_workflow "OpenShell / Branch Checks" "branch-checks.yml" "Branch Checks" - evaluate_workflow "OpenShell / E2E" "branch-e2e.yml" "Branch E2E Checks" "test:e2e" "Core E2E result" - evaluate_workflow "OpenShell / GPU E2E" "branch-e2e.yml" "Branch E2E Checks" "test:e2e-gpu" "GPU E2E result" + # E2E is opt-in on PRs and does not run for merge groups. + if [ "$CONTEXT_KIND" != "merge_group" ]; then + evaluate_workflow "OpenShell / E2E" "branch-e2e.yml" "Branch E2E Checks" "test:e2e" "Core E2E result" + evaluate_workflow "OpenShell / GPU E2E" "branch-e2e.yml" "Branch E2E Checks" "test:e2e-gpu" "GPU E2E result" + fi evaluate_workflow "OpenShell / Helm Lint" "helm-lint.yml" "Helm Lint" diff --git a/CI.md b/CI.md index 1a1a74280f..a433754151 100644 --- a/CI.md +++ b/CI.md @@ -72,8 +72,8 @@ Windows checks are not required for merging and do not run in merge queues. Main and manual runs also build release binaries, with `continue-on-error: true` so Windows failures do not fail the workflow. -Every approved `Branch E2E Checks` run builds the RPM packages, including -runs without optional E2E labels. Core integration qualification builds and installs +`Branch E2E Checks` builds binaries and images only when an E2E suite is +selected. DEB and RPM packages build only when integration qualification is selected. Core integration qualification builds and installs the DEB on Ubuntu with Docker and installs the CLI and gateway RPMs on Fedora with rootful and rootless Podman. These lanes run conformance using the matching runtime images. Release Dev and Release Tag use the same package installers. @@ -433,8 +433,6 @@ Important: if a PR requires manual admission, every new commit needs another `/o GitHub merge queue is required for `main`. Repository administrators must enable **Require merge queue** in the branch ruleset for `main` and keep these required status contexts aligned with the PR gates: - `OpenShell / Branch Checks` -- `OpenShell / E2E` -- `OpenShell / GPU E2E` - `OpenShell / Helm Lint` - `OpenShell / Trivy Changes` @@ -445,7 +443,7 @@ its own stable result status. Merge-group runs use the `merge_group` event. The event is distinct from `pull_request` and `push`, and GitHub will not report required checks for queued PRs unless the workflows include it. In this repository: - `Branch Checks` runs the standard non-E2E gates on the merge-group SHA. -- `Branch E2E Checks` runs core E2E and GPU E2E for merge groups. Kubernetes HA E2E remains optional and label-driven on PRs. +- `Branch E2E Checks` does not run for merge groups. E2E suites remain opt-in on PRs. - `Helm Lint` runs for merge groups without the PR diff optimization, because the merge-group branch is the final integration state. - `Trivy Changes` compares the merge-group configuration with its base and rejects new High or Critical findings. - `Required CI Gates` posts the same `OpenShell / ...` statuses to the merge-group SHA and does not require a `pull-request/` mirror for merge-group events. @@ -471,7 +469,7 @@ The bot's full administrator documentation is internal to NVIDIA. The only comma | File | Role | |---|---| | `.github/workflows/branch-checks.yml` | Required non-E2E checks. Triggers on `push: pull-request/[0-9]+` for PR mirrors and `merge_group` for queued merges. | -| `.github/workflows/branch-e2e.yml` | Standard, GPU, Kubernetes HA, and Kubernetes credential-driver E2E. PR mirror pushes use `test:e2e`, `test:e2e-gpu`, and `test:e2e-kubernetes` labels; merge groups run core and GPU E2E. | +| `.github/workflows/branch-e2e.yml` | Standard, GPU, Kubernetes HA, and Kubernetes credential-driver E2E. PR mirror pushes use `test:e2e`, `test:e2e-gpu`, and `test:e2e-kubernetes` labels; merge groups do not run E2E. | | `.github/workflows/build-binaries.yml`, `build-vm-driver.yml` | Shared binary matrices used by branch and release workflows. The VM driver remains separate because its build consumes the runtime binaries. | | `.github/workflows/build-images.yml` | Builds and pushes multi-platform images, then uploads the same OCI images as workflow artifacts. | | `.github/workflows/package-release-binaries.yml` | Packages raw build artifacts into release tarballs without rebuilding them. | @@ -507,11 +505,14 @@ These workflows run after merge to publish dev/tagged artifacts and verify them. Require these statuses in the branch ruleset for PR and merge-queue CI: - `OpenShell / Branch Checks` -- `OpenShell / E2E` -- `OpenShell / GPU E2E` - `OpenShell / Helm Lint` - `OpenShell / Trivy Changes` +The following statuses are opt-in and controlled by labels: + +- `OpenShell / E2E`: `test:e2e` +- `OpenShell / GPU E2E`: `test:e2e-gpu` + For mirror-based workflows, require the statuses published by `Required CI Gates`, not their underlying jobs. `OpenShell / Trivy Changes` is the stable result job of the direct pull-request workflow. Together these From 33869a1df53506269fca0c6929f818a2829de6ea Mon Sep 17 00:00:00 2001 From: alangou Date: Tue, 6 Oct 2026 13:06:12 +0000 Subject: [PATCH 04/10] fix(images): update gateway base and allow supervisor base override (#4236) Signed-off-by: Adrien Langou --- CONTRIBUTING.md | 16 ++++++++++++++++ deploy/docker/Dockerfile.gateway | 2 +- deploy/docker/Dockerfile.supervisor | 3 ++- docs/how-it-works/sandboxes/runtimes.mdx | 5 +++++ skills/debug-openshell-cluster/SKILL.md | 7 +++++++ 5 files changed, 31 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7da8be04b3..7ea44911a0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -291,6 +291,22 @@ each worktree. Cache reuse therefore depends on the compiler inputs: outputs that embed absolute paths, including Rust dependencies in some builds, can still miss across worktrees. +## Container Image Base Overrides + +The gateway and supervisor Dockerfiles accept `GATEWAY_BASE_IMAGE` and +`SUPERVISOR_BASE_IMAGE` build arguments. Omitting them keeps the pinned +distroless defaults. To build with your own bases, pass the argument through +the corresponding task: + +```shell +mise run build:docker:gateway -- --build-arg GATEWAY_BASE_IMAGE=registry.example.com/gateway-base:tag +mise run build:docker:supervisor -- --build-arg SUPERVISOR_BASE_IMAGE=registry.example.com/supervisor-base:tag +``` + +Choose bases that provide the GNU runtime libraries required by the binaries +and CA certificates for the target architecture. Validate the resulting images +with your deployment. The sandbox image uses `scratch` and has no base override. + ## Main Tasks These are the primary `mise` tasks for day-to-day development: diff --git a/deploy/docker/Dockerfile.gateway b/deploy/docker/Dockerfile.gateway index 0576c2e465..413605617a 100644 --- a/deploy/docker/Dockerfile.gateway +++ b/deploy/docker/Dockerfile.gateway @@ -8,7 +8,7 @@ # # Distroless Debian provides the glibc runtime required by the binary. -ARG GATEWAY_BASE_IMAGE=gcr.io/distroless/cc-debian13:nonroot@sha256:54df941ed0d06a1bd95ef5e0ce391fd8d9f94b64782dc9a60062727849ee3f97 +ARG GATEWAY_BASE_IMAGE=gcr.io/distroless/cc-debian13:nonroot@sha256:e792ab3d241a468a4fd7519ddbbebe66b49b5f365771716ea688ad40b6c6f1c2 FROM ${GATEWAY_BASE_IMAGE} AS gateway ARG TARGETARCH diff --git a/deploy/docker/Dockerfile.supervisor b/deploy/docker/Dockerfile.supervisor index 26fa44a765..e0483d5094 100644 --- a/deploy/docker/Dockerfile.supervisor +++ b/deploy/docker/Dockerfile.supervisor @@ -8,7 +8,8 @@ # Keep the GNU runtime and CA roots without a shell or unused native TLS libraries. # The default variant preserves UID 0 and /; compute drivers set the runtime UID. -FROM gcr.io/distroless/base-nossl-debian13@sha256:af5cb8dd589b8520b8c06bebb9efb73d7e16406cab58e85c51761fff49d370a0 AS supervisor +ARG SUPERVISOR_BASE_IMAGE=gcr.io/distroless/base-nossl-debian13@sha256:af5cb8dd589b8520b8c06bebb9efb73d7e16406cab58e85c51761fff49d370a0 +FROM ${SUPERVISOR_BASE_IMAGE} AS supervisor ARG TARGETARCH diff --git a/docs/how-it-works/sandboxes/runtimes.mdx b/docs/how-it-works/sandboxes/runtimes.mdx index a8aec4fb4f..b3b9fa0511 100644 --- a/docs/how-it-works/sandboxes/runtimes.mdx +++ b/docs/how-it-works/sandboxes/runtimes.mdx @@ -44,6 +44,11 @@ driver TOML overrides the compiled release defaults. Helm renders Kubernetes image fields into TOML, so they apply when these environment variables are unset. Sandbox requests cannot override either trusted image. +When building your own gateway or supervisor image, the `GATEWAY_BASE_IMAGE` +and `SUPERVISOR_BASE_IMAGE` build arguments override their pinned bases. See +[Container Image Base Overrides](https://github.com/NVIDIA/OpenShell/blob/main/CONTRIBUTING.md#container-image-base-overrides) +for build commands and base compatibility requirements. + For TLS-enabled Docker, Podman, and MicroVM gateways, set `guest_tls_ca` in `[openshell.gateway]` or use the package-managed local CA. Remove the retired `guest_tls_cert` and `guest_tls_key` fields. Supervisors receive only the diff --git a/skills/debug-openshell-cluster/SKILL.md b/skills/debug-openshell-cluster/SKILL.md index 4d98d1d61a..56c98e1074 100644 --- a/skills/debug-openshell-cluster/SKILL.md +++ b/skills/debug-openshell-cluster/SKILL.md @@ -93,6 +93,11 @@ Use gateway metadata, deployment values, or the user's setup notes to identify t Before debugging the compute platform, inspect gateway logs for failures in dependencies initialized before the listener becomes ready. +The gateway container uses a Distroless Debian runtime. For OS-library +vulnerability findings, check the deployed image digest and package version; +deploy a rebuilt gateway image with the patched base. Updating the gateway +binary alone does not update the libraries supplied by its container image. + For resource-admission failures, distinguish disabled caller driver config from missing resource approval. Helm defaults `server.drivers.kubernetes.allowDriverConfig` to false and `resourceAdmission.enabled` to true. Existing PVCs, RuntimeClasses, @@ -213,6 +218,8 @@ rationale, configured and effective modes, active generation, and the explicit `previous_policy_active` state. The published supervisor image uses a shell-free distroless Debian 13 base. +For custom builds using `SUPERVISOR_BASE_IMAGE`, check the selected base's GNU +runtime libraries, CA certificates, and inherited user and working directory. Use container logs, engine inspection and the configured exec health probe for diagnostics; `exec ... sh`, package installation and in-container shell scripts are unavailable. Workload shells belong to the separate sandbox image. Preserve From 0bca9fb8280045224c910610cba005b7fa5a6a83 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 13:07:34 +0000 Subject: [PATCH 05/10] chore(deps-dev): bump source-map-js in /sdk/typescript (#4224) Bumps [source-map-js](https://github.com/7rulnik/source-map-js) from 1.2.1 to 1.2.2. - [Release notes](https://github.com/7rulnik/source-map-js/releases) - [Changelog](https://github.com/7rulnik/source-map-js/blob/main/CHANGELOG.md) - [Commits](https://github.com/7rulnik/source-map-js/compare/v1.2.1...v1.2.2) --- updated-dependencies: - dependency-name: source-map-js dependency-version: 1.2.2 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdk/typescript/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sdk/typescript/package-lock.json b/sdk/typescript/package-lock.json index 6c3664cedd..52bf056287 100644 --- a/sdk/typescript/package-lock.json +++ b/sdk/typescript/package-lock.json @@ -1659,9 +1659,9 @@ "license": "ISC" }, "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", "dev": true, "license": "BSD-3-Clause", "engines": { From 8760396975f4b0f13cd6fd88ac1a46342ee84343 Mon Sep 17 00:00:00 2001 From: Simon Scatton <44714756+SDAChess@users.noreply.github.com> Date: Tue, 6 Oct 2026 15:25:53 +0000 Subject: [PATCH 06/10] fix(compute): revalidate late exit events after restart (#4235) Signed-off-by: Simon Scatton --- crates/openshell-server/src/compute/mod.rs | 76 ++++++++++++++++++- e2e/rust/tests/sandbox_lifecycle.rs | 87 ++++++++++++++-------- 2 files changed, 128 insertions(+), 35 deletions(-) diff --git a/crates/openshell-server/src/compute/mod.rs b/crates/openshell-server/src/compute/mod.rs index ba7d2d7dfa..edcefad33c 100644 --- a/crates/openshell-server/src/compute/mod.rs +++ b/crates/openshell-server/src/compute/mod.rs @@ -4434,7 +4434,10 @@ impl ComputeRuntime { SandboxPhase::try_from(sandbox.phase()).unwrap_or(SandboxPhase::Unknown) }); - if existing_phase != SandboxPhase::Starting { + let ready_container_exit = existing_phase == SandboxPhase::Ready + && (driver_snapshot_confirms_stopped(&incoming) + || driver_snapshot_reports_runtime_restart(&incoming)); + if existing_phase != SandboxPhase::Starting && !ready_container_exit { return self.apply_sandbox_update_locked(incoming, existing).await; } @@ -4442,10 +4445,12 @@ impl ComputeRuntime { // durable phase to Starting. In particular, an old-generation Ready // event followed by its terminal event can otherwise promote and then // stop the new generation before the replacement supervisor connects. + // The replacement may already be Ready when the old exit arrives, so + // terminal container snapshots need the same check after readiness. // Release the global watch lock, wait for that lifecycle operation, // and then reread both the driver and store before applying an // authoritative observation. Taking the per-sandbox gate only for - // this ambiguous phase avoids delaying unrelated watch events behind + // these ambiguous snapshots avoids delaying unrelated watch events behind // slow lifecycle operations. let existing_name = existing_sandbox.as_ref().map_or_else( || incoming.name.clone(), @@ -4475,7 +4480,7 @@ impl ComputeRuntime { { warn!( sandbox_id = %incoming.id, - "Could not validate driver snapshot during sandbox start; retaining current sandbox state" + "Could not validate driver snapshot against current runtime; retaining current sandbox state" ); return Ok(()); } @@ -11604,6 +11609,65 @@ mod tests { } } + #[tokio::test] + async fn container_exit_after_replacement_ready_is_revalidated() { + for (reason, live_exited) in [ + ("ContainerExited", false), + ("ContainerExited", true), + ("ContainerStopped", false), + ("ContainerStopped", true), + ("ContainerRuntimeRestart", false), + ("ContainerRuntimeRestart", true), + ] { + let driver = ControlledDriver::new(); + let mut sandbox = + sandbox_record("sb-restarted", "sandbox-restarted", SandboxPhase::Starting); + sandbox.status.as_mut().unwrap().restart_count = 1; + sandbox.status.as_mut().unwrap().exit_code = Some(17); + sandbox.status.as_mut().unwrap().main_process_instance_id = "previous".to_string(); + let mut exited = ready_driver_sandbox(sandbox.object_id(), sandbox.object_name()); + exited.status = Some(make_driver_status(make_driver_condition( + reason, + "container exited", + ))); + let live = if live_exited { + exited.clone() + } else { + ready_driver_sandbox(sandbox.object_id(), sandbox.object_name()) + }; + driver.set_get_outcome(ControlledGetOutcome::Sandbox(Box::new(live))); + let runtime = test_runtime(driver).await; + runtime.store.put_message(&sandbox).await.unwrap(); + register_test_supervisor_session(&runtime, sandbox.object_id()); + runtime + .supervisor_session_connected(sandbox.object_id(), "replacement") + .await + .unwrap(); + + runtime.apply_sandbox_update(exited).await.unwrap(); + + let stored = runtime + .store + .get_message::(sandbox.object_id()) + .await + .unwrap() + .unwrap(); + assert_eq!( + stored.phase(), + if live_exited { + SandboxPhase::Error + } else { + SandboxPhase::Ready + } as i32, + "only a live {reason} may fail the replacement runtime" + ); + let status = stored.status.unwrap(); + assert_eq!(status.main_process_instance_id, "replacement"); + assert_eq!(status.restart_count, 1); + assert_eq!(status.exit_code, None); + } + } + #[tokio::test] async fn stale_ready_snapshot_queued_before_start_is_revalidated() { let driver = ControlledDriver::new(); @@ -13375,7 +13439,8 @@ mod tests { #[tokio::test] async fn unexpected_term_runtime_restart_transitions_to_error() { - let runtime = test_runtime(Arc::new(TestDriver::default())).await; + let driver = ControlledDriver::new(); + let runtime = test_runtime(driver.clone()).await; let sandbox = sandbox_record("sb-term-exit", "sandbox-term-exit", SandboxPhase::Ready); runtime.store.put_message(&sandbox).await.unwrap(); let mut runtime_restart = ready_driver_sandbox(sandbox.object_id(), sandbox.object_name()); @@ -13383,6 +13448,9 @@ mod tests { "ContainerRuntimeRestart", "container exited with status 143", ))); + driver.set_get_outcome(ControlledGetOutcome::Sandbox(Box::new( + runtime_restart.clone(), + ))); runtime.apply_sandbox_update(runtime_restart).await.unwrap(); diff --git a/e2e/rust/tests/sandbox_lifecycle.rs b/e2e/rust/tests/sandbox_lifecycle.rs index 5e708e097c..b299a5d1d7 100644 --- a/e2e/rust/tests/sandbox_lifecycle.rs +++ b/e2e/rust/tests/sandbox_lifecycle.rs @@ -9,9 +9,11 @@ use std::process::Stdio; use std::time::Duration; use openshell_e2e::harness::binary::{openshell_cmd, openshell_tty_cmd}; -use openshell_e2e::harness::cli::{run_cli, wait_for_sandbox_phase}; +use openshell_e2e::harness::cli::{ + run_cli, wait_for_sandbox_exec_contains, wait_for_sandbox_phase, +}; use openshell_e2e::harness::output::{extract_field, strip_ansi}; -use openshell_e2e::harness::sandbox::{SandboxGuard, unique_sandbox_name}; +use openshell_e2e::harness::sandbox::{E2E_WORKLOAD_IMAGE, SandboxGuard, unique_sandbox_name}; use serial_test::serial; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::time::{Instant, sleep}; @@ -1300,45 +1302,81 @@ async fn canonical_main_exit_255_is_not_retried_as_transport_failure() { #[tokio::test] #[serial(sandbox_lifecycle)] async fn on_failure_policy_replaces_runtime_and_preserves_workspace() { - const FIRST_MARKER: &str = "initial-main-ready"; const SCRIPT: &str = r#" marker=/sandbox/.openshell-restart-e2e if [ -e "$marker" ]; then printf 'replacement-%s\n' "$(cat /proc/sys/kernel/random/uuid)" > /sandbox/replacement-run - printf 'replacement-main-ready\n' sleep 300 else touch "$marker" printf 'initial-%s\n' "$(cat /proc/sys/kernel/random/uuid)" > /sandbox/initial-run - printf 'initial-main-ready\n' - sleep 2 + while [ ! -e /sandbox/.openshell-restart-release ]; do sleep 0.05; done exit 17 fi "#; - let mut sandbox = SandboxGuard::create_keep_with_args( - &["--restart-policy", "on-failure"], - &["sh", "-lc", SCRIPT], - FIRST_MARKER, + let mut sandbox = SandboxGuard::manage_existing(unique_sandbox_name()); + let (output, exit_code) = run_cli(&[ + "sandbox", + "create", + "--name", + &sandbox.name, + "--from", + E2E_WORKLOAD_IMAGE, + "--detach", + "--restart-policy", + "on-failure", + "--", + "sh", + "-lc", + SCRIPT, + ]) + .await; + assert_eq!( + exit_code, 0, + "create sandbox with OnFailure policy: {output}" + ); + wait_for_sandbox_exec_contains( + &sandbox.name, + &["cat", "/sandbox/initial-run"], + "initial-", + SANDBOX_RESTART_TIMEOUT, ) .await - .expect("create sandbox with OnFailure restart policy"); + .expect("initial main process should write its workspace marker"); + + let initial_details = sandbox_details(&sandbox.name).await; + let initial_instance = extract_field(&initial_details, "Main process instance") + .expect("initial main process instance"); + sandbox + .exec(&["touch", "/sandbox/.openshell-restart-release"]) + .await + .expect("release initial main process"); let deadline = Instant::now() + SANDBOX_RESTART_TIMEOUT; - let mut observed_replacement_starting = false; + let mut last_runs = Err("replacement is not ready".to_string()); let final_details = loop { let details = sandbox_details(&sandbox.name).await; - observed_replacement_starting |= details.contains("Phase: Starting"); - if observed_replacement_starting - && details.contains("Phase: Ready") - && details.contains("Restart count: 1") + let replacement_instance = extract_field(&details, "Main process instance") + .is_some_and(|instance| instance != "-" && instance != initial_instance); + if details.contains("Phase: Ready") + && extract_field(&details, "Restart count").as_deref() == Some("1") + && replacement_instance { - break details; + last_runs = sandbox + .exec(&["cat", "/sandbox/initial-run", "/sandbox/replacement-run"]) + .await; + if let Ok(runs) = &last_runs + && runs.contains("initial-") + && runs.contains("replacement-") + { + break details; + } } assert!( Instant::now() < deadline, "sandbox did not complete its policy-driven restart within \ - {SANDBOX_RESTART_TIMEOUT:?}; last details:\n{details}" + {SANDBOX_RESTART_TIMEOUT:?}; last details:\n{details}\nworkspace: {last_runs:?}" ); sleep(Duration::from_millis(250)).await; }; @@ -1347,19 +1385,6 @@ fi final_details.contains("Restart policy: on-failure"), "restart policy should remain visible after replacement:\n{final_details}" ); - let runs = sandbox - .exec(&["cat", "/sandbox/initial-run", "/sandbox/replacement-run"]) - .await - .expect("replacement sandbox should retain the first run's workspace"); - assert!( - runs.contains("initial-"), - "missing initial run marker:\n{runs}" - ); - assert!( - runs.contains("replacement-"), - "missing replacement run marker:\n{runs}" - ); - sandbox.cleanup().await; } From aa108b4d292fb4287574c69d32de6924eb71bddd Mon Sep 17 00:00:00 2001 From: Emilien Macchi Date: Tue, 6 Oct 2026 12:34:08 -0400 Subject: [PATCH 07/10] CARRY: fix(konflux): use the odh-openshell-sandbox-openclaw image name The odh-openshell-sandbox-openclaw-ci Component publishes the OpenClaw image to quay.io/opendatahub/odh-openshell-sandbox-openclaw, but the README, Dockerfile labels, smoke test and provider profile comments call it odh-openshell-openclaw, which does not exist on Quay. The README examples point users at an image they cannot pull. Rename every reference, including the name and com.redhat.component labels and the smoke test that checks them. The README Konflux note now says that odh-konflux-central owns the pipelines, and which tags they publish. Related: RHAISTRAT-1845 Signed-off-by: Emilien Macchi --- deploy/docker/Dockerfile.konflux.openclaw | 8 ++++---- deploy/konflux/openclaw/README.md | 10 +++++----- deploy/konflux/openclaw/model-provider-profile.yaml | 6 +++--- deploy/konflux/openclaw/smoke-test.sh | 8 ++++---- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/deploy/docker/Dockerfile.konflux.openclaw b/deploy/docker/Dockerfile.konflux.openclaw index 591a60adfb..ae0c8b574e 100644 --- a/deploy/docker/Dockerfile.konflux.openclaw +++ b/deploy/docker/Dockerfile.konflux.openclaw @@ -88,14 +88,14 @@ ENV HOME=/sandbox \ OPENCLAW_DISABLE_BONJOUR=1 \ OPENCLAW_OFFLINE=1 -LABEL com.redhat.component="odh-openshell-openclaw-container" \ - name="opendatahub/odh-openshell-openclaw" \ +LABEL com.redhat.component="odh-openshell-sandbox-openclaw-container" \ + name="opendatahub/odh-openshell-sandbox-openclaw" \ version="${OPENCLAW_VERSION}" \ summary="OpenClaw agent harness reference image for OpenShell sandboxes" \ description="Unsupported reference composition that runs the OpenClaw agent harness inside an OpenShell sandbox; not RHOAI product content" \ maintainer="['managed-open-data-hub@redhat.com']" \ - io.k8s.display-name="odh-openshell-openclaw" \ - io.k8s.description="odh-openshell-openclaw" \ + io.k8s.display-name="odh-openshell-sandbox-openclaw" \ + io.k8s.description="odh-openshell-sandbox-openclaw" \ io.openshift.expose-services="" \ io.openshift.tags="openshell,sandbox,openclaw,agent" \ help="See the README for usage: https://github.com/opendatahub-io/openshell/blob/main/deploy/konflux/openclaw/README.md" \ diff --git a/deploy/konflux/openclaw/README.md b/deploy/konflux/openclaw/README.md index cf98278166..d21f1a81bd 100644 --- a/deploy/konflux/openclaw/README.md +++ b/deploy/konflux/openclaw/README.md @@ -1,6 +1,6 @@ # OpenClaw reference harness image -`odh-openshell-openclaw` runs the [OpenClaw](https://github.com/openclaw/openclaw) agent harness inside an OpenShell sandbox. It is built from `deploy/docker/Dockerfile.konflux.openclaw` and the files in this directory. +`odh-openshell-sandbox-openclaw` runs the [OpenClaw](https://github.com/openclaw/openclaw) agent harness inside an OpenShell sandbox. It is built from `deploy/docker/Dockerfile.konflux.openclaw` and the files in this directory. ## Status @@ -42,7 +42,7 @@ Create an interactive sandbox. On first run, `openclaw-start` onboards OpenClaw ```shell openshell sandbox create --name openclaw \ - --from quay.io/opendatahub/odh-openshell-openclaw@sha256: \ + --from quay.io/opendatahub/odh-openshell-sandbox-openclaw@sha256: \ --provider model \ --env MODEL_BASE_URL=http://..svc.cluster.local:8000/v1 \ --env MODEL_ID= \ @@ -53,7 +53,7 @@ For headless use, onboard in a detached sandbox and run one-shot turns with `age ```shell openshell sandbox create --name openclaw \ - --from quay.io/opendatahub/odh-openshell-openclaw@sha256: \ + --from quay.io/opendatahub/odh-openshell-sandbox-openclaw@sha256: \ --provider model \ --env MODEL_BASE_URL=http://..svc.cluster.local:8000/v1 \ --env MODEL_ID= \ @@ -118,7 +118,7 @@ kind: ImageSetConfiguration apiVersion: mirror.openshift.io/v2alpha1 mirror: additionalImages: - - name: quay.io/opendatahub/odh-openshell-openclaw@sha256: + - name: quay.io/opendatahub/odh-openshell-sandbox-openclaw@sha256: ``` ## Build your own on UBI @@ -153,7 +153,7 @@ The npm inputs come from AIPCC agentic commit dc756bc3 (OpenClaw 2026.9.5). AIPC ## Konflux -The Tekton pipelines follow once the component is registered in odh-konflux-central. The build is hermetic for linux/x86_64 and linux/arm64 with this prefetch input: +odh-konflux-central owns the Tekton pipelines. Its onboarder sync opens a PR here that copies `pipelineruns/openshell/odh-openshell-sandbox-openclaw-*.yaml` over `.tekton/`, so change their params there and run the sync. Merges to `main` publish `odh-stable`, and each `v*-rhaiv.*` tag rebuilds the same pinned OpenClaw release under that tag; only the commit named in [Status](#status) is verified. The build is hermetic for linux/x86_64 and linux/arm64 with this prefetch input: ```json [{"type": "npm", "path": "deploy/konflux/openclaw"}, {"type": "rpm", "path": "deploy/konflux/openclaw"}] diff --git a/deploy/konflux/openclaw/model-provider-profile.yaml b/deploy/konflux/openclaw/model-provider-profile.yaml index c3efc81e3c..8c7cc3e4c3 100644 --- a/deploy/konflux/openclaw/model-provider-profile.yaml +++ b/deploy/konflux/openclaw/model-provider-profile.yaml @@ -2,8 +2,8 @@ # SPDX-License-Identifier: Apache-2.0 # Example provider profile for the OpenClaw reference image -# (odh-openshell-openclaw). OpenShell does not load it; copy it, edit it and -# import it explicitly: +# (odh-openshell-sandbox-openclaw). OpenShell does not load it; copy it, edit +# it and import it explicitly: # openshell profile lint -f model-provider-profile.yaml # openshell profile import -f model-provider-profile.yaml --global # openshell provider create --name model --type openclaw-model \ @@ -15,7 +15,7 @@ # get the credential. # # Client binaries: node (OpenClaw runs on the image's Node.js). -# Reference layout: /usr/bin/node in odh-openshell-openclaw. +# Reference layout: /usr/bin/node in odh-openshell-sandbox-openclaw. # Credential scope: CUSTOM_API_KEY, sent as a bearer authorization header to # the host:port below and nowhere else. openclaw-start # stores it in the OpenClaw config as an env reference, so diff --git a/deploy/konflux/openclaw/smoke-test.sh b/deploy/konflux/openclaw/smoke-test.sh index b788a06094..45a0bf1412 100755 --- a/deploy/konflux/openclaw/smoke-test.sh +++ b/deploy/konflux/openclaw/smoke-test.sh @@ -5,8 +5,8 @@ set -euo pipefail -# Contract smoke tests for a built odh-openshell-openclaw image. Called by -# build-local.sh after a build; also runnable by hand. +# Contract smoke tests for a built odh-openshell-sandbox-openclaw image. +# Called by build-local.sh after a build; also runnable by hand. # # Usage: smoke-test.sh # @@ -102,8 +102,8 @@ got_harness_version=$(label io.openshell.harness.version) got_s2i1=$(label io.openshift.s2i.scripts-url) got_s2i2=$(label io.s2i.scripts-url) got_license=$(label com.redhat.license_terms) -if [[ "${got_name}" == "opendatahub/odh-openshell-openclaw" \ - && "${got_component}" == "odh-openshell-openclaw-container" \ +if [[ "${got_name}" == "opendatahub/odh-openshell-sandbox-openclaw" \ + && "${got_component}" == "odh-openshell-sandbox-openclaw-container" \ && "${got_harness}" == "openclaw" \ && -n "${got_harness_version}" \ && -z "${got_s2i1}" \ From 4c1918facee72e4c605fd3ebdcf9bbca6b71ca9e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 6 Oct 2026 00:12:09 +0000 Subject: [PATCH 08/10] CI: update Tekton pipelines - Pull pipelines use odh-pr tag - Push pipelines use odh-stable tag - Target branch: main Signed-off-by: github-actions[bot] --- .tekton/odh-openshell-e2e-pull-request.yaml | 52 +++++++++++++++++++ .tekton/odh-openshell-e2e-push.yaml | 47 +++++++++++++++++ ...enshell-sandbox-openclaw-pull-request.yaml | 52 +++++++++++++++++++ .../odh-openshell-sandbox-openclaw-push.yaml | 47 +++++++++++++++++ 4 files changed, 198 insertions(+) create mode 100644 .tekton/odh-openshell-e2e-pull-request.yaml create mode 100644 .tekton/odh-openshell-e2e-push.yaml create mode 100644 .tekton/odh-openshell-sandbox-openclaw-pull-request.yaml create mode 100644 .tekton/odh-openshell-sandbox-openclaw-push.yaml diff --git a/.tekton/odh-openshell-e2e-pull-request.yaml b/.tekton/odh-openshell-e2e-pull-request.yaml new file mode 100644 index 0000000000..93cf4b826e --- /dev/null +++ b/.tekton/odh-openshell-e2e-pull-request.yaml @@ -0,0 +1,52 @@ +apiVersion: tekton.dev/v1 +kind: PipelineRun +metadata: + annotations: + build.appstudio.openshift.io/repo: https://github.com/opendatahub-io/openshell?rev={{revision}} + build.appstudio.redhat.com/commit_sha: '{{revision}}' + build.appstudio.redhat.com/target_branch: '{{target_branch}}' + pipelinesascode.tekton.dev/cancel-in-progress: "false" + pipelinesascode.tekton.dev/max-keep-runs: "3" + pipelinesascode.tekton.dev/on-cel-expression: event == "pull_request" && target_branch + == "main" + creationTimestamp: null + labels: + appstudio.openshift.io/application: opendatahub-builds + appstudio.openshift.io/component: odh-openshell-e2e-ci + pipelines.appstudio.openshift.io/type: build + name: odh-openshell-e2e-on-pull-request + namespace: open-data-hub-tenant +spec: + params: + - name: git-url + value: '{{source_url}}' + - name: revision + value: '{{revision}}' + - name: output-image + value: quay.io/opendatahub/odh-openshell-e2e:odh-pr + - name: dockerfile + value: deploy/docker/Dockerfile.konflux.e2e-odh + - name: path-context + value: . + #add these additional params + - name: additional-tags + value: + - 'odh-pr-{{revision}}' + - name: pipeline-type + value: pull-request + pipelineRef: + resolver: git + params: + - name: url + value: https://github.com/opendatahub-io/odh-konflux-central.git + - name: revision + value: main + - name: pathInRepo + value: pipeline/multi-arch-container-build.yaml + taskRunTemplate: + serviceAccountName: build-pipeline-odh-openshell-e2e-ci + workspaces: + - name: git-auth + secret: + secretName: '{{ git_auth_secret }}' +status: {} diff --git a/.tekton/odh-openshell-e2e-push.yaml b/.tekton/odh-openshell-e2e-push.yaml new file mode 100644 index 0000000000..4d5f0f3ebb --- /dev/null +++ b/.tekton/odh-openshell-e2e-push.yaml @@ -0,0 +1,47 @@ +apiVersion: tekton.dev/v1 +kind: PipelineRun +# +metadata: + annotations: + build.appstudio.openshift.io/repo: https://github.com/opendatahub-io/openshell?rev={{revision}} + build.appstudio.redhat.com/commit_sha: '{{revision}}' + build.appstudio.redhat.com/target_branch: '{{target_branch}}' + pipelinesascode.tekton.dev/cancel-in-progress: "false" + pipelinesascode.tekton.dev/max-keep-runs: "3" + pipelinesascode.tekton.dev/on-cel-expression: event == "push" && target_branch + == "main" + creationTimestamp: null + labels: + appstudio.openshift.io/application: opendatahub-builds + appstudio.openshift.io/component: odh-openshell-e2e-ci + pipelines.appstudio.openshift.io/type: build + name: odh-openshell-e2e-on-push + namespace: open-data-hub-tenant +spec: + params: + - name: git-url + value: '{{source_url}}' + - name: revision + value: '{{revision}}' + - name: output-image + value: quay.io/opendatahub/odh-openshell-e2e:odh-stable + - name: dockerfile + value: deploy/docker/Dockerfile.konflux.e2e-odh + - name: path-context + value: . + pipelineRef: + resolver: git + params: + - name: url + value: https://github.com/opendatahub-io/odh-konflux-central.git + - name: revision + value: main + - name: pathInRepo + value: pipeline/multi-arch-container-build.yaml + taskRunTemplate: + serviceAccountName: build-pipeline-odh-openshell-e2e-ci + workspaces: + - name: git-auth + secret: + secretName: '{{ git_auth_secret }}' +status: {} diff --git a/.tekton/odh-openshell-sandbox-openclaw-pull-request.yaml b/.tekton/odh-openshell-sandbox-openclaw-pull-request.yaml new file mode 100644 index 0000000000..c04af196e8 --- /dev/null +++ b/.tekton/odh-openshell-sandbox-openclaw-pull-request.yaml @@ -0,0 +1,52 @@ +apiVersion: tekton.dev/v1 +kind: PipelineRun +metadata: + annotations: + build.appstudio.openshift.io/repo: https://github.com/opendatahub-io/openshell?rev={{revision}} + build.appstudio.redhat.com/commit_sha: '{{revision}}' + build.appstudio.redhat.com/target_branch: '{{target_branch}}' + pipelinesascode.tekton.dev/cancel-in-progress: "false" + pipelinesascode.tekton.dev/max-keep-runs: "3" + pipelinesascode.tekton.dev/on-cel-expression: event == "pull_request" && target_branch + == "main" + creationTimestamp: null + labels: + appstudio.openshift.io/application: opendatahub-builds + appstudio.openshift.io/component: odh-openshell-sandbox-openclaw-ci + pipelines.appstudio.openshift.io/type: build + name: odh-openshell-sandbox-openclaw-on-pull-request + namespace: open-data-hub-tenant +spec: + params: + - name: git-url + value: '{{source_url}}' + - name: revision + value: '{{revision}}' + - name: output-image + value: quay.io/opendatahub/odh-openshell-sandbox-openclaw:odh-pr + - name: dockerfile + value: deploy/docker/Dockerfile.konflux.openclaw + - name: path-context + value: . + #add these additional params + - name: additional-tags + value: + - 'odh-pr-{{revision}}' + - name: pipeline-type + value: pull-request + pipelineRef: + resolver: git + params: + - name: url + value: https://github.com/opendatahub-io/odh-konflux-central.git + - name: revision + value: main + - name: pathInRepo + value: pipeline/multi-arch-container-build.yaml + taskRunTemplate: + serviceAccountName: build-pipeline-odh-openshell-sandbox-openclaw-ci + workspaces: + - name: git-auth + secret: + secretName: '{{ git_auth_secret }}' +status: {} diff --git a/.tekton/odh-openshell-sandbox-openclaw-push.yaml b/.tekton/odh-openshell-sandbox-openclaw-push.yaml new file mode 100644 index 0000000000..74b557472b --- /dev/null +++ b/.tekton/odh-openshell-sandbox-openclaw-push.yaml @@ -0,0 +1,47 @@ +apiVersion: tekton.dev/v1 +kind: PipelineRun +# +metadata: + annotations: + build.appstudio.openshift.io/repo: https://github.com/opendatahub-io/openshell?rev={{revision}} + build.appstudio.redhat.com/commit_sha: '{{revision}}' + build.appstudio.redhat.com/target_branch: '{{target_branch}}' + pipelinesascode.tekton.dev/cancel-in-progress: "false" + pipelinesascode.tekton.dev/max-keep-runs: "3" + pipelinesascode.tekton.dev/on-cel-expression: event == "push" && target_branch + == "main" + creationTimestamp: null + labels: + appstudio.openshift.io/application: opendatahub-builds + appstudio.openshift.io/component: odh-openshell-sandbox-openclaw-ci + pipelines.appstudio.openshift.io/type: build + name: odh-openshell-sandbox-openclaw-on-push + namespace: open-data-hub-tenant +spec: + params: + - name: git-url + value: '{{source_url}}' + - name: revision + value: '{{revision}}' + - name: output-image + value: quay.io/opendatahub/odh-openshell-sandbox-openclaw:odh-stable + - name: dockerfile + value: deploy/docker/Dockerfile.konflux.openclaw + - name: path-context + value: . + pipelineRef: + resolver: git + params: + - name: url + value: https://github.com/opendatahub-io/odh-konflux-central.git + - name: revision + value: main + - name: pathInRepo + value: pipeline/multi-arch-container-build.yaml + taskRunTemplate: + serviceAccountName: build-pipeline-odh-openshell-sandbox-openclaw-ci + workspaces: + - name: git-auth + secret: + secretName: '{{ git_auth_secret }}' +status: {} From 3712073cd9ebee32bb18ace8b401bed71da06f0e Mon Sep 17 00:00:00 2001 From: Emilien Macchi Date: Mon, 5 Oct 2026 22:46:21 -0400 Subject: [PATCH 09/10] CARRY: fix(konflux): make OpenClaw and e2e PipelineRuns hermetic The onboarding sync added the OpenClaw and e2e PipelineRuns without the hermetic, build-platforms and prefetch-input params that every other OpenShell component carries. The ODH pipeline then falls back to a non-hermetic linux/x86_64 build and skips the Hermeto prefetch. The OpenClaw build's npm ci --offline finds no cached tarballs and fails with ENOTCACHED on zod-to-json-schema 3.25.2. Add the params to both OpenClaw PipelineRuns with the npm and rpm prefetch inputs from build-local.sh, building linux/x86_64 and linux/arm64 as deploy/konflux/openclaw/README.md documents. Add them to both e2e PipelineRuns too, with the two Cargo, rpm and generic inputs from build-local.sh and the linux-m2xlarge builders that the e2e-odh README requires. The e2e Dockerfile untars Rust, nextest, oc and Helm from the generic prefetch output, so it cannot build without them. odh-konflux-central syncs these files from pipelineruns/openshell and overwrites them. opendatahub-io/odh-konflux-central#706 added the same params there, so the next sync keeps them. Related: RHAISTRAT-1845 Signed-off-by: Emilien Macchi --- .tekton/odh-openshell-e2e-pull-request.yaml | 14 ++++++++++++++ .tekton/odh-openshell-e2e-push.yaml | 14 ++++++++++++++ ...dh-openshell-sandbox-openclaw-pull-request.yaml | 12 ++++++++++++ .tekton/odh-openshell-sandbox-openclaw-push.yaml | 12 ++++++++++++ 4 files changed, 52 insertions(+) diff --git a/.tekton/odh-openshell-e2e-pull-request.yaml b/.tekton/odh-openshell-e2e-pull-request.yaml index 93cf4b826e..7b1f6a586e 100644 --- a/.tekton/odh-openshell-e2e-pull-request.yaml +++ b/.tekton/odh-openshell-e2e-pull-request.yaml @@ -28,6 +28,20 @@ spec: value: deploy/docker/Dockerfile.konflux.e2e-odh - name: path-context value: . + - name: hermetic + value: "true" + - name: build-platforms + value: + - linux-m2xlarge/amd64 + - linux-m2xlarge/arm64 + - name: prefetch-input + value: | + [ + {"type": "cargo", "path": "."}, + {"type": "cargo", "path": "e2e/rust"}, + {"type": "rpm", "path": "deploy/konflux/e2e-odh"}, + {"type": "generic", "path": "deploy/konflux/e2e-odh", "lockfile": "generic-fetcher.yaml"} + ] #add these additional params - name: additional-tags value: diff --git a/.tekton/odh-openshell-e2e-push.yaml b/.tekton/odh-openshell-e2e-push.yaml index 4d5f0f3ebb..3bcfb7c17f 100644 --- a/.tekton/odh-openshell-e2e-push.yaml +++ b/.tekton/odh-openshell-e2e-push.yaml @@ -29,6 +29,20 @@ spec: value: deploy/docker/Dockerfile.konflux.e2e-odh - name: path-context value: . + - name: hermetic + value: "true" + - name: build-platforms + value: + - linux-m2xlarge/amd64 + - linux-m2xlarge/arm64 + - name: prefetch-input + value: | + [ + {"type": "cargo", "path": "."}, + {"type": "cargo", "path": "e2e/rust"}, + {"type": "rpm", "path": "deploy/konflux/e2e-odh"}, + {"type": "generic", "path": "deploy/konflux/e2e-odh", "lockfile": "generic-fetcher.yaml"} + ] pipelineRef: resolver: git params: diff --git a/.tekton/odh-openshell-sandbox-openclaw-pull-request.yaml b/.tekton/odh-openshell-sandbox-openclaw-pull-request.yaml index c04af196e8..eb8a3b8771 100644 --- a/.tekton/odh-openshell-sandbox-openclaw-pull-request.yaml +++ b/.tekton/odh-openshell-sandbox-openclaw-pull-request.yaml @@ -28,6 +28,18 @@ spec: value: deploy/docker/Dockerfile.konflux.openclaw - name: path-context value: . + - name: hermetic + value: "true" + - name: build-platforms + value: + - linux/x86_64 + - linux/arm64 + - name: prefetch-input + value: | + [ + {"type": "npm", "path": "deploy/konflux/openclaw"}, + {"type": "rpm", "path": "deploy/konflux/openclaw"} + ] #add these additional params - name: additional-tags value: diff --git a/.tekton/odh-openshell-sandbox-openclaw-push.yaml b/.tekton/odh-openshell-sandbox-openclaw-push.yaml index 74b557472b..12b673c8d7 100644 --- a/.tekton/odh-openshell-sandbox-openclaw-push.yaml +++ b/.tekton/odh-openshell-sandbox-openclaw-push.yaml @@ -29,6 +29,18 @@ spec: value: deploy/docker/Dockerfile.konflux.openclaw - name: path-context value: . + - name: hermetic + value: "true" + - name: build-platforms + value: + - linux/x86_64 + - linux/arm64 + - name: prefetch-input + value: | + [ + {"type": "npm", "path": "deploy/konflux/openclaw"}, + {"type": "rpm", "path": "deploy/konflux/openclaw"} + ] pipelineRef: resolver: git params: From 5601d71b014545678c9fcb572dad7e6cc09fc448 Mon Sep 17 00:00:00 2001 From: "John T. Myers" <9696606+johntmyers@users.noreply.github.com> Date: Tue, 6 Oct 2026 18:29:42 +0000 Subject: [PATCH 10/10] fix(gator): preserve literal Codex account routing identity (#4215) * fix(gator): preserve literal Codex account routing identity Signed-off-by: John Myers * chore(gator): retain latest Codex default Signed-off-by: John Myers --------- Signed-off-by: John Myers Co-authored-by: John Myers --- .../skills/launch-openshell-gator/SKILL.md | 2 ++ scripts/agents/gator/README.md | 2 ++ scripts/agents/gator/agent.yaml | 2 +- scripts/agents/run.sh | 5 ++++ .../agents/runtime/harnesses/codex/exec.sh | 6 +++-- .../runtime/harnesses/codex/exec_test.sh | 26 ++++++++++++++++--- 6 files changed, 37 insertions(+), 6 deletions(-) diff --git a/.agents/skills/launch-openshell-gator/SKILL.md b/.agents/skills/launch-openshell-gator/SKILL.md index 2f9680d411..566dea13b2 100644 --- a/.agents/skills/launch-openshell-gator/SKILL.md +++ b/.agents/skills/launch-openshell-gator/SKILL.md @@ -80,6 +80,8 @@ jq -e '.tokens.access_token and .tokens.refresh_token and .tokens.account_id' "$ If this fails, run the local Codex login flow outside the gator launch. If Codex was recently reauthenticated and gateway refresh fails later, relaunch with `--reset-refresh` once. +The launcher passes the non-secret host account ID as literal `CODEX_ACCOUNT_ID` for Codex's local workspace routing. The harness must not use the opaque `CODEX_AUTH_ACCOUNT_ID` credential placeholder for that comparison. Access tokens remain placeholders, and refresh material remains gateway-only. + ### Step 5: Verify Gateway Is Registered And Alive Use the target gateway from the operator request or current session context. Do not assume a gateway name. If the operator did not specify one, list registered gateways and ask before launching when the correct target is ambiguous. diff --git a/scripts/agents/gator/README.md b/scripts/agents/gator/README.md index a683a2030b..243e41f124 100644 --- a/scripts/agents/gator/README.md +++ b/scripts/agents/gator/README.md @@ -37,8 +37,10 @@ The launcher: - Scans `profile_paths` in manifest order and imports or updates `providers/github-gator.yaml`. - Creates or updates the `github-gator` provider from `gh auth token`. - Selects the requested harness and bakes the common runtime into the immutable sandbox payload. +- Builds with the latest Codex CLI by default. Docker can reuse the cached install layer; rebuild with that layer's cache disabled to fetch a newer release. - For `--harness codex`, imports `providers/codex-gator.yaml`, creates or updates the `codex-gator` provider from `$HOME/.codex/auth.json`, and stores the refresh token as gateway-only refresh material. - For `--harness codex`, configures gateway-managed refresh for `CODEX_AUTH_ACCESS_TOKEN` and rotates it before launching the sandbox. +- Passes the non-secret Codex account ID from host auth as `CODEX_ACCOUNT_ID`. The harness writes it literally into `auth.json` for local workspace discovery; access tokens remain opaque provider placeholders and refresh tokens remain gateway-only. - Enables `agent_policy_proposals_enabled` and `proposal_approval_mode=auto` at gateway scope. - Uses the gator image policy copied to `/etc/openshell/policy.yaml`. - Installs the gator-specific `gh` wrapper from `gator/bin/gh` as `/usr/local/bin/gh` to fail closed when same-head-SHA history cannot be checked, prevent duplicate dispositions, and require versioned review payloads. diff --git a/scripts/agents/gator/agent.yaml b/scripts/agents/gator/agent.yaml index c375c9bddc..18f555ff15 100644 --- a/scripts/agents/gator/agent.yaml +++ b/scripts/agents/gator/agent.yaml @@ -2,7 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 id: gator -payload_version: 9 +payload_version: 10 display_name: Gator Gate Agent description: Validate and monitor OpenShell GitHub issues and pull requests through the gator state machine. diff --git a/scripts/agents/run.sh b/scripts/agents/run.sh index 75f96565b5..d778d33a72 100755 --- a/scripts/agents/run.sh +++ b/scripts/agents/run.sh @@ -801,7 +801,12 @@ HARNESS_ENV_ARGS=( case "$HARNESS" in codex) + # Codex compares account IDs locally during workspace discovery. Unlike + # bearer tokens, this non-secret identifier must not be a placeholder. + [[ -n "${CODEX_AUTH_ACCOUNT_ID:-}" ]] || fail "missing Codex account ID" + [[ "$CODEX_AUTH_ACCOUNT_ID" != openshell:resolve:* ]] || fail "Codex account ID must be a literal identifier" HARNESS_ENV_ARGS+=( + "CODEX_ACCOUNT_ID=$CODEX_AUTH_ACCOUNT_ID" "CODEX_MODEL=${CODEX_MODEL:-$HARNESS_MODEL}" "CODEX_REASONING=${CODEX_REASONING:-$HARNESS_REASONING}" ) diff --git a/scripts/agents/runtime/harnesses/codex/exec.sh b/scripts/agents/runtime/harnesses/codex/exec.sh index ef92258795..8726714d68 100755 --- a/scripts/agents/runtime/harnesses/codex/exec.sh +++ b/scripts/agents/runtime/harnesses/codex/exec.sh @@ -16,7 +16,8 @@ require_env() { } require_env CODEX_AUTH_ACCESS_TOKEN -require_env CODEX_AUTH_ACCOUNT_ID +require_env CODEX_ACCOUNT_ID +[[ "$CODEX_ACCOUNT_ID" != openshell:resolve:* ]] || { echo "Codex account ID must be a literal identifier" >&2; exit 1; } require_env GITHUB_TOKEN PROMPT_FILE="$1" @@ -58,7 +59,8 @@ fs.writeFileSync(path, JSON.stringify({ id_token: providerValue("CODEX_AUTH_ID_TOKEN") || fallbackIdToken, access_token: providerValue("CODEX_AUTH_ACCESS_TOKEN"), refresh_token: providerValue("CODEX_AUTH_REFRESH_TOKEN") || "gateway-managed-refresh-token", - account_id: providerValue("CODEX_AUTH_ACCOUNT_ID"), + // Workspace discovery compares this non-secret identifier locally. + account_id: process.env.CODEX_ACCOUNT_ID, }, last_refresh: new Date().toISOString(), }, null, 2)); diff --git a/scripts/agents/runtime/harnesses/codex/exec_test.sh b/scripts/agents/runtime/harnesses/codex/exec_test.sh index 6f1b998217..8c2fc5cdef 100755 --- a/scripts/agents/runtime/harnesses/codex/exec_test.sh +++ b/scripts/agents/runtime/harnesses/codex/exec_test.sh @@ -11,6 +11,7 @@ trap 'rm -rf "$TMP_DIR"' EXIT ACCESS_PLACEHOLDER="openshell:resolve:env:s$(printf 'a%.0s' {1..64})_CODEX_AUTH_ACCESS_TOKEN" ACCOUNT_PLACEHOLDER="openshell:resolve:env:v42_CODEX_AUTH_ACCOUNT_ID" +ACCOUNT_ID="test-codex-account" cat > "$TMP_DIR/codex" <<'MOCK' #!/usr/bin/env bash @@ -23,8 +24,11 @@ fi access="$(jq -r '.tokens.access_token' "$HOME/.codex/auth.json")" account="$(jq -r '.tokens.account_id' "$HOME/.codex/auth.json")" [[ "$access" == "$EXPECTED_ACCESS_PLACEHOLDER" ]] -[[ "$account" == "$EXPECTED_ACCOUNT_PLACEHOLDER" ]] -printf '%s\n' 'ok - Codex auth preserves opaque provider placeholders' +[[ "$account" == "$EXPECTED_ACCOUNT_ID" ]] +[[ "$account" != openshell:resolve:* ]] +[[ "$(jq -r '.tokens.refresh_token' "$HOME/.codex/auth.json")" == "gateway-managed-refresh-token" ]] +[[ "$(node -e 'console.log((require("fs").statSync(process.env.HOME + "/.codex/auth.json").mode & 0o777).toString(8))')" == 600 ]] +printf '%s\n' 'ok - Codex auth preserves token placeholders and uses a literal account ID' MOCK chmod +x "$TMP_DIR/codex" printf '%s\n' 'test prompt' > "$TMP_DIR/prompt.md" @@ -33,7 +37,23 @@ OPENSHELL_AGENT_HOME="$TMP_DIR/home" \ CODEX_BIN="$TMP_DIR/codex" \ CODEX_AUTH_ACCESS_TOKEN="$ACCESS_PLACEHOLDER" \ CODEX_AUTH_ACCOUNT_ID="$ACCOUNT_PLACEHOLDER" \ +CODEX_ACCOUNT_ID="$ACCOUNT_ID" \ GITHUB_TOKEN="openshell:resolve:env:v42_GITHUB_TOKEN" \ EXPECTED_ACCESS_PLACEHOLDER="$ACCESS_PLACEHOLDER" \ -EXPECTED_ACCOUNT_PLACEHOLDER="$ACCOUNT_PLACEHOLDER" \ +EXPECTED_ACCOUNT_ID="$ACCOUNT_ID" \ bash "$SCRIPT_DIR/exec.sh" "$TMP_DIR/prompt.md" + +# Fail before launching Codex when the routing identity is absent or opaque. +for invalid_account_id in "" "$ACCOUNT_PLACEHOLDER"; do + if OPENSHELL_AGENT_HOME="$TMP_DIR/invalid-home" \ + CODEX_BIN="$TMP_DIR/codex" \ + CODEX_AUTH_ACCESS_TOKEN="$ACCESS_PLACEHOLDER" \ + CODEX_ACCOUNT_ID="$invalid_account_id" \ + GITHUB_TOKEN="placeholder" \ + bash "$SCRIPT_DIR/exec.sh" "$TMP_DIR/prompt.md" >"$TMP_DIR/error" 2>&1; then + echo "unexpected success with invalid Codex account ID" >&2 + exit 1 + fi + [[ ! -e "$TMP_DIR/invalid-home/.codex/auth.json" ]] +done +printf '%s\n' 'ok - missing and placeholder account IDs are rejected'