Skip to content

feat(sandbox,kubernetes): warm pool gRPC PoC (Milestone 1)#13

Open
rhuss wants to merge 19 commits into
mainfrom
6113-warm-pool-grpc-poc
Open

feat(sandbox,kubernetes): warm pool gRPC PoC (Milestone 1)#13
rhuss wants to merge 19 commits into
mainfrom
6113-warm-pool-grpc-poc

Conversation

@rhuss

@rhuss rhuss commented Jul 11, 2026

Copy link
Copy Markdown
Owner

Summary

  • Implement ActivateSandbox gRPC endpoint on the supervisor for claim-time identity binding of warm pool pods
  • Supervisor starts in unidentified mode (--unidentified flag), listens on gRPC port, serves /readyz, and bootstraps fully upon activation
  • K8s driver detects warm pools (exact image match), creates SandboxClaim, calls ActivateSandbox on the claimed pod
  • Cold-start fallback on any activation failure (timeout, OPA error, connectivity)
  • Target: sub-2s sandbox creation from warm pool vs ~16.7s cold start

Related Issue

Brainstorm 06: Warm Pool gRPC PoC. Implements NVIDIA-endorsed gRPC-push direction for warm pool identity binding.

Changes

New files

  • proto/supervisor.proto - New Supervisor gRPC service with ActivateSandbox RPC
  • crates/openshell-sandbox/src/activation.rs - ActivateSandbox handler with full bootstrap sequence
  • crates/openshell-sandbox/src/health.rs - HTTP /readyz endpoint (wires up existing --health-check flag)
  • crates/openshell-driver-kubernetes/src/warm_pool.rs - SandboxWarmPool/SandboxClaim CRD interaction
  • crates/openshell-driver-kubernetes/src/activation_client.rs - Gateway-side ActivateSandbox gRPC client
  • crates/openshell-sandbox/tests/activation_smoke.rs - Functional equivalence smoke test
  • crates/openshell-sandbox/tests/activation_timing.rs - Readiness timing integration test

Modified files

  • crates/openshell-sandbox/src/main.rs - --unidentified CLI flag, startup branching
  • crates/openshell-sandbox/src/lib.rs - run_unidentified() + bootstrap_sandbox() extracted from run_sandbox()
  • crates/openshell-core/src/grpc_client.rs - Direct JWT path (skip IssueSandboxToken for warm pool)
  • crates/openshell-driver-kubernetes/src/driver.rs - Warm pool detection + claim path in create_sandbox()
  • architecture/sandbox.md - Unidentified mode + activation flow documentation
  • architecture/compute-runtimes.md - Warm pool claim path documentation

Testing

  • 3602 unit tests passing (including new activation tests)
  • 0 clippy errors
  • 3 mid-implementation checkpoint reviews (0 blocking findings)
  • Deep code review: 100% spec compliance (13/13 FRs), 2 Important findings fixed
  • E2E validation requires a cluster with warm pool operator (manual, not in CI)

Checklist

  • Code follows project conventions (conventional commits, DCO sign-off)
  • Architecture docs updated
  • Unit tests added for new code
  • Cold-start path unchanged (fallback verified)
  • No secrets or credentials committed

🤖 Generated with Claude Code


Open in Devin Review

Summary by CodeRabbit

  • New Features
    • Added warm-pool activation for faster sandbox startup, with cold-start fallback when unavailable.
    • Introduced unidentified supervisor startup with gRPC activation and /readyz readiness probing.
    • Added new Git branching workflow extension plus Spex “collaboration”, “gates”, “deep review”, “teams”, “worktrees”, and “detach” capabilities.
    • Expanded autonomous Spex workflows with stage tracking, triage state, and richer review/merge flows.
  • Documentation
    • Added/updated configuration templates, command specs, review/checklist materials, and warm-pool PoC/E2E guides.

rhuss added 6 commits July 11, 2026 08:37
Parent brainstorm (01) defines the layered measurement approach for
evaluating Agent Sandbox warm pooling on OpenShift. Child documents
cover cluster setup (02), measurements (03), and results synthesis (04).

Assisted-By: 🤖 Claude Code
Assisted-By: 🤖 Claude Code
Signed-off-by: Roland Huß <rhuss@redhat.com>
Assisted-By: 🤖 Claude Code
Signed-off-by: Roland Huß <rhuss@redhat.com>
Assisted-By: 🤖 Claude Code
Signed-off-by: Roland Huß <rhuss@redhat.com>
Assisted-By: 🤖 Claude Code
Signed-off-by: Roland Huß <rhuss@redhat.com>
Add ActivateSandbox gRPC endpoint to the supervisor for claim-time
identity binding of warm pool pods. The supervisor starts in an
unidentified mode (no gateway connection, no identity, no OPA),
listens for activation, and bootstraps fully when the gateway pushes
identity and policy configuration.

Key changes:
- New proto/supervisor.proto with Supervisor service definition
- Supervisor unidentified mode (--unidentified flag) with /readyz endpoint
- ActivateSandbox handler with full bootstrap sequence
- K8s driver warm pool detection and SandboxClaim flow
- Cold-start fallback on activation failure
- Direct JWT path (skip IssueSandboxToken for warm pool)
- Integration tests for activation timing and smoke test

Assisted-By: 🤖 Claude Code
Signed-off-by: Roland Huß <rhuss@redhat.com>
@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@rhuss, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 2 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e7a5dc96-3d34-47db-b8eb-1dca17968aa7

📥 Commits

Reviewing files that changed from the base of the PR and between f20f49f and a605921.

📒 Files selected for processing (7)
  • brainstorm/00-overview.md
  • brainstorm/01-warm-pool-feasibility.md
  • brainstorm/02-cluster-setup.md
  • brainstorm/04-results-and-recommendations.md
  • brainstorm/05-warm-pool-grpc-push.md
  • rfc/NNNN-warm-pool-feasibility/README.md
  • rfc/NNNN-warm-pool-grpc-push/README.md
📝 Walkthrough

Walkthrough

Changes

Adds Kubernetes warm-pool sandbox activation through a new Supervisor gRPC service, unidentified supervisor startup, /readyz, shared bootstrap handling, retryable activation failures, and cold-start fallback. It also adds extensive Spex extensions, workflows, state utilities, templates, integration artifacts, and review documentation.

Estimated code review effort: 5 (Critical) | ~120 minutes

Suggested reviewers: taylormutch, mjamiv, elezar, sjenning, drew

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: a warm pool gRPC proof-of-concept for sandbox and Kubernetes integration.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 6113-warm-pool-grpc-poc

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

devin-ai-integration[bot]

This comment was marked as resolved.

Applied fixes from bot review comments:
- Comment #3564029644: Use policy from GetSandboxConfig instead of empty default
- Comment #3564029715: Require TLS for ActivateSandbox, reject plaintext fallback

Assisted-By: 🤖 Claude Code
Signed-off-by: Roland Huß <rhuss@redhat.com>

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 new potential issues.

View 3 additional findings in Devin Review.

Open in Devin Review

.serve(addr);

if health_check {
ready.store(true, std::sync::atomic::Ordering::Release);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Health readiness is signaled before the gRPC activation server has bound its socket

The readiness flag is set to true (ready.store(true) at crates/openshell-sandbox/src/lib.rs:949) before the gRPC server future is polled for the first time (which is when the socket actually binds), so a probe can report the pod as ready while the activation port is not yet accepting connections.

Impact: The gateway may attempt an ActivateSandbox call immediately after the readiness probe passes and receive a connection-refused error, causing an unnecessary cold-start fallback.

Mechanism: serve(addr) binds on first poll, not on construction

At crates/openshell-sandbox/src/lib.rs:944-950:

let server = tonic::transport::Server::builder()
    .add_service(SupervisorServer::new(service))
    .serve(addr);  // Returns a future; socket not bound yet

if health_check {
    ready.store(true, ...);  // Readiness signaled HERE
}

tokio::select! {  // server is first polled HERE (socket binds)

The health HTTP server (spawned earlier at line 927) is already accepting connections, so a Kubernetes readiness probe hitting /readyz can succeed before the select! block even starts executing.

Prompt for agents
The readiness flag should only be set after the gRPC server has successfully bound its socket. One approach: use tonic's `Server::builder().serve_with_incoming()` with a pre-bound `TcpListener`, or use `tokio::net::TcpListener::bind(addr).await` first, then set the ready flag, then pass the listener to the server. Alternatively, move the `ready.store(true)` inside the `tokio::select!` block after the first successful poll of the server future (though this is harder to express). The simplest fix is to bind the TCP listener before the select and set ready after the bind succeeds.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +944 to +946
let server = tonic::transport::Server::builder()
.add_service(SupervisorServer::new(service))
.serve(addr);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟨 Unauthenticated gRPC activation endpoint allows any network-reachable caller to push identity and policy

The supervisor's ActivateSandbox gRPC endpoint at crates/openshell-sandbox/src/lib.rs:944-946 binds to 0.0.0.0:9090 with no authentication or authorization. Any pod or process that can reach the supervisor's IP on port 9090 can call ActivateSandbox and push arbitrary sandbox identity, JWT token, and policy configuration. The only protection is the AtomicBool idempotency guard (crates/openshell-sandbox/src/activation.rs:54) which prevents a second activation, but a race between a malicious caller and the legitimate gateway could result in the supervisor being activated with attacker-controlled identity and policy.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

sandbox_name: sandbox.name.clone(),
sandbox_token,
gateway_endpoint: self.config.grpc_endpoint.clone(),
policy: Some(openshell_core::proto::sandbox::v1::SandboxPolicy::default()),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟥 Warm pool sandbox can run with no policy enforcement if gateway settings fetch fails

The driver sends SandboxPolicy::default() (an empty policy with no rules) in the activation request (crates/openshell-driver-kubernetes/src/driver.rs:1035). The bootstrap_sandbox function at crates/openshell-sandbox/src/lib.rs:839-844 falls back to compiling this empty policy when fetch_settings_snapshot fails. An empty OPA policy means no network restrictions, no filesystem restrictions, and no process restrictions are enforced on the sandbox, giving the agent unrestricted access.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

The Milestone 1 implementation used agents.x-k8s.io/v1alpha1 for the
warm pool CRDs, but the Red Hat Agent Sandbox operator v0.9.0 ships
extensions.agents.x-k8s.io/v1beta1. This fixes the API group, version,
status field parsing (conditions instead of phase, podIPs instead of
podIP), warmPoolRef format, and image matching (resolve SandboxTemplate
reference instead of reading image from the pool directly).

Also adds a smoke test guide and automated script for validating the
end-to-end warm pool claim + ActivateSandbox flow, along with K8s
manifests for deploying the unidentified supervisor warm pool.

Validated on ROSA HCP 4.22.3: 3 runs averaged 1.9s (claim 1.35s +
activate 0.56s), under the 2s target.

Assisted-By: 🤖 Claude Code
Signed-off-by: Roland Huß <rhuss@redhat.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (8)
.specify/extensions/spex/scripts/spex-ship-statusline.sh-269-271 (1)

269-271: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

render_ship() and render_watch() use @tsv with read despite the known field-misalignment risk.

The comment at line 148 explicitly warns that @tsv causes read to collapse consecutive tabs for empty fields, which is why render_flow() uses IFS='|' with join("|"). However, render_ship() (line 270) and render_watch() (line 332) still use @tsv with default IFS.

When any field defaults to an empty string (e.g., stage_index missing from state JSON, or pr_number absent in watch mode), @tsv produces consecutive tabs that read collapses, shifting all subsequent fields. The guard checks at lines 273 and 335 won't catch this because the misaligned fields receive non-empty values from the next field.

For example, if stage_index is absent in render_ship(), INDEX gets "8" (from total_stages), TOTAL gets "smart" (from ask), and $((DISPLAY_INDEX * 9 / TOTAL)) fails with a non-numeric division error.

🐛 Proposed fix: apply the same pipe-delimiter pattern used in render_flow()
 # render_ship() - line 269
-  read -r STAGE INDEX TOTAL ASK STATUS < <(
-    echo "$STATE_JSON" | jq -r '[.stage // "", .stage_index // "", .total_stages // 8, .ask // "smart", .status // "running"] | `@tsv`' 2>/dev/null
+  IFS='|' read -r STAGE INDEX TOTAL ASK STATUS < <(
+    echo "$STATE_JSON" | jq -r '[.stage // "", .stage_index // "", .total_stages // 8, .ask // "smart", .status // "running"] | map(tostring) | join("|")' 2>/dev/null
   )
 # render_watch() - line 331
-  read -r PR_NUMBER CI_STATUS STARTED_AT TIMEOUT TRIAGE_COUNT FIX_ATTEMPTS < <(
-    echo "$STATE_JSON" | jq -r '[.pr_number // "", .last_ci_status // "pending", .watch_started_at // "", .watch_timeout_minutes // 30, .triage_count // 0, .ci_fix_attempts // 0] | `@tsv`' 2>/dev/null
+  IFS='|' read -r PR_NUMBER CI_STATUS STARTED_AT TIMEOUT TRIAGE_COUNT FIX_ATTEMPTS < <(
+    echo "$STATE_JSON" | jq -r '[.pr_number // "", .last_ci_status // "pending", .watch_started_at // "", .watch_timeout_minutes // 30, .triage_count // 0, .ci_fix_attempts // 0] | map(tostring) | join("|")' 2>/dev/null
   )

Also applies to: 331-333

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.specify/extensions/spex/scripts/spex-ship-statusline.sh around lines 269 -
271, Update the state-field parsing in render_ship() and render_watch() to avoid
jq `@tsv` with default read splitting; use the same pipe-delimited join and IFS
pattern as render_flow(). Preserve the existing field order, defaults, and
subsequent guard/calculation logic so empty fields remain positional and do not
shift later values.

Source: Linters/SAST tools

crates/openshell-sandbox/src/lib.rs-948-951 (1)

948-951: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Readiness flag is set before the gRPC server begins accepting connections.

ready.store(true, ...) executes after the server future is created but before it is polled in tokio::select!. In Kubernetes, a readiness probe hitting /readyz during this window would mark the pod ready before the activation gRPC endpoint is listening, causing activation requests to fail with connection refused.

🛡️ Proposed fix: set readiness after server starts listening
-    if health_check {
-        ready.store(true, std::sync::atomic::Ordering::Release);
-        info!(port = health_port, "Health endpoint /readyz is ready");
-    }
-
-    tokio::select! {
-        result = server => {
+    // Use a ready signal channel to notify when the server is listening
+    let (server_ready_tx, server_ready_rx) = tokio::sync::oneshot::channel();
+    let server = tonic::transport::Server::builder()
+        .add_service(SupervisorServer::new(service))
+        .serve_with_shutdown(addr, async move {
+            let _ = server_ready_tx.send(());
+            // Keep the server running until activation completes
+            std::future::pending::<()>().await;
+        });
+
+    if health_check {
+        tokio::spawn(async move {
+            let _ = server_ready_rx.await;
+            ready.store(true, std::sync::atomic::Ordering::Release);
+            info!(port = health_port, "Health endpoint /readyz is ready");
+        });
+    }
+
+    tokio::select! {
+        result = server => {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/openshell-sandbox/src/lib.rs` around lines 948 - 951, Move the
readiness update in the health_check startup flow so ready.store(true,
Ordering::Release) runs only after the gRPC server has successfully begun
listening, not immediately after creating its future and before tokio::select!
polls it. Keep the /readyz log aligned with that successful-listening point.
experiments/manifests/sandbox-template-unidentified.yaml-17-17 (1)

17-17: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Pin image tags instead of using :latest for reproducible warm pool timing.

Both the init container and main container use base:latest. Since the PR reports validated warm pool startup timing of 1.9s across three runs, a drifting :latest image could silently invalidate those measurements. Pin to a digest or specific tag for reproducibility.

Also applies to: 35-35

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@experiments/manifests/sandbox-template-unidentified.yaml` at line 17, Replace
the :latest tag for the base image in both the init container and main container
with a fixed digest or immutable version tag, preserving the same image source
and ensuring warm pool timing remains reproducible.
.specify/extensions/spex-gates/commands/speckit.spex-gates.verify.md-155-155 (1)

155-155: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fix grep -c ... || echo 0 double-output bug.

When grep -c finds 0 matches, it outputs "0" to stdout and returns exit code 1. The || echo 0 then also outputs "0", so the command substitution captures both, producing "0\n0" — not a valid integer for the later comparison at line 165.

🐛 Proposed fix
-HAS_SCENARIOS=$(grep -c '\*\*Given\*\*' "$SPEC_FILE" 2>/dev/null || echo 0)
+HAS_SCENARIOS=$(grep -c '\*\*Given\*\*' "$SPEC_FILE" 2>/dev/null) || HAS_SCENARIOS=0
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.specify/extensions/spex-gates/commands/speckit.spex-gates.verify.md at line
155, Update the HAS_SCENARIOS assignment in the verification logic to handle
grep’s nonzero status for zero matches without emitting a second fallback value.
Ensure the command substitution always produces exactly one integer, preserving
the existing count for matched scenarios and returning 0 when none are found.
crates/openshell-driver-kubernetes/src/activation_client.rs-75-92 (1)

75-92: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Test does not exercise the path its name implies.

connection_to_nonexistent_host_fails passes None for tls, so activate_sandbox short-circuits at the TLS guard (Lines 36-40) and returns ConnectionFailed before ever attempting to connect to 192.0.2.1. The assertion therefore passes trivially and never covers connection/timeout behavior against an unreachable host. Provide a TlsConfig (dummy PEMs) so the connection attempt is actually made, or rename the test to reflect that it validates the missing-TLS guard.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/openshell-driver-kubernetes/src/activation_client.rs` around lines 75
- 92, Update the connection_to_nonexistent_host_fails test to pass a valid
TlsConfig with dummy PEM values to activate_sandbox instead of None, ensuring
execution reaches the unreachable-host connection path. Keep the existing
ConnectionFailed or Timeout assertions unchanged.
.specify/extensions/spex-gates/scripts/spex-flow-state.sh-81-103 (1)

81-103: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Heredoc JSON creation doesn't escape variable values.

do_create() interpolates $branch and $spec_dir directly into JSON via heredoc. If --spec-dir is passed with double quotes or backslashes, the resulting JSON is malformed. Since jq is already used for updates, use it for initial creation too.

🛡️ Proposed fix: use jq -n for initial JSON creation
     if [ "$collab_enabled" = true ]; then
-      cat > "$STATE_FILE" <<EOF
-{
-  "mode": "flow",
-  "started_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
-  "feature_branch": "$branch",
-  "spec_dir": "$spec_dir",
-  "implemented": false,
-  "clarified": false,
-  "triage_spec_passed": false,
-  "triage_impl_passed": false
-}
-EOF
+      jq -n \
+        --arg started "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
+        --arg branch "$branch" \
+        --arg dir "$spec_dir" \
+        '{mode:"flow",started_at:$started,feature_branch:$branch,spec_dir:$dir,implemented:false,clarified:false,triage_spec_passed:false,triage_impl_passed:false}' \
+        > "$STATE_FILE"
     else
-      cat > "$STATE_FILE" <<EOF
-{
-  "mode": "flow",
-  "started_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
-  "feature_branch": "$branch",
-  "spec_dir": "$spec_dir",
-  "implemented": false,
-  "clarified": false
-}
-EOF
+      jq -n \
+        --arg started "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
+        --arg branch "$branch" \
+        --arg dir "$spec_dir" \
+        '{mode:"flow",started_at:$started,feature_branch:$branch,spec_dir:$dir,implemented:false,clarified:false}' \
+        > "$STATE_FILE"
     fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.specify/extensions/spex-gates/scripts/spex-flow-state.sh around lines 81 -
103, Update do_create() to build both initial state documents with jq -n rather
than interpolating branch and spec_dir directly in heredocs. Pass the values as
jq arguments so quotes, backslashes, and other special characters are
JSON-escaped, while preserving the existing fields and mode-specific triage
flags.
.specify/extensions/spex-collab/commands/speckit.spex-collab.phase-split.md-15-17 (1)

15-17: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Ship pipeline guard is missing a return statement.

The bash snippet echoes "Ship mode active, skipping phase split" but never returns, so execution would continue past the guard and display the proposal anyway. Line 21 explicitly states "If ship mode is detected, return immediately."

🔧 Proposed fix
     if [ "$MODE" = "ship" ] || [ "$STATUS" = "running" ]; then
       echo "Ship mode active, skipping phase split"
+      return 0
     fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.specify/extensions/spex-collab/commands/speckit.spex-collab.phase-split.md
around lines 15 - 17, Add an immediate return after the guard message in the
phase-split flow for the condition checking MODE="ship" or STATUS="running".
Preserve the existing echo, and ensure execution stops before displaying the
proposal as required by the documented behavior.
experiments/smoke-test.sh-63-65 (1)

63-65: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Add nc and lsof to preflight checks.

The script uses nc -z (line 321) to verify port-forward readiness and lsof -ti (line 52) to clean up port-forwards, but neither tool is checked in the preflight section. If nc is missing, the readiness loop silently fails and waits 5 seconds per run before proceeding with a possibly-unready port-forward. If lsof is missing, stale port-forwards from previous runs are not killed, causing the new port-forward to fail on an already-bound port.

🔧 Proposed fix
 for tool in kubectl grpcurl; do
   command -v "$tool" &>/dev/null || { fail "$tool not found"; exit 1; }
 done
+for tool in nc lsof; do
+  command -v "$tool" &>/dev/null || { fail "$tool not found"; exit 1; }
+done
 pass "kubectl and grpcurl available"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@experiments/smoke-test.sh` around lines 63 - 65, Extend the preflight
tool-validation loop to include nc and lsof alongside kubectl and grpcurl, so
both port-forward readiness checks and cleanup prerequisites are validated
before the script proceeds. Preserve the existing missing-tool failure behavior.
🧹 Nitpick comments (14)
.specify/extensions/spex/scripts/spex-detach.sh (1)

1-5: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicate shim script — consider consolidating.

This file is identical to .specify/extensions/spex-detach/scripts/spex-detach.sh. Both are 5-line shims delegating to their respective spex-detach.py via the same python-resolve.sh resolver. If the two spex-detach.py scripts are also identical, consider symlinking or sharing a single entry point to avoid drift.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.specify/extensions/spex/scripts/spex-detach.sh around lines 1 - 5,
Consolidate the duplicate spex-detach.sh entry points by verifying whether both
spex-detach.py implementations are identical, then use a single shared entry
point or symlink rather than maintaining two shims. Preserve invocation
arguments and resolution through python-resolve.sh.
.specify/extensions/spex-detach/scripts/spex-detach.py (2)

9-13: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

git()'s check argument diverges from subprocess.run semantics.

Here check=True returns None on failure (rather than raising, as subprocess.run(check=True) does), while check=False returns "". A None return would blow up in the common .split()/.strip() call sites if check=True is ever used. The parameter appears unused today, so consider dropping it or renaming to avoid a future footgun.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.specify/extensions/spex-detach/scripts/spex-detach.py around lines 9 - 13,
Update the git() helper’s unused check parameter to avoid implying
subprocess.run(check=True) semantics: remove the parameter and its conditional
failure branch, while preserving the existing stdout-or-empty-string return
behavior for all callers.

112-112: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Split semicolon-joined statements to satisfy Ruff (E702).

Ruff flags these lines as errors (E702, multiple statements on one line). If Ruff runs in CI, this file will fail linting. Split each x = ...; i += ... onto separate lines.

♻️ Example fix (apply to all flagged lines)
-            branch = args[i + 1]; i += 2
+            branch = args[i + 1]
+            i += 2

Also applies to: 133-133, 136-136, 140-140, 242-242, 245-245, 248-248, 250-250

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.specify/extensions/spex-detach/scripts/spex-detach.py at line 112, Split
the semicolon-joined assignment and index-increment statements throughout the
argument parsing and related logic, including the statements near branch, lines
133, 136, 140, 242, 245, 248, and 250, into separate statements on separate
lines. Preserve the existing execution order and behavior while removing all
Ruff E702 violations.

Source: Linters/SAST tools

.specify/extensions/spex/scripts/spex-ship-statusline.sh (1)

172-177: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

next_step is set but never used.

The next_step variable is assigned in the if/elif chain (lines 173–177) but is never referenced afterward. Milestone rendering at lines 198–206 uses the mile_next array for the running-state check, making next_step dead code.

♻️ Remove dead code
   # Next milestone (first incomplete linear stage)
-  local next_step=""
-  if [ "$has_spec" = false ]; then next_step="specify"
-  elif [ "$has_plan" = false ]; then next_step="plan"
-  elif [ "$has_tasks" = false ]; then next_step="tasks"
-  elif [ "$has_impl" = false ]; then next_step="implement"
-  fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.specify/extensions/spex/scripts/spex-ship-statusline.sh around lines 172 -
177, Remove the unused next_step variable and its if/elif assignment chain from
the statusline script, leaving the existing mile_next-based milestone rendering
and running-state logic unchanged.

Source: Linters/SAST tools

specs/002-warm-pool-grpc-poc/contracts/supervisor.proto (1)

52-60: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Consider adding a timeout error code.

The ErrorCode enum lacks a timeout/deadline category. Activation involves potentially long-running steps (OPA compilation, gateway connection, entrypoint start). Without ERROR_CODE_TIMEOUT, the client cannot distinguish between "activation failed" and "activation is still running but exceeded the deadline," which matters for retry decisions and cold-start fallback logic.

♻️ Suggested addition
 enum ErrorCode {
   ERROR_CODE_UNSPECIFIED = 0;
   ERROR_CODE_INVALID_REQUEST = 1;
   ERROR_CODE_POLICY_COMPILATION_FAILED = 2;
   ERROR_CODE_GATEWAY_UNREACHABLE = 3;
   ERROR_CODE_TOKEN_INVALID = 4;
   ERROR_CODE_ALREADY_ACTIVATED = 5;
   ERROR_CODE_INTERNAL = 6;
+  ERROR_CODE_TIMEOUT = 7;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@specs/002-warm-pool-grpc-poc/contracts/supervisor.proto` around lines 52 -
60, Add ERROR_CODE_TIMEOUT to the ErrorCode enum as a distinct timeout/deadline
category, preserving all existing numeric assignments and placing it at a new
value after ERROR_CODE_INTERNAL.
experiments/manifests/sandbox-template-unidentified.yaml (2)

78-82: 🔒 Security & Privacy | 🔵 Trivial | 🏗️ Heavy lift

Plan AppArmor and capability hardening before production.

Unconfined AppArmor combined with SYS_ADMIN, SYS_PTRACE, NET_ADMIN, SYSLOG, and runAsUser: 0 grants near-host-level access. This is likely required for nested container sandbox functionality in the PoC, but should be tightened before any non-experiment deployment — consider a dedicated AppArmor profile and scoped capability drops where possible.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@experiments/manifests/sandbox-template-unidentified.yaml` around lines 78 -
82, Restrict this sandbox manifest’s privileged security settings before any
non-experiment deployment: replace the Unconfined appArmorProfile with a
dedicated restrictive profile, minimize the capabilities listed under
capabilities.add and drop all unnecessary privileges, and avoid runAsUser: 0 by
using a non-root user where nested-container functionality permits.

108-109: 🩺 Stability & Availability | 🔵 Trivial

Gate this manifest on ImageVolume-capable clusters

This template hardcodes the Kubernetes image volume path, so it only works on K8s 1.35+ (or 1.33–1.34 with ImageVolume enabled); older clusters will reject the pod.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@experiments/manifests/sandbox-template-unidentified.yaml` around lines 108 -
109, Gate the sandbox manifest template containing the image reference under an
ImageVolume-capable-cluster condition. Ensure it is only rendered or applied for
Kubernetes 1.35+ or clusters explicitly enabling ImageVolume, while preserving
the existing image reference for supported clusters.
.specify/extensions/git/scripts/powershell/create-new-feature.ps1 (1)

136-137: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add Write-Verbose to empty catch blocks for debuggability.

PSScriptAnalyzer flags the empty catch blocks at lines 137 and 342. While intentionally swallowing errors (offline git fetch, missing HEAD ref), a Write-Verbose would aid troubleshooting without changing behavior.

💡 Suggested improvement
 try {
     git fetch --all --prune 2>$null | Out-Null
-} catch { }
+} catch {
+    Write-Verbose "git fetch failed (offline?): $_"
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.specify/extensions/git/scripts/powershell/create-new-feature.ps1 around
lines 136 - 137, Add Write-Verbose statements to the intentionally empty catch
blocks in the git fetch and missing HEAD ref handling within the feature
creation script, including useful context about the swallowed errors while
preserving the existing behavior of ignoring those failures.
.specify/extensions/spex/scripts/spex-flow-state.sh (2)

108-115: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use jq --arg for the running phase to avoid expression injection.

do_running constructs a jq expression via printf '.running = "%s"' "$phase". If $phase contains a double quote, the jq expression would break and the script would exit under set -e. Using jq --arg would handle this safely.

♻️ Proposed refactor for safe jq expression construction

This requires a small modification to update_state to pass through extra jq arguments:

 update_state() {
   local expr="$1"
+  shift
   ensure_flow
   if is_flow; then
     local tmp
     tmp=$(mktemp)
-    jq "$expr" "$STATE_FILE" > "$tmp" && mv "$tmp" "$STATE_FILE"
+    jq "$@" "$expr" "$STATE_FILE" > "$tmp" && mv "$tmp" "$STATE_FILE"
   fi
 }

Then update do_running:

 do_running() {
   local phase="${1:-}"
   if [ "$phase" = "done" ]; then
     update_state '.running = ""'
   elif [ -n "$phase" ]; then
-    update_state "$(printf '.running = "%s"' "$phase")"
+    update_state --arg phase "$phase" '.running = $phase'
   fi
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.specify/extensions/spex/scripts/spex-flow-state.sh around lines 108 - 115,
Update update_state to accept and forward additional jq arguments, then change
do_running’s non-empty phase branch to pass phase via jq --arg and use a static
.running = $phase filter instead of constructing the expression with printf.
Preserve the existing done and empty-phase behavior.

81-103: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Prefer jq -n over heredoc for JSON construction.

The do_create function builds JSON via heredoc with variable interpolation, while update_state correctly uses jq --arg for safe string passing. If $branch or $spec_dir ever contain double quotes or backslashes, the heredoc would produce invalid JSON. Using jq -n would be consistent with the rest of the script and handle escaping automatically.

♻️ Proposed refactor for safe JSON construction
     if [ "$collab_enabled" = true ]; then
-      cat > "$STATE_FILE" <<EOF
-{
-  "mode": "flow",
-  "started_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
-  "feature_branch": "$branch",
-  "spec_dir": "$spec_dir",
-  "implemented": false,
-  "clarified": false,
-  "triage_spec_passed": false,
-  "triage_impl_passed": false
-}
-EOF
+      jq -n \
+        --arg started_at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
+        --arg branch "$branch" \
+        --arg dir "$spec_dir" \
+        '{mode:"flow", started_at:$started_at, feature_branch:$branch, spec_dir:$dir, implemented:false, clarified:false, triage_spec_passed:false, triage_impl_passed:false}' \
+        > "$STATE_FILE"
     else
-      cat > "$STATE_FILE" <<EOF
-{
-  "mode": "flow",
-  "started_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
-  "feature_branch": "$branch",
-  "spec_dir": "$spec_dir",
-  "implemented": false,
-  "clarified": false
-}
-EOF
+      jq -n \
+        --arg started_at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
+        --arg branch "$branch" \
+        --arg dir "$spec_dir" \
+        '{mode:"flow", started_at:$started_at, feature_branch:$branch, spec_dir:$dir, implemented:false, clarified:false}' \
+        > "$STATE_FILE"
     fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.specify/extensions/spex/scripts/spex-flow-state.sh around lines 81 - 103,
Update the JSON construction in do_create to use jq -n with --arg for branch,
spec_dir, and the UTC timestamp instead of interpolated heredocs. Preserve both
state object variants, including their existing mode, flags, and triage fields,
while relying on jq to escape string values safely.
crates/openshell-sandbox/tests/activation_smoke.rs (1)

94-105: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

No per-RPC timeout on activate_sandbox calls — test can hang indefinitely.

connect_timeout on the channel only bounds connection establishment, not the RPC itself. If the sandbox process hangs during activation, both activate_sandbox calls will block forever with no timeout, hanging the test runner.

⏱️ Proposed fix: wrap calls in tokio::time::timeout
     let activate_start = Instant::now();
-    let resp = client
+    let resp = tokio::time::timeout(Duration::from_secs(10), client
         .activate_sandbox(ActivateSandboxRequest {
             sandbox_id: "smoke-test-sandbox".into(),
             sandbox_name: "smoke-test".into(),
             sandbox_token: "fake-jwt-token".into(),
             gateway_endpoint: "https://gateway.invalid:8443".into(),
             policy: Some(SandboxPolicy::default()),
         })
-        .await
-        .expect("gRPC call should not return transport error")
-        .into_inner();
+    .await)
+        .expect("activate_sandbox should complete within 10s")
+        .expect("gRPC call should not return transport error")
+        .into_inner();

Apply the same pattern to the second activate_sandbox call (lines 128-138).

Also applies to: 128-138

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/openshell-sandbox/tests/activation_smoke.rs` around lines 94 - 105,
Wrap both `activate_sandbox` calls in
`crates/openshell-sandbox/tests/activation_smoke.rs` with `tokio::time::timeout`
using an appropriate per-RPC duration. Handle the timeout result while
preserving the existing transport-error expectation and response extraction for
successful calls.
experiments/smoke-test.sh (1)

314-315: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove unused POD_IP variable.

POD_IP is extracted from the claim status but never referenced — the activation call uses localhost:${LOCAL_PORT} via port-forward. Shellcheck confirms this (SC2034).

♻️ Proposed fix
 POD_NAME=$(kubectl -n "$NAMESPACE" get sandboxclaim "$CLAIM_NAME" \
   -o jsonpath='{.status.sandbox.name}')
-POD_IP=$(kubectl -n "$NAMESPACE" get sandboxclaim "$CLAIM_NAME" \
-  -o jsonpath='{.status.sandbox.podIPs[0]}')
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@experiments/smoke-test.sh` around lines 314 - 315, Remove the unused POD_IP
assignment from the smoke-test flow, including its kubectl lookup, while
preserving the existing port-forward activation path using
localhost:${LOCAL_PORT}.
crates/openshell-driver-kubernetes/src/warm_pool.rs (2)

72-126: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoff

Consider batching template fetches in find_matching_pool for production readiness.

Each pool with ready replicas triggers a separate tmpl_api.get() call (line 104), creating an N+1 query pattern. For a PoC with a handful of pools this is fine, but as pool count grows this will linearly increase API server load and latency. Consider collecting all template names first and batching via a single list call with a field selector, or caching template images.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/openshell-driver-kubernetes/src/warm_pool.rs` around lines 72 - 126,
Update find_matching_pool to avoid one tmpl_api.get call per ready pool: collect
the referenced template names, fetch templates in a batched or cached manner,
and then match each pool’s template image against image. Preserve skipping
unready or malformed pools, warning on fetch failures, and returning the first
matching pool.

155-233: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add exponential backoff or distinguish NotFound in wait_for_claim_ready polling.

The fixed 200ms poll interval (line 163) results in up to 75 API calls over a 15s timeout. Additionally, any API error — including a NotFound for a non-existent claim — is logged and retried until timeout (lines 226-228), wasting the full timeout budget. Consider: (1) using exponential backoff with jitter to reduce API server pressure, and (2) checking for kube::Error::Api with NotFound code to fail fast instead of polling until timeout.

♻️ Proposed improvement for fast-fail on NotFound
         Err(e) => {
+            if let kube::Error::Api(api_err) = &e {
+                if api_err.code == 404 {
+                    return Err(WarmPoolError::ClaimFailed(format!(
+                        "SandboxClaim {claim_name} not found: {e}"
+                    )));
+                }
+            }
             warn!(claim = %claim_name, error = %e, "Error polling SandboxClaim");
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/openshell-driver-kubernetes/src/warm_pool.rs` around lines 155 - 233,
Update wait_for_claim_ready to distinguish Kubernetes NotFound errors from
transient polling failures: when api.get returns kube::Error::Api with a 404
status, return the appropriate claim-not-found error immediately instead of
retrying. Replace the fixed poll_interval sleep with bounded exponential backoff
and jitter, while preserving the timeout deadline and existing handling for
successful, failed, and other transient responses.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.specify/extensions/git/scripts/powershell/create-new-feature.ps1:
- Around line 386-403: Update the PowerShell create-new-feature flow to match
the bash script by creating the FEATURE_DIR and SPEC_FILE artifacts and
including SPEC_FILE in both the JSON object and human-readable output. Remove
HAS_GIT from the emitted output so the PowerShell contract matches the bash
script, while preserving the existing DRY_RUN behavior.

In @.specify/extensions/spex-collab/commands/speckit.spex-collab.reviewers.md:
- Line 97: Update the template reference in the speckit command instructions to
use the correct .specify/extensions/spex-collab/templates/reviewers-template.md
path, preserving the existing structural-skeleton guidance and consistency with
the other .specify/ references.

In @.specify/extensions/spex/commands/speckit.spex.submit.md:
- Around line 67-73: Fix DEFAULT_BRANCH initialization in the submit workflow so
failure or empty output from git symbolic-ref falls back to "main" rather than
allowing the sed pipeline to succeed with an empty value. Preserve the
CURRENT_BRANCH comparison and ensure DEFAULT_BRANCH remains valid for both the
default-branch guard and the later brainstorm exclusion diff.

In @.specify/extensions/spex/scripts/spex-detach.py:
- Around line 90-93: Update validate_path_component to reject absolute path
components in addition to values containing "..", using the platform-appropriate
path check before os.path.join is used for project or feature. Preserve the
existing stderr error and exit behavior for both traversal and absolute-path
violations.
- Around line 192-228: Update the git apply failure path in the surrounding
branch-creation flow so cleanup runs before terminating, or raise an exception
caught by the existing except Exception handler instead of calling sys.exit(1)
directly. Preserve the existing error JSON and exit status while ensuring
original_branch is restored and pr_branch is deleted when git apply fails.

In @.specify/extensions/spex/scripts/spex-ship-state.py:
- Around line 69-97: Stop verify_stage_artifacts from selecting a feature
directory by filename or newest modification time. Thread the orchestrator’s
resolved feature directory from check-prerequisites.sh --json --paths-only into
verify_stage_artifacts (or expose it through a --feature-dir argument), and use
that explicit path for artifact checks; retain filename-based discovery only as
an intentional, safe fallback if required.

In `@crates/openshell-driver-kubernetes/src/activation_client.rs`:
- Line 41: Update the endpoint construction around endpoint_uri to bracket IPv6
pod addresses before appending ACTIVATION_PORT, while preserving valid
formatting for IPv4 addresses. Ensure IPv6 values produce an authority like
https://[address]:port.
- Around line 49-54: Update the TLS configuration in activate_sandbox() so the
endpoint connecting to the pod IP validates against the supervisor certificate:
configure the pod IP as a server IP SAN when generating or loading the
certificate, or set tls_config.domain_name(...) to a DNS SAN covered by that
certificate. Preserve the existing CA certificate and client identity
configuration.

In `@crates/openshell-driver-kubernetes/src/driver.rs`:
- Around line 909-941: Update read_activation_tls and its try_warm_pool caller
so a configured but missing or incomplete TLS secret fails closed instead of
returning a usable plaintext activation path. Propagate the failure or return
None from try_warm_pool to trigger cold start, while preserving the existing
behavior when no client_tls_secret_name is configured and ensuring sandbox_token
is never sent over a downgraded plain channel.
- Around line 985-1065: Delete the successfully created warm-pool claim before
returning None from the wait_for_claim_ready and activate_sandbox failure
branches. Reuse the existing Kubernetes client, namespace, and claim_name with
the warm-pool claim deletion helper, while preserving the current warning logs
and cold-start fallback behavior.

In `@crates/openshell-sandbox/src/health.rs`:
- Around line 36-37: Update the accept loop in the health server around
listener.accept so transient accept errors are logged and the loop continues
instead of being propagated with ?. Preserve normal stream handling and only
terminate for errors that cannot be recovered from if the surrounding design
explicitly distinguishes them.

In `@crates/openshell-sandbox/src/lib.rs`:
- Around line 944-946: Update the activation gRPC listener construction around
SupervisorServer::new and Server::builder to configure TLS with .tls_config()
before serving, using the project’s existing certificate and private-key
configuration. Ensure the resulting serve path remains compatible with
SupervisorServer and fails clearly if TLS configuration cannot be applied.
- Around line 953-965: Update the gRPC server branch in the tokio::select! block
to return a non-zero exit status when server returns an error, while preserving
the existing warning log. Keep the activation_rx success path returning the
normal successful status.

In `@crates/openshell-sandbox/tests/activation_smoke.rs`:
- Around line 62-77: Wrap the spawned child in a Drop guard immediately after
the Command chain in the activation smoke test, ensuring the guard kills and
waits for the process when assertions unwind. Remove the redundant manual
cleanup at the test’s end, while preserving the existing child process usage
throughout the test.

---

Minor comments:
In @.specify/extensions/spex-collab/commands/speckit.spex-collab.phase-split.md:
- Around line 15-17: Add an immediate return after the guard message in the
phase-split flow for the condition checking MODE="ship" or STATUS="running".
Preserve the existing echo, and ensure execution stops before displaying the
proposal as required by the documented behavior.

In @.specify/extensions/spex-gates/commands/speckit.spex-gates.verify.md:
- Line 155: Update the HAS_SCENARIOS assignment in the verification logic to
handle grep’s nonzero status for zero matches without emitting a second fallback
value. Ensure the command substitution always produces exactly one integer,
preserving the existing count for matched scenarios and returning 0 when none
are found.

In @.specify/extensions/spex-gates/scripts/spex-flow-state.sh:
- Around line 81-103: Update do_create() to build both initial state documents
with jq -n rather than interpolating branch and spec_dir directly in heredocs.
Pass the values as jq arguments so quotes, backslashes, and other special
characters are JSON-escaped, while preserving the existing fields and
mode-specific triage flags.

In @.specify/extensions/spex/scripts/spex-ship-statusline.sh:
- Around line 269-271: Update the state-field parsing in render_ship() and
render_watch() to avoid jq `@tsv` with default read splitting; use the same
pipe-delimited join and IFS pattern as render_flow(). Preserve the existing
field order, defaults, and subsequent guard/calculation logic so empty fields
remain positional and do not shift later values.

In `@crates/openshell-driver-kubernetes/src/activation_client.rs`:
- Around line 75-92: Update the connection_to_nonexistent_host_fails test to
pass a valid TlsConfig with dummy PEM values to activate_sandbox instead of
None, ensuring execution reaches the unreachable-host connection path. Keep the
existing ConnectionFailed or Timeout assertions unchanged.

In `@crates/openshell-sandbox/src/lib.rs`:
- Around line 948-951: Move the readiness update in the health_check startup
flow so ready.store(true, Ordering::Release) runs only after the gRPC server has
successfully begun listening, not immediately after creating its future and
before tokio::select! polls it. Keep the /readyz log aligned with that
successful-listening point.

In `@experiments/manifests/sandbox-template-unidentified.yaml`:
- Line 17: Replace the :latest tag for the base image in both the init container
and main container with a fixed digest or immutable version tag, preserving the
same image source and ensuring warm pool timing remains reproducible.

In `@experiments/smoke-test.sh`:
- Around line 63-65: Extend the preflight tool-validation loop to include nc and
lsof alongside kubectl and grpcurl, so both port-forward readiness checks and
cleanup prerequisites are validated before the script proceeds. Preserve the
existing missing-tool failure behavior.

---

Nitpick comments:
In @.specify/extensions/git/scripts/powershell/create-new-feature.ps1:
- Around line 136-137: Add Write-Verbose statements to the intentionally empty
catch blocks in the git fetch and missing HEAD ref handling within the feature
creation script, including useful context about the swallowed errors while
preserving the existing behavior of ignoring those failures.

In @.specify/extensions/spex-detach/scripts/spex-detach.py:
- Around line 9-13: Update the git() helper’s unused check parameter to avoid
implying subprocess.run(check=True) semantics: remove the parameter and its
conditional failure branch, while preserving the existing stdout-or-empty-string
return behavior for all callers.
- Line 112: Split the semicolon-joined assignment and index-increment statements
throughout the argument parsing and related logic, including the statements near
branch, lines 133, 136, 140, 242, 245, 248, and 250, into separate statements on
separate lines. Preserve the existing execution order and behavior while
removing all Ruff E702 violations.

In @.specify/extensions/spex/scripts/spex-detach.sh:
- Around line 1-5: Consolidate the duplicate spex-detach.sh entry points by
verifying whether both spex-detach.py implementations are identical, then use a
single shared entry point or symlink rather than maintaining two shims. Preserve
invocation arguments and resolution through python-resolve.sh.

In @.specify/extensions/spex/scripts/spex-flow-state.sh:
- Around line 108-115: Update update_state to accept and forward additional jq
arguments, then change do_running’s non-empty phase branch to pass phase via jq
--arg and use a static .running = $phase filter instead of constructing the
expression with printf. Preserve the existing done and empty-phase behavior.
- Around line 81-103: Update the JSON construction in do_create to use jq -n
with --arg for branch, spec_dir, and the UTC timestamp instead of interpolated
heredocs. Preserve both state object variants, including their existing mode,
flags, and triage fields, while relying on jq to escape string values safely.

In @.specify/extensions/spex/scripts/spex-ship-statusline.sh:
- Around line 172-177: Remove the unused next_step variable and its if/elif
assignment chain from the statusline script, leaving the existing
mile_next-based milestone rendering and running-state logic unchanged.

In `@crates/openshell-driver-kubernetes/src/warm_pool.rs`:
- Around line 72-126: Update find_matching_pool to avoid one tmpl_api.get call
per ready pool: collect the referenced template names, fetch templates in a
batched or cached manner, and then match each pool’s template image against
image. Preserve skipping unready or malformed pools, warning on fetch failures,
and returning the first matching pool.
- Around line 155-233: Update wait_for_claim_ready to distinguish Kubernetes
NotFound errors from transient polling failures: when api.get returns
kube::Error::Api with a 404 status, return the appropriate claim-not-found error
immediately instead of retrying. Replace the fixed poll_interval sleep with
bounded exponential backoff and jitter, while preserving the timeout deadline
and existing handling for successful, failed, and other transient responses.

In `@crates/openshell-sandbox/tests/activation_smoke.rs`:
- Around line 94-105: Wrap both `activate_sandbox` calls in
`crates/openshell-sandbox/tests/activation_smoke.rs` with `tokio::time::timeout`
using an appropriate per-RPC duration. Handle the timeout result while
preserving the existing transport-error expectation and response extraction for
successful calls.

In `@experiments/manifests/sandbox-template-unidentified.yaml`:
- Around line 78-82: Restrict this sandbox manifest’s privileged security
settings before any non-experiment deployment: replace the Unconfined
appArmorProfile with a dedicated restrictive profile, minimize the capabilities
listed under capabilities.add and drop all unnecessary privileges, and avoid
runAsUser: 0 by using a non-root user where nested-container functionality
permits.
- Around line 108-109: Gate the sandbox manifest template containing the image
reference under an ImageVolume-capable-cluster condition. Ensure it is only
rendered or applied for Kubernetes 1.35+ or clusters explicitly enabling
ImageVolume, while preserving the existing image reference for supported
clusters.

In `@experiments/smoke-test.sh`:
- Around line 314-315: Remove the unused POD_IP assignment from the smoke-test
flow, including its kubectl lookup, while preserving the existing port-forward
activation path using localhost:${LOCAL_PORT}.

In `@specs/002-warm-pool-grpc-poc/contracts/supervisor.proto`:
- Around line 52-60: Add ERROR_CODE_TIMEOUT to the ErrorCode enum as a distinct
timeout/deadline category, preserving all existing numeric assignments and
placing it at a new value after ERROR_CODE_INTERNAL.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 585113c5-54d0-4d9e-9024-a41244983105

📥 Commits

Reviewing files that changed from the base of the PR and between 99801e3 and 9057afb.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (132)
  • .claude/agent-memory/arch-doc-writer/MEMORY.md
  • .specify/.pr-triage-state.json
  • .specify/.spex-state
  • .specify/extensions.yml
  • .specify/extensions/.registry
  • .specify/extensions/git/README.md
  • .specify/extensions/git/commands/speckit.git.commit.md
  • .specify/extensions/git/commands/speckit.git.feature.md
  • .specify/extensions/git/commands/speckit.git.initialize.md
  • .specify/extensions/git/commands/speckit.git.remote.md
  • .specify/extensions/git/commands/speckit.git.validate.md
  • .specify/extensions/git/config-template.yml
  • .specify/extensions/git/extension.yml
  • .specify/extensions/git/git-config.yml
  • .specify/extensions/git/scripts/bash/auto-commit.sh
  • .specify/extensions/git/scripts/bash/create-new-feature.sh
  • .specify/extensions/git/scripts/bash/git-common.sh
  • .specify/extensions/git/scripts/bash/initialize-repo.sh
  • .specify/extensions/git/scripts/powershell/auto-commit.ps1
  • .specify/extensions/git/scripts/powershell/create-new-feature.ps1
  • .specify/extensions/git/scripts/powershell/git-common.ps1
  • .specify/extensions/git/scripts/powershell/initialize-repo.ps1
  • .specify/extensions/spex-collab/commands/speckit.spex-collab.phase-manager.md
  • .specify/extensions/spex-collab/commands/speckit.spex-collab.phase-split.md
  • .specify/extensions/spex-collab/commands/speckit.spex-collab.reconcile.md
  • .specify/extensions/spex-collab/commands/speckit.spex-collab.reviewers.md
  • .specify/extensions/spex-collab/commands/speckit.spex-collab.revise.md
  • .specify/extensions/spex-collab/commands/speckit.spex-collab.triage.md
  • .specify/extensions/spex-collab/config-template.yml
  • .specify/extensions/spex-collab/extension.yml
  • .specify/extensions/spex-collab/scripts/sanitize-gh-json.py
  • .specify/extensions/spex-collab/scripts/spex-flow-state.sh
  • .specify/extensions/spex-collab/scripts/spex-triage-state.sh
  • .specify/extensions/spex-collab/templates/reviewers-template.md
  • .specify/extensions/spex-deep-review/commands/speckit.spex-deep-review.run.md
  • .specify/extensions/spex-deep-review/config-template.yml
  • .specify/extensions/spex-deep-review/extension.yml
  • .specify/extensions/spex-deep-review/scripts/spex-flow-state.sh
  • .specify/extensions/spex-detach/commands/speckit.spex-detach.detach.md
  • .specify/extensions/spex-detach/config-template.yml
  • .specify/extensions/spex-detach/extension.yml
  • .specify/extensions/spex-detach/scripts/spex-detach.py
  • .specify/extensions/spex-detach/scripts/spex-detach.sh
  • .specify/extensions/spex-gates/commands/speckit.spex-gates.review-code.md
  • .specify/extensions/spex-gates/commands/speckit.spex-gates.review-plan.md
  • .specify/extensions/spex-gates/commands/speckit.spex-gates.review-spec.md
  • .specify/extensions/spex-gates/commands/speckit.spex-gates.stamp.md
  • .specify/extensions/spex-gates/commands/speckit.spex-gates.verify.md
  • .specify/extensions/spex-gates/extension.yml
  • .specify/extensions/spex-gates/scripts/spex-closeout-gate.sh
  • .specify/extensions/spex-gates/scripts/spex-flow-state.sh
  • .specify/extensions/spex-teams/commands/speckit.spex-teams.implement.md
  • .specify/extensions/spex-teams/commands/speckit.spex-teams.orchestrate.md
  • .specify/extensions/spex-teams/commands/speckit.spex-teams.research.md
  • .specify/extensions/spex-teams/config-template.yml
  • .specify/extensions/spex-teams/extension.yml
  • .specify/extensions/spex-worktrees/commands/speckit.spex-worktrees.manage.md
  • .specify/extensions/spex-worktrees/extension.yml
  • .specify/extensions/spex/commands/speckit.spex.brainstorm.md
  • .specify/extensions/spex/commands/speckit.spex.clear.md
  • .specify/extensions/spex/commands/speckit.spex.evolve.md
  • .specify/extensions/spex/commands/speckit.spex.extensions.md
  • .specify/extensions/spex/commands/speckit.spex.finish.md
  • .specify/extensions/spex/commands/speckit.spex.flow-state.md
  • .specify/extensions/spex/commands/speckit.spex.help.md
  • .specify/extensions/spex/commands/speckit.spex.ship.md
  • .specify/extensions/spex/commands/speckit.spex.smoke-test.md
  • .specify/extensions/spex/commands/speckit.spex.spec-kit.md
  • .specify/extensions/spex/commands/speckit.spex.spec-refactoring.md
  • .specify/extensions/spex/commands/speckit.spex.submit.md
  • .specify/extensions/spex/commands/speckit.spex.using-superpowers.md
  • .specify/extensions/spex/extension.yml
  • .specify/extensions/spex/scripts/spex-detach.py
  • .specify/extensions/spex/scripts/spex-detach.sh
  • .specify/extensions/spex/scripts/spex-finish-context.sh
  • .specify/extensions/spex/scripts/spex-flow-state.sh
  • .specify/extensions/spex/scripts/spex-ship-state.py
  • .specify/extensions/spex/scripts/spex-ship-state.sh
  • .specify/extensions/spex/scripts/spex-ship-statusline.sh
  • .specify/extensions/spex/scripts/spex-worktree-cwd.sh
  • .specify/feature.json
  • .specify/init-options.json
  • .specify/integration.json
  • .specify/integrations/claude.manifest.json
  • .specify/integrations/opencode.manifest.json
  • .specify/integrations/speckit.manifest.json
  • .specify/scripts/bash/check-prerequisites.sh
  • .specify/scripts/bash/common.sh
  • .specify/scripts/bash/create-new-feature.sh
  • .specify/scripts/bash/setup-plan.sh
  • .specify/templates/checklist-template.md
  • .specify/templates/constitution-template.md
  • .specify/templates/plan-template.md
  • .specify/templates/spec-template.md
  • .specify/templates/tasks-template.md
  • .specify/workflows/runs/a931d65a/inputs.json
  • .specify/workflows/runs/a931d65a/log.jsonl
  • .specify/workflows/runs/a931d65a/state.json
  • .specify/workflows/runs/a931d65a/workflow.yml
  • .specify/workflows/speckit/workflow.yml
  • .specify/workflows/workflow-registry.json
  • architecture/compute-runtimes.md
  • architecture/sandbox.md
  • brainstorm/06-warm-pool-grpc-poc.md
  • brainstorm/07-warm-pool-sandbox-profile.md
  • crates/openshell-core/src/grpc_client.rs
  • crates/openshell-core/src/proto/mod.rs
  • crates/openshell-driver-kubernetes/src/activation_client.rs
  • crates/openshell-driver-kubernetes/src/driver.rs
  • crates/openshell-driver-kubernetes/src/lib.rs
  • crates/openshell-driver-kubernetes/src/warm_pool.rs
  • crates/openshell-sandbox/Cargo.toml
  • crates/openshell-sandbox/src/activation.rs
  • crates/openshell-sandbox/src/health.rs
  • crates/openshell-sandbox/src/lib.rs
  • crates/openshell-sandbox/src/main.rs
  • crates/openshell-sandbox/tests/activation_smoke.rs
  • crates/openshell-sandbox/tests/activation_timing.rs
  • experiments/SMOKE-TEST.md
  • experiments/manifests/sandbox-template-unidentified.yaml
  • experiments/manifests/warm-pool-unidentified.yaml
  • experiments/smoke-test.sh
  • proto/supervisor.proto
  • specs/002-warm-pool-grpc-poc/REVIEW-CODE.md
  • specs/002-warm-pool-grpc-poc/checklists/requirements.md
  • specs/002-warm-pool-grpc-poc/contracts/supervisor.proto
  • specs/002-warm-pool-grpc-poc/data-model.md
  • specs/002-warm-pool-grpc-poc/plan.md
  • specs/002-warm-pool-grpc-poc/research.md
  • specs/002-warm-pool-grpc-poc/review-findings.md
  • specs/002-warm-pool-grpc-poc/spec.md
  • specs/002-warm-pool-grpc-poc/tasks.md

Comment on lines +386 to +403
if ($Json) {
$obj = [PSCustomObject]@{
BRANCH_NAME = $branchName
FEATURE_NUM = $featureNum
HAS_GIT = $hasGit
}
if ($DryRun) {
$obj | Add-Member -NotePropertyName 'DRY_RUN' -NotePropertyValue $true
}
$obj | ConvertTo-Json -Compress
} else {
Write-Output "BRANCH_NAME: $branchName"
Write-Output "FEATURE_NUM: $featureNum"
Write-Output "HAS_GIT: $hasGit"
if (-not $DryRun) {
Write-Output "SPECIFY_FEATURE environment variable set to: $branchName"
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for consumers that parse create-new-feature output
rg -nP 'SPEC_FILE|BRANCH_NAME|FEATURE_NUM|HAS_GIT' --type=sh -g '!**/create-new-feature*' -g '!**/*.ps1'

Repository: rhuss/OpenShell

Length of output: 153


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the two scripts and inspect their relevant sections.
git ls-files '.specify/**/create-new-feature.*' '.specify/**/create-new-feature*' | sed -n '1,20p'

echo '--- PowerShell outline ---'
ast-grep outline .specify/extensions/git/scripts/powershell/create-new-feature.ps1 | sed -n '1,220p'

echo '--- Bash outline ---'
ast-grep outline .specify/scripts/bash/create-new-feature.sh | sed -n '1,220p'

echo '--- PowerShell relevant lines ---'
ps_file=.specify/extensions/git/scripts/powershell/create-new-feature.ps1
ps_lines=$(wc -l < "$ps_file")
start=$(( ps_lines > 320 ? 320 : 1 ))
end=$(( ps_lines < 460 ? ps_lines : 460 ))
sed -n "${start},${end}p" "$ps_file" | cat -n

echo '--- Bash relevant lines ---'
sh_file=.specify/scripts/bash/create-new-feature.sh
sh_lines=$(wc -l < "$sh_file")
start=$(( sh_lines > 1 ? 1 : 1 ))
end=$(( sh_lines < 260 ? sh_lines : 260 ))
sed -n "${start},${end}p" "$sh_file" | cat -n

echo '--- Search for SPEC_FILE / HAS_GIT consumers ---'
rg -n --hidden --glob '!.git' 'SPEC_FILE|HAS_GIT|FEATURE_NUM|BRANCH_NAME' .

Repository: rhuss/OpenShell

Length of output: 35387


Align PowerShell output and side effects with the bash script

.specify/extensions/git/scripts/powershell/create-new-feature.ps1 still skips FEATURE_DIR/SPEC_FILE creation and never emits SPEC_FILE, while .specify/extensions/git/scripts/bash/create-new-feature.sh does both. That leaves Windows behavior and the output contract inconsistent; HAS_GIT is extra here, but SPEC_FILE is the missing field that callers expect.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.specify/extensions/git/scripts/powershell/create-new-feature.ps1 around
lines 386 - 403, Update the PowerShell create-new-feature flow to match the bash
script by creating the FEATURE_DIR and SPEC_FILE artifacts and including
SPEC_FILE in both the JSON object and human-readable output. Remove HAS_GIT from
the emitted output so the PowerShell contract matches the bash script, while
preserving the existing DRY_RUN behavior.


## Compose REVIEWERS.md

Read the template from `spex/extensions/spex-collab/templates/reviewers-template.md` for the structural skeleton.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Fix incorrect template path — missing .specify/ prefix.

Line 97 references spex/extensions/spex-collab/templates/reviewers-template.md, but the actual template lives at .specify/extensions/spex-collab/templates/reviewers-template.md. This is inconsistent with other paths in the same file (lines 29, 41 use .specify/). An agent following this command would fail to find the template.

🔧 Proposed fix
-Read the template from `spex/extensions/spex-collab/templates/reviewers-template.md` for the structural skeleton.
+Read the template from `.specify/extensions/spex-collab/templates/reviewers-template.md` for the structural skeleton.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Read the template from `spex/extensions/spex-collab/templates/reviewers-template.md` for the structural skeleton.
Read the template from `.specify/extensions/spex-collab/templates/reviewers-template.md` for the structural skeleton.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.specify/extensions/spex-collab/commands/speckit.spex-collab.reviewers.md at
line 97, Update the template reference in the speckit command instructions to
use the correct .specify/extensions/spex-collab/templates/reviewers-template.md
path, preserving the existing structural-skeleton guidance and consistency with
the other .specify/ references.

Comment on lines +67 to +73
```bash
DEFAULT_BRANCH=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@' || echo "main")
CURRENT_BRANCH=$(git branch --show-current)
if [ "$CURRENT_BRANCH" = "$DEFAULT_BRANCH" ]; then
echo "ERROR: Cannot submit from the default branch. Switch to a feature branch first."
# STOP
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

DEFAULT_BRANCH fallback never triggers due to pipeline precedence.

sed always exits 0 even on empty input, so || echo "main" never fires when git symbolic-ref fails (e.g., no remote HEAD configured). This leaves DEFAULT_BRANCH empty, causing the default-branch guard on line 70 to silently pass and allowing PR submission from the default branch — the exact scenario the check is meant to prevent. An empty DEFAULT_BRANCH also breaks the brainstorm exclusion diff on line 316 (git diff ""..."$BRANCH").

🔧 Proposed fix
-DEFAULT_BRANCH=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@' || echo "main")
+DEFAULT_BRANCH=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@')
+DEFAULT_BRANCH=${DEFAULT_BRANCH:-main}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
```bash
DEFAULT_BRANCH=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@' || echo "main")
CURRENT_BRANCH=$(git branch --show-current)
if [ "$CURRENT_BRANCH" = "$DEFAULT_BRANCH" ]; then
echo "ERROR: Cannot submit from the default branch. Switch to a feature branch first."
# STOP
fi
DEFAULT_BRANCH=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@')
DEFAULT_BRANCH=${DEFAULT_BRANCH:-main}
CURRENT_BRANCH=$(git branch --show-current)
if [ "$CURRENT_BRANCH" = "$DEFAULT_BRANCH" ]; then
echo "ERROR: Cannot submit from the default branch. Switch to a feature branch first."
# STOP
fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.specify/extensions/spex/commands/speckit.spex.submit.md around lines 67 -
73, Fix DEFAULT_BRANCH initialization in the submit workflow so failure or empty
output from git symbolic-ref falls back to "main" rather than allowing the sed
pipeline to succeed with an empty value. Preserve the CURRENT_BRANCH comparison
and ensure DEFAULT_BRANCH remains valid for both the default-branch guard and
the later brainstorm exclusion diff.

Comment on lines +90 to +93
def validate_path_component(name, value):
if ".." in value:
print("ERROR: {} contains '..' path traversal".format(name), file=sys.stderr)
sys.exit(1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

validate_path_component doesn't block absolute paths.

Only .. is rejected. Since os.path.join(target, project, feature) (line 273) discards preceding components when a later segment is absolute (Python os.path.join semantics), a --project or --feature value like /etc would redirect shutil.copytree writes outside target, bypassing the archive sandbox.

🛡️ Proposed fix
 def validate_path_component(name, value):
     if ".." in value:
         print("ERROR: {} contains '..' path traversal".format(name), file=sys.stderr)
         sys.exit(1)
+    if os.path.isabs(value):
+        print("ERROR: {} must not be an absolute path".format(name), file=sys.stderr)
+        sys.exit(1)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def validate_path_component(name, value):
if ".." in value:
print("ERROR: {} contains '..' path traversal".format(name), file=sys.stderr)
sys.exit(1)
def validate_path_component(name, value):
if ".." in value:
print("ERROR: {} contains '..' path traversal".format(name), file=sys.stderr)
sys.exit(1)
if os.path.isabs(value):
print("ERROR: {} must not be an absolute path".format(name), file=sys.stderr)
sys.exit(1)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.specify/extensions/spex/scripts/spex-detach.py around lines 90 - 93, Update
validate_path_component to reject absolute path components in addition to values
containing "..", using the platform-appropriate path check before os.path.join
is used for project or feature. Preserve the existing stderr error and exit
behavior for both traversal and absolute-path violations.

Comment on lines +192 to +228
try:
r = subprocess.run(["git", "checkout", "-b", pr_branch, merge_base, "--quiet"], capture_output=True, text=True)
if r.returncode != 0:
json.dump({"error": "Failed to create PR branch"}, sys.stderr)
print(file=sys.stderr)
sys.exit(1)

p = subprocess.run(["git", "apply", "--index"], input=diff_output, capture_output=True)
if p.returncode != 0:
json.dump({"error": "Failed to apply filtered diff"}, sys.stderr)
print(file=sys.stderr)
sys.exit(1)

files_changed = len(git("diff", "--cached", "--name-only").split("\n"))

log_cmd = ["git", "log", "--format=%s", "{}..{}".format(merge_base, original_branch), "--", "."] + pathspec_excludes
r = subprocess.run(log_cmd, capture_output=True, text=True)
commit_subject = r.stdout.strip().split("\n")[0] if r.returncode == 0 and r.stdout.strip() else ""
if not commit_subject:
commit_subject = "feat: {}".format(original_branch.replace("-", " ").replace("_", " "))

subprocess.run(["git", "commit", "-m", commit_subject, "--quiet"], capture_output=True)
commit_sha = git("rev-parse", "HEAD")

subprocess.run(["git", "checkout", original_branch, "--quiet"], capture_output=True)

print(json.dumps({
"pr_branch": pr_branch,
"merge_base": merge_base,
"commit": commit_sha,
"files_changed": files_changed,
"empty": False,
}))
except Exception:
subprocess.run(["git", "checkout", original_branch, "--quiet"], capture_output=True, text=True)
git("branch", "-D", pr_branch)
raise

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Diff-apply failure leaves repo checked out on the half-built PR branch.

When git apply --index fails (line 200), the code calls sys.exit(1) directly instead of going through the except Exception cleanup block. sys.exit() raises SystemExit, which is not caught by except Exception, so the checkout back to original_branch and the pr_branch cleanup (lines 226-227) never run for this specific failure. The caller is left on a broken, half-applied pr_branch instead of their original branch.

🐛 Proposed fix
         p = subprocess.run(["git", "apply", "--index"], input=diff_output, capture_output=True)
         if p.returncode != 0:
             json.dump({"error": "Failed to apply filtered diff"}, sys.stderr)
             print(file=sys.stderr)
+            subprocess.run(["git", "checkout", original_branch, "--quiet"], capture_output=True)
+            git("branch", "-D", pr_branch)
             sys.exit(1)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try:
r = subprocess.run(["git", "checkout", "-b", pr_branch, merge_base, "--quiet"], capture_output=True, text=True)
if r.returncode != 0:
json.dump({"error": "Failed to create PR branch"}, sys.stderr)
print(file=sys.stderr)
sys.exit(1)
p = subprocess.run(["git", "apply", "--index"], input=diff_output, capture_output=True)
if p.returncode != 0:
json.dump({"error": "Failed to apply filtered diff"}, sys.stderr)
print(file=sys.stderr)
sys.exit(1)
files_changed = len(git("diff", "--cached", "--name-only").split("\n"))
log_cmd = ["git", "log", "--format=%s", "{}..{}".format(merge_base, original_branch), "--", "."] + pathspec_excludes
r = subprocess.run(log_cmd, capture_output=True, text=True)
commit_subject = r.stdout.strip().split("\n")[0] if r.returncode == 0 and r.stdout.strip() else ""
if not commit_subject:
commit_subject = "feat: {}".format(original_branch.replace("-", " ").replace("_", " "))
subprocess.run(["git", "commit", "-m", commit_subject, "--quiet"], capture_output=True)
commit_sha = git("rev-parse", "HEAD")
subprocess.run(["git", "checkout", original_branch, "--quiet"], capture_output=True)
print(json.dumps({
"pr_branch": pr_branch,
"merge_base": merge_base,
"commit": commit_sha,
"files_changed": files_changed,
"empty": False,
}))
except Exception:
subprocess.run(["git", "checkout", original_branch, "--quiet"], capture_output=True, text=True)
git("branch", "-D", pr_branch)
raise
try:
r = subprocess.run(["git", "checkout", "-b", pr_branch, merge_base, "--quiet"], capture_output=True, text=True)
if r.returncode != 0:
json.dump({"error": "Failed to create PR branch"}, sys.stderr)
print(file=sys.stderr)
sys.exit(1)
p = subprocess.run(["git", "apply", "--index"], input=diff_output, capture_output=True)
if p.returncode != 0:
json.dump({"error": "Failed to apply filtered diff"}, sys.stderr)
print(file=sys.stderr)
subprocess.run(["git", "checkout", original_branch, "--quiet"], capture_output=True)
git("branch", "-D", pr_branch)
sys.exit(1)
files_changed = len(git("diff", "--cached", "--name-only").split("\n"))
log_cmd = ["git", "log", "--format=%s", "{}..{}".format(merge_base, original_branch), "--", "."] + pathspec_excludes
r = subprocess.run(log_cmd, capture_output=True, text=True)
commit_subject = r.stdout.strip().split("\n")[0] if r.returncode == 0 and r.stdout.strip() else ""
if not commit_subject:
commit_subject = "feat: {}".format(original_branch.replace("-", " ").replace("_", " "))
subprocess.run(["git", "commit", "-m", commit_subject, "--quiet"], capture_output=True)
commit_sha = git("rev-parse", "HEAD")
subprocess.run(["git", "checkout", original_branch, "--quiet"], capture_output=True)
print(json.dumps({
"pr_branch": pr_branch,
"merge_base": merge_base,
"commit": commit_sha,
"files_changed": files_changed,
"empty": False,
}))
except Exception:
subprocess.run(["git", "checkout", original_branch, "--quiet"], capture_output=True, text=True)
git("branch", "-D", pr_branch)
raise
🧰 Tools
🪛 ast-grep (0.44.1)

[info] 217-223: use jsonify instead of json.dumps for JSON output
Context: json.dumps({
"pr_branch": pr_branch,
"merge_base": merge_base,
"commit": commit_sha,
"files_changed": files_changed,
"empty": False,
})
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)


[error] 207-207: Use of unsanitized data to create processes
Context: subprocess.run(log_cmd, capture_output=True, text=True)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(os-system-unsanitized-data)


[error] 192-192: Command coming from incoming request
Context: subprocess.run(["git", "checkout", "-b", pr_branch, merge_base, "--quiet"], capture_output=True, text=True)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(subprocess-from-request)


[error] 198-198: Command coming from incoming request
Context: subprocess.run(["git", "apply", "--index"], input=diff_output, capture_output=True)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(subprocess-from-request)


[error] 207-207: Command coming from incoming request
Context: subprocess.run(log_cmd, capture_output=True, text=True)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(subprocess-from-request)


[error] 212-212: Command coming from incoming request
Context: subprocess.run(["git", "commit", "-m", commit_subject, "--quiet"], capture_output=True)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(subprocess-from-request)


[error] 215-215: Command coming from incoming request
Context: subprocess.run(["git", "checkout", original_branch, "--quiet"], capture_output=True)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(subprocess-from-request)


[error] 225-225: Command coming from incoming request
Context: subprocess.run(["git", "checkout", original_branch, "--quiet"], capture_output=True, text=True)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(subprocess-from-request)

🪛 Ruff (0.15.20)

[error] 193-193: subprocess call: check for execution of untrusted input

(S603)


[error] 193-193: Starting a process with a partial executable path

(S607)


[error] 199-199: Starting a process with a partial executable path

(S607)


[warning] 207-207: Consider iterable unpacking instead of concatenation

Replace with iterable unpacking

(RUF005)


[error] 208-208: subprocess call: check for execution of untrusted input

(S603)


[error] 213-213: subprocess call: check for execution of untrusted input

(S603)


[error] 213-213: Starting a process with a partial executable path

(S607)


[error] 216-216: subprocess call: check for execution of untrusted input

(S603)


[error] 216-216: Starting a process with a partial executable path

(S607)


[error] 226-226: subprocess call: check for execution of untrusted input

(S603)


[error] 226-226: Starting a process with a partial executable path

(S607)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.specify/extensions/spex/scripts/spex-detach.py around lines 192 - 228,
Update the git apply failure path in the surrounding branch-creation flow so
cleanup runs before terminating, or raise an exception caught by the existing
except Exception handler instead of calling sys.exit(1) directly. Preserve the
existing error JSON and exit status while ensuring original_branch is restored
and pr_branch is deleted when git apply fails.

Comment on lines +985 to +1065
let claim_name = match crate::warm_pool::create_claim(
&self.client,
&self.config.namespace,
pool_name,
&sandbox.id,
)
.await
{
Ok(name) => name,
Err(e) => {
warn!(
sandbox_id = %sandbox.id,
error = %e,
"Failed to create warm pool claim, falling back to cold start"
);
return None;
}
};

let pod_ip = match crate::warm_pool::wait_for_claim_ready(
&self.client,
&self.config.namespace,
&claim_name,
Duration::from_secs(10),
)
.await
{
Ok(ip) => ip,
Err(e) => {
warn!(
sandbox_id = %sandbox.id,
claim = %claim_name,
error = %e,
"Warm pool claim failed, falling back to cold start"
);
return None;
}
};

let tls = self.read_activation_tls().await;

let sandbox_token = sandbox
.spec
.as_ref()
.map(|s| s.sandbox_token.clone())
.unwrap_or_default();

let request = openshell_core::proto::supervisor::v1::ActivateSandboxRequest {
sandbox_id: sandbox.id.clone(),
sandbox_name: sandbox.name.clone(),
sandbox_token,
gateway_endpoint: self.config.grpc_endpoint.clone(),
policy: Some(openshell_core::proto::sandbox::v1::SandboxPolicy::default()),
};

match crate::activation_client::activate_sandbox(&pod_ip, request, tls.as_ref()).await {
Ok(resp) if resp.success => {
info!(
sandbox_id = %sandbox.id,
pod_ip = %pod_ip,
"Warm pool activation succeeded"
);
Some(Ok(()))
}
Ok(resp) => {
warn!(
sandbox_id = %sandbox.id,
error_message = %resp.error_message,
error_code = resp.error_code,
"Warm pool activation returned failure, falling back to cold start"
);
None
}
Err(e) => {
warn!(
sandbox_id = %sandbox.id,
error = %e,
"Warm pool activation error, falling back to cold start"
);
None
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether warm_pool exposes a claim release/delete helper and whether it is invoked anywhere.
rg -nP 'fn (create_claim|delete_claim|release_claim|wait_for_claim_ready)' crates/openshell-driver-kubernetes/src/warm_pool.rs -C2
rg -nP 'delete_claim|release_claim' crates/openshell-driver-kubernetes/src -C2

Repository: rhuss/OpenShell

Length of output: 348


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect warm_pool claim lifecycle and the surrounding activation flow.
ast-grep outline crates/openshell-driver-kubernetes/src/warm_pool.rs --view expanded || true
ast-grep outline crates/openshell-driver-kubernetes/src/driver.rs --view expanded || true

echo '--- warm_pool.rs (relevant section) ---'
sed -n '1,260p' crates/openshell-driver-kubernetes/src/warm_pool.rs | cat -n

echo '--- driver.rs (relevant section) ---'
sed -n '940,1095p' crates/openshell-driver-kubernetes/src/driver.rs | cat -n

Repository: rhuss/OpenShell

Length of output: 33940


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for any cleanup or TTL/owner-reference logic that might automatically reap claims.
rg -n "ownerReferences|owner_references|ttl|garbage|garbage collection|cleanup|finalizer|finalizers|delete_claim|release_claim|claim_name|warm pool" crates/openshell-driver-kubernetes/src -C2

Repository: rhuss/OpenShell

Length of output: 25079


Delete the warm-pool claim on fallback paths.
create_claim succeeds, but the wait_for_claim_ready and activate_sandbox failure branches return None without cleanup, leaving the warm pod claimed and reducing pool capacity. Clean up the claim before falling back to cold start.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/openshell-driver-kubernetes/src/driver.rs` around lines 985 - 1065,
Delete the successfully created warm-pool claim before returning None from the
wait_for_claim_ready and activate_sandbox failure branches. Reuse the existing
Kubernetes client, namespace, and claim_name with the warm-pool claim deletion
helper, while preserving the current warning logs and cold-start fallback
behavior.

Comment on lines +36 to +37
loop {
let (stream, _) = listener.accept().await?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Accept loop terminates on transient errors.

listener.accept().await? propagates any accept error, killing the health server. For a Kubernetes readiness endpoint, a single transient failure (EMFILE, ECONNABORTED) would cause readiness probes to fail and potentially trigger pod restarts. Log and continue instead.

🔧 Proposed fix: handle accept errors gracefully
         let ready = self.ready;
         loop {
-            let (stream, _) = listener.accept().await?;
+            match listener.accept().await {
+                Ok((stream, _)) => {
+                    let io = TokioIo::new(stream);
+                    let ready = ready.clone();
+                    tokio::spawn(async move {
+                        let svc = service_fn(move |req| {
+                            let ready = ready.clone();
+                            async move { Ok::<_, Infallible>(handle_request(req, &ready)) }
+                        });
+                        if let Err(err) = hyper_util::server::conn::auto::Builder::new(
+                            hyper_util::rt::TokioExecutor::new(),
+                        )
+                        .serve_connection(io, svc)
+                        .await
+                        {
+                            debug!(error = %err, "Health check connection error");
+                        }
+                    });
+                }
+                Err(err) => {
+                    debug!(error = %err, "Health check accept error; continuing");
+                }
             }
-            let io = TokioIo::new(stream);
-            let ready = ready.clone();
-            tokio::spawn(async move {
-                let svc = service_fn(move |req| {
-                    let ready = ready.clone();
-                    async move { Ok::<_, Infallible>(handle_request(req, &ready)) }
-                });
-                if let Err(err) = hyper_util::server::conn::auto::Builder::new(
-                    hyper_util::rt::TokioExecutor::new(),
-                )
-                .serve_connection(io, svc)
-                .await
-                {
-                    debug!(error = %err, "Health check connection error");
-                }
-            });
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
loop {
let (stream, _) = listener.accept().await?;
let ready = self.ready;
loop {
match listener.accept().await {
Ok((stream, _)) => {
let io = TokioIo::new(stream);
let ready = ready.clone();
tokio::spawn(async move {
let svc = service_fn(move |req| {
let ready = ready.clone();
async move { Ok::<_, Infallible>(handle_request(req, &ready)) }
});
if let Err(err) = hyper_util::server::conn::auto::Builder::new(
hyper_util::rt::TokioExecutor::new(),
)
.serve_connection(io, svc)
.await
{
debug!(error = %err, "Health check connection error");
}
});
}
Err(err) => {
debug!(error = %err, "Health check accept error; continuing");
}
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/openshell-sandbox/src/health.rs` around lines 36 - 37, Update the
accept loop in the health server around listener.accept so transient accept
errors are logged and the loop continues instead of being propagated with ?.
Preserve normal stream handling and only terminate for errors that cannot be
recovered from if the surrounding design explicitly distinguishes them.

Comment on lines +944 to +946
let server = tonic::transport::Server::builder()
.add_service(SupervisorServer::new(service))
.serve(addr);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for TLS configuration in the activation and supervisor code
rg -n 'tls_config|TlsConfig|ServerTlsConfig|tls\(' --type rust crates/openshell-sandbox/ crates/openshell-core/

# Check the proto definition for TLS requirements
fd 'supervisor.proto' | xargs cat 2>/dev/null

# Check if there's a TLS config in the activation module
ast-grep outline crates/openshell-sandbox/src/activation.rs --items all --type function,struct,impl 2>/dev/null || true

Repository: rhuss/OpenShell

Length of output: 5146


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Relevant server setup =="
rg -n 'tonic::transport::Server::builder|SupervisorServer::new|tls_config|ServerTlsConfig|Identity|Certificate|rustls|tls\(' crates/openshell-sandbox/src crates/openshell-core/src

echo
echo "== Files in openshell-sandbox src =="
git ls-files crates/openshell-sandbox/src

echo
echo "== lib.rs around supervisor server =="
sed -n '900,980p' crates/openshell-sandbox/src/lib.rs

echo
echo "== activation module outline if present =="
fd -a 'activation.rs' crates/openshell-sandbox/src || true

Repository: rhuss/OpenShell

Length of output: 4835


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== activation.rs outline =="
ast-grep outline crates/openshell-sandbox/src/activation.rs --items all 2>/dev/null || true

echo
echo "== activation.rs contents around likely server/wrapper logic =="
wc -l crates/openshell-sandbox/src/activation.rs
sed -n '1,260p' crates/openshell-sandbox/src/activation.rs

echo
echo "== search for activation port / topology / sidecar references =="
rg -n 'activation_grpc_port|SUPERVISOR_TOPOLOGY|sidecar|mesh|proxy|TLS|tls_config|rustls|tonic::transport::Server::builder' crates/openshell-sandbox/src crates/openshell-core/src

Repository: rhuss/OpenShell

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== activation server port and topology context =="
sed -n '860,960p' crates/openshell-sandbox/src/lib.rs

echo
echo "== grpc client TLS docs and env vars =="
sed -n '120,190p' crates/openshell-core/src/grpc_client.rs

echo
echo "== TLS config docs in config.rs =="
sed -n '370,470p' crates/openshell-core/src/config.rs

echo
echo "== driver_utils TLS mount docs =="
sed -n '50,90p' crates/openshell-core/src/driver_utils.rs

echo
echo "== any server-side TLS builder usage in sandbox code =="
rg -n 'ServerTlsConfig|tls_config\(|serve_with_(incoming|tls)|accept_http1|require_http2' crates/openshell-sandbox/src crates/openshell-core/src

Repository: rhuss/OpenShell

Length of output: 12591


Add TLS to the activation gRPC listener. crates/openshell-sandbox/src/lib.rs:937-946 starts SupervisorServer with a plain tonic::transport::Server::builder(), so ActivateSandbox’s JWT still travels in cleartext unless TLS is terminated outside this code. Wire in .tls_config() here, or document the external termination layer.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/openshell-sandbox/src/lib.rs` around lines 944 - 946, Update the
activation gRPC listener construction around SupervisorServer::new and
Server::builder to configure TLS with .tls_config() before serving, using the
project’s existing certificate and private-key configuration. Ensure the
resulting serve path remains compatible with SupervisorServer and fails clearly
if TLS configuration cannot be applied.

Comment thread crates/openshell-sandbox/src/lib.rs
Comment on lines +62 to +77
let mut child = Command::new(env!("CARGO_BIN_EXE_openshell-sandbox"))
.arg("--unidentified")
.arg("--health-check")
.arg("--health-port")
.arg(health_port.to_string())
.arg("--")
.arg("/bin/true")
.env("OPENSHELL_LOG_LEVEL", "info")
.env("OPENSHELL_ACTIVATION_PORT", grpc_port.to_string())
.env_remove("RUST_LOG")
.env_remove("OPENSHELL_POLICY_RULES")
.env_remove("OPENSHELL_POLICY_DATA")
.env_remove("OPENSHELL_SANDBOX_ID")
.env_remove("OPENSHELL_ENDPOINT")
.spawn()
.expect("failed to spawn openshell-sandbox in unidentified mode");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Child process is orphaned if any assertion fails before cleanup.

std::process::Child does not kill the child on Drop. If any assert! between line 77 and line 156 panics, the sandbox process keeps running — holding the health and gRPC ports and consuming resources. In local dev this causes port conflicts on subsequent runs; in CI it wastes runner resources.

🔒️ Proposed fix: wrap child in a Drop guard
+struct ChildGuard(std::process::Child);
+impl Drop for ChildGuard {
+    fn drop(&mut self) {
+        let _ = self.0.kill();
+        let _ = self.0.wait();
+    }
+}
+
 #[tokio::test]
 async fn activated_supervisor_exercises_cold_start_bootstrap() {
     let health_port = free_port();
     let grpc_port = free_port();

-    let mut child = Command::new(env!("CARGO_BIN_EXE_openshell-sandbox"))
+    let mut child = ChildGuard(
+        Command::new(env!("CARGO_BIN_EXE_openshell-sandbox"))
         .arg("--unidentified")
         .arg("--health-check")
         .arg("--health-port")
         .arg(health_port.to_string())
         .arg("--")
         .arg("/bin/true")
         .env("OPENSHELL_LOG_LEVEL", "info")
         .env("OPENSHELL_ACTIVATION_PORT", grpc_port.to_string())
         .env_remove("RUST_LOG")
         .env_remove("OPENSHELL_POLICY_RULES")
         .env_remove("OPENSHELL_POLICY_DATA")
         .env_remove("OPENSHELL_SANDBOX_ID")
         .env_remove("OPENSHELL_ENDPOINT")
         .spawn()
-        .expect("failed to spawn openshell-sandbox in unidentified mode");
+        .expect("failed to spawn openshell-sandbox in unidentified mode"),
+    );

Then remove the manual cleanup at the end since Drop handles it:

-    let _ = child.kill();
-    let _ = child.wait();
+    // Child is automatically killed when `child` (ChildGuard) goes out of scope.

Also applies to: 156-157

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/openshell-sandbox/tests/activation_smoke.rs` around lines 62 - 77,
Wrap the spawned child in a Drop guard immediately after the Command chain in
the activation smoke test, ensuring the guard kills and waits for the process
when assertions unwind. Remove the redundant manual cleanup at the test’s end,
while preserving the existing child process usage throughout the test.

rhuss added 10 commits July 13, 2026 09:56
The supervisor's activation gRPC endpoint doesn't serve TLS in the PoC.
Skip mTLS for the ActivateSandbox call and connect via plaintext HTTP.
Production will add TLS to the supervisor's gRPC listener.

Also makes the activation client support both TLS and plaintext channels
based on whether TLS config is provided.

Validated on ROSA HCP 4.22.3: gateway detects warm pool, claims pod,
and calls ActivateSandbox successfully in ~340ms (in-cluster).

Assisted-By: 🤖 Claude Code
Signed-off-by: Roland Huß <rhuss@redhat.com>
Captures the plan for completing the end-to-end warm pool flow with
full SSH support, proxy, OPA enforcement, and entrypoint process.
Approach: extract shared bootstrap from run_sandbox() for functional
parity between cold start and warm pool paths.

Assisted-By: 🤖 Claude Code
Signed-off-by: Roland Huß <rhuss@redhat.com>
Assisted-By: 🤖 Claude Code
Signed-off-by: Roland Huß <rhuss@redhat.com>
Assisted-By: 🤖 Claude Code
Signed-off-by: Roland Huß <rhuss@redhat.com>
Specification for completing the end-to-end warm pool flow with full
SSH support. Extracts networking/proxy/OPA/entrypoint from run_sandbox()
into shared bootstrap code for functional parity between cold start and
warm pool paths. Target: SSH shell in under 3 seconds.

Assisted-By: 🤖 Claude Code
Signed-off-by: Roland Huß <rhuss@redhat.com>
Five-phase plan to extract post-identity bootstrap from run_sandbox()
into shared code. Core design: BootstrapContext struct + shared
post_identity_bootstrap() function called by both cold-start and warm
pool paths.

Assisted-By: 🤖 Claude Code
Signed-off-by: Roland Huß <rhuss@redhat.com>
26 tasks across 5 phases: define BootstrapContext, extract
post_identity_bootstrap(), wire into bootstrap_sandbox(), update
lifecycle, build/deploy/test on cluster.

Assisted-By: 🤖 Claude Code
Signed-off-by: Roland Huß <rhuss@redhat.com>
…or warm pool E2E

Refactor run_sandbox() to extract shared bootstrap logic into
BootstrapContext struct and post_identity_bootstrap() function.
This enables the warm pool activation path to run the full
networking/SSH/process stack after gRPC identity push.

- Define BootstrapContext with 18 fields for shared parameter passing
- Extract post_identity_bootstrap() (~400 lines) from run_sandbox()
- Wire bootstrap_sandbox() to build context and call shared function
- Change oneshot channel from Sender<String> to Sender<i32> for exit codes
- Update run_unidentified() to propagate exit code directly

All 94 existing tests pass unchanged.

Assisted-By: 🤖 Claude Code
- Add pool-quickstart.md: 5-minute tester recipe using image tag
  trick for warm vs cold comparison (no cluster changes needed)
- Update SMOKE-TEST.md with Milestone 2 SSH results and mTLS cert
  extraction steps
- Update plan.md and tasks.md with review-plan fixes (consolidated
  tasks, added interfaces, global constraints)
- Add REVIEW-CODE.md and review-findings.md from deep review

Assisted-By: 🤖 Claude Code
Replace the cluster admin password with a placeholder. The password
was committed in plaintext and is visible in git history on a public
fork. The cluster password should be rotated separately.

Assisted-By: 🤖 Claude Code
Signed-off-by: Roland Huß <rhuss@redhat.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/openshell-sandbox/src/lib.rs (1)

1012-1021: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Handle a dropped activation sender to avoid a stuck process.

Ok(exit_code) = activation_rx is a refutable pattern. If the spawned post_identity_bootstrap task in activation.rs panics before sending, the oneshot sender is dropped and activation_rx resolves to Err(RecvError). That disables this select branch, leaving only the indefinitely-serving server branch — the process neither exits nor completes activation and must be reaped by a liveness probe. This is FINDING-1 in the review doc; the fix is trivial.

🛠️ Handle the dropped sender explicitly
-        Ok(exit_code) = activation_rx => {
-            info!(
-                exit_code = exit_code,
-                "Activation complete, process exited"
-            );
-            return Ok(exit_code);
-        }
+        activation_result = activation_rx => {
+            match activation_result {
+                Ok(exit_code) => {
+                    info!(exit_code, "Activation complete, process exited");
+                    return Ok(exit_code);
+                }
+                Err(_) => {
+                    warn!("Activation task dropped its sender without an exit code");
+                    return Ok(1);
+                }
+            }
+        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/openshell-sandbox/src/lib.rs` around lines 1012 - 1021, Handle the Err
case from activation_rx in the select branch instead of matching only
Ok(exit_code). When the activation sender is dropped, log the activation failure
and return an error or otherwise terminate the process so it cannot remain stuck
serving indefinitely; preserve the existing successful exit_code path.
♻️ Duplicate comments (2)
crates/openshell-driver-kubernetes/src/driver.rs (2)

909-1026: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

read_activation_tls() is defined but never called — warm-pool activation is unconditionally plaintext, contradicting the PR's stated "TLS when configured" behavior.

try_warm_pool hardcodes tls: Option<TlsConfig> = None at line 1026 instead of calling self.read_activation_tls().await. Combined with the earlier-flagged fallback-to-None behavior inside read_activation_tls itself, this means even a correctly configured client_tls_secret_name can never enable mTLS for activation today — the helper is effectively dead code. If this is intentional for the current PoC milestone (as the inline comment at lines 1024-1025 states), consider removing the unused helper to avoid confusion/lint noise; otherwise wire it in.

#!/bin/bash
# Confirm read_activation_tls has no caller anywhere in the crate.
rg -n 'read_activation_tls' crates/openshell-driver-kubernetes/src -C3
🔒 Proposed fix (wire it in)
-        // PoC: supervisor serves plain gRPC, skip mTLS for activation.
-        // Production will add TLS to the supervisor's activation endpoint.
-        let tls: Option<crate::activation_client::TlsConfig> = None;
+        let tls = self.read_activation_tls().await;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/openshell-driver-kubernetes/src/driver.rs` around lines 909 - 1026,
Wire the warm-pool activation flow in try_warm_pool to call
self.read_activation_tls().await instead of unconditionally assigning None, so
configured TLS secrets enable mTLS. Remove the PoC plaintext comment and
preserve the helper’s None fallback when TLS is not configured or cannot be
loaded.

985-1067: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Warm-pool claim isn't released on fallback paths (still unresolved).

create_claim succeeds at line 985, but the wait_for_claim_ready failure branch (1013-1021) and the activate_sandbox failure/error branches (1051-1067) all return None without deleting claim_name, permanently reducing warm-pool capacity on every fallback. No delete_claim/release_claim helper currently exists in warm_pool.rs per prior investigation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/openshell-driver-kubernetes/src/driver.rs` around lines 985 - 1067,
Release the successfully created warm-pool claim before every cold-start
fallback after claim creation. In the wait_for_claim_ready error branch and both
activate_sandbox failure branches, delete claim_name through the Kubernetes
client using the appropriate warm_pool cleanup logic, log cleanup failures
without replacing the original fallback, and preserve the existing None returns.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/openshell-driver-kubernetes/src/activation_client.rs`:
- Around line 42-49: Update the TLS configuration in the activation client’s
endpoint-building flow to set an explicit domain name covered by the supervisor
certificate via ClientTlsConfig::domain_name, rather than relying on the raw
pod_ip URI host. Reuse the existing configured supervisor DNS name or localhost
SAN value, while preserving the CA certificate, client identity, and endpoint
error mapping.
- Around line 36-56: Update both URI constructions in the TLS and plaintext
branches of the endpoint setup to bracket IPv6 pod IP literals before appending
ACTIVATION_PORT, while preserving valid endpoint formatting for IPv4 addresses
and hostnames. Apply the same authority formatting consistently to the `https`
and `http` paths.

In `@crates/openshell-sandbox/src/activation.rs`:
- Around line 179-196: Update run_unidentified’s activation_rx result handling
to add an Err(_) arm for a dropped activation sender, returning a non-zero exit
code when the spawned bootstrap task exits without sending. Preserve the
existing Ok(exit_code) behavior.

---

Outside diff comments:
In `@crates/openshell-sandbox/src/lib.rs`:
- Around line 1012-1021: Handle the Err case from activation_rx in the select
branch instead of matching only Ok(exit_code). When the activation sender is
dropped, log the activation failure and return an error or otherwise terminate
the process so it cannot remain stuck serving indefinitely; preserve the
existing successful exit_code path.

---

Duplicate comments:
In `@crates/openshell-driver-kubernetes/src/driver.rs`:
- Around line 909-1026: Wire the warm-pool activation flow in try_warm_pool to
call self.read_activation_tls().await instead of unconditionally assigning None,
so configured TLS secrets enable mTLS. Remove the PoC plaintext comment and
preserve the helper’s None fallback when TLS is not configured or cannot be
loaded.
- Around line 985-1067: Release the successfully created warm-pool claim before
every cold-start fallback after claim creation. In the wait_for_claim_ready
error branch and both activate_sandbox failure branches, delete claim_name
through the Kubernetes client using the appropriate warm_pool cleanup logic, log
cleanup failures without replacing the original fallback, and preserve the
existing None returns.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 136edaa2-8bea-4e0d-9135-bfe32a52eaeb

📥 Commits

Reviewing files that changed from the base of the PR and between 9057afb and f20f49f.

📒 Files selected for processing (21)
  • .gitignore
  • .specify/.pr-triage-state.json
  • .specify/feature.json
  • .specify/integrations/opencode.manifest.json
  • .specify/integrations/speckit.manifest.json
  • AGENTS.md
  • brainstorm/00-overview.md
  • brainstorm/08-warm-pool-e2e-ssh.md
  • brainstorm/idea-inbox.md
  • crates/openshell-driver-kubernetes/src/activation_client.rs
  • crates/openshell-driver-kubernetes/src/driver.rs
  • crates/openshell-sandbox/src/activation.rs
  • crates/openshell-sandbox/src/lib.rs
  • experiments/SMOKE-TEST.md
  • experiments/pool-quickstart.md
  • specs/003-warm-pool-e2e-ssh/REVIEW-CODE.md
  • specs/003-warm-pool-e2e-ssh/checklists/requirements.md
  • specs/003-warm-pool-e2e-ssh/plan.md
  • specs/003-warm-pool-e2e-ssh/review-findings.md
  • specs/003-warm-pool-e2e-ssh/spec.md
  • specs/003-warm-pool-e2e-ssh/tasks.md
🚧 Files skipped from review as they are similar to previous changes (4)
  • .specify/integrations/speckit.manifest.json
  • .specify/feature.json
  • .specify/integrations/opencode.manifest.json
  • experiments/SMOKE-TEST.md

Comment on lines +36 to +56
let (endpoint_uri, mut endpoint) = if let Some(tls) = tls {
let uri = format!("https://{pod_ip}:{ACTIVATION_PORT}");
let mut ep = Channel::from_shared(uri.clone())
.map_err(|e| ActivationError::ConnectionFailed(e.to_string()))?
.connect_timeout(Duration::from_secs(2));

let tls_config = ClientTlsConfig::new()
.ca_certificate(Certificate::from_pem(&tls.ca_cert))
.identity(Identity::from_pem(&tls.client_cert, &tls.client_key));
ep = ep
.tls_config(tls_config)
.map_err(|e| ActivationError::ConnectionFailed(e.to_string()))?;

(uri, ep)
} else {
let uri = format!("http://{pod_ip}:{ACTIVATION_PORT}");
let ep = Channel::from_shared(uri.clone())
.map_err(|e| ActivationError::ConnectionFailed(e.to_string()))?
.connect_timeout(Duration::from_secs(2));
(uri, ep)
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

IPv6 pod IPs still produce an invalid endpoint authority (now in both branches).

Both the TLS (https://{pod_ip}:{ACTIVATION_PORT}, line 37) and plaintext (http://{pod_ip}:{ACTIVATION_PORT}, line 51) URIs format pod_ip unbracketed. An IPv6 pod IP (e.g. fd00::1) yields an invalid authority like https://fd00::1:9090 — IPv6 literals must be bracketed (https://[fd00::1]:9090). This was previously flagged on the single-branch version and now applies to both new branches.

🐛 Proposed fix
+    let host = |ip: &str| {
+        if ip.contains(':') && !ip.starts_with('[') {
+            format!("[{ip}]")
+        } else {
+            ip.to_string()
+        }
+    };
     let (endpoint_uri, mut endpoint) = if let Some(tls) = tls {
-        let uri = format!("https://{pod_ip}:{ACTIVATION_PORT}");
+        let uri = format!("https://{}:{ACTIVATION_PORT}", host(pod_ip));
         ...
     } else {
-        let uri = format!("http://{pod_ip}:{ACTIVATION_PORT}");
+        let uri = format!("http://{}:{ACTIVATION_PORT}", host(pod_ip));
         ...
     };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/openshell-driver-kubernetes/src/activation_client.rs` around lines 36
- 56, Update both URI constructions in the TLS and plaintext branches of the
endpoint setup to bracket IPv6 pod IP literals before appending ACTIVATION_PORT,
while preserving valid endpoint formatting for IPv4 addresses and hostnames.
Apply the same authority formatting consistently to the `https` and `http`
paths.

Comment on lines +42 to +49
let tls_config = ClientTlsConfig::new()
.ca_certificate(Certificate::from_pem(&tls.ca_cert))
.identity(Identity::from_pem(&tls.client_cert, &tls.client_key));
ep = ep
.tls_config(tls_config)
.map_err(|e| ActivationError::ConnectionFailed(e.to_string()))?;

(uri, ep)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

TLS hostname verification still not pinned to a SAN the supervisor cert actually covers.

The TLS branch builds ClientTlsConfig with CA + client identity but never calls .domain_name(...), so tonic will verify the server cert against the raw pod_ip used in the URI. Per the earlier review, the default supervisor certs only cover DNS SANs plus 127.0.0.1, not arbitrary pod IPs, unless serverIpAddresses is configured — this will fail TLS handshakes in the common case.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/openshell-driver-kubernetes/src/activation_client.rs` around lines 42
- 49, Update the TLS configuration in the activation client’s endpoint-building
flow to set an explicit domain name covered by the supervisor certificate via
ClientTlsConfig::domain_name, rather than relying on the raw pod_ip URI host.
Reuse the existing configured supervisor DNS name or localhost SAN value, while
preserving the CA certificate, client identity, and endpoint error mapping.

Comment on lines +179 to +196
let sender = self.activation_tx.lock().unwrap().take();
if let Some(sender) = sender {
let sandbox_id = req.sandbox_id.clone();
tokio::spawn(async move {
let exit_code = match crate::post_identity_bootstrap(bootstrap_ctx).await {
Ok(code) => code,
Err(e) => {
tracing::error!(
sandbox_id = %sandbox_id,
error = %e,
"post_identity_bootstrap failed"
);
1
}
};
let _ = sender.send(exit_code);
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== activation.rs around cited lines ==\n'
sed -n '150,230p' crates/openshell-sandbox/src/activation.rs

printf '\n== search for post_identity_bootstrap ==\n'
rg -n "post_identity_bootstrap|activation_rx|activation_tx|run_unidentified" crates/openshell-sandbox/src -n

printf '\n== lib.rs around run_unidentified select ==\n'
sed -n '960,1060p' crates/openshell-sandbox/src/lib.rs

Repository: rhuss/OpenShell

Length of output: 7775


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== activation.rs around cited lines ==\n'
sed -n '150,230p' crates/openshell-sandbox/src/activation.rs

printf '\n== search for post_identity_bootstrap ==\n'
rg -n "post_identity_bootstrap|activation_rx|activation_tx|run_unidentified" crates/openshell-sandbox/src

printf '\n== lib.rs around run_unidentified select ==\n'
sed -n '960,1060p' crates/openshell-sandbox/src/lib.rs

Repository: rhuss/OpenShell

Length of output: 7775


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== post_identity_bootstrap implementation ==\n'
sed -n '330,430p' crates/openshell-sandbox/src/lib.rs

printf '\n== any panic handling / catch_unwind around bootstrap or activation ==\n'
rg -n "catch_unwind|panic|abort|spawn\(" crates/openshell-sandbox/src/lib.rs crates/openshell-sandbox/src/activation.rs crates/openshell-sandbox/src/main.rs

Repository: rhuss/OpenShell

Length of output: 5399


Handle a dropped activation sender in run_unidentified
crates/openshell-sandbox/src/lib.rs:1012 — if the spawned bootstrap task panics or exits before sending, activation_rx resolves to Err(_). The current Ok(exit_code) = activation_rx arm skips that case and can leave the supervisor waiting indefinitely. Add an Err(_) arm and return a non-zero exit code.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/openshell-sandbox/src/activation.rs` around lines 179 - 196, Update
run_unidentified’s activation_rx result handling to add an Err(_) arm for a
dropped activation sender, returning a non-zero exit code when the spawned
bootstrap task exits without sending. Preserve the existing Ok(exit_code)
behavior.

Bring over the warm pool feasibility RFC, gRPC-push RFC, and brainstorm
documents from the 6111-warm-pool-feasibility branch so the PoC branch
is self-contained for reviewers.

Signed-off-by: Roland Huß <rhuss@redhat.com>
rhuss added a commit that referenced this pull request Jul 15, 2026
- Remove dead boolCount function that would fail golangci-lint (#1)
- Emit EventAdded for the first watch event instead of EventModified,
  matching k8s watch semantics (#7)
- Add mutex locking to all mock server methods that access the shared
  sandboxes map, fixing latent race conditions (#12)
- Skip HealthCheck integration test that calls an unimplemented stub (#13)
- Scope doc.go examples: mark sections for sub-clients not yet available
  in this PR with "available in a future release" (#4)
- Document Config.Timeout/RetryPolicy/Logger and WatchOptions fields
  as reserved for future use (#2, #6)

Signed-off-by: Roland Huß <rhuss@redhat.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant