harmonize with coredesktop

This commit is contained in:
David Young
2026-10-28 14:35:07 -06:00
parent 866720c3c1
commit 9b9f1d5fdd
35 changed files with 1445 additions and 8 deletions
Executable
+127
View File
@@ -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" "$@"
}