harmonize with coredesktop
This commit is contained in:
Executable
+127
@@ -0,0 +1,127 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Purpose : Run an AI agent inside Anthropic's sandbox runtime (srt). srt uses
|
||||
# bubblewrap to limit which files the agent can read and write, and
|
||||
# removes its network; socat links it to a proxy outside the sandbox
|
||||
# that only lets through the domains in its config.
|
||||
#
|
||||
# sandbox/bin/<agent> links here and shell/bashrc puts sandbox/bin first on
|
||||
# PATH, so typing an agent's name runs it sandboxed. `unsandboxed <agent>` runs
|
||||
# the agent's real command instead.
|
||||
#
|
||||
# The config is sandbox/config/base.json, then sandbox/config/<agent>.json,
|
||||
# then your own ~/.config/miniserver/sandbox/base.json and <agent>.json, in
|
||||
# the same format. Later files add to the lists in earlier ones. Domains in
|
||||
# MINISERVER_SANDBOX_DOMAINS (space-separated) are allowed too; setup.sh uses
|
||||
# it for Hermes' gateway, whose messaging server differs on each machine.
|
||||
|
||||
# Wrapped in { ...; exit; } so bash reads the whole script up front; update
|
||||
# replaces this file (git pull) while agents are running.
|
||||
{
|
||||
|
||||
SANDBOX="$HOME/.local/share/MiniServer/sandbox"
|
||||
USER_CONFIG="$HOME/.config/miniserver/sandbox"
|
||||
name="$(basename "$0")"
|
||||
|
||||
# The agent's real command: the first one on PATH outside sandbox/bin
|
||||
real_command() {
|
||||
local dir bin_dir IFS=:
|
||||
bin_dir="$(realpath "$SANDBOX/bin")"
|
||||
for dir in $PATH; do
|
||||
[ -n "$dir" ] && [ "$(realpath "$dir" 2> /dev/null)" != "$bin_dir" ] || continue
|
||||
if [ -x "$dir/$1" ] && [ ! -d "$dir/$1" ]; then
|
||||
echo "$dir/$1"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
if [ "$name" = "unsandboxed" ]; then
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "Usage: unsandboxed <agent> [arguments]"
|
||||
exit 2
|
||||
fi
|
||||
agent="$1"
|
||||
shift
|
||||
real="$(real_command "$agent")" || { echo "$agent isn't installed."; exit 127; }
|
||||
exec "$real" "$@"
|
||||
fi
|
||||
|
||||
agent="$name"
|
||||
real="$(real_command "$agent")" || { echo "$agent isn't installed."; exit 127; }
|
||||
|
||||
# Commands that share another agent's sandbox config
|
||||
case "$agent" in
|
||||
hermes-agent) profile=hermes ;;
|
||||
*) profile="$agent" ;;
|
||||
esac
|
||||
|
||||
srt="$(command -v srt || echo "$HOME/.local/bin/srt")"
|
||||
if [ ! -x "$srt" ]; then
|
||||
echo "The sandbox (srt) isn't installed; run update. To run $agent without it: unsandboxed $agent"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The agent can change anything in the directory it starts in
|
||||
case "$PWD" in
|
||||
"$HOME"|/)
|
||||
echo "Start $agent from a project directory. In the sandbox it can change anything in the directory it starts in, and this is $PWD."
|
||||
exit 1 ;;
|
||||
esac
|
||||
|
||||
configs=("$SANDBOX/config/base.json")
|
||||
[ -f "$SANDBOX/config/$profile.json" ] && configs+=("$SANDBOX/config/$profile.json")
|
||||
ours=("${configs[@]}")
|
||||
for file in "$USER_CONFIG/base.json" "$USER_CONFIG/$profile.json"; do
|
||||
[ -f "$file" ] && configs+=("$file")
|
||||
done
|
||||
|
||||
# Objects are merged and lists joined, so later files only add to earlier ones
|
||||
MERGE='
|
||||
def merge(a; b):
|
||||
reduce (b | to_entries[]) as $e (a;
|
||||
.[$e.key] |= (
|
||||
if type == "object" and ($e.value | type) == "object" then merge(.; $e.value)
|
||||
elif type == "array" and ($e.value | type) == "array" then . + $e.value | unique
|
||||
else $e.value end));
|
||||
reduce .[] as $config ({}; merge(.; $config))'
|
||||
|
||||
# srt runs in place of this script (exec below), so a stop signal, e.g. from
|
||||
# systemd, reaches it and it passes it on to the agent. That leaves nothing to
|
||||
# remove the settings file afterwards, so it's named after this process, and
|
||||
# files whose process has ended are removed here.
|
||||
SETTINGS_DIR="${XDG_RUNTIME_DIR:-/tmp}/miniserver-sandbox"
|
||||
mkdir -p -m 700 "$SETTINGS_DIR" || exit 1
|
||||
for old in "$SETTINGS_DIR"/*.json; do
|
||||
pid="${old%.json}"
|
||||
pid="${pid##*.}"
|
||||
[[ "$pid" =~ ^[0-9]+$ ]] && ! kill -0 "$pid" 2> /dev/null && rm -f "$old"
|
||||
done
|
||||
|
||||
# Outside the sandbox's writable paths, so the agent can't change its own rules
|
||||
settings="$SETTINGS_DIR/$agent.$$.json"
|
||||
if ! jq -s --arg domains "$MINISERVER_SANDBOX_DOMAINS" \
|
||||
"$MERGE"' | .network.allowedDomains += ($domains | split(" ") | map(select(. != "")))' \
|
||||
"${configs[@]}" > "$settings"; then
|
||||
echo "Couldn't read the sandbox config: ${configs[*]}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Bubblewrap can only make writable a directory that exists, and the agent
|
||||
# can't create one itself in a parent it may not write to
|
||||
while IFS= read -r path; do
|
||||
path="${path/#\~/$HOME}"
|
||||
case "$path" in "$HOME"/*) mkdir -p "$path" ;; esac
|
||||
done < <(jq -rs "$MERGE | .filesystem.allowWrite[]? | strings" "${ours[@]}")
|
||||
|
||||
# srt sets TMPDIR to /tmp/claude, which nothing creates, unless told otherwise;
|
||||
# temp files then fail, and GTK aborts. /tmp is writable in every profile.
|
||||
export CLAUDE_CODE_TMPDIR=/tmp
|
||||
|
||||
# Hermes' terminal reaches its gateway, in another sandbox, through a relay
|
||||
case "$profile" in
|
||||
hermes) exec "$srt" --settings "$settings" -- bash "$SANDBOX/hermes-bridge" "$real" "$@" ;;
|
||||
esac
|
||||
exec "$srt" --settings "$settings" -- "$real" "$@"
|
||||
}
|
||||
Reference in New Issue
Block a user