|
| 1 | +#!/usr/bin/env bash |
| 2 | + |
| 3 | +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. |
| 4 | +# SPDX-License-Identifier: Apache-2.0 |
| 5 | + |
| 6 | +# Regression smoke for the mechanistic policy mapper. |
| 7 | +# |
| 8 | +# Triggers an L4 CONNECT deny from inside a sandbox, waits for the denial |
| 9 | +# aggregator to flush, and asserts that a pending mechanistic chunk appears |
| 10 | +# under `openshell rule get --status pending`. |
| 11 | +# |
| 12 | +# This is deliberately L4-only. L7 denials (method/path 403s) are the agent |
| 13 | +# loop's job; the mechanistic mapper only covers L4 CONNECT denials. See #1333. |
| 14 | +# |
| 15 | +# Prereqs: a running gateway with agent_policy_proposals_enabled=true. |
| 16 | + |
| 17 | +set -euo pipefail |
| 18 | + |
| 19 | +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
| 20 | +REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" |
| 21 | + |
| 22 | +if [[ -z "${OPENSHELL_BIN:-}" ]]; then |
| 23 | + if [[ -x "${REPO_ROOT}/target/debug/openshell" ]]; then |
| 24 | + OPENSHELL_BIN="${REPO_ROOT}/target/debug/openshell" |
| 25 | + else |
| 26 | + OPENSHELL_BIN="openshell" |
| 27 | + fi |
| 28 | +fi |
| 29 | + |
| 30 | +RUN_ID="${RUN_ID:-$(date +%Y%m%d-%H%M%S)}" |
| 31 | +SANDBOX="${SANDBOX:-mechanistic-smoke-${RUN_ID}}" |
| 32 | +KEEP_SANDBOX="${KEEP_SANDBOX:-0}" |
| 33 | +# Allow override so CI can set a shorter interval via OPENSHELL_DENIAL_FLUSH_INTERVAL_SECS. |
| 34 | +FLUSH_WAIT="${FLUSH_WAIT:-15}" |
| 35 | + |
| 36 | +BOLD='\033[1m' |
| 37 | +CYAN='\033[36m' |
| 38 | +GREEN='\033[32m' |
| 39 | +RED='\033[31m' |
| 40 | +RESET='\033[0m' |
| 41 | + |
| 42 | +step() { printf "\n${BOLD}${CYAN}==> %s${RESET}\n\n" "$1"; } |
| 43 | +ok() { printf " ${GREEN}✓${RESET} %s\n" "$*"; } |
| 44 | +fail() { printf "\n${RED}FAIL:${RESET} %s\n" "$*" >&2; exit 1; } |
| 45 | + |
| 46 | +TMP_DIR="" |
| 47 | + |
| 48 | +cleanup() { |
| 49 | + if [[ "$KEEP_SANDBOX" != "1" ]]; then |
| 50 | + "$OPENSHELL_BIN" sandbox delete "$SANDBOX" >/dev/null 2>&1 || true |
| 51 | + fi |
| 52 | + [[ -z "$TMP_DIR" ]] || rm -rf "$TMP_DIR" |
| 53 | +} |
| 54 | +trap cleanup EXIT |
| 55 | + |
| 56 | +preflight() { |
| 57 | + step "Preflight" |
| 58 | + local raw_settings |
| 59 | + if ! raw_settings="$("$OPENSHELL_BIN" settings get --global --json 2>&1)"; then |
| 60 | + fail "cannot reach gateway: ${raw_settings}" |
| 61 | + fi |
| 62 | + local enabled |
| 63 | + enabled="$(printf '%s' "$raw_settings" \ |
| 64 | + | jq -r '.settings.agent_policy_proposals_enabled // "<unset>"')" |
| 65 | + [[ "$enabled" == "true" ]] \ |
| 66 | + || fail "set agent_policy_proposals_enabled=true first: |
| 67 | + $OPENSHELL_BIN settings set --global --key agent_policy_proposals_enabled --value true --yes" |
| 68 | + ok "agent_policy_proposals_enabled=true" |
| 69 | +} |
| 70 | + |
| 71 | +create_sandbox() { |
| 72 | + step "Creating sandbox '${SANDBOX}' (no network policy)" |
| 73 | + TMP_DIR="$(mktemp -d)" |
| 74 | + SSH_CONFIG="${TMP_DIR}/ssh_config" |
| 75 | + |
| 76 | + "$OPENSHELL_BIN" sandbox delete "$SANDBOX" >/dev/null 2>&1 || true |
| 77 | + "$OPENSHELL_BIN" sandbox create \ |
| 78 | + --name "$SANDBOX" \ |
| 79 | + --no-auto-providers \ |
| 80 | + --no-tty \ |
| 81 | + --keep \ |
| 82 | + -- bash -lc "echo sandbox ready" \ |
| 83 | + | sed 's/^/ /' |
| 84 | + |
| 85 | + "$OPENSHELL_BIN" sandbox ssh-config "$SANDBOX" > "$SSH_CONFIG" |
| 86 | + SSH_HOST="$(awk '/^Host / { print $2; exit }' "$SSH_CONFIG")" |
| 87 | + [[ -n "$SSH_HOST" ]] || fail "could not parse SSH host" |
| 88 | + |
| 89 | + for _i in $(seq 1 30); do |
| 90 | + ssh -F "$SSH_CONFIG" "$SSH_HOST" true >/dev/null 2>&1 && { ok "SSH up"; return; } |
| 91 | + sleep 2 |
| 92 | + done |
| 93 | + fail "SSH timed out" |
| 94 | +} |
| 95 | + |
| 96 | +trigger_l4_deny() { |
| 97 | + step "Triggering L4 CONNECT deny from inside sandbox" |
| 98 | + # blocked.invalid is guaranteed unroutable and not in any policy. |
| 99 | + ssh -F "$SSH_CONFIG" "$SSH_HOST" \ |
| 100 | + "curl -sf --max-time 5 https://blocked.invalid/ || true" >/dev/null 2>&1 || true |
| 101 | + ok "curl attempted (deny expected)" |
| 102 | +} |
| 103 | + |
| 104 | +assert_pending_chunk() { |
| 105 | + step "Waiting ${FLUSH_WAIT}s then checking for pending chunk" |
| 106 | + sleep "$FLUSH_WAIT" |
| 107 | + local output |
| 108 | + output="$("$OPENSHELL_BIN" rule get "$SANDBOX" --status pending 2>&1)" |
| 109 | + printf '%s\n' "$output" | sed 's/^/ /' |
| 110 | + printf '%s\n' "$output" | grep -qi "blocked.invalid" \ |
| 111 | + || fail "no pending chunk for blocked.invalid" |
| 112 | + ok "pending mechanistic chunk present for blocked.invalid" |
| 113 | +} |
| 114 | + |
| 115 | +main() { |
| 116 | + command -v jq >/dev/null || fail "jq is required" |
| 117 | + preflight |
| 118 | + create_sandbox |
| 119 | + trigger_l4_deny |
| 120 | + assert_pending_chunk |
| 121 | + step "Smoke pass" |
| 122 | +} |
| 123 | + |
| 124 | +main "$@" |
0 commit comments