Add the configure app

This commit is contained in:
2026-10-26 15:16:53 -06:00
parent 63d56da089
commit bbd6b83f64
21 changed files with 1010 additions and 1 deletions
Executable
+119
View File
@@ -0,0 +1,119 @@
#!/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/coredesktop/sandbox/base.json and <agent>.json, in
# the same format. Later files add to the lists in earlier ones. Domains in
# COREDESKTOP_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/coredesktop/sandbox"
USER_CONFIG="$HOME/.config/coredesktop/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}/coredesktop-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 "$COREDESKTOP_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[@]}")
exec "$srt" --settings "$settings" -- "$real" "$@"
}
+1
View File
@@ -0,0 +1 @@
../agent
+1
View File
@@ -0,0 +1 @@
../agent
+1
View File
@@ -0,0 +1 @@
../agent
+1
View File
@@ -0,0 +1 @@
../agent
+82
View File
@@ -0,0 +1,82 @@
{
"network": {
"allowedDomains": [
"anthropic.com",
"*.anthropic.com",
"openai.com",
"*.openai.com",
"chatgpt.com",
"*.chatgpt.com",
"openrouter.ai",
"*.openrouter.ai",
"nousresearch.com",
"*.nousresearch.com",
"generativelanguage.googleapis.com",
"api.mistral.ai",
"api.deepseek.com",
"api.x.ai",
"api.groq.com",
"api.together.xyz",
"*.githubcopilot.com",
"github.com",
"*.github.com",
"*.githubusercontent.com",
"gitea.federated.computer",
"registry.npmjs.org",
"registry.yarnpkg.com",
"pypi.org",
"files.pythonhosted.org",
"crates.io",
"*.crates.io",
"rubygems.org",
"*.rubygems.org",
"proxy.golang.org",
"sum.golang.org",
"archlinux.org",
"*.archlinux.org"
],
"deniedDomains": []
},
"filesystem": {
"denyRead": [
"~/.ssh",
"~/.gnupg",
"~/.password-store",
"~/.local/share/keyrings",
"~/.netrc",
"~/.git-credentials",
"~/.aws",
"~/.azure",
"~/.config/gcloud",
"~/.kube",
"~/.docker",
"~/.config/gh",
"~/.config/tea",
"~/.config/borg",
"~/.config/Bitwarden",
"~/.config/Element",
"~/.mozilla",
"~/.config/chromium",
"~/.config/google-chrome",
"~/.config/BraveSoftware",
"~/.config/net.imput.helium",
"~/.claude",
"~/.claude.json",
"~/.hermes",
"~/.config/opencode",
"~/.local/share/opencode"
],
"allowRead": [],
"allowWrite": [
".",
"/tmp",
"~/.npm",
"~/.cache/pip",
"~/.cargo/registry",
"~/.cargo/git",
"~/go/pkg/mod"
],
"denyWrite": []
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"network": {
"allowedDomains": []
},
"filesystem": {
"allowRead": [
"~/.hermes"
],
"allowWrite": [
"~/.hermes"
]
}
}
+21
View File
@@ -0,0 +1,21 @@
{
"network": {
"allowedDomains": [
"opencode.ai",
"*.opencode.ai",
"models.dev"
]
},
"filesystem": {
"allowRead": [
"~/.config/opencode",
"~/.local/share/opencode"
],
"allowWrite": [
"~/.config/opencode",
"~/.local/share/opencode",
"~/.local/state/opencode",
"~/.cache/opencode"
]
}
}
+78
View File
@@ -0,0 +1,78 @@
#!/bin/bash
#
# Purpose : Set up sandboxed AI agents (see sandbox/agent): install or update
# Anthropic's sandbox runtime (srt) and Hermes Agent, and turn on
# Claude Code's built-in sandbox. Run by install.sh and on every
# update (updates.d/72-sandbox.sh).
COREDESKTOP="$HOME/.local/share/coredesktop"
STATUS=0
# npm's global prefix is under /usr, so install to ~/.local; srt lands in ~/.local/bin
echo "Installing the sandbox runtime (srt)..."
npm install --global --prefix "$HOME/.local" --no-fund --no-audit @anthropic-ai/sandbox-runtime || STATUS=1
# --non-interactive skips Hermes' setup questions and its background gateway
# service; `configure hermes` chooses a model provider afterwards
if [ ! -x "$HOME/.local/bin/hermes" ]; then
echo "Installing Hermes Agent..."
curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash -s -- --non-interactive || STATUS=1
fi
# Claude Code runs every command it starts in its own sandbox, also built on
# bubblewrap and socat; it can't run inside srt because bubblewrap can't be
# nested. Anything already in the sandbox section of your settings wins over
# these defaults.
CLAUDE_SETTINGS="$HOME/.claude/settings.json"
if [ -f "$CLAUDE_SETTINGS" ]; then
domains="$(jq '.network.allowedDomains' "$COREDESKTOP/sandbox/config/base.json")"
tmp="$(mktemp "$CLAUDE_SETTINGS.XXXXXX")"
if jq --argjson domains "$domains" \
'.sandbox = ({enabled: true, network: {allowedDomains: $domains}} * (.sandbox // {}))' \
"$CLAUDE_SETTINGS" > "$tmp"; then
if cmp -s "$tmp" "$CLAUDE_SETTINGS"; then
rm -f "$tmp"
else
echo "Turning on Claude Code's sandbox..."
mv -f "$tmp" "$CLAUDE_SETTINGS"
fi
else
echo "Couldn't read $CLAUDE_SETTINGS; Claude Code's sandbox was not turned on."
rm -f "$tmp"
STATUS=1
fi
fi
# Hermes' gateway acts on messages from outside this machine, so it runs in
# the sandbox too: a systemd drop-in starts it through sandbox/bin/hermes. It
# also needs to reach the Matrix server in Hermes' settings, which differs on
# each machine; for other messaging services, add their domains to
# ~/.config/coredesktop/sandbox/hermes.json.
GATEWAY_UNIT="$HOME/.config/systemd/user/hermes-gateway.service"
if [ -f "$GATEWAY_UNIT" ]; then
if [ -x "$HOME/.local/bin/srt" ]; then
homeserver="$(sed -nE "s#^MATRIX_HOMESERVER=[\"']?https?://([^/:\"']+).*#\1#p" "$HOME/.hermes/.env" 2> /dev/null | head -n 1)"
dropin="$GATEWAY_UNIT.d/coredesktop-sandbox.conf"
tmp="$(mktemp)"
cat > "$tmp" << EOF
# Written by CoreDesktop (sandbox/setup.sh): run Hermes' gateway in the sandbox
[Service]
Environment="COREDESKTOP_SANDBOX_DOMAINS=$homeserver"
ExecStart=
ExecStart=%h/.local/share/coredesktop/sandbox/bin/hermes gateway run
EOF
if cmp -s "$tmp" "$dropin"; then
rm -f "$tmp"
else
echo "Running Hermes' gateway in the sandbox..."
mkdir -p "$(dirname "$dropin")" && mv -f "$tmp" "$dropin" && chmod 644 "$dropin" \
&& systemctl --user daemon-reload \
&& systemctl --user try-restart hermes-gateway.service || STATUS=1
fi
else
echo "Hermes' gateway isn't sandboxed yet: the sandbox (srt) isn't installed."
STATUS=1
fi
fi
exit $STATUS