Port the launcher to Go on the podman API bindings
This commit is contained in:
@@ -1 +1,3 @@
|
||||
dist/
|
||||
/agent-sandbox
|
||||
/agent-sandbox-*
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
version: "2"
|
||||
run:
|
||||
build-tags:
|
||||
- remote
|
||||
- exclude_graphdriver_btrfs
|
||||
- containers_image_openpgp
|
||||
+15
-4
@@ -21,11 +21,22 @@ repos:
|
||||
hooks:
|
||||
- id: shellcheck
|
||||
|
||||
- repo: https://github.com/dnephin/pre-commit-golang
|
||||
rev: v0.5.1
|
||||
hooks:
|
||||
- id: go-fmt
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: unit-tests
|
||||
name: unit tests
|
||||
entry: tests/test.sh
|
||||
- id: go-vet
|
||||
name: go vet
|
||||
entry: make vet
|
||||
language: system
|
||||
pass_filenames: false
|
||||
files: ^(agent-sandbox|allowed-hosts|tests/)
|
||||
types: [go]
|
||||
- id: go-unit-tests
|
||||
name: go unit tests
|
||||
entry: make test
|
||||
language: system
|
||||
pass_filenames: false
|
||||
files: (\.go|allowed-hosts)$
|
||||
|
||||
@@ -2,6 +2,21 @@ when:
|
||||
- event: pull_request
|
||||
|
||||
steps:
|
||||
- name: agent-sandbox-bin
|
||||
image: "golang:1.25"
|
||||
commands:
|
||||
- make bin
|
||||
backend_options:
|
||||
kubernetes:
|
||||
serviceAccountName: default
|
||||
resources:
|
||||
requests:
|
||||
memory: 512Mi
|
||||
cpu: 1
|
||||
limits:
|
||||
memory: 2Gi
|
||||
cpu: 2
|
||||
|
||||
- name: agent-sandbox
|
||||
image: "artifactapi.k8s.syd1.au.unkin.net/docker-internal/plugin-docker-buildx:latest"
|
||||
settings:
|
||||
|
||||
@@ -3,9 +3,9 @@ when:
|
||||
|
||||
steps:
|
||||
- name: lint
|
||||
image: "git.unkin.net/unkin/almalinux9-gobuilder:20260606"
|
||||
image: "golangci/golangci-lint:latest"
|
||||
commands:
|
||||
- make lint
|
||||
- golangci-lint run ./...
|
||||
backend_options:
|
||||
kubernetes:
|
||||
serviceAccountName: default
|
||||
|
||||
@@ -4,9 +4,9 @@ when:
|
||||
|
||||
steps:
|
||||
- name: test
|
||||
image: "git.unkin.net/unkin/almalinux9-base:20260606"
|
||||
image: "golang:1.25"
|
||||
commands:
|
||||
- tests/test.sh
|
||||
- make test
|
||||
backend_options:
|
||||
kubernetes:
|
||||
serviceAccountName: default
|
||||
@@ -61,12 +61,29 @@ steps:
|
||||
memory: 4Gi
|
||||
cpu: 2
|
||||
|
||||
# The packaged launcher pins the images pushed above.
|
||||
# The binary pins the images pushed above via main.version.
|
||||
- name: build
|
||||
image: "git.unkin.net/unkin/almalinux9-gobuilder:20260606"
|
||||
commands:
|
||||
- "make bin VERSION=${CI_COMMIT_TAG}"
|
||||
- cp dist/agent-sandbox agent-sandbox-linux-amd64
|
||||
depends_on: [test]
|
||||
backend_options:
|
||||
kubernetes:
|
||||
serviceAccountName: default
|
||||
resources:
|
||||
requests:
|
||||
memory: 512Mi
|
||||
cpu: 1
|
||||
limits:
|
||||
memory: 2Gi
|
||||
cpu: 2
|
||||
|
||||
- name: package
|
||||
image: "git.unkin.net/unkin/almalinux9-rpmbuilder:latest"
|
||||
commands:
|
||||
- "./scripts/build-rpm.sh ${CI_COMMIT_TAG}"
|
||||
depends_on: [agent-sandbox, agent-sandbox-router]
|
||||
depends_on: [build, agent-sandbox, agent-sandbox-router]
|
||||
backend_options:
|
||||
kubernetes:
|
||||
serviceAccountName: default
|
||||
@@ -105,3 +122,42 @@ steps:
|
||||
limits:
|
||||
memory: 2Gi
|
||||
cpu: 2
|
||||
|
||||
- name: release
|
||||
image: "git.unkin.net/unkin/almalinux9-base:20260606"
|
||||
environment:
|
||||
RELEASER_TOKEN:
|
||||
from_secret: RELEASER_TOKEN
|
||||
commands:
|
||||
- |
|
||||
curl --output /usr/local/bin/tea https://artifactapi.k8s.syd1.au.unkin.net/api/v1/remote/gitea-dl/tea/0.12.0/tea-0.12.0-linux-amd64 && chmod +x /usr/local/bin/tea
|
||||
tea logins add --name gitea --url https://git.unkin.net --token "$${RELEASER_TOKEN}" --no-version-check
|
||||
CUR_SHA=$$(git rev-list -n1 "${CI_COMMIT_TAG}")
|
||||
PREV_TAG=""
|
||||
for t in $$(git tag --sort=-v:refname); do
|
||||
[ "$$t" = "${CI_COMMIT_TAG}" ] && continue
|
||||
[ "$$(git rev-list -n1 "$$t")" = "$$CUR_SHA" ] && continue
|
||||
if git merge-base --is-ancestor "$$t" "${CI_COMMIT_TAG}" 2>/dev/null; then
|
||||
PREV_TAG="$$t"; break
|
||||
fi
|
||||
done
|
||||
if [ -n "$$PREV_TAG" ]; then
|
||||
NOTES=$$(git log "$${PREV_TAG}..${CI_COMMIT_TAG}" --pretty=format:"- %s")
|
||||
else
|
||||
NOTES=$$(git log --pretty=format:"- %s")
|
||||
fi
|
||||
tea releases create --tag "${CI_COMMIT_TAG}" --title "${CI_COMMIT_TAG}" --note "$${NOTES}" --login gitea --repo "${CI_REPO}"
|
||||
ASSETS="agent-sandbox-linux-amd64 $$(ls dist/*.rpm)"
|
||||
sha256sum $$ASSETS > sha256sums.txt
|
||||
tea releases assets create "${CI_COMMIT_TAG}" $$ASSETS sha256sums.txt --login gitea --repo "${CI_REPO}"
|
||||
depends_on: [upload-rpm]
|
||||
backend_options:
|
||||
kubernetes:
|
||||
serviceAccountName: default
|
||||
resources:
|
||||
requests:
|
||||
memory: 128Mi
|
||||
cpu: 100m
|
||||
limits:
|
||||
memory: 512Mi
|
||||
cpu: 500m
|
||||
|
||||
@@ -3,9 +3,9 @@ when:
|
||||
|
||||
steps:
|
||||
- name: test
|
||||
image: "git.unkin.net/unkin/almalinux9-base:20260606"
|
||||
image: "golang:1.25"
|
||||
commands:
|
||||
- tests/test.sh
|
||||
- make test
|
||||
backend_options:
|
||||
kubernetes:
|
||||
serviceAccountName: default
|
||||
|
||||
@@ -1,32 +1,59 @@
|
||||
BINARY := agent-sandbox
|
||||
DIST := dist
|
||||
IMAGE := localhost/agent-sandbox:dev
|
||||
ROUTER_IMAGE := localhost/agent-sandbox-router:dev
|
||||
VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo dev)
|
||||
SHELL_FILES := agent-sandbox router/start.sh router/forwarder tests/test.sh scripts/build-rpm.sh
|
||||
# dev runs the local :dev images; a release tag pins the published images.
|
||||
VERSION ?= dev
|
||||
GOFLAGS := -ldflags="-s -w -X main.version=$(VERSION)"
|
||||
# podman-remote's build tags: the API bindings without cgo storage drivers.
|
||||
TAGS := remote,exclude_graphdriver_btrfs,containers_image_openpgp
|
||||
OS ?= $(shell go env GOOS)
|
||||
ARCH ?= $(shell go env GOARCH)
|
||||
|
||||
.PHONY: all build test lint pre-commit rpm clean patch minor major _tag
|
||||
.PHONY: all build bin images test vet lint fmt pre-commit clean completions rpm rpm-package patch minor major _tag
|
||||
|
||||
all: build
|
||||
|
||||
# Local images; the in-repo launcher (VERSION=dev) runs these by default.
|
||||
build:
|
||||
build: bin images
|
||||
|
||||
bin:
|
||||
CGO_ENABLED=0 GOOS=$(OS) GOARCH=$(ARCH) go build -tags $(TAGS) $(GOFLAGS) -o $(DIST)/$(BINARY) .
|
||||
|
||||
images:
|
||||
podman build --format docker -t $(IMAGE) .
|
||||
podman build --format docker -t $(ROUTER_IMAGE) router
|
||||
|
||||
test:
|
||||
tests/test.sh
|
||||
go test -tags $(TAGS) -v -race ./...
|
||||
|
||||
vet:
|
||||
go vet -tags $(TAGS) ./...
|
||||
|
||||
lint:
|
||||
uvx --from shellcheck-py shellcheck $(SHELL_FILES)
|
||||
uvx --from hadolint-py hadolint Containerfile router/Containerfile
|
||||
golangci-lint run ./...
|
||||
|
||||
fmt:
|
||||
gofmt -w .
|
||||
|
||||
pre-commit:
|
||||
@uvx pre-commit run --all-files
|
||||
|
||||
rpm:
|
||||
./scripts/build-rpm.sh $(VERSION)
|
||||
|
||||
clean:
|
||||
rm -rf dist
|
||||
rm -rf $(DIST)
|
||||
|
||||
# Generate bash/zsh/fish completions into dist/completions.
|
||||
completions: bin
|
||||
@mkdir -p $(DIST)/completions
|
||||
$(DIST)/$(BINARY) completion bash > $(DIST)/completions/$(BINARY).bash
|
||||
$(DIST)/$(BINARY) completion zsh > $(DIST)/completions/_$(BINARY)
|
||||
$(DIST)/$(BINARY) completion fish > $(DIST)/completions/$(BINARY).fish
|
||||
|
||||
# Build the binary then package it (with completions) into an RPM via nfpm.
|
||||
rpm: bin rpm-package
|
||||
|
||||
# Package an already-built binary into an RPM (used by CI after the build step).
|
||||
rpm-package:
|
||||
./scripts/build-rpm.sh $(VERSION)
|
||||
|
||||
# Bump helpers — reads the latest semver tag and creates the next one.
|
||||
# If no tag exists yet, starts from v0.0.0.
|
||||
|
||||
@@ -77,13 +77,18 @@ pod on that network that holds the sandbox's netns, DNS and hosts file.
|
||||
|
||||
## Development
|
||||
|
||||
`agent-sandbox` is a Go CLI built on the podman API bindings. Each session
|
||||
starts its own `podman system service` on a private socket under
|
||||
`$XDG_RUNTIME_DIR` (mode 0700, never mounted into a container) and stops it on
|
||||
exit.
|
||||
|
||||
```sh
|
||||
make build # local images (localhost/agent-sandbox{,-router}:dev)
|
||||
make test lint pre-commit
|
||||
make build # dist/agent-sandbox + local images (localhost/agent-sandbox{,-router}:dev)
|
||||
make test vet lint pre-commit
|
||||
make rpm # dist/agent-sandbox-*.rpm
|
||||
make patch|minor|major
|
||||
```
|
||||
|
||||
The in-repo launcher runs the local `:dev` images. The RPM's launcher pins the
|
||||
images published by the release pipeline for its tag:
|
||||
A `dev` build runs the local `:dev` images. Release builds pin the images
|
||||
published by the release pipeline for their tag:
|
||||
`artifactapi.k8s.syd1.au.unkin.net/docker-internal/agent-sandbox{,-router}`.
|
||||
|
||||
-241
@@ -1,241 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Run Claude Code in a rootless podman sandbox whose only way out is a
|
||||
# per-session router that resolves and forwards allowlisted hosts only.
|
||||
# Usage: agent-sandbox [--exec CMD...] [claude args...]
|
||||
set -euo pipefail
|
||||
|
||||
VERSION=dev
|
||||
REGISTRY=artifactapi.k8s.syd1.au.unkin.net/docker-internal
|
||||
if [[ $VERSION == dev ]]; then
|
||||
IMAGE=${AGENT_SANDBOX_IMAGE:-localhost/agent-sandbox:dev}
|
||||
ROUTER_IMAGE=${AGENT_SANDBOX_ROUTER_IMAGE:-localhost/agent-sandbox-router:dev}
|
||||
else
|
||||
IMAGE=${AGENT_SANDBOX_IMAGE:-$REGISTRY/agent-sandbox:$VERSION}
|
||||
ROUTER_IMAGE=${AGENT_SANDBOX_ROUTER_IMAGE:-$REGISTRY/agent-sandbox-router:$VERSION}
|
||||
fi
|
||||
CONFIG_DIR=${XDG_CONFIG_HOME:-$HOME/.config}/agent-sandbox
|
||||
ALLOWLIST=${AGENT_SANDBOX_ALLOWLIST:-$CONFIG_DIR/allowed-hosts}
|
||||
HOME_VOLUME=${AGENT_SANDBOX_HOME_VOLUME:-agent-sandbox-home}
|
||||
EGRESS_NET=agent-sandbox-egress
|
||||
# Every .domain entry resolves here; exact hosts get 127.1.x.y of their own.
|
||||
WILDCARD_IP=127.0.0.2
|
||||
PORT_BASE=10000
|
||||
|
||||
die() {
|
||||
echo "agent-sandbox: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Print "host port" per allowlist entry, sorted and unique; port is empty for
|
||||
# plain web (80/443) access.
|
||||
allowlist_entries() {
|
||||
local line extra host port n=0
|
||||
local label='[a-z0-9]([a-z0-9-]*[a-z0-9])?'
|
||||
while IFS= read -r line || [[ -n $line ]]; do
|
||||
n=$((n + 1))
|
||||
line=${line%%#*}
|
||||
read -r line extra <<<"$line" || true
|
||||
[[ -z $line ]] && continue
|
||||
[[ -n $extra ]] && die "$1:$n: one entry per line"
|
||||
line=${line,,}
|
||||
host=${line%:*}
|
||||
port=
|
||||
[[ $line == *:* ]] && port=${line##*:}
|
||||
[[ $host =~ ^\.?$label(\.$label)*$ ]] || die "$1:$n: invalid host '$host'"
|
||||
if [[ -n $port ]]; then
|
||||
if ! [[ $port =~ ^[0-9]+$ ]] || ((port < 1 || port > 65535)); then
|
||||
die "$1:$n: invalid port '$port'"
|
||||
fi
|
||||
[[ $host == .* ]] && die "$1:$n: ports need an exact host, not '$host'"
|
||||
[[ $port == 80 || $port == 443 ]] && port=
|
||||
fi
|
||||
echo "$host $port"
|
||||
done <"$1" | LC_ALL=C sort -u
|
||||
}
|
||||
|
||||
# Write the router config (dnsmasq.conf, nginx.conf) for allowlist $1 into dir
|
||||
# $2, listening on router IP $3. Prints the sandbox's loopback forwards as
|
||||
# "bind:port:router:router_port" words.
|
||||
router_config() {
|
||||
local allowlist=$1 dir=$2 rip=$3 host port i=0 j=0
|
||||
local -A ip=()
|
||||
local hosts tcp="" forwards="0.0.0.0:443:$rip:443 0.0.0.0:80:$rip:80"
|
||||
|
||||
local entries
|
||||
entries=$(allowlist_entries "$allowlist") || return
|
||||
hosts=$(awk '{print $1}' <<<"$entries" | uniq)
|
||||
for host in $hosts; do
|
||||
[[ $host == .* ]] && continue
|
||||
ip[$host]=127.1.$((i / 250)).$((i % 250 + 1))
|
||||
i=$((i + 1))
|
||||
done
|
||||
while read -r host port; do
|
||||
[[ -z $port ]] && continue
|
||||
tcp+=" server { listen $rip:$((PORT_BASE + j)); set \$up $host:$port; proxy_pass \$up; }"$'\n'
|
||||
forwards+=" ${ip[$host]}:$port:$rip:$((PORT_BASE + j))"
|
||||
j=$((j + 1))
|
||||
done <<<"$entries"
|
||||
|
||||
{
|
||||
printf '%s\n' no-resolv no-hosts bind-interfaces "listen-address=$rip" "address=/#/"
|
||||
for host in $hosts; do
|
||||
if [[ $host == .* ]]; then
|
||||
echo "address=/${host#.}/$WILDCARD_IP"
|
||||
else
|
||||
echo "host-record=$host,${ip[$host]}"
|
||||
fi
|
||||
done
|
||||
} >"$dir/dnsmasq.conf"
|
||||
|
||||
# nginx "hostnames" rejects an exact key that a .domain key already covers.
|
||||
local map=""
|
||||
for host in $hosts; do
|
||||
[[ $host != .* && " ${hosts//$'\n'/ } " == *" .$host "* ]] && continue
|
||||
map+=" $host 1;"$'\n'
|
||||
done
|
||||
cat >"$dir/nginx.conf" <<EOF
|
||||
load_module /usr/lib/nginx/modules/ngx_stream_module.so;
|
||||
worker_processes 1;
|
||||
pid /tmp/nginx.pid;
|
||||
error_log stderr warn;
|
||||
events { worker_connections 1024; }
|
||||
|
||||
stream {
|
||||
resolver @RESOLVER@ ipv6=off valid=30s;
|
||||
map \$ssl_preread_server_name \$tls_allowed {
|
||||
hostnames;
|
||||
default 0;
|
||||
$map
|
||||
}
|
||||
map \$tls_allowed \$tls_upstream {
|
||||
1 \$ssl_preread_server_name:443;
|
||||
default 127.0.0.1:9;
|
||||
}
|
||||
server {
|
||||
listen $rip:443;
|
||||
ssl_preread on;
|
||||
proxy_pass \$tls_upstream;
|
||||
}
|
||||
$tcp}
|
||||
|
||||
http {
|
||||
resolver @RESOLVER@ ipv6=off valid=30s;
|
||||
access_log off;
|
||||
map \$host \$http_allowed {
|
||||
hostnames;
|
||||
default 0;
|
||||
$map
|
||||
}
|
||||
server {
|
||||
listen $rip:80;
|
||||
if (\$http_allowed = 0) { return 403; }
|
||||
location / {
|
||||
proxy_pass http://\$host;
|
||||
proxy_set_header Host \$host;
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
echo "$forwards"
|
||||
}
|
||||
|
||||
# True if path $1 is $2 or lies under it.
|
||||
inside() {
|
||||
[[ $1 == "$2" || $1 == "${2%/}"/* ]]
|
||||
}
|
||||
|
||||
# Refuse launch dirs that would mount $HOME or the allowlist read-write.
|
||||
check_launch_dir() {
|
||||
local pwd home dir
|
||||
pwd=$(realpath "$PWD")
|
||||
home=$(realpath -m "$HOME")
|
||||
inside "$home" "$pwd" && die "refusing to mount $pwd: it is or contains \$HOME"
|
||||
for dir in "$CONFIG_DIR" "$(dirname "$ALLOWLIST")" "$(dirname "$(realpath -m "$ALLOWLIST")")"; do
|
||||
dir=$(realpath -m "$dir")
|
||||
inside "$dir" "$pwd" && die "refusing to mount $pwd: it contains $dir"
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
main() {
|
||||
local cmd=(claude "$@")
|
||||
if [[ ${1:-} == --exec ]]; then
|
||||
shift
|
||||
cmd=("$@")
|
||||
fi
|
||||
[[ $PWD == *:* ]] && die "cannot mount a directory containing ':'"
|
||||
check_launch_dir
|
||||
|
||||
if [[ ! -f $ALLOWLIST ]]; then
|
||||
local default
|
||||
for default in "$(dirname "$(readlink -f "$0")")/allowed-hosts" /usr/share/agent-sandbox/allowed-hosts; do
|
||||
[[ -f $default ]] && break
|
||||
done
|
||||
mkdir -p "$(dirname "$ALLOWLIST")"
|
||||
cp "$default" "$ALLOWLIST"
|
||||
echo "agent-sandbox: created $ALLOWLIST from defaults" >&2
|
||||
fi
|
||||
allowlist_entries "$ALLOWLIST" >/dev/null || exit 1
|
||||
|
||||
local session router tmp subnet rip forwards
|
||||
session=agent-sandbox-$(date +%s)-$$
|
||||
router=$session-router
|
||||
tmp=$(mktemp -d "${XDG_RUNTIME_DIR:-/tmp}/$session.XXXX")
|
||||
# shellcheck disable=SC2064
|
||||
trap "podman pod rm -f -t 0 $session >/dev/null 2>&1; podman rm -f -t 0 $router >/dev/null 2>&1; podman network rm -f $session >/dev/null 2>&1; rm -rf '$tmp'" EXIT
|
||||
trap 'exit 130' INT TERM HUP
|
||||
|
||||
podman network create --ignore "$EGRESS_NET" >/dev/null
|
||||
podman volume create --ignore "$HOME_VOLUME" >/dev/null
|
||||
podman network create --internal --disable-dns "$session" >/dev/null
|
||||
subnet=$(podman network inspect -f '{{range .Subnets}}{{.Subnet}}{{end}}' "$session")
|
||||
[[ $subnet == *.0/24 ]] || die "unexpected subnet $subnet"
|
||||
rip=${subnet%.0/24}.2
|
||||
|
||||
forwards=$(router_config "$ALLOWLIST" "$tmp" "$rip")
|
||||
chmod 755 "$tmp"
|
||||
chmod 644 "$tmp"/*
|
||||
|
||||
podman run -d --rm --name "$router" \
|
||||
--network "$session:ip=$rip" --network "$EGRESS_NET" \
|
||||
-v "$tmp:/etc/router:ro,Z" \
|
||||
"$ROUTER_IMAGE" >/dev/null
|
||||
wait_port "$router" "$rip" 443
|
||||
|
||||
# The pod owns the netns, DNS and hosts file; claude only joins it.
|
||||
podman pod create --name "$session" \
|
||||
--network "$session" --dns "$rip" --no-hosts --hostname agent-sandbox \
|
||||
--userns keep-id:uid=1000,gid=1000 >/dev/null
|
||||
# Loopback forwarders live in their own container: claude can't see, signal
|
||||
# or replace them. Claude runs unprivileged; podman's default bounding set
|
||||
# (no NET_ADMIN/NET_RAW/SYS_ADMIN) stays only so nested containers work.
|
||||
podman run -d --pod "$session" --name "$session-forwarder" \
|
||||
--user 0:0 --read-only --cap-drop all --cap-add NET_BIND_SERVICE \
|
||||
--entrypoint /usr/local/bin/forwarder -e FORWARDS="$forwards" \
|
||||
"$ROUTER_IMAGE" >/dev/null
|
||||
wait_port "$session-forwarder" 127.0.0.1 443
|
||||
|
||||
local tty=()
|
||||
[[ -t 0 && -t 1 ]] && tty=(-t)
|
||||
podman run --rm -i "${tty[@]}" --init --pod "$session" --name "$session-claude" \
|
||||
--cap-drop NET_ADMIN,NET_RAW,SYS_ADMIN \
|
||||
--security-opt label=disable \
|
||||
--device /dev/fuse --device /dev/net/tun \
|
||||
-v "$HOME_VOLUME:/home/claude" \
|
||||
-v "$PWD:$PWD" -w "$PWD" \
|
||||
-e DISABLE_AUTOUPDATER=1 -e CLAUDE_CODE_TMPDIR=/var/tmp \
|
||||
"$IMAGE" "${cmd[@]}"
|
||||
}
|
||||
|
||||
wait_port() {
|
||||
local i
|
||||
for i in $(seq 50); do
|
||||
podman exec "$1" nc -z "$2" "$3" 2>/dev/null && return
|
||||
sleep 0.1
|
||||
done
|
||||
die "$1 did not start: $(podman logs "$1" 2>&1 | tail -5)"
|
||||
}
|
||||
|
||||
if [[ ${BASH_SOURCE[0]} == "$0" ]]; then
|
||||
main "$@"
|
||||
fi
|
||||
@@ -0,0 +1,125 @@
|
||||
module git.unkin.net/unkin/agent-sandbox
|
||||
|
||||
go 1.25.7
|
||||
|
||||
require (
|
||||
github.com/containers/podman/v5 v5.8.4
|
||||
github.com/docker/docker v28.5.1+incompatible
|
||||
github.com/opencontainers/runtime-spec v1.2.1
|
||||
github.com/spf13/cobra v1.10.2
|
||||
go.podman.io/common v0.67.1
|
||||
golang.org/x/term v0.44.0
|
||||
)
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||
github.com/BurntSushi/toml v1.5.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/VividCortex/ewma v1.2.0 // indirect
|
||||
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect
|
||||
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||
github.com/chzyer/readline v1.5.1 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/containerd/platforms v1.0.0-rc.1 // indirect
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.17.0 // indirect
|
||||
github.com/containers/buildah v1.43.2 // indirect
|
||||
github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect
|
||||
github.com/containers/ocicrypt v1.2.1 // indirect
|
||||
github.com/containers/psgo v1.9.1-0.20250826150930-4ae76f200c86 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.6.0 // indirect
|
||||
github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.5.2 // indirect
|
||||
github.com/disiqueira/gotree/v3 v3.0.2 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.9.4 // indirect
|
||||
github.com/docker/go-connections v0.6.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.1-0.20241109141217-c266b19b28e9 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/go-containerregistry v0.20.6 // indirect
|
||||
github.com/google/go-intervals v0.0.2 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/gorilla/schema v1.4.1 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jinzhu/copier v0.4.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kevinburke/ssh_config v1.4.0 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||
github.com/kr/fs v0.1.0 // indirect
|
||||
github.com/letsencrypt/boulder v0.0.0-20240620165639-de9c06129bec // indirect
|
||||
github.com/manifoldco/promptui v0.9.0 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.32 // indirect
|
||||
github.com/miekg/pkcs11 v1.1.1 // indirect
|
||||
github.com/mistifyio/go-zfs/v3 v3.1.0 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/sys/capability v0.4.0 // indirect
|
||||
github.com/moby/sys/mountinfo v0.7.2 // indirect
|
||||
github.com/moby/sys/user v0.4.0 // indirect
|
||||
github.com/moby/sys/userns v0.1.0 // indirect
|
||||
github.com/moby/term v0.5.2 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/nxadm/tail v1.4.11 // indirect
|
||||
github.com/opencontainers/cgroups v0.0.6 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/opencontainers/runc v1.3.4 // indirect
|
||||
github.com/opencontainers/runtime-tools v0.9.1-0.20250523060157-0ea5ed0382a2 // indirect
|
||||
github.com/opencontainers/selinux v1.13.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pkg/sftp v1.13.9 // indirect
|
||||
github.com/proglottis/gpgme v0.1.5 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.9.1 // indirect
|
||||
github.com/sigstore/fulcio v1.7.1 // indirect
|
||||
github.com/sigstore/protobuf-specs v0.4.1 // indirect
|
||||
github.com/sigstore/sigstore v1.9.5 // indirect
|
||||
github.com/sirupsen/logrus v1.9.4 // indirect
|
||||
github.com/skeema/knownhosts v1.3.2 // indirect
|
||||
github.com/smallstep/pkcs7 v0.1.1 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 // indirect
|
||||
github.com/sylabs/sif/v2 v2.22.0 // indirect
|
||||
github.com/tchap/go-patricia/v2 v2.3.3 // indirect
|
||||
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect
|
||||
github.com/ulikunitz/xz v0.5.15 // indirect
|
||||
github.com/vbatts/tar-split v0.12.1 // indirect
|
||||
github.com/vbauerster/mpb/v8 v8.10.2 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
|
||||
go.opentelemetry.io/otel v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.36.0 // indirect
|
||||
go.podman.io/image/v5 v5.39.2 // indirect
|
||||
go.podman.io/storage v1.62.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
golang.org/x/crypto v0.53.0 // indirect
|
||||
golang.org/x/net v0.55.0 // indirect
|
||||
golang.org/x/sync v0.21.0 // indirect
|
||||
golang.org/x/sys v0.46.0 // indirect
|
||||
golang.org/x/text v0.38.0 // indirect
|
||||
golang.org/x/time v0.11.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e // indirect
|
||||
google.golang.org/grpc v1.72.2 // indirect
|
||||
google.golang.org/protobuf v1.36.9 // indirect
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||
tags.cncf.io/container-device-interface v1.0.1 // indirect
|
||||
)
|
||||
@@ -0,0 +1,416 @@
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow=
|
||||
github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4=
|
||||
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
|
||||
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
|
||||
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
|
||||
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
|
||||
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
|
||||
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/containerd/platforms v1.0.0-rc.1 h1:83KIq4yy1erSRgOVHNk1HYdPvzdJ5CnsWaRoJX4C41E=
|
||||
github.com/containerd/platforms v1.0.0-rc.1/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4=
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.17.0 h1:+TyQIsR/zSFI1Rm31EQBwpAA1ovYgIKHy7kctL3sLcE=
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.17.0/go.mod h1:s06tWAiJcXQo9/8AReBCIo/QxcXFZ2n4qfsRnpl71SM=
|
||||
github.com/containers/buildah v1.43.2 h1:KbxgDNWtbMTQtbSElh4e/n8pJa9EDF+/jtIKPr1QtZI=
|
||||
github.com/containers/buildah v1.43.2/go.mod h1:dXSaAu+S9aiY0j1teX+7btxBKSZlLfWU5LXnXwPaxf8=
|
||||
github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 h1:Qzk5C6cYglewc+UyGf6lc8Mj2UaPTHy/iF2De0/77CA=
|
||||
github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01/go.mod h1:9rfv8iPl1ZP7aqh9YA68wnZv2NUDbXdcdPHVz0pFbPY=
|
||||
github.com/containers/ocicrypt v1.2.1 h1:0qIOTT9DoYwcKmxSt8QJt+VzMY18onl9jUXsxpVhSmM=
|
||||
github.com/containers/ocicrypt v1.2.1/go.mod h1:aD0AAqfMp0MtwqWgHM1bUwe1anx0VazI108CRrSKINQ=
|
||||
github.com/containers/podman/v5 v5.8.4 h1:2RX95keVopdkWz1x9G+KhTGLySruxnBXn6oTuuLgGvE=
|
||||
github.com/containers/podman/v5 v5.8.4/go.mod h1:5mMXU913/zkU+TdeLoKhIijyKjuMzBrxHCiTgvfPTjg=
|
||||
github.com/containers/psgo v1.9.1-0.20250826150930-4ae76f200c86 h1:bYj0TVlkRZtMJYd6SbFOi1gjUJDJmVsYCpJla3URD7Y=
|
||||
github.com/containers/psgo v1.9.1-0.20250826150930-4ae76f200c86/go.mod h1:52GX23ST30pXpeviDvNpOCXoHqr+WJDv+7BjEH+AabY=
|
||||
github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo=
|
||||
github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
|
||||
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 h1:uX1JmpONuD549D73r6cgnxyUu18Zb7yHAy5AYU0Pm4Q=
|
||||
github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw=
|
||||
github.com/cyphar/filepath-securejoin v0.5.2 h1:w/T2bhKr4pgwG0SUGjU4S/Is9+zUknLh5ROTJLzWX8E=
|
||||
github.com/cyphar/filepath-securejoin v0.5.2/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/disiqueira/gotree/v3 v3.0.2 h1:ik5iuLQQoufZBNPY518dXhiO5056hyNBIK9lWhkNRq8=
|
||||
github.com/disiqueira/gotree/v3 v3.0.2/go.mod h1:ZuyjE4+mUQZlbpkI24AmruZKhg3VHEgPLDY8Qk+uUu8=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/cli v28.5.1+incompatible h1:ESutzBALAD6qyCLqbQSEf1a/U8Ybms5agw59yGVc+yY=
|
||||
github.com/docker/cli v28.5.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
|
||||
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM=
|
||||
github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker-credential-helpers v0.9.4 h1:76ItO69/AP/V4yT9V4uuuItG0B1N8hvt0T0c0NN/DzI=
|
||||
github.com/docker/docker-credential-helpers v0.9.4/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
|
||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||
github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8=
|
||||
github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
|
||||
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/godbus/dbus/v5 v5.1.1-0.20241109141217-c266b19b28e9 h1:Kzr9J0S0V2PRxiX6B6xw1kWjzsIyjLO2Ibi4fNTaYBM=
|
||||
github.com/godbus/dbus/v5 v5.1.1-0.20241109141217-c266b19b28e9/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-containerregistry v0.20.6 h1:cvWX87UxxLgaH76b4hIvya6Dzz9qHB31qAwjAohdSTU=
|
||||
github.com/google/go-containerregistry v0.20.6/go.mod h1:T0x8MuoAoKX/873bkeSfLD2FAkwCDf9/HZgsFJ02E2Y=
|
||||
github.com/google/go-intervals v0.0.2 h1:FGrVEiUnTRKR8yE04qzXYaJMtnIYqobR5QbblK3ixcM=
|
||||
github.com/google/go-intervals v0.0.2/go.mod h1:MkaR3LNRfeKLPmqgJYs4E66z5InYjmCjbbr4TQlcT6Y=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc=
|
||||
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
|
||||
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E=
|
||||
github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
|
||||
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
|
||||
github.com/jmhodges/clock v1.2.0 h1:eq4kys+NI0PLngzaHEe7AmPT90XMGIEySD1JfV1PDIs=
|
||||
github.com/jmhodges/clock v1.2.0/go.mod h1:qKjhA7x7u/lQpPB1XAqX1b1lCI/w3/fNuYpI/ZjLynI=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ=
|
||||
github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
||||
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/letsencrypt/boulder v0.0.0-20240620165639-de9c06129bec h1:2tTW6cDth2TSgRbAhD7yjZzTQmcN25sDRPEeinR51yQ=
|
||||
github.com/letsencrypt/boulder v0.0.0-20240620165639-de9c06129bec/go.mod h1:TmwEoGCwIti7BCeJ9hescZgRtatxRE+A72pCoPfmcfk=
|
||||
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
|
||||
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
||||
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU=
|
||||
github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
|
||||
github.com/mistifyio/go-zfs/v3 v3.1.0 h1:FZaylcg0hjUp27i23VcJJQiuBeAZjrC8lPqCGM1CopY=
|
||||
github.com/mistifyio/go-zfs/v3 v3.1.0/go.mod h1:CzVgeB0RvF2EGzQnytKVvVSDwmKJXxkOTUGbNrTja/k=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
||||
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
||||
github.com/moby/sys/capability v0.4.0 h1:4D4mI6KlNtWMCM1Z/K0i7RV1FkX+DBDHKVJpCndZoHk=
|
||||
github.com/moby/sys/capability v0.4.0/go.mod h1:4g9IK291rVkms3LKCDOoYlnV8xKwoDTpIrNEE35Wq0I=
|
||||
github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=
|
||||
github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4=
|
||||
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
|
||||
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
|
||||
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
|
||||
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
|
||||
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY=
|
||||
github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc=
|
||||
github.com/onsi/ginkgo/v2 v2.28.0 h1:Rrf+lVLmtlBIKv6KrIGJCjyY8N36vDVcutbGJkyqjJc=
|
||||
github.com/onsi/ginkgo/v2 v2.28.0/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
|
||||
github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
|
||||
github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
|
||||
github.com/opencontainers/cgroups v0.0.6 h1:tfZFWTIIGaUUFImTyuTg+Mr5x8XRiSdZESgEBW7UxuI=
|
||||
github.com/opencontainers/cgroups v0.0.6/go.mod h1:oWVzJsKK0gG9SCRBfTpnn16WcGEqDI8PAcpMGbqWxcs=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/opencontainers/runc v1.3.4 h1:+lwmPUTzbgv0JFqu8zBU2WtHYbm+JPPS9hxB/PvWd30=
|
||||
github.com/opencontainers/runc v1.3.4/go.mod h1:o1wyv76EDlTkcf0KTFgN8bMWLPvgF/HfX709lDv+rr4=
|
||||
github.com/opencontainers/runtime-spec v1.2.1 h1:S4k4ryNgEpxW1dzyqffOmhI1BHYcjzU8lpJfSlR0xww=
|
||||
github.com/opencontainers/runtime-spec v1.2.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||
github.com/opencontainers/runtime-tools v0.9.1-0.20250523060157-0ea5ed0382a2 h1:2xZEHOdeQBV6PW8ZtimN863bIOl7OCW/X10K0cnxKeA=
|
||||
github.com/opencontainers/runtime-tools v0.9.1-0.20250523060157-0ea5ed0382a2/go.mod h1:MXdPzqAA8pHC58USHqNCSjyLnRQ6D+NjbpP+02Z1U/0=
|
||||
github.com/opencontainers/selinux v1.13.1 h1:A8nNeceYngH9Ow++M+VVEwJVpdFmrlxsN22F+ISDCJE=
|
||||
github.com/opencontainers/selinux v1.13.1/go.mod h1:S10WXZ/osk2kWOYKy1x2f/eXF5ZHJoUs8UU/2caNRbg=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/sftp v1.13.9 h1:4NGkvGudBL7GteO3m6qnaQ4pC0Kvf0onSVc9gR3EWBw=
|
||||
github.com/pkg/sftp v1.13.9/go.mod h1:OBN7bVXdstkFFN/gdnHPUb5TE8eb8G1Rp9wCItqjkkA=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/proglottis/gpgme v0.1.5 h1:KCGyOw8sQ+SI96j6G8D8YkOGn+1TwbQTT9/zQXoVlz0=
|
||||
github.com/proglottis/gpgme v0.1.5/go.mod h1:5LoXMgpE4bttgwwdv9bLs/vwqv3qV7F4glEEZ7mRKrM=
|
||||
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
|
||||
github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
|
||||
github.com/sebdah/goldie/v2 v2.7.1 h1:PkBHymaYdtvEkZV7TmyqKxdmn5/Vcj+8TpATWZjnG5E=
|
||||
github.com/sebdah/goldie/v2 v2.7.1/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.9.1 h1:nZZaNz4DiERIQguNy0cL5qTdn9lR8XKHf4RUyG1Sx3g=
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.9.1/go.mod h1:np53YzT0zXGMv6x4iEWc9Z59uR+x+ndLwCLqPYpLXVU=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/sigstore/fulcio v1.7.1 h1:RcoW20Nz49IGeZyu3y9QYhyyV3ZKQ85T+FXPKkvE+aQ=
|
||||
github.com/sigstore/fulcio v1.7.1/go.mod h1:7lYY+hsd8Dt+IvKQRC+KEhWpCZ/GlmNvwIa5JhypMS8=
|
||||
github.com/sigstore/protobuf-specs v0.4.1 h1:5SsMqZbdkcO/DNHudaxuCUEjj6x29tS2Xby1BxGU7Zc=
|
||||
github.com/sigstore/protobuf-specs v0.4.1/go.mod h1:+gXR+38nIa2oEupqDdzg4qSBT0Os+sP7oYv6alWewWc=
|
||||
github.com/sigstore/sigstore v1.9.5 h1:Wm1LT9yF4LhQdEMy5A2JeGRHTrAWGjT3ubE5JUSrGVU=
|
||||
github.com/sigstore/sigstore v1.9.5/go.mod h1:VtxgvGqCmEZN9X2zhFSOkfXxvKUjpy8RpUW39oCtoII=
|
||||
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||
github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg=
|
||||
github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow=
|
||||
github.com/smallstep/pkcs7 v0.1.1 h1:x+rPdt2W088V9Vkjho4KtoggyktZJlMduZAtRHm68LU=
|
||||
github.com/smallstep/pkcs7 v0.1.1/go.mod h1:dL6j5AIz9GHjVEBTXtW+QliALcgM19RtXaTeyxI+AfA=
|
||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 h1:pnnLyeX7o/5aX8qUQ69P/mLojDqwda8hFOCBTmP/6hw=
|
||||
github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6/go.mod h1:39R/xuhNgVhi+K0/zst4TLrJrVmbm6LVgl4A0+ZFS5M=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/sylabs/sif/v2 v2.22.0 h1:Y+xXufp4RdgZe02SR3nWEg7S6q4tPWN237WHYzkDSKA=
|
||||
github.com/sylabs/sif/v2 v2.22.0/go.mod h1:W1XhWTmG1KcG7j5a3KSYdMcUIFvbs240w/MMVW627hs=
|
||||
github.com/tchap/go-patricia/v2 v2.3.3 h1:xfNEsODumaEcCcY3gI0hYPZ/PcpVv5ju6RMAhgwZDDc=
|
||||
github.com/tchap/go-patricia/v2 v2.3.3/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k=
|
||||
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0=
|
||||
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs=
|
||||
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
||||
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo=
|
||||
github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=
|
||||
github.com/vbauerster/mpb/v8 v8.10.2 h1:2uBykSHAYHekE11YvJhKxYmLATKHAGorZwFlyNw4hHM=
|
||||
github.com/vbauerster/mpb/v8 v8.10.2/go.mod h1:+Ja4P92E3/CorSZgfDtK46D7AVbDqmBQRTmyTqPElo0=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
|
||||
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
|
||||
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk=
|
||||
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
|
||||
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
|
||||
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
|
||||
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
|
||||
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
|
||||
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
|
||||
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
|
||||
go.podman.io/common v0.67.1 h1:HddYLJfkfFUmFJ0V3PVoewguFM9eHkqk0g+fOc2B9R4=
|
||||
go.podman.io/common v0.67.1/go.mod h1:XVmSLtnhJwGb+ImYn6BXiozxVE3mYZJgiiBuDOiOquk=
|
||||
go.podman.io/image/v5 v5.39.2 h1:EJua/pRtvgLV/a5y8/RvA+ekKukZh0UuKMvLdTmEWFk=
|
||||
go.podman.io/image/v5 v5.39.2/go.mod h1:SlaR6Pra1ATIx4BcuZ16oafb3QcCHISaKcJbtlN/G/0=
|
||||
go.podman.io/storage v1.62.0 h1:0QjX1XlzVmbiaulb+aR/CG6p9+pzaqwIeZPe3tEjHbY=
|
||||
go.podman.io/storage v1.62.0/go.mod h1:A3UBK0XypjNZ6pghRhuxg62+2NIm5lcUGv/7XyMhMUI=
|
||||
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
||||
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
|
||||
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
||||
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e h1:UdXH7Kzbj+Vzastr5nVfccbmFsmYNygVLSPk1pEfDoY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e/go.mod h1:085qFyf2+XaZlRdCgKNCIZ3afY2p4HHZdoIRpId8F4A=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e h1:ztQaXfzEXTmCBvbtWYRhJxW+0iJcz2qXfd38/e9l7bA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8=
|
||||
google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
|
||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
||||
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
||||
tags.cncf.io/container-device-interface v1.0.1 h1:KqQDr4vIlxwfYh0Ed/uJGVgX+CHAkahrgabg6Q8GYxc=
|
||||
tags.cncf.io/container-device-interface v1.0.1/go.mod h1:JojJIOeW3hNbcnOH2q0NrWNha/JuHoDZcmYxAZwb2i0=
|
||||
@@ -0,0 +1,392 @@
|
||||
// agent-sandbox runs Claude Code in a rootless podman sandbox whose only way
|
||||
// out is a per-session router that resolves and forwards allowlisted hosts.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/containers/podman/v5/libpod/define"
|
||||
"github.com/containers/podman/v5/pkg/specgen"
|
||||
spec "github.com/opencontainers/runtime-spec/specs-go"
|
||||
"github.com/spf13/cobra"
|
||||
"go.podman.io/common/libnetwork/types"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
const (
|
||||
registry = "artifactapi.k8s.syd1.au.unkin.net/docker-internal"
|
||||
egressNet = "agent-sandbox-egress"
|
||||
)
|
||||
|
||||
var version = "dev"
|
||||
|
||||
//go:embed allowed-hosts
|
||||
var defaultAllowlist []byte
|
||||
|
||||
func main() {
|
||||
// A closed stdout must end the session through cleanup, not kill us.
|
||||
signal.Ignore(syscall.SIGPIPE)
|
||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
|
||||
code := 0
|
||||
root := &cobra.Command{
|
||||
Use: "agent-sandbox [--exec CMD...] [claude args...]",
|
||||
Short: "Run Claude Code in a rootless podman sandbox with allowlisted egress",
|
||||
Args: cobra.ArbitraryArgs,
|
||||
DisableFlagParsing: true,
|
||||
SilenceUsage: true,
|
||||
Run: func(_ *cobra.Command, args []string) { code = launch(ctx, args) },
|
||||
}
|
||||
root.AddCommand(&cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Print the version",
|
||||
Run: func(*cobra.Command, []string) { fmt.Println(version) },
|
||||
})
|
||||
if err := root.Execute(); err != nil {
|
||||
code = 1
|
||||
}
|
||||
stop()
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func imageNames() (string, string) {
|
||||
img, router := registry+"/agent-sandbox:"+version, registry+"/agent-sandbox-router:"+version
|
||||
if version == "dev" {
|
||||
img, router = "localhost/agent-sandbox:dev", "localhost/agent-sandbox-router:dev"
|
||||
}
|
||||
return envOr("AGENT_SANDBOX_IMAGE", img), envOr("AGENT_SANDBOX_ROUTER_IMAGE", router)
|
||||
}
|
||||
|
||||
func envOr(key, def string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func configDir() string {
|
||||
return filepath.Join(envOr("XDG_CONFIG_HOME", filepath.Join(os.Getenv("HOME"), ".config")), "agent-sandbox")
|
||||
}
|
||||
|
||||
func allowlistPath() string {
|
||||
return envOr("AGENT_SANDBOX_ALLOWLIST", filepath.Join(configDir(), "allowed-hosts"))
|
||||
}
|
||||
|
||||
func warn(format string, a ...any) {
|
||||
fmt.Fprintf(os.Stderr, "agent-sandbox: "+format+"\n", a...)
|
||||
}
|
||||
|
||||
// launch runs one session and returns the process exit code.
|
||||
func launch(ctx context.Context, args []string) int {
|
||||
cmd := append([]string{"claude"}, args...)
|
||||
if len(args) > 0 && args[0] == "--exec" {
|
||||
cmd = args[1:]
|
||||
}
|
||||
pwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
warn("%v", err)
|
||||
return 1
|
||||
}
|
||||
if strings.Contains(pwd, ":") {
|
||||
warn("cannot mount a directory containing ':'")
|
||||
return 1
|
||||
}
|
||||
allowlist := allowlistPath()
|
||||
if err := checkLaunchDir(pwd, allowlist); err != nil {
|
||||
warn("%v", err)
|
||||
return 1
|
||||
}
|
||||
if err := ensureAllowlist(allowlist); err != nil {
|
||||
warn("%v", err)
|
||||
return 1
|
||||
}
|
||||
entries, err := parseAllowlist(allowlist)
|
||||
if err != nil {
|
||||
warn("%v", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
session := fmt.Sprintf("agent-sandbox-%d-%d", time.Now().Unix(), os.Getpid())
|
||||
runtimeDir := envOr("XDG_RUNTIME_DIR", "/tmp")
|
||||
tmp, err := os.MkdirTemp(runtimeDir, session+".")
|
||||
if err != nil {
|
||||
warn("%v", err)
|
||||
return 1
|
||||
}
|
||||
defer func() { _ = os.RemoveAll(tmp) }()
|
||||
// The API socket lives apart from tmp, which is mounted into the router.
|
||||
apiDir, err := os.MkdirTemp(runtimeDir, session+"-api.")
|
||||
if err != nil {
|
||||
warn("%v", err)
|
||||
return 1
|
||||
}
|
||||
defer func() { _ = os.RemoveAll(apiDir) }()
|
||||
|
||||
conn, eng, stopService, err := startService(apiDir)
|
||||
if err != nil {
|
||||
warn("%v", err)
|
||||
return 125
|
||||
}
|
||||
defer stopService()
|
||||
defer func() {
|
||||
_ = eng.PodRemove(conn, session)
|
||||
_ = eng.ContainerRemove(conn, session+"-router")
|
||||
_ = eng.NetworkRemove(conn, session)
|
||||
}()
|
||||
|
||||
opCtx, cancel := context.WithCancel(conn)
|
||||
defer cancel()
|
||||
defer context.AfterFunc(ctx, cancel)()
|
||||
code := runSession(opCtx, eng, session, tmp, pwd, entries, cmd)
|
||||
if ctx.Err() != nil {
|
||||
return 130
|
||||
}
|
||||
return code
|
||||
}
|
||||
|
||||
// fail reports a podman API error with podman's own exit code for it.
|
||||
func fail(err error) int {
|
||||
warn("%v", err)
|
||||
return 125
|
||||
}
|
||||
|
||||
func runSession(ctx context.Context, eng engine, session, tmp, pwd string, entries []entry, cmd []string) int {
|
||||
image, routerImage := imageNames()
|
||||
homeVolume := envOr("AGENT_SANDBOX_HOME_VOLUME", "agent-sandbox-home")
|
||||
router := session + "-router"
|
||||
yes := true
|
||||
|
||||
if _, err := eng.NetworkCreate(ctx, &types.Network{Name: egressNet, Driver: "bridge", DNSEnabled: true}, true); err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
if err := eng.VolumeCreate(ctx, homeVolume); err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
n, err := eng.NetworkCreate(ctx, &types.Network{Name: session, Driver: "bridge", Internal: true}, false)
|
||||
if err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
subnet := ""
|
||||
for _, s := range n.Subnets {
|
||||
subnet += s.Subnet.String()
|
||||
}
|
||||
if !strings.HasSuffix(subnet, ".0/24") {
|
||||
warn("unexpected subnet %s", subnet)
|
||||
return 1
|
||||
}
|
||||
rip := strings.TrimSuffix(subnet, ".0/24") + ".2"
|
||||
|
||||
dnsmasq, nginx, forwards := routerConfig(entries, rip)
|
||||
for name, body := range map[string]string{"dnsmasq.conf": dnsmasq, "nginx.conf": nginx} {
|
||||
f := filepath.Join(tmp, name)
|
||||
err := os.WriteFile(f, []byte(body), 0o644)
|
||||
if err == nil {
|
||||
err = os.Chmod(f, 0o644)
|
||||
}
|
||||
if err != nil {
|
||||
warn("%v", err)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
if err := os.Chmod(tmp, 0o755); err != nil {
|
||||
warn("%v", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
r := newSpec(routerImage, router)
|
||||
r.Remove = &yes
|
||||
r.NetNS = specgen.Namespace{NSMode: specgen.Bridge}
|
||||
r.Networks = map[string]types.PerNetworkOptions{
|
||||
session: {StaticIPs: []net.IP{net.ParseIP(rip)}},
|
||||
egressNet: {},
|
||||
}
|
||||
r.Mounts = []spec.Mount{{Type: "bind", Source: tmp, Destination: "/etc/router", Options: []string{"ro", "Z"}}}
|
||||
if err := eng.ContainerCreate(ctx, r); err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
if err := eng.ContainerStart(ctx, router); err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
if err := waitPort(ctx, eng, router, rip, "443"); err != nil {
|
||||
warn("%v", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
// The pod owns the netns, DNS and hosts file; claude only joins it.
|
||||
userns, _ := specgen.ParseUserNamespace("keep-id:uid=1000,gid=1000")
|
||||
p := &specgen.PodSpecGenerator{}
|
||||
p.Name = session
|
||||
p.Hostname = "agent-sandbox"
|
||||
p.NetNS = specgen.Namespace{NSMode: specgen.Bridge}
|
||||
p.Networks = map[string]types.PerNetworkOptions{session: {}}
|
||||
p.DNSServer = []net.IP{net.ParseIP(rip)}
|
||||
p.NoManageHosts = true
|
||||
p.Userns = userns
|
||||
if err := eng.PodCreate(ctx, p); err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
|
||||
// Loopback forwarders live in their own container: claude can't see, signal
|
||||
// or replace them. Claude runs unprivileged; podman's default bounding set
|
||||
// (no NET_ADMIN/NET_RAW/SYS_ADMIN) stays only so nested containers work.
|
||||
f := newSpec(routerImage, session+"-forwarder")
|
||||
f.Pod = session
|
||||
f.User = "0:0"
|
||||
f.ReadOnlyFilesystem = &yes
|
||||
f.ReadWriteTmpfs = &yes
|
||||
f.CapDrop = []string{"all"}
|
||||
f.CapAdd = []string{"NET_BIND_SERVICE"}
|
||||
f.Entrypoint = []string{"/usr/local/bin/forwarder"}
|
||||
f.Env = map[string]string{"FORWARDS": forwards}
|
||||
if err := eng.ContainerCreate(ctx, f); err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
if err := eng.ContainerStart(ctx, f.Name); err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
if err := waitPort(ctx, eng, f.Name, "127.0.0.1", "443"); err != nil {
|
||||
warn("%v", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
c := newSpec(image, session+"-claude")
|
||||
c.Remove = &yes
|
||||
c.Stdin = &yes
|
||||
c.Terminal = ptr(term.IsTerminal(int(os.Stdin.Fd())) && term.IsTerminal(int(os.Stdout.Fd())))
|
||||
c.Init = &yes
|
||||
c.Pod = session
|
||||
c.CapDrop = []string{"NET_ADMIN", "NET_RAW", "SYS_ADMIN"}
|
||||
c.SelinuxOpts = []string{"disable"}
|
||||
c.Annotations = map[string]string{define.InspectAnnotationLabel: "disable"}
|
||||
c.Devices = []spec.LinuxDevice{{Path: "/dev/fuse"}, {Path: "/dev/net/tun"}}
|
||||
c.Volumes = []*specgen.NamedVolume{{Name: homeVolume, Dest: "/home/claude"}}
|
||||
c.Mounts = []spec.Mount{{Type: "bind", Source: pwd, Destination: pwd}}
|
||||
c.WorkDir = pwd
|
||||
c.Env = map[string]string{"DISABLE_AUTOUPDATER": "1", "CLAUDE_CODE_TMPDIR": "/var/tmp"}
|
||||
c.Command = cmd
|
||||
if err := eng.ContainerCreate(ctx, c); err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
code, err := eng.ContainerRun(ctx, c.Name)
|
||||
if err != nil {
|
||||
warn("%v", err)
|
||||
}
|
||||
return code
|
||||
}
|
||||
|
||||
func ptr[T any](v T) *T { return &v }
|
||||
|
||||
// newSpec mirrors `podman run` defaults the API leaves to the client.
|
||||
func newSpec(image, name string) *specgen.SpecGenerator {
|
||||
s := specgen.NewSpecGenerator(image, false)
|
||||
s.RawImageName = image
|
||||
s.Name = name
|
||||
s.HTTPProxy = ptr(true)
|
||||
s.SdNotifyMode = define.SdNotifyModeContainer
|
||||
return s
|
||||
}
|
||||
|
||||
func waitPort(ctx context.Context, eng engine, container, ip, port string) error {
|
||||
for range 50 {
|
||||
if code, err := eng.ContainerExec(ctx, container, []string{"nc", "-z", ip, port}); err == nil && code == 0 {
|
||||
return nil
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("%s did not start: %s", container, eng.ContainerLogs(context.WithoutCancel(ctx), container, 5))
|
||||
}
|
||||
|
||||
// ensureAllowlist seeds a missing allowlist from the packaged default.
|
||||
func ensureAllowlist(path string) error {
|
||||
if fi, err := os.Stat(path); err == nil && fi.Mode().IsRegular() {
|
||||
return nil
|
||||
}
|
||||
src := defaultAllowlist
|
||||
if exe, err := os.Executable(); err == nil {
|
||||
for _, p := range []string{filepath.Join(filepath.Dir(exe), "allowed-hosts"), "/usr/share/agent-sandbox/allowed-hosts"} {
|
||||
if b, err := os.ReadFile(p); err == nil {
|
||||
src = b
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(path, src, 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
warn("created %s from defaults", path)
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkLaunchDir refuses launch dirs that would mount $HOME or the allowlist
|
||||
// read-write.
|
||||
func checkLaunchDir(pwd, allowlist string) error {
|
||||
pwd = realpath(pwd)
|
||||
if inside(realpath(os.Getenv("HOME")), pwd) {
|
||||
return fmt.Errorf("refusing to mount %s: it is or contains $HOME", pwd)
|
||||
}
|
||||
for _, dir := range []string{configDir(), filepath.Dir(allowlist), filepath.Dir(realpath(allowlist))} {
|
||||
if dir = realpath(dir); inside(dir, pwd) {
|
||||
return fmt.Errorf("refusing to mount %s: it contains %s", pwd, dir)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// inside reports whether path p is dir or lies under it.
|
||||
func inside(p, dir string) bool {
|
||||
return p == dir || strings.HasPrefix(p, strings.TrimSuffix(dir, "/")+"/")
|
||||
}
|
||||
|
||||
// realpath resolves every symlink in p like `realpath -m`: missing components
|
||||
// are allowed, and ".." applies to the resolved path.
|
||||
func realpath(p string) string {
|
||||
if !filepath.IsAbs(p) {
|
||||
wd, _ := os.Getwd()
|
||||
p = filepath.Join(wd, p)
|
||||
}
|
||||
rest := strings.Split(p, "/")
|
||||
resolved := "/"
|
||||
for hops := 0; len(rest) > 0; {
|
||||
part := rest[0]
|
||||
rest = rest[1:]
|
||||
switch part {
|
||||
case "", ".":
|
||||
continue
|
||||
case "..":
|
||||
resolved = filepath.Dir(resolved)
|
||||
continue
|
||||
}
|
||||
next := filepath.Join(resolved, part)
|
||||
fi, err := os.Lstat(next)
|
||||
if err != nil || fi.Mode()&os.ModeSymlink == 0 || hops > 255 {
|
||||
resolved = next
|
||||
continue
|
||||
}
|
||||
target, err := os.Readlink(next)
|
||||
if err != nil {
|
||||
resolved = next
|
||||
continue
|
||||
}
|
||||
hops++
|
||||
if filepath.IsAbs(target) {
|
||||
resolved = "/"
|
||||
}
|
||||
rest = append(strings.Split(target, "/"), rest...)
|
||||
}
|
||||
return resolved
|
||||
}
|
||||
+473
@@ -0,0 +1,473 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/containers/podman/v5/pkg/specgen"
|
||||
spec "github.com/opencontainers/runtime-spec/specs-go"
|
||||
"go.podman.io/common/libnetwork/types"
|
||||
)
|
||||
|
||||
func writeList(t *testing.T, lines ...string) string {
|
||||
t.Helper()
|
||||
f := filepath.Join(t.TempDir(), "hosts")
|
||||
if err := os.WriteFile(f, []byte(strings.Join(lines, "\n")+"\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
func lines(s string, re string) []string {
|
||||
var out []string
|
||||
for _, l := range strings.Split(s, "\n") {
|
||||
if regexp.MustCompile(re).MatchString(l) {
|
||||
out = append(out, l)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func check[T any](t *testing.T, name string, want, got T) {
|
||||
t.Helper()
|
||||
if !reflect.DeepEqual(want, got) {
|
||||
t.Errorf("%s:\nwant %#v\ngot %#v", name, want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouterConfig(t *testing.T) {
|
||||
f := writeList(t, "# comment", "", " API.Anthropic.com # trailing", ".example.org", "git.example.net:22",
|
||||
"db:5432", "db", "web:443", "web:80", "git.example.net:5432")
|
||||
entries, err := parseAllowlist(f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
check(t, "entries", []entry{{".example.org", ""}, {"api.anthropic.com", ""}, {"db", ""}, {"db", "5432"},
|
||||
{"git.example.net", "22"}, {"git.example.net", "5432"}, {"web", ""}}, entries)
|
||||
|
||||
dnsmasq, nginx, forwards := routerConfig(entries, "10.89.7.2")
|
||||
check(t, "forwards", "0.0.0.0:443:10.89.7.2:443 0.0.0.0:80:10.89.7.2:80 127.1.0.2:5432:10.89.7.2:10000 "+
|
||||
"127.1.0.3:22:10.89.7.2:10001 127.1.0.3:5432:10.89.7.2:10002", forwards)
|
||||
check(t, "dnsmasq", "no-resolv\nno-hosts\nbind-interfaces\nlisten-address=10.89.7.2\naddress=/#/\n"+
|
||||
"address=/example.org/127.0.0.2\nhost-record=api.anthropic.com,127.1.0.1\nhost-record=db,127.1.0.2\n"+
|
||||
"host-record=git.example.net,127.1.0.3\nhost-record=web,127.1.0.4\n", dnsmasq)
|
||||
check(t, "dnsmasq-never-forwards", []string(nil), lines(dnsmasq, `^(server|resolv-file)=`))
|
||||
|
||||
tlsMap := nginx[strings.Index(nginx, "map $ssl_preread_server_name"):]
|
||||
httpMap := nginx[strings.Index(nginx, "map $host"):]
|
||||
want := []string{" .example.org 1;", " api.anthropic.com 1;", " db 1;", " git.example.net 1;", " web 1;"}
|
||||
check(t, "tls-map", want, lines(tlsMap[:strings.Index(tlsMap, "}")], ` 1;$`))
|
||||
check(t, "http-map", want, lines(httpMap[:strings.Index(httpMap, "}")], ` 1;$`))
|
||||
check(t, "tls-default-deny", []string{" default 127.0.0.1:9;"}, lines(nginx, `default 127`))
|
||||
check(t, "http-default-deny", []string{" if ($http_allowed = 0) { return 403; }"}, lines(nginx, `return 403`))
|
||||
check(t, "tcp-servers", []string{
|
||||
" server { listen 10.89.7.2:10000; set $up db:5432; proxy_pass $up; }",
|
||||
" server { listen 10.89.7.2:10001; set $up git.example.net:22; proxy_pass $up; }",
|
||||
" server { listen 10.89.7.2:10002; set $up git.example.net:5432; proxy_pass $up; }",
|
||||
}, lines(nginx, `set \$up`))
|
||||
check(t, "listens-on-router-only", len(lines(nginx, `listen `)), len(lines(nginx, `listen 10\.89\.7\.2:`)))
|
||||
check(t, "resolver-placeholder", 2, strings.Count(nginx, "resolver @RESOLVER@ ipv6=off valid=30s;"))
|
||||
}
|
||||
|
||||
func TestRouterConfigOverlap(t *testing.T) {
|
||||
entries, err := parseAllowlist(writeList(t, ".example.org", "example.org", "example.org:22", "a.example.org", "example.org"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dnsmasq, nginx, _ := routerConfig(entries, "10.89.7.2")
|
||||
check(t, "overlap-dnsmasq", []string{"address=/example.org/127.0.0.2", "host-record=a.example.org,127.1.0.1",
|
||||
"host-record=example.org,127.1.0.2"}, lines(dnsmasq, `^(address=/e|host-record)`))
|
||||
check(t, "overlap-map", []string{" .example.org 1;", " a.example.org 1;", " .example.org 1;", " a.example.org 1;"},
|
||||
lines(nginx, `^ .* 1;$`))
|
||||
}
|
||||
|
||||
func TestParseAllowlistRejects(t *testing.T) {
|
||||
for _, bad := range []string{"exa mple.com", "example.com:0", "example.com:70000", "example.com:ssh", "*.example.com",
|
||||
"-bad.com", "http://example.com", "a..b", ".example.com:22", "example.com:+22", "example.com:-1"} {
|
||||
if _, err := parseAllowlist(writeList(t, bad)); err == nil {
|
||||
t.Errorf("accepted %q", bad)
|
||||
}
|
||||
}
|
||||
_, err := parseAllowlist(writeList(t, "ok.example.com", "bad host", "z.example.com"))
|
||||
check(t, "bad-line-error", true, err != nil && strings.HasSuffix(err.Error(), ":2: one entry per line"))
|
||||
}
|
||||
|
||||
func TestDefaultAllowlistParses(t *testing.T) {
|
||||
if _, err := parseAllowlist("allowed-hosts"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRealpath(t *testing.T) {
|
||||
d, _ := filepath.EvalSymlinks(t.TempDir())
|
||||
_ = os.MkdirAll(d+"/a/b", 0o755)
|
||||
_ = os.Symlink(d+"/a/b", d+"/abs")
|
||||
_ = os.Symlink("a/b", d+"/rel")
|
||||
_ = os.Symlink(d+"/missing/x", d+"/dangling")
|
||||
_ = os.Symlink(d+"/loop", d+"/loop")
|
||||
for in, want := range map[string]string{
|
||||
d + "/abs": d + "/a/b",
|
||||
d + "/rel/c/d": d + "/a/b/c/d",
|
||||
d + "/rel/..": d + "/a",
|
||||
d + "/dangling/y": d + "/missing/x/y",
|
||||
d + "/a/./b/": d + "/a/b",
|
||||
"/": "/",
|
||||
d + "/loop/z": d + "/loop/z",
|
||||
d + "/nope/../a/b/..": d + "/a",
|
||||
} {
|
||||
check(t, in, want, realpath(in))
|
||||
}
|
||||
}
|
||||
|
||||
type fakeEngine struct {
|
||||
calls []string
|
||||
specs map[string]*specgen.SpecGenerator
|
||||
pod *specgen.PodSpecGenerator
|
||||
nets map[string]types.Network
|
||||
apiDir string
|
||||
execCode int
|
||||
run func(ctx context.Context) (int, error)
|
||||
}
|
||||
|
||||
func (f *fakeEngine) call(c string) { f.calls = append(f.calls, c) }
|
||||
|
||||
func (f *fakeEngine) NetworkCreate(_ context.Context, n *types.Network, ignore bool) (types.Network, error) {
|
||||
f.call("network create " + n.Name)
|
||||
f.nets[n.Name] = *n
|
||||
sub, _ := types.ParseCIDR("10.89.7.0/24")
|
||||
return types.Network{Name: n.Name, Subnets: []types.Subnet{{Subnet: sub}}}, nil
|
||||
}
|
||||
func (f *fakeEngine) NetworkRemove(_ context.Context, name string) error {
|
||||
f.call("network rm " + name)
|
||||
return nil
|
||||
}
|
||||
func (f *fakeEngine) VolumeCreate(_ context.Context, name string) error {
|
||||
f.call("volume create " + name)
|
||||
return nil
|
||||
}
|
||||
func (f *fakeEngine) PodCreate(_ context.Context, s *specgen.PodSpecGenerator) error {
|
||||
f.call("pod create " + s.Name)
|
||||
f.pod = s
|
||||
return nil
|
||||
}
|
||||
func (f *fakeEngine) PodRemove(_ context.Context, name string) error {
|
||||
f.call("pod rm " + name)
|
||||
return nil
|
||||
}
|
||||
func (f *fakeEngine) ContainerCreate(_ context.Context, s *specgen.SpecGenerator) error {
|
||||
f.call("create " + s.Name)
|
||||
f.specs[s.Name] = s
|
||||
return nil
|
||||
}
|
||||
func (f *fakeEngine) ContainerStart(_ context.Context, name string) error {
|
||||
f.call("start " + name)
|
||||
return nil
|
||||
}
|
||||
func (f *fakeEngine) ContainerRun(ctx context.Context, name string) (int, error) {
|
||||
f.call("run " + name)
|
||||
if f.run != nil {
|
||||
return f.run(ctx)
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
func (f *fakeEngine) ContainerExec(_ context.Context, name string, cmd []string) (int, error) {
|
||||
f.call("exec " + name + " " + strings.Join(cmd, " "))
|
||||
return f.execCode, nil
|
||||
}
|
||||
func (f *fakeEngine) ContainerLogs(_ context.Context, name string, tail int) string {
|
||||
f.call("logs " + name)
|
||||
return "boom"
|
||||
}
|
||||
func (f *fakeEngine) ContainerRemove(_ context.Context, name string) error {
|
||||
f.call("rm " + name)
|
||||
return nil
|
||||
}
|
||||
|
||||
type env struct {
|
||||
work string
|
||||
good string
|
||||
fake *fakeEngine
|
||||
}
|
||||
|
||||
func setup(t *testing.T) *env {
|
||||
work, _ := filepath.EvalSymlinks(t.TempDir())
|
||||
for _, d := range []string{"home/proj", "other"} {
|
||||
_ = os.MkdirAll(filepath.Join(work, d), 0o755)
|
||||
}
|
||||
e := &env{work: work, good: writeList(t, "api.anthropic.com")}
|
||||
t.Setenv("HOME", work+"/home")
|
||||
t.Setenv("XDG_RUNTIME_DIR", work)
|
||||
t.Setenv("XDG_CONFIG_HOME", "")
|
||||
t.Setenv("AGENT_SANDBOX_ALLOWLIST", e.good)
|
||||
t.Setenv("AGENT_SANDBOX_IMAGE", "")
|
||||
t.Setenv("AGENT_SANDBOX_ROUTER_IMAGE", "")
|
||||
t.Setenv("AGENT_SANDBOX_HOME_VOLUME", "")
|
||||
orig := startService
|
||||
startService = func(dir string) (context.Context, engine, func(), error) {
|
||||
e.fake.call("service start")
|
||||
e.fake.apiDir = dir
|
||||
return context.Background(), e.fake, func() { e.fake.call("service stop") }, nil
|
||||
}
|
||||
t.Cleanup(func() { startService = orig })
|
||||
e.reset()
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *env) reset() {
|
||||
e.fake = &fakeEngine{specs: map[string]*specgen.SpecGenerator{}, nets: map[string]types.Network{}}
|
||||
}
|
||||
|
||||
func (e *env) launch(t *testing.T, dir string, args ...string) int {
|
||||
return e.launchCtx(t, context.Background(), dir, args...)
|
||||
}
|
||||
|
||||
func (e *env) launchCtx(t *testing.T, ctx context.Context, dir string, args ...string) int {
|
||||
t.Chdir(dir)
|
||||
t.Setenv("PWD", dir)
|
||||
return launch(ctx, args)
|
||||
}
|
||||
|
||||
func TestLaunchRefusals(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
dir string
|
||||
prep func(t *testing.T, e *env)
|
||||
}{
|
||||
{"bad-line", "home/proj", func(t *testing.T, e *env) {
|
||||
t.Setenv("AGENT_SANDBOX_ALLOWLIST", writeList(t, "ok.example.com", "bad host"))
|
||||
}},
|
||||
{"home", "home", nil},
|
||||
{"home-ancestor", ".", nil},
|
||||
{"root", "/", nil},
|
||||
{"home-symlink", "homelink", func(t *testing.T, e *env) { _ = os.Symlink(e.work+"/home", e.work+"/homelink") }},
|
||||
{"config-parent", "other", func(t *testing.T, e *env) { t.Setenv("XDG_CONFIG_HOME", e.work+"/other/cfg") }},
|
||||
{"allowlist-dir", "home/proj", func(t *testing.T, e *env) {
|
||||
t.Setenv("AGENT_SANDBOX_ALLOWLIST", e.work+"/home/proj/hosts")
|
||||
}},
|
||||
{"colon", "home/a:b", func(t *testing.T, e *env) { _ = os.Mkdir(e.work+"/home/a:b", 0o755) }},
|
||||
{"symlinked-allowlist-target", "home/dotfiles", func(t *testing.T, e *env) {
|
||||
_ = os.MkdirAll(e.work+"/home/dotfiles", 0o755)
|
||||
_ = os.MkdirAll(e.work+"/home/.config/agent-sandbox", 0o755)
|
||||
b, _ := os.ReadFile(e.good)
|
||||
_ = os.WriteFile(e.work+"/home/dotfiles/hosts", b, 0o644)
|
||||
_ = os.Symlink(e.work+"/home/dotfiles/hosts", e.work+"/home/.config/agent-sandbox/allowed-hosts")
|
||||
t.Setenv("XDG_CONFIG_HOME", e.work+"/home/.config")
|
||||
t.Setenv("AGENT_SANDBOX_ALLOWLIST", "")
|
||||
}},
|
||||
{"symlinked-config-dir", "home/dotfiles", func(t *testing.T, e *env) {
|
||||
_ = os.MkdirAll(e.work+"/home/dotfiles", 0o755)
|
||||
_ = os.Symlink(e.work+"/home/dotfiles", e.work+"/home/cfglink")
|
||||
t.Setenv("XDG_CONFIG_HOME", e.work+"/home/cfglink")
|
||||
t.Setenv("AGENT_SANDBOX_ALLOWLIST", "")
|
||||
}},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
e := setup(t)
|
||||
if c.prep != nil {
|
||||
c.prep(t, e)
|
||||
}
|
||||
dir := c.dir
|
||||
if !filepath.IsAbs(dir) {
|
||||
dir = filepath.Join(e.work, dir)
|
||||
}
|
||||
check(t, "exit", 1, e.launch(t, dir, "--exec", "true"))
|
||||
check(t, "podman calls", []string(nil), e.fake.calls)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func leftovers(t *testing.T, dir string) {
|
||||
t.Helper()
|
||||
entries, _ := os.ReadDir(dir)
|
||||
for _, d := range entries {
|
||||
if strings.HasPrefix(d.Name(), "agent-sandbox-") {
|
||||
t.Errorf("leftover %s", d.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLaunch(t *testing.T) {
|
||||
e := setup(t)
|
||||
dir := e.work + "/home/proj"
|
||||
var apiMode os.FileMode
|
||||
e.fake.run = func(context.Context) (int, error) {
|
||||
fi, err := os.Stat(e.fake.apiDir)
|
||||
if err == nil {
|
||||
apiMode = fi.Mode().Perm()
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
check(t, "exit", 0, e.launch(t, dir, "--exec", "true"))
|
||||
|
||||
f := e.fake
|
||||
session := strings.TrimPrefix(f.calls[3], "network create ")
|
||||
check(t, "session", true, regexp.MustCompile(`^agent-sandbox-\d+-\d+$`).MatchString(session))
|
||||
router, fwd, claude := session+"-router", session+"-forwarder", session+"-claude"
|
||||
check(t, "calls", []string{
|
||||
"service start",
|
||||
"network create agent-sandbox-egress",
|
||||
"volume create agent-sandbox-home",
|
||||
"network create " + session,
|
||||
"create " + router,
|
||||
"start " + router,
|
||||
"exec " + router + " nc -z 10.89.7.2 443",
|
||||
"pod create " + session,
|
||||
"create " + fwd,
|
||||
"start " + fwd,
|
||||
"exec " + fwd + " nc -z 127.0.0.1 443",
|
||||
"create " + claude,
|
||||
"run " + claude,
|
||||
"pod rm " + session,
|
||||
"rm " + router,
|
||||
"network rm " + session,
|
||||
"service stop",
|
||||
}, f.calls)
|
||||
|
||||
check(t, "egress net", types.Network{Name: "agent-sandbox-egress", Driver: "bridge", DNSEnabled: true}, f.nets["agent-sandbox-egress"])
|
||||
check(t, "session net", types.Network{Name: session, Driver: "bridge", Internal: true}, f.nets[session])
|
||||
|
||||
yes := true
|
||||
r := f.specs[router]
|
||||
tmp := r.Mounts[0].Source
|
||||
check(t, "tmp under runtime dir", true, strings.HasPrefix(tmp, e.work+"/"+session+"."))
|
||||
check(t, "api dir private", os.FileMode(0o700), apiMode)
|
||||
check(t, "api dir not mounted", false, strings.HasPrefix(f.apiDir, tmp))
|
||||
for _, d := range []string{tmp, f.apiDir} {
|
||||
if _, err := os.Stat(d); !os.IsNotExist(err) {
|
||||
t.Errorf("%s not removed", d)
|
||||
}
|
||||
}
|
||||
leftovers(t, e.work)
|
||||
|
||||
check(t, "router image", "localhost/agent-sandbox-router:dev", r.Image)
|
||||
check(t, "router rm", &yes, r.Remove)
|
||||
check(t, "router netns", specgen.Bridge, r.NetNS.NSMode)
|
||||
check(t, "router nets", map[string]types.PerNetworkOptions{
|
||||
session: {StaticIPs: []net.IP{net.ParseIP("10.89.7.2")}},
|
||||
"agent-sandbox-egress": {},
|
||||
}, r.Networks)
|
||||
check(t, "router mounts", []spec.Mount{{Type: "bind", Source: tmp, Destination: "/etc/router", Options: []string{"ro", "Z"}}}, r.Mounts)
|
||||
|
||||
p := f.pod
|
||||
check(t, "pod hostname", "agent-sandbox", p.Hostname)
|
||||
check(t, "pod netns", specgen.Bridge, p.NetNS.NSMode)
|
||||
check(t, "pod nets", map[string]types.PerNetworkOptions{session: {}}, p.Networks)
|
||||
check(t, "pod dns", []net.IP{net.ParseIP("10.89.7.2")}, p.DNSServer)
|
||||
check(t, "pod no-hosts", true, p.NoManageHosts)
|
||||
check(t, "pod userns", specgen.Namespace{NSMode: specgen.KeepID, Value: "uid=1000,gid=1000"}, p.Userns)
|
||||
|
||||
fw := f.specs[fwd]
|
||||
check(t, "fwd pod", session, fw.Pod)
|
||||
check(t, "fwd user", "0:0", fw.User)
|
||||
check(t, "fwd ro", &yes, fw.ReadOnlyFilesystem)
|
||||
check(t, "fwd caps", [][]string{{"all"}, {"NET_BIND_SERVICE"}}, [][]string{fw.CapDrop, fw.CapAdd})
|
||||
check(t, "fwd entrypoint", []string{"/usr/local/bin/forwarder"}, fw.Entrypoint)
|
||||
check(t, "fwd env", map[string]string{"FORWARDS": "0.0.0.0:443:10.89.7.2:443 0.0.0.0:80:10.89.7.2:80"}, fw.Env)
|
||||
check(t, "fwd rm", (*bool)(nil), fw.Remove)
|
||||
|
||||
c := f.specs[claude]
|
||||
check(t, "claude image", "localhost/agent-sandbox:dev", c.Image)
|
||||
check(t, "claude flags", []*bool{&yes, &yes, &yes}, []*bool{c.Remove, c.Stdin, c.Init})
|
||||
check(t, "claude tty", false, *c.Terminal)
|
||||
check(t, "claude pod", session, c.Pod)
|
||||
check(t, "claude cap-drop", []string{"NET_ADMIN", "NET_RAW", "SYS_ADMIN"}, c.CapDrop)
|
||||
check(t, "claude cap-add", []string(nil), c.CapAdd)
|
||||
check(t, "claude label", []string{"disable"}, c.SelinuxOpts)
|
||||
check(t, "claude devices", []spec.LinuxDevice{{Path: "/dev/fuse"}, {Path: "/dev/net/tun"}}, c.Devices)
|
||||
check(t, "claude volumes", []*specgen.NamedVolume{{Name: "agent-sandbox-home", Dest: "/home/claude"}}, c.Volumes)
|
||||
check(t, "claude mounts", []spec.Mount{{Type: "bind", Source: dir, Destination: dir}}, c.Mounts)
|
||||
check(t, "claude workdir", dir, c.WorkDir)
|
||||
check(t, "claude env", map[string]string{"DISABLE_AUTOUPDATER": "1", "CLAUDE_CODE_TMPDIR": "/var/tmp"}, c.Env)
|
||||
check(t, "claude cmd", []string{"true"}, c.Command)
|
||||
check(t, "claude privileged", (*bool)(nil), c.Privileged)
|
||||
}
|
||||
|
||||
func TestLaunchClaudeArgsAndOverrides(t *testing.T) {
|
||||
e := setup(t)
|
||||
t.Setenv("AGENT_SANDBOX_IMAGE", "img:1")
|
||||
t.Setenv("AGENT_SANDBOX_ROUTER_IMAGE", "rimg:1")
|
||||
t.Setenv("AGENT_SANDBOX_HOME_VOLUME", "vol")
|
||||
check(t, "exit", 0, e.launch(t, e.work+"/home/proj", "--continue", "-p", "hi"))
|
||||
for name, s := range e.fake.specs {
|
||||
switch {
|
||||
case strings.HasSuffix(name, "-claude"):
|
||||
check(t, "claude cmd", []string{"claude", "--continue", "-p", "hi"}, s.Command)
|
||||
check(t, "claude image", "img:1", s.Image)
|
||||
check(t, "volume", "vol", s.Volumes[0].Name)
|
||||
default:
|
||||
check(t, name+" image", "rimg:1", s.Image)
|
||||
}
|
||||
}
|
||||
check(t, "volume create", "volume create vol", e.fake.calls[2])
|
||||
}
|
||||
|
||||
func TestImageNamesPinVersion(t *testing.T) {
|
||||
t.Setenv("AGENT_SANDBOX_IMAGE", "")
|
||||
t.Setenv("AGENT_SANDBOX_ROUTER_IMAGE", "")
|
||||
defer func(v string) { version = v }(version)
|
||||
version = "v1.2.3"
|
||||
img, router := imageNames()
|
||||
check(t, "pinned", [2]string{registry + "/agent-sandbox:v1.2.3", registry + "/agent-sandbox-router:v1.2.3"}, [2]string{img, router})
|
||||
}
|
||||
|
||||
func TestLaunchExitCode(t *testing.T) {
|
||||
e := setup(t)
|
||||
e.fake.run = func(context.Context) (int, error) { return 7, nil }
|
||||
check(t, "exit", 7, e.launch(t, e.work+"/home/proj", "--exec", "false"))
|
||||
check(t, "cleanup last", "service stop", e.fake.calls[len(e.fake.calls)-1])
|
||||
}
|
||||
|
||||
func TestLaunchSignalCleansUp(t *testing.T) {
|
||||
e := setup(t)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
e.fake.run = func(opCtx context.Context) (int, error) {
|
||||
cancel()
|
||||
<-opCtx.Done()
|
||||
return 125, opCtx.Err()
|
||||
}
|
||||
check(t, "exit", 130, e.launchCtx(t, ctx, e.work+"/home/proj", "--exec", "sleep", "60"))
|
||||
n := len(e.fake.calls)
|
||||
check(t, "cleanup", []string{"pod rm", "rm", "network rm", "service stop"},
|
||||
[]string{e.fake.calls[n-4][:6], e.fake.calls[n-3][:2], e.fake.calls[n-2][:10], e.fake.calls[n-1]})
|
||||
leftovers(t, e.work)
|
||||
}
|
||||
|
||||
func TestLaunchServiceFails(t *testing.T) {
|
||||
e := setup(t)
|
||||
startService = func(string) (context.Context, engine, func(), error) { return nil, nil, nil, errors.New("no podman") }
|
||||
check(t, "exit", 125, e.launch(t, e.work+"/home/proj", "--exec", "true"))
|
||||
leftovers(t, e.work)
|
||||
}
|
||||
|
||||
func TestLaunchSeedsAllowlist(t *testing.T) {
|
||||
e := setup(t)
|
||||
t.Setenv("AGENT_SANDBOX_ALLOWLIST", "")
|
||||
t.Setenv("XDG_CONFIG_HOME", e.work+"/cfg")
|
||||
check(t, "exit", 0, e.launch(t, e.work+"/home/proj", "--exec", "true"))
|
||||
got, err := os.ReadFile(e.work + "/cfg/agent-sandbox/allowed-hosts")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
check(t, "seeded", string(defaultAllowlist), string(got))
|
||||
}
|
||||
|
||||
func TestLaunchRouterNeverStarts(t *testing.T) {
|
||||
e := setup(t)
|
||||
e.fake.execCode = 1
|
||||
check(t, "exit", 1, e.launch(t, e.work+"/home/proj", "--exec", "true"))
|
||||
for _, c := range e.fake.calls {
|
||||
if strings.HasPrefix(c, "pod create") {
|
||||
t.Error("pod created after router failed")
|
||||
}
|
||||
}
|
||||
check(t, "logs fetched", true, strings.Contains(strings.Join(e.fake.calls, "\n"), "logs "))
|
||||
check(t, "service stopped", "service stop", e.fake.calls[len(e.fake.calls)-1])
|
||||
}
|
||||
+13
-2
@@ -2,7 +2,7 @@
|
||||
name: agent-sandbox
|
||||
version: ${PACKAGE_VERSION}
|
||||
release: 1
|
||||
arch: noarch
|
||||
arch: amd64
|
||||
platform: linux
|
||||
description: "Run Claude Code in a rootless podman sandbox with allowlisted egress"
|
||||
maintainer: Ben Vincent <ben@unkin.net>
|
||||
@@ -11,7 +11,6 @@ license: MIT
|
||||
|
||||
depends:
|
||||
- podman
|
||||
- bash
|
||||
|
||||
contents:
|
||||
- src: dist/agent-sandbox
|
||||
@@ -22,3 +21,15 @@ contents:
|
||||
dst: /usr/share/agent-sandbox/allowed-hosts
|
||||
file_info:
|
||||
mode: 0644
|
||||
- src: dist/completions/agent-sandbox.bash
|
||||
dst: /usr/share/bash-completion/completions/agent-sandbox
|
||||
file_info:
|
||||
mode: 0644
|
||||
- src: dist/completions/_agent-sandbox
|
||||
dst: /usr/share/zsh/site-functions/_agent-sandbox
|
||||
file_info:
|
||||
mode: 0644
|
||||
- src: dist/completions/agent-sandbox.fish
|
||||
dst: /usr/share/fish/vendor_completions.d/agent-sandbox.fish
|
||||
file_info:
|
||||
mode: 0644
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/containers/podman/v5/libpod/define"
|
||||
"github.com/containers/podman/v5/pkg/api/handlers"
|
||||
"github.com/containers/podman/v5/pkg/bindings"
|
||||
"github.com/containers/podman/v5/pkg/bindings/containers"
|
||||
"github.com/containers/podman/v5/pkg/bindings/images"
|
||||
"github.com/containers/podman/v5/pkg/bindings/network"
|
||||
"github.com/containers/podman/v5/pkg/bindings/pods"
|
||||
"github.com/containers/podman/v5/pkg/bindings/volumes"
|
||||
entitiesTypes "github.com/containers/podman/v5/pkg/domain/entities/types"
|
||||
"github.com/containers/podman/v5/pkg/specgen"
|
||||
dockerContainer "github.com/docker/docker/api/types/container"
|
||||
"go.podman.io/common/libnetwork/types"
|
||||
)
|
||||
|
||||
// engine is the slice of the podman API a session uses.
|
||||
type engine interface {
|
||||
NetworkCreate(ctx context.Context, n *types.Network, ignoreIfExists bool) (types.Network, error)
|
||||
NetworkRemove(ctx context.Context, name string) error
|
||||
VolumeCreate(ctx context.Context, name string) error
|
||||
PodCreate(ctx context.Context, s *specgen.PodSpecGenerator) error
|
||||
PodRemove(ctx context.Context, name string) error
|
||||
ContainerCreate(ctx context.Context, s *specgen.SpecGenerator) error
|
||||
ContainerStart(ctx context.Context, name string) error
|
||||
// ContainerRun attaches stdio, starts the container and returns its exit code.
|
||||
ContainerRun(ctx context.Context, name string) (int, error)
|
||||
ContainerExec(ctx context.Context, name string, cmd []string) (int, error)
|
||||
ContainerLogs(ctx context.Context, name string, tail int) string
|
||||
ContainerRemove(ctx context.Context, name string) error
|
||||
}
|
||||
|
||||
// startService runs a private podman API service on a socket in dir and
|
||||
// returns the connection context, an engine bound to it and a stop func.
|
||||
var startService = func(dir string) (context.Context, engine, func(), error) {
|
||||
sock := filepath.Join(dir, "podman.sock")
|
||||
logPath := filepath.Join(dir, "service.log")
|
||||
logf, err := os.Create(logPath)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
defer func() { _ = logf.Close() }()
|
||||
cmd := exec.Command("podman", "system", "service", "--time=0", "unix://"+sock)
|
||||
cmd.Stdout, cmd.Stderr = logf, logf
|
||||
// Own process group: terminal signals must not kill it before cleanup.
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
if err := cmd.Start(); err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
exited := make(chan struct{})
|
||||
go func() { _ = cmd.Wait(); close(exited) }()
|
||||
stop := func() {
|
||||
_ = cmd.Process.Signal(syscall.SIGTERM)
|
||||
select {
|
||||
case <-exited:
|
||||
case <-time.After(5 * time.Second):
|
||||
_ = cmd.Process.Kill()
|
||||
<-exited
|
||||
}
|
||||
}
|
||||
fail := func(err error) (context.Context, engine, func(), error) {
|
||||
stop()
|
||||
b, _ := os.ReadFile(logPath)
|
||||
return nil, nil, nil, fmt.Errorf("podman API service: %w: %s", err, strings.TrimSpace(string(b)))
|
||||
}
|
||||
for deadline := time.Now().Add(30 * time.Second); ; {
|
||||
if c, err := net.Dial("unix", sock); err == nil {
|
||||
_ = c.Close()
|
||||
break
|
||||
}
|
||||
select {
|
||||
case <-exited:
|
||||
return fail(errors.New("exited"))
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
}
|
||||
if time.Now().After(deadline) {
|
||||
return fail(errors.New("timed out"))
|
||||
}
|
||||
}
|
||||
ctx, err := bindings.NewConnection(context.Background(), "unix://"+sock)
|
||||
if err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
return ctx, bindingsEngine{}, stop, nil
|
||||
}
|
||||
|
||||
type bindingsEngine struct{}
|
||||
|
||||
func (bindingsEngine) NetworkCreate(ctx context.Context, n *types.Network, ignore bool) (types.Network, error) {
|
||||
return network.CreateWithOptions(ctx, n, new(network.ExtraCreateOptions).WithIgnoreIfExists(ignore))
|
||||
}
|
||||
|
||||
func (bindingsEngine) NetworkRemove(ctx context.Context, name string) error {
|
||||
_, err := network.Remove(ctx, name, new(network.RemoveOptions).WithForce(true))
|
||||
return err
|
||||
}
|
||||
|
||||
func (bindingsEngine) VolumeCreate(ctx context.Context, name string) error {
|
||||
_, err := volumes.Create(ctx, entitiesTypes.VolumeCreateOptions{Name: name, IgnoreIfExists: true}, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func (bindingsEngine) PodCreate(ctx context.Context, s *specgen.PodSpecGenerator) error {
|
||||
_, err := pods.CreatePodFromSpec(ctx, &entitiesTypes.PodSpec{PodSpecGen: *s})
|
||||
return err
|
||||
}
|
||||
|
||||
func (bindingsEngine) PodRemove(ctx context.Context, name string) error {
|
||||
_, err := pods.Remove(ctx, name, new(pods.RemoveOptions).WithForce(true).WithTimeout(0))
|
||||
return err
|
||||
}
|
||||
|
||||
// ContainerCreate pulls a missing image first, as `podman run` does.
|
||||
func (bindingsEngine) ContainerCreate(ctx context.Context, s *specgen.SpecGenerator) error {
|
||||
if ok, err := images.Exists(ctx, s.Image, nil); err != nil {
|
||||
return err
|
||||
} else if !ok {
|
||||
var w io.Writer = os.Stderr
|
||||
if _, err := images.Pull(ctx, s.Image, new(images.PullOptions).WithPolicy("missing").WithProgressWriter(w)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
r, err := containers.CreateWithSpec(ctx, s, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, w := range r.Warnings {
|
||||
fmt.Fprintln(os.Stderr, w)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bindingsEngine) ContainerStart(ctx context.Context, name string) error {
|
||||
return containers.Start(ctx, name, nil)
|
||||
}
|
||||
|
||||
// ContainerRun mirrors podman-remote's startAndAttach: attach, start, then
|
||||
// wait right away so a --rm container can't be removed before we see its code.
|
||||
func (bindingsEngine) ContainerRun(ctx context.Context, name string) (int, error) {
|
||||
attached := make(chan error, 1)
|
||||
ready := make(chan bool)
|
||||
go func() {
|
||||
attached <- containers.Attach(ctx, name, os.Stdin, os.Stdout, os.Stderr, ready, new(containers.AttachOptions).WithStream(true))
|
||||
}()
|
||||
select {
|
||||
case <-ready:
|
||||
case err := <-attached:
|
||||
return define.ExitCode(err), err
|
||||
}
|
||||
if err := containers.Start(ctx, name, nil); err != nil {
|
||||
return define.ExitCode(err), err
|
||||
}
|
||||
type result struct {
|
||||
code int32
|
||||
err error
|
||||
}
|
||||
waited := make(chan result, 1)
|
||||
go func() {
|
||||
code, err := containers.Wait(ctx, name, nil)
|
||||
waited <- result{code, err}
|
||||
}()
|
||||
select {
|
||||
case err := <-attached:
|
||||
if errors.Is(err, define.ErrDetach) {
|
||||
return 0, nil
|
||||
}
|
||||
if err != nil {
|
||||
return 125, err
|
||||
}
|
||||
r := <-waited
|
||||
return int(r.code), r.err
|
||||
case r := <-waited:
|
||||
if r.err != nil {
|
||||
return 125, r.err
|
||||
}
|
||||
return int(r.code), <-attached
|
||||
}
|
||||
}
|
||||
|
||||
func (bindingsEngine) ContainerExec(ctx context.Context, name string, cmd []string) (int, error) {
|
||||
id, err := containers.ExecCreate(ctx, name, &handlers.ExecCreateConfig{ExecOptions: dockerContainer.ExecOptions{Cmd: cmd, AttachStderr: true}})
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
defer func() { _ = containers.ExecRemove(ctx, id, new(containers.ExecRemoveOptions).WithForce(true)) }()
|
||||
if err := containers.ExecStartAndAttach(ctx, id, new(containers.ExecStartAndAttachOptions).WithAttachError(true).WithErrorStream(io.Discard)); err != nil {
|
||||
return -1, err
|
||||
}
|
||||
s, err := containers.ExecInspect(ctx, id, nil)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
return s.ExitCode, nil
|
||||
}
|
||||
|
||||
func (bindingsEngine) ContainerLogs(ctx context.Context, name string, tail int) string {
|
||||
lines := make(chan string)
|
||||
done := make(chan struct{})
|
||||
var out []string
|
||||
go func() {
|
||||
for l := range lines {
|
||||
out = append(out, l)
|
||||
}
|
||||
close(done)
|
||||
}()
|
||||
opts := new(containers.LogOptions).WithStdout(true).WithStderr(true).WithTail(fmt.Sprint(tail))
|
||||
_ = containers.Logs(ctx, name, opts, lines, lines)
|
||||
close(lines)
|
||||
<-done
|
||||
return strings.Join(out, "\n")
|
||||
}
|
||||
|
||||
func (bindingsEngine) ContainerRemove(ctx context.Context, name string) error {
|
||||
_, err := containers.Remove(ctx, name, new(containers.RemoveOptions).WithForce(true).WithTimeout(0))
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// Every .domain entry resolves here; exact hosts get 127.1.x.y of their own.
|
||||
wildcardIP = "127.0.0.2"
|
||||
portBase = 10000
|
||||
)
|
||||
|
||||
var hostRE = regexp.MustCompile(`^\.?[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$`)
|
||||
|
||||
// entry is one allowlist line; port is empty for plain web (80/443) access.
|
||||
type entry struct{ host, port string }
|
||||
|
||||
// parseAllowlist returns the entries of allowlist file path, sorted bytewise
|
||||
// on "host port" and unique.
|
||||
func parseAllowlist(path string) ([]entry, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
seen := map[string]bool{}
|
||||
var keys []string
|
||||
sc := bufio.NewScanner(f)
|
||||
for n := 1; sc.Scan(); n++ {
|
||||
line, _, _ := strings.Cut(sc.Text(), "#")
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) == 0 {
|
||||
continue
|
||||
}
|
||||
if len(fields) > 1 {
|
||||
return nil, fmt.Errorf("%s:%d: one entry per line", path, n)
|
||||
}
|
||||
line = strings.ToLower(fields[0])
|
||||
host, port := line, ""
|
||||
if i := strings.LastIndex(line, ":"); i >= 0 {
|
||||
host, port = line[:i], line[i+1:]
|
||||
}
|
||||
if !hostRE.MatchString(host) {
|
||||
return nil, fmt.Errorf("%s:%d: invalid host '%s'", path, n, host)
|
||||
}
|
||||
if port != "" {
|
||||
p, err := strconv.Atoi(port)
|
||||
if err != nil || strings.ContainsAny(port, "+-") || p < 1 || p > 65535 {
|
||||
return nil, fmt.Errorf("%s:%d: invalid port '%s'", path, n, port)
|
||||
}
|
||||
if strings.HasPrefix(host, ".") {
|
||||
return nil, fmt.Errorf("%s:%d: ports need an exact host, not '%s'", path, n, host)
|
||||
}
|
||||
port = strconv.Itoa(p)
|
||||
if p == 80 || p == 443 {
|
||||
port = ""
|
||||
}
|
||||
}
|
||||
if k := host + " " + port; !seen[k] {
|
||||
seen[k] = true
|
||||
keys = append(keys, k)
|
||||
}
|
||||
}
|
||||
if err := sc.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sort.Strings(keys)
|
||||
entries := make([]entry, len(keys))
|
||||
for i, k := range keys {
|
||||
entries[i].host, entries[i].port, _ = strings.Cut(k, " ")
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
// routerConfig renders dnsmasq.conf and nginx.conf for the router listening on
|
||||
// rip, and the sandbox's loopback forwards as "bind:port:router:router_port"
|
||||
// words.
|
||||
func routerConfig(entries []entry, rip string) (dnsmasq, nginx, forwards string) {
|
||||
var hosts []string
|
||||
isHost := map[string]bool{}
|
||||
for _, e := range entries {
|
||||
if !isHost[e.host] {
|
||||
isHost[e.host] = true
|
||||
hosts = append(hosts, e.host)
|
||||
}
|
||||
}
|
||||
ip := map[string]string{}
|
||||
i := 0
|
||||
for _, h := range hosts {
|
||||
if !strings.HasPrefix(h, ".") {
|
||||
ip[h] = fmt.Sprintf("127.1.%d.%d", i/250, i%250+1)
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
var tcp strings.Builder
|
||||
fwd := []string{"0.0.0.0:443:" + rip + ":443", "0.0.0.0:80:" + rip + ":80"}
|
||||
j := 0
|
||||
for _, e := range entries {
|
||||
if e.port == "" {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(&tcp, " server { listen %s:%d; set $up %s:%s; proxy_pass $up; }\n", rip, portBase+j, e.host, e.port)
|
||||
fwd = append(fwd, fmt.Sprintf("%s:%s:%s:%d", ip[e.host], e.port, rip, portBase+j))
|
||||
j++
|
||||
}
|
||||
|
||||
var d strings.Builder
|
||||
fmt.Fprintf(&d, "no-resolv\nno-hosts\nbind-interfaces\nlisten-address=%s\naddress=/#/\n", rip)
|
||||
for _, h := range hosts {
|
||||
if strings.HasPrefix(h, ".") {
|
||||
fmt.Fprintf(&d, "address=/%s/%s\n", h[1:], wildcardIP)
|
||||
} else {
|
||||
fmt.Fprintf(&d, "host-record=%s,%s\n", h, ip[h])
|
||||
}
|
||||
}
|
||||
|
||||
// nginx "hostnames" rejects an exact key that a .domain key already covers.
|
||||
var m strings.Builder
|
||||
for _, h := range hosts {
|
||||
if !strings.HasPrefix(h, ".") && isHost["."+h] {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(&m, " %s 1;\n", h)
|
||||
}
|
||||
|
||||
nginx = strings.NewReplacer("@RIP@", rip, "@MAP@", m.String(), "@TCP@", tcp.String()).Replace(nginxTemplate)
|
||||
return d.String(), nginx, strings.Join(fwd, " ")
|
||||
}
|
||||
|
||||
// @RESOLVER@ is filled in by the router's start.sh.
|
||||
const nginxTemplate = `load_module /usr/lib/nginx/modules/ngx_stream_module.so;
|
||||
worker_processes 1;
|
||||
pid /tmp/nginx.pid;
|
||||
error_log stderr warn;
|
||||
events { worker_connections 1024; }
|
||||
|
||||
stream {
|
||||
resolver @RESOLVER@ ipv6=off valid=30s;
|
||||
map $ssl_preread_server_name $tls_allowed {
|
||||
hostnames;
|
||||
default 0;
|
||||
@MAP@
|
||||
}
|
||||
map $tls_allowed $tls_upstream {
|
||||
1 $ssl_preread_server_name:443;
|
||||
default 127.0.0.1:9;
|
||||
}
|
||||
server {
|
||||
listen @RIP@:443;
|
||||
ssl_preread on;
|
||||
proxy_pass $tls_upstream;
|
||||
}
|
||||
@TCP@}
|
||||
|
||||
http {
|
||||
resolver @RESOLVER@ ipv6=off valid=30s;
|
||||
access_log off;
|
||||
map $host $http_allowed {
|
||||
hostnames;
|
||||
default 0;
|
||||
@MAP@
|
||||
}
|
||||
server {
|
||||
listen @RIP@:80;
|
||||
if ($http_allowed = 0) { return 403; }
|
||||
location / {
|
||||
proxy_pass http://$host;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
+20
-10
@@ -1,16 +1,26 @@
|
||||
#!/usr/bin/env bash
|
||||
# Package the launcher and default allowlist into an RPM with nfpm. The
|
||||
# packaged launcher defaults to the published images for this version.
|
||||
# Package the (already built) agent-sandbox binary, its shell completions and
|
||||
# the default allowlist into an RPM with nfpm.
|
||||
# Usage: scripts/build-rpm.sh [version] (defaults to $CI_COMMIT_TAG)
|
||||
set -euo pipefail
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")/.."
|
||||
|
||||
TAG="${1:-${CI_COMMIT_TAG:-v0.0.0-dev}}"
|
||||
mkdir -p dist
|
||||
sed "s/^VERSION=dev$/VERSION=$TAG/" agent-sandbox >dist/agent-sandbox
|
||||
grep -q "^VERSION=$TAG$" dist/agent-sandbox
|
||||
VERSION="${1:-${CI_COMMIT_TAG:-v0.0.0-dev}}"
|
||||
[[ $VERSION == dev ]] && VERSION=v0.0.0-dev
|
||||
DIST=dist
|
||||
BINARY=agent-sandbox
|
||||
|
||||
export PACKAGE_VERSION="${TAG#v}"
|
||||
envsubst <packaging/nfpm.yaml >dist/nfpm.yaml
|
||||
nfpm pkg --config dist/nfpm.yaml --target dist --packager rpm
|
||||
ls -1 dist/*.rpm
|
||||
if [[ ! -f $DIST/$BINARY ]]; then
|
||||
echo "ERROR: $DIST/$BINARY not found; run 'make bin' first" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$DIST/completions"
|
||||
"./$DIST/$BINARY" completion bash >"$DIST/completions/$BINARY.bash"
|
||||
"./$DIST/$BINARY" completion zsh >"$DIST/completions/_$BINARY"
|
||||
"./$DIST/$BINARY" completion fish >"$DIST/completions/$BINARY.fish"
|
||||
|
||||
export PACKAGE_VERSION="${VERSION#v}"
|
||||
envsubst <packaging/nfpm.yaml >"$DIST/nfpm.yaml"
|
||||
nfpm pkg --config "$DIST/nfpm.yaml" --target "$DIST" --packager rpm
|
||||
ls -1 "$DIST"/*.rpm
|
||||
|
||||
-108
@@ -1,108 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Unit tests for the launcher's allowlist parsing and router config generation.
|
||||
# Usage: tests/test.sh
|
||||
# shellcheck disable=SC2016 # nginx variables are literal here
|
||||
set -uo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
# shellcheck source=agent-sandbox
|
||||
source ./agent-sandbox
|
||||
set +e
|
||||
|
||||
fails=0
|
||||
check() {
|
||||
local name=$1 want=$2 got=$3
|
||||
if [[ $want == "$got" ]]; then
|
||||
echo "ok $name"
|
||||
else
|
||||
echo "FAIL $name"
|
||||
diff <(echo "$want") <(echo "$got")
|
||||
fails=$((fails + 1))
|
||||
fi
|
||||
}
|
||||
list() {
|
||||
local f
|
||||
f=$(mktemp)
|
||||
printf '%s\n' "$@" >"$f"
|
||||
echo "$f"
|
||||
}
|
||||
|
||||
f=$(list '# comment' '' ' API.Anthropic.com # trailing' '.example.org' 'git.example.net:22' \
|
||||
'db:5432' 'db' 'web:443' 'web:80' 'git.example.net:5432')
|
||||
check entries "$(printf '%s\n' '.example.org ' 'api.anthropic.com ' 'db ' 'db 5432' \
|
||||
'git.example.net 22' 'git.example.net 5432' 'web ')" "$(allowlist_entries "$f")"
|
||||
|
||||
dir=$(mktemp -d)
|
||||
forwards=$(router_config "$f" "$dir" 10.89.7.2)
|
||||
check forwards "0.0.0.0:443:10.89.7.2:443 0.0.0.0:80:10.89.7.2:80 127.1.0.2:5432:10.89.7.2:10000 127.1.0.3:22:10.89.7.2:10001 127.1.0.3:5432:10.89.7.2:10002" "$forwards"
|
||||
|
||||
check dnsmasq "$(printf '%s\n' no-resolv no-hosts bind-interfaces listen-address=10.89.7.2 'address=/#/' \
|
||||
'address=/example.org/127.0.0.2' 'host-record=api.anthropic.com,127.1.0.1' 'host-record=db,127.1.0.2' \
|
||||
'host-record=git.example.net,127.1.0.3' 'host-record=web,127.1.0.4')" "$(cat "$dir/dnsmasq.conf")"
|
||||
check dnsmasq-never-forwards "" "$(grep -E '^(server|resolv-file)=' "$dir/dnsmasq.conf")"
|
||||
|
||||
nginx=$(cat "$dir/nginx.conf")
|
||||
check tls-map "$(printf ' %s 1;\n' .example.org api.anthropic.com db git.example.net web)" \
|
||||
"$(sed -n '/map \$ssl_preread_server_name/,/}/p' <<<"$nginx" | grep ' 1;$')"
|
||||
check http-map "$(printf ' %s 1;\n' .example.org api.anthropic.com db git.example.net web)" \
|
||||
"$(sed -n '/map \$host/,/}/p' <<<"$nginx" | grep ' 1;$')"
|
||||
check tls-default-deny " default 127.0.0.1:9;" "$(grep 'default 127' <<<"$nginx")"
|
||||
check http-default-deny " if (\$http_allowed = 0) { return 403; }" "$(grep 'return 403' <<<"$nginx")"
|
||||
check tcp-servers "$(printf '%s\n' \
|
||||
' server { listen 10.89.7.2:10000; set $up db:5432; proxy_pass $up; }' \
|
||||
' server { listen 10.89.7.2:10001; set $up git.example.net:22; proxy_pass $up; }' \
|
||||
' server { listen 10.89.7.2:10002; set $up git.example.net:5432; proxy_pass $up; }')" \
|
||||
"$(grep 'set \$up' <<<"$nginx")"
|
||||
check listens-on-router-only "" "$(grep -E 'listen ' <<<"$nginx" | grep -v 'listen 10\.89\.7\.2:')"
|
||||
|
||||
f=$(list '.example.org' 'example.org' 'example.org:22' 'a.example.org' 'example.org')
|
||||
dir=$(mktemp -d)
|
||||
router_config "$f" "$dir" 10.89.7.2 >/dev/null
|
||||
check overlap-dnsmasq "$(printf '%s\n' 'address=/example.org/127.0.0.2' 'host-record=a.example.org,127.1.0.1' \
|
||||
'host-record=example.org,127.1.0.2')" "$(grep -E '^(address=/e|host-record)' "$dir/dnsmasq.conf")"
|
||||
check overlap-map "$(printf ' %s 1;\n' .example.org a.example.org .example.org a.example.org)" \
|
||||
"$(grep '^ .* 1;$' "$dir/nginx.conf")"
|
||||
|
||||
for bad in 'exa mple.com' 'example.com:0' 'example.com:70000' 'example.com:ssh' '*.example.com' \
|
||||
'-bad.com' 'http://example.com' 'a..b' '.example.com:22'; do
|
||||
(allowlist_entries "$(list "$bad")") >/dev/null 2>&1
|
||||
check "rejects '$bad'" 1 "$?"
|
||||
done
|
||||
|
||||
check default-allowlist-parses 0 "$( (allowlist_entries allowed-hosts >/dev/null) 2>&1; echo $?)"
|
||||
|
||||
f=$(list 'ok.example.com' 'bad host' 'z.example.com')
|
||||
check router-config-fails-on-bad-line 1 "$( (router_config "$f" "$(mktemp -d)" 10.89.7.2) >/dev/null 2>&1; echo $?)"
|
||||
|
||||
# Launch through main with a stub podman that logs its calls.
|
||||
work=$(mktemp -d)
|
||||
mkdir -p "$work/bin" "$work/home/proj" "$work/other"
|
||||
printf '%s\n' '#!/bin/sh' "echo \"\$*\" >>$work/podman.log" \
|
||||
'case "$*" in "network inspect"*) echo 10.89.7.0/24 ;; esac' >"$work/bin/podman"
|
||||
chmod +x "$work/bin/podman"
|
||||
launch() {
|
||||
local dir=$1
|
||||
shift
|
||||
rm -f "$work/podman.log"
|
||||
(cd "$dir" && env PATH="$work/bin:$PATH" HOME="$work/home" XDG_RUNTIME_DIR="$work" "$@" \
|
||||
"$OLDPWD/agent-sandbox" --exec true) >/dev/null 2>&1
|
||||
echo "$? $([[ -s $work/podman.log ]] && echo podman || echo none)"
|
||||
}
|
||||
good=$(list 'api.anthropic.com')
|
||||
check launch-from-home-subdir "0 podman" "$(launch "$work/home/proj" AGENT_SANDBOX_ALLOWLIST="$good")"
|
||||
check launch-refuses-bad-line "1 none" "$(launch "$work/home/proj" AGENT_SANDBOX_ALLOWLIST="$(list 'ok.example.com' 'bad host')")"
|
||||
check launch-refuses-home "1 none" "$(launch "$work/home" AGENT_SANDBOX_ALLOWLIST="$good")"
|
||||
check launch-refuses-home-ancestor "1 none" "$(launch "$work" AGENT_SANDBOX_ALLOWLIST="$good")"
|
||||
check launch-refuses-root "1 none" "$(launch / AGENT_SANDBOX_ALLOWLIST="$good")"
|
||||
ln -s "$work/home" "$work/homelink"
|
||||
check launch-refuses-home-symlink "1 none" "$(launch "$work/homelink" AGENT_SANDBOX_ALLOWLIST="$good")"
|
||||
check launch-refuses-config-parent "1 none" "$(launch "$work/other" XDG_CONFIG_HOME="$work/other/cfg" AGENT_SANDBOX_ALLOWLIST="$good")"
|
||||
check launch-refuses-allowlist-dir "1 none" "$(launch "$work/home/proj" AGENT_SANDBOX_ALLOWLIST="$work/home/proj/hosts")"
|
||||
mkdir -p "$work/home/dotfiles" "$work/home/.config/agent-sandbox"
|
||||
cp "$good" "$work/home/dotfiles/hosts"
|
||||
ln -s "$work/home/dotfiles/hosts" "$work/home/.config/agent-sandbox/allowed-hosts"
|
||||
check launch-refuses-symlinked-allowlist-target "1 none" "$(launch "$work/home/dotfiles" XDG_CONFIG_HOME="$work/home/.config")"
|
||||
rm "$work/home/.config/agent-sandbox/allowed-hosts"
|
||||
ln -s "$work/home/dotfiles" "$work/home/cfglink"
|
||||
check launch-refuses-symlinked-config-dir "1 none" "$(launch "$work/home/dotfiles" XDG_CONFIG_HOME="$work/home/cfglink")"
|
||||
|
||||
((fails == 0)) || { echo "$fails failed"; exit 1; }
|
||||
Reference in New Issue
Block a user