Add the configure app
This commit is contained in:
@@ -202,6 +202,44 @@ Use "sibling list" to see what's installed and "sibling remove {name}" to uninst
|
||||
|
||||
To make your own repository work as a sibling project, see [skills/sibling-project-format.md](skills/sibling-project-format.md).
|
||||
|
||||
## Configure
|
||||
|
||||
Some of what CoreDesktop installs needs you to sign in or make a choice before you can use it. To see what's set up and set up the rest, run in the terminal:
|
||||
|
||||
```
|
||||
configure
|
||||
```
|
||||
|
||||
It covers:
|
||||
|
||||
- **Your Federated Computer Core.** Give it your Superpanel's address (e.g. https://superpanel.example.com), your Core email, name and password, and it adds Superpanel to your applications, joins this computer to your Core's Tailscale network, adds your Nextcloud (calendar, contacts and files) and email to GNOME's Online Accounts, and mounts your Nextcloud files at /mnt/example.com ("example.com" in the Files sidebar). For Nextcloud and the files it creates a Nextcloud app password, which you can revoke in Nextcloud under Settings > Security.
|
||||
- Signing in to Claude Code, opencode and Hermes Agent and choosing their model providers.
|
||||
|
||||
To set up one thing, name it, e.g. `configure hermes`; `configure --list` shows the names.
|
||||
|
||||
## Sandboxed AI agents
|
||||
|
||||
CoreDesktop installs the AI coding agents opencode, Hermes Agent and Claude Code, and runs them in a sandbox. In the sandbox, an agent:
|
||||
|
||||
- can change files only in the directory you start it from (plus its own settings and a few package caches), so start it from a project directory
|
||||
- can't read your SSH keys, password stores, browser profiles, cloud and Git credentials, or the other agents' logins
|
||||
- can reach only AI providers and common developer sites: GitHub, Gitea, npm, PyPI, crates.io, RubyGems, the Go module proxy and archlinux.org
|
||||
|
||||
Typing `opencode`, `hermes` or `hermes-agent` runs it in the sandbox. To run one without it, type `unsandboxed opencode`. Hermes' gateway, the background service that lets you talk to Hermes through Matrix and other messaging apps, runs in the sandbox too, and can reach the Matrix server in Hermes' settings. For other messaging services, add their domains as shown below. Claude Code uses its own built-in sandbox for every command it runs (see `/sandbox` inside Claude).
|
||||
|
||||
Before using an agent the first time, sign in and choose a model provider with `configure` (see below).
|
||||
|
||||
If an agent needs a site or folder the sandbox blocks, add it in `~/.config/coredesktop/sandbox/base.json` (for every agent) or `~/.config/coredesktop/sandbox/opencode.json` (for one agent):
|
||||
|
||||
```json
|
||||
{
|
||||
"network": { "allowedDomains": ["docs.example.com"] },
|
||||
"filesystem": { "allowWrite": ["~/notes"] }
|
||||
}
|
||||
```
|
||||
|
||||
The sandbox is Anthropic's [sandbox runtime](https://github.com/anthropics/sandbox-runtime), which uses bubblewrap and socat. The default rules are in [sandbox/config/](sandbox/config/).
|
||||
|
||||
## mDNS Networking
|
||||
|
||||
If you want to use mDNS networking on your LAN (the ability to log in to other local machines using the format "machine.local"), then you should replace /etc/nsswitch.conf with the one in this repository and add /etc/mdns.allow. These two files are formatted to avoid any conflicts with systemd-resolved that may cause nss-mdns not to work. See: https://wiki.archlinux.org/title/Avahi#Hostname_resolution (section 4.2) for more information.
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Purpose : Set up the things CoreDesktop installs that need your input, such
|
||||
# as connecting to your Federated Computer Core and signing in to
|
||||
# the AI agents.
|
||||
#
|
||||
# Each file in configure.d/ is one item. It sets TITLE and DESCRIPTION and
|
||||
# defines is_configured and run_configure. It can also define is_installed;
|
||||
# the item is left out when that fails. To add an item, add a file there.
|
||||
|
||||
# Wrapped in { ...; exit; } so bash reads the whole script up front; update
|
||||
# replaces this file while it runs.
|
||||
{
|
||||
|
||||
COREDESKTOP="$HOME/.local/share/coredesktop"
|
||||
PATH="$HOME/.local/bin:$PATH"
|
||||
|
||||
# Run an agent's real command rather than its sandboxed one in sandbox/bin:
|
||||
# setting an agent up writes outside its sandbox
|
||||
unsandboxed() {
|
||||
"$COREDESKTOP/sandbox/bin/unsandboxed" "$@"
|
||||
}
|
||||
|
||||
# Ask a yes/no question; the default is no
|
||||
confirm() {
|
||||
local answer
|
||||
read -rp "$1 [y/N] " answer
|
||||
[[ "$answer" == [yY]* ]]
|
||||
}
|
||||
|
||||
# Ask a yes/no question; the default is yes
|
||||
confirm_yes() {
|
||||
local answer
|
||||
read -rp "$1 [Y/n] " answer
|
||||
[[ "$answer" != [nN]* ]]
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat << EOF
|
||||
Usage: configure [item]
|
||||
|
||||
configure Show what's set up and choose what to set up
|
||||
configure <item> Set up one item
|
||||
configure --list Show what's set up, and the item names
|
||||
|
||||
EOF
|
||||
}
|
||||
|
||||
# configure.d/30-claude.sh -> claude
|
||||
item_name() {
|
||||
local name
|
||||
name="$(basename "$1" .sh)"
|
||||
echo "${name#[0-9][0-9]-}"
|
||||
}
|
||||
|
||||
# Prints "<state>|<title>|<description>"; state is done, todo or absent
|
||||
item_info() {
|
||||
(
|
||||
source "$1"
|
||||
if declare -f is_installed > /dev/null && ! is_installed &> /dev/null; then
|
||||
echo "absent||"
|
||||
elif is_configured &> /dev/null; then
|
||||
echo "done|$TITLE|$DESCRIPTION"
|
||||
else
|
||||
echo "todo|$TITLE|$DESCRIPTION"
|
||||
fi
|
||||
)
|
||||
}
|
||||
|
||||
configure_item() {
|
||||
(
|
||||
source "$1"
|
||||
echo
|
||||
echo -e "\e[1m$TITLE\e[0m"
|
||||
run_configure
|
||||
)
|
||||
local status=$?
|
||||
if [ $status -ne 0 ]; then
|
||||
echo -e "\e[31m$(item_name "$1") was not set up.\e[0m"
|
||||
fi
|
||||
return $status
|
||||
}
|
||||
|
||||
# Fill ITEMS, STATES, TITLES and DESCRIPTIONS with the installed items
|
||||
load_items() {
|
||||
local file state title description
|
||||
ITEMS=() STATES=() TITLES=() DESCRIPTIONS=()
|
||||
for file in "$COREDESKTOP"/configure.d/*.sh; do
|
||||
[ -f "$file" ] || continue
|
||||
IFS='|' read -r state title description < <(item_info "$file")
|
||||
[ "$state" = absent ] && continue
|
||||
ITEMS+=("$file") STATES+=("$state") TITLES+=("$title") DESCRIPTIONS+=("$description")
|
||||
done
|
||||
}
|
||||
|
||||
show_items() {
|
||||
local i mark
|
||||
for i in "${!ITEMS[@]}"; do
|
||||
if [ "${STATES[$i]}" = done ]; then
|
||||
mark="\e[32mdone\e[0m "
|
||||
else
|
||||
mark="\e[33mto do\e[0m"
|
||||
fi
|
||||
printf " %2d) $mark %-22s %s\n" $((i + 1)) "${TITLES[$i]}" "${DESCRIPTIONS[$i]}"
|
||||
done
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
"") ;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0 ;;
|
||||
-l|--list)
|
||||
load_items
|
||||
for i in "${!ITEMS[@]}"; do
|
||||
printf '%-6s %-14s %s\n' "${STATES[$i]/todo/to do}" "$(item_name "${ITEMS[$i]}")" "${TITLES[$i]}"
|
||||
done
|
||||
exit 0 ;;
|
||||
*)
|
||||
for file in "$COREDESKTOP"/configure.d/*.sh; do
|
||||
if [ "$(item_name "$file")" = "$1" ]; then
|
||||
configure_item "$file"
|
||||
exit
|
||||
fi
|
||||
done
|
||||
echo "No item named $1. These are: configure --list"
|
||||
exit 2 ;;
|
||||
esac
|
||||
|
||||
while true; do
|
||||
load_items
|
||||
echo
|
||||
echo "CoreDesktop configuration"
|
||||
echo
|
||||
show_items
|
||||
echo
|
||||
echo " a) Set up everything still to do"
|
||||
echo " q) Quit"
|
||||
echo
|
||||
read -rp "Choose: " choice || exit 0
|
||||
|
||||
case "$choice" in
|
||||
q|Q|"")
|
||||
exit 0 ;;
|
||||
a|A)
|
||||
for i in "${!ITEMS[@]}"; do
|
||||
[ "${STATES[$i]}" = todo ] && configure_item "${ITEMS[$i]}"
|
||||
done ;;
|
||||
*)
|
||||
if [[ "$choice" =~ ^[0-9]+$ ]] && (( choice >= 1 && choice <= ${#ITEMS[@]} )); then
|
||||
configure_item "${ITEMS[$((choice - 1))]}"
|
||||
else
|
||||
echo "Choose a number from the list, a or q."
|
||||
fi ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
Executable
+315
@@ -0,0 +1,315 @@
|
||||
#!/bin/bash
|
||||
|
||||
TITLE="Federated Computer Core"
|
||||
DESCRIPTION="Superpanel, Tailscale, Nextcloud, email and files"
|
||||
|
||||
# What you entered last time, so running this again only does what's missing
|
||||
CORE_CONF="$HOME/.config/coredesktop/core.conf"
|
||||
GOA_ACCOUNT="$COREDESKTOP/configure.d/lib/goa-account"
|
||||
[ -f "$CORE_CONF" ] && source "$CORE_CONF"
|
||||
|
||||
# CORE_DOMAIN, CORE_EMAIL and CORE_NAME come from core.conf; the rest follow
|
||||
core_names() {
|
||||
CORE_USER="${CORE_EMAIL%@*}"
|
||||
NEXTCLOUD="https://nextcloud.$CORE_DOMAIN"
|
||||
WEBDAV="$NEXTCLOUD/remote.php/dav/files/$CORE_USER"
|
||||
MOUNT="/mnt/$CORE_DOMAIN"
|
||||
SUPERPANEL_DESKTOP="$HOME/.local/share/applications/core-$CORE_DOMAIN-superpanel.desktop"
|
||||
}
|
||||
|
||||
superpanel_done() {
|
||||
[ -f "$SUPERPANEL_DESKTOP" ]
|
||||
}
|
||||
|
||||
# Tailscale has no status field naming its login server, so core.conf
|
||||
# records that it was set up for this Core
|
||||
tailscale_done() {
|
||||
[ "$CORE_TAILSCALE" = "$CORE_DOMAIN" ] \
|
||||
&& tailscale status --json 2> /dev/null | jq -e '.BackendState == "Running"' > /dev/null
|
||||
}
|
||||
|
||||
nextcloud_done() {
|
||||
"$GOA_ACCOUNT" exists owncloud "$CORE_USER@nextcloud.$CORE_DOMAIN" 2> /dev/null
|
||||
}
|
||||
|
||||
email_done() {
|
||||
"$GOA_ACCOUNT" exists imap_smtp "$CORE_EMAIL" 2> /dev/null
|
||||
}
|
||||
|
||||
files_done() {
|
||||
grep -qs " $MOUNT davfs " /etc/fstab && grep -qs "^$MOUNT " "$HOME/.davfs2/secrets"
|
||||
}
|
||||
|
||||
is_configured() {
|
||||
[ -n "$CORE_DOMAIN" ] || return 1
|
||||
core_names
|
||||
superpanel_done && tailscale_done && nextcloud_done && email_done && files_done
|
||||
}
|
||||
|
||||
save_conf() {
|
||||
mkdir -p "$(dirname "$CORE_CONF")"
|
||||
{
|
||||
printf 'CORE_DOMAIN=%q\n' "$CORE_DOMAIN"
|
||||
printf 'CORE_EMAIL=%q\n' "$CORE_EMAIL"
|
||||
printf 'CORE_NAME=%q\n' "$CORE_NAME"
|
||||
printf 'CORE_TAILSCALE=%q\n' "$CORE_TAILSCALE"
|
||||
} > "$CORE_CONF"
|
||||
}
|
||||
|
||||
# Basic auth header for curl, read from stdin (-H @-) so the password isn't
|
||||
# in the process list
|
||||
auth_header() {
|
||||
printf 'Authorization: Basic %s\n' "$(printf '%s:%s' "$1" "$2" | base64 -w0)"
|
||||
}
|
||||
|
||||
# Create a Nextcloud app password for the Online Accounts account and the
|
||||
# files mount, so they don't keep your Core password. It shows in Nextcloud
|
||||
# under Settings > Security as "CoreDesktop on <this computer>", where it can
|
||||
# be revoked. Returns 2 if Nextcloud says the password is wrong.
|
||||
nextcloud_app_password() {
|
||||
local response code
|
||||
response="$(auth_header "$CORE_USER" "$PASSWORD" | curl -s -w '\n%{http_code}' -H @- \
|
||||
-H 'OCS-APIRequest: true' -A "CoreDesktop on $HOSTNAME" \
|
||||
"$NEXTCLOUD/ocs/v2.php/core/getapppassword?format=json")"
|
||||
code="${response##*$'\n'}"
|
||||
case "$code" in
|
||||
200) APP_PASSWORD="$(jq -r '.ocs.data.apppassword // empty' <<< "${response%$'\n'*}")"
|
||||
[ -n "$APP_PASSWORD" ] ;;
|
||||
401) return 2 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
desktop_entry() {
|
||||
local file="$1" name="$2" comment="$3" url="$4" icon="$5"
|
||||
mkdir -p "$(dirname "$file")"
|
||||
cat > "$file" << EOF
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=$name
|
||||
Comment=$comment
|
||||
Exec=xdg-open $url
|
||||
Icon=$icon
|
||||
Categories=Network;
|
||||
EOF
|
||||
}
|
||||
|
||||
setup_superpanel() {
|
||||
local url="https://superpanel.$CORE_DOMAIN"
|
||||
local icon="$HOME/.local/share/icons/core-$CORE_DOMAIN-superpanel.png"
|
||||
|
||||
# The login page carries Superpanel's icon inline
|
||||
mkdir -p "$(dirname "$icon")"
|
||||
if ! curl -s "$url/" | grep -o 'rel="icon"[^>]*base64,[^"]*' | head -n 1 \
|
||||
| sed 's/.*base64,//' | base64 -d > "$icon" 2> /dev/null || [ ! -s "$icon" ]; then
|
||||
rm -f "$icon"
|
||||
icon="applications-internet"
|
||||
fi
|
||||
desktop_entry "$SUPERPANEL_DESKTOP" "Superpanel" "Manage your Federated Computer Core ($CORE_DOMAIN)" "$url" "$icon"
|
||||
echo "Added Superpanel to your applications."
|
||||
}
|
||||
|
||||
setup_tailscale() {
|
||||
# --reset and --force-reauth move the machine from any other tailnet;
|
||||
# --operator lets you, and the Tailscale GNOME extension, use it without sudo
|
||||
echo "Open the link below to sign in to your Core's Tailscale network."
|
||||
sudo tailscale up --reset --force-reauth --login-server "https://headscale.$CORE_DOMAIN" \
|
||||
--accept-routes --operator="$USER" || return 1
|
||||
CORE_TAILSCALE="$CORE_DOMAIN"
|
||||
save_conf
|
||||
}
|
||||
|
||||
setup_nextcloud() {
|
||||
[ -n "$APP_PASSWORD" ] || { echo "This needs your Core password, and Nextcloud didn't accept it."; return 1; }
|
||||
"$GOA_ACCOUNT" add << EOF || return 1
|
||||
$(jq -n --arg user "$CORE_USER" --arg host "nextcloud.$CORE_DOMAIN" --arg url "$NEXTCLOUD" \
|
||||
--arg password "$APP_PASSWORD" '{
|
||||
provider: "owncloud",
|
||||
identity: $user,
|
||||
presentation_identity: "\($user)@\($host)",
|
||||
credentials: {password: $password},
|
||||
details: {
|
||||
Uri: "\($url)/remote.php/webdav",
|
||||
FilesEnabled: "true",
|
||||
CalendarEnabled: "true",
|
||||
CalDavUri: "\($url)/remote.php/dav",
|
||||
ContactsEnabled: "true",
|
||||
CardDavUri: "\($url)/remote.php/dav",
|
||||
AcceptSslErrors: "false"
|
||||
}}')
|
||||
EOF
|
||||
echo "Added Nextcloud to Online Accounts (calendar, contacts and files)."
|
||||
}
|
||||
|
||||
# The mail server publishes its settings (Thunderbird's autoconfig format).
|
||||
# Prints: imap_host imap_port imap_socket smtp_host smtp_port smtp_socket username
|
||||
mail_settings() {
|
||||
curl -sf --max-time 15 \
|
||||
"https://autoconfig.$CORE_DOMAIN/mail/config-v1.1.xml?emailaddress=$CORE_EMAIL" \
|
||||
| python3 -I -c '
|
||||
import sys, xml.etree.ElementTree as ET
|
||||
provider = ET.parse(sys.stdin).getroot().find("emailProvider")
|
||||
imap = provider.find("incomingServer[@type=\"imap\"]")
|
||||
smtp = provider.find("outgoingServer[@type=\"smtp\"]")
|
||||
print(*(s.findtext(f) for s in (imap, smtp) for f in ("hostname", "port", "socketType")),
|
||||
imap.findtext("username"))'
|
||||
}
|
||||
|
||||
# Online Accounts takes host:port, and only the host for the usual port
|
||||
host_port() {
|
||||
if [ "$2" = "$3" ]; then echo "$1"; else echo "$1:$2"; fi
|
||||
}
|
||||
|
||||
setup_email() {
|
||||
[ -n "$PASSWORD" ] || { echo "This needs your Core password."; return 1; }
|
||||
local imap_host imap_port imap_socket smtp_host smtp_port smtp_socket username
|
||||
if ! read -r imap_host imap_port imap_socket smtp_host smtp_port smtp_socket username < <(mail_settings) \
|
||||
|| [ -z "$smtp_socket" ]; then
|
||||
echo "The mail server didn't give its settings; using mail.$CORE_DOMAIN with the usual ports."
|
||||
imap_host="mail.$CORE_DOMAIN" imap_port=993 imap_socket=SSL
|
||||
smtp_host="mail.$CORE_DOMAIN" smtp_port=587 smtp_socket=STARTTLS
|
||||
username="%EMAILADDRESS%"
|
||||
fi
|
||||
case "$username" in
|
||||
%EMAILLOCALPART%) username="$CORE_USER" ;;
|
||||
*) username="$CORE_EMAIL" ;;
|
||||
esac
|
||||
[ "$imap_socket" = SSL ] && imap_std=993 || imap_std=143
|
||||
[ "$smtp_socket" = SSL ] && smtp_std=465 || smtp_std=587
|
||||
|
||||
"$GOA_ACCOUNT" add << EOF || return 1
|
||||
$(jq -n --arg email "$CORE_EMAIL" --arg name "$CORE_NAME" --arg user "$username" \
|
||||
--arg password "$PASSWORD" \
|
||||
--arg imap "$(host_port "$imap_host" "$imap_port" "$imap_std")" --arg imap_socket "$imap_socket" \
|
||||
--arg smtp "$(host_port "$smtp_host" "$smtp_port" "$smtp_std")" --arg smtp_socket "$smtp_socket" '{
|
||||
provider: "imap_smtp",
|
||||
identity: $email,
|
||||
presentation_identity: $email,
|
||||
credentials: {"imap-password": $password, "smtp-password": $password},
|
||||
details: {
|
||||
Enabled: "true",
|
||||
EmailAddress: $email,
|
||||
Name: $name,
|
||||
ImapHost: $imap,
|
||||
ImapUserName: $user,
|
||||
ImapUseSsl: (if $imap_socket == "SSL" then "true" else "false" end),
|
||||
ImapUseTls: (if $imap_socket == "STARTTLS" then "true" else "false" end),
|
||||
ImapAcceptSslErrors: "false",
|
||||
SmtpHost: $smtp,
|
||||
SmtpUseAuth: "true",
|
||||
SmtpUserName: $user,
|
||||
SmtpAuthLogin: "false",
|
||||
SmtpAuthPlain: "true",
|
||||
SmtpUseSsl: (if $smtp_socket == "SSL" then "true" else "false" end),
|
||||
SmtpUseTls: (if $smtp_socket == "STARTTLS" then "true" else "false" end),
|
||||
SmtpAcceptSslErrors: "false"
|
||||
}}')
|
||||
EOF
|
||||
echo "Added $CORE_EMAIL to Online Accounts."
|
||||
}
|
||||
|
||||
# Mount your Nextcloud files at /mnt/<domain> with davfs2. You can mount it
|
||||
# without sudo once it's in /etc/fstab with "user" and you're in Arch's davfs2
|
||||
# group (network). It's mounted now and each time you log in.
|
||||
setup_files() {
|
||||
[ -n "$APP_PASSWORD" ] || { echo "This needs your Core password, and Nextcloud didn't accept it."; return 1; }
|
||||
local secret
|
||||
|
||||
sudo install -d -o "$USER" -g "$(id -gn)" "$MOUNT" || return 1
|
||||
if ! grep -qs " $MOUNT davfs " /etc/fstab; then
|
||||
echo "$WEBDAV $MOUNT davfs rw,user,noauto,uid=$USER,_netdev 0 0" | sudo tee -a /etc/fstab > /dev/null || return 1
|
||||
fi
|
||||
if ! id -nG "$USER" | grep -qw network; then
|
||||
sudo usermod -aG network "$USER" || return 1
|
||||
fi
|
||||
|
||||
# davfs2 only reads this file if no one else can
|
||||
mkdir -p -m 700 "$HOME/.davfs2"
|
||||
touch "$HOME/.davfs2/secrets"
|
||||
chmod 600 "$HOME/.davfs2/secrets"
|
||||
secret="${APP_PASSWORD//\\/\\\\}"
|
||||
secret="${secret//\"/\\\"}"
|
||||
sed -i "\\#^$MOUNT #d" "$HOME/.davfs2/secrets"
|
||||
printf '%s %s "%s"\n' "$MOUNT" "$CORE_USER" "$secret" >> "$HOME/.davfs2/secrets"
|
||||
|
||||
mkdir -p "$HOME/.config/autostart"
|
||||
cat > "$HOME/.config/autostart/core-$CORE_DOMAIN-files.desktop" << EOF
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Nextcloud files ($CORE_DOMAIN)
|
||||
Comment=Mount your Nextcloud files at $MOUNT
|
||||
Exec=sh -c 'mountpoint -q $MOUNT || mount $MOUNT'
|
||||
X-GNOME-Autostart-enabled=true
|
||||
NoDisplay=true
|
||||
EOF
|
||||
|
||||
# Shown in the Files sidebar as the domain
|
||||
local bookmarks="$HOME/.config/gtk-3.0/bookmarks"
|
||||
mkdir -p "$(dirname "$bookmarks")"
|
||||
grep -qs "^file://$MOUNT " "$bookmarks" || echo "file://$MOUNT $CORE_DOMAIN" >> "$bookmarks"
|
||||
|
||||
# sg picks up the network group now, without logging out and in again
|
||||
if ! mountpoint -q "$MOUNT"; then
|
||||
sg network -c "mount $MOUNT" || return 1
|
||||
fi
|
||||
echo "Your Nextcloud files are at $MOUNT (\"$CORE_DOMAIN\" in the Files sidebar)."
|
||||
}
|
||||
|
||||
# Run a step unless it's done; a failed step doesn't stop the ones after it
|
||||
step() {
|
||||
local name="$1" title="$2"
|
||||
if "${name}_done"; then
|
||||
echo " $title: already set up"
|
||||
return 0
|
||||
fi
|
||||
if ! confirm_yes "Set up $title?"; then
|
||||
return 0
|
||||
fi
|
||||
"setup_$name" || { echo -e "\e[31m$title was not set up.\e[0m"; STATUS=1; }
|
||||
}
|
||||
|
||||
run_configure() {
|
||||
local url
|
||||
STATUS=0
|
||||
|
||||
read -erp "Your Superpanel's address (e.g. https://superpanel.example.com): " \
|
||||
-i "${CORE_DOMAIN:+https://superpanel.$CORE_DOMAIN}" url
|
||||
url="${url#*://}"
|
||||
url="${url%%/*}"
|
||||
CORE_DOMAIN="${url#superpanel.}"
|
||||
if [ -z "$CORE_DOMAIN" ] || ! curl -s -o /dev/null --max-time 15 "https://superpanel.$CORE_DOMAIN/"; then
|
||||
echo "Couldn't reach https://superpanel.$CORE_DOMAIN."
|
||||
return 1
|
||||
fi
|
||||
|
||||
read -erp "Your Core email: " -i "${CORE_EMAIL:-$USER@$CORE_DOMAIN}" CORE_EMAIL
|
||||
read -erp "Your name, as shown on email you send: " \
|
||||
-i "${CORE_NAME:-$(getent passwd "$USER" | cut -d: -f5 | cut -d, -f1)}" CORE_NAME
|
||||
[[ "$CORE_EMAIL" == *@* ]] || { echo "That isn't an email address."; return 1; }
|
||||
core_names
|
||||
save_conf
|
||||
|
||||
# Nextcloud, email and files need your Core password; Nextcloud checks it
|
||||
if ! nextcloud_done || ! email_done || ! files_done; then
|
||||
local tries
|
||||
for tries in 1 2 3; do
|
||||
read -rsp "Your Core password: " PASSWORD
|
||||
echo
|
||||
nextcloud_app_password
|
||||
case $? in
|
||||
0) break ;;
|
||||
2) echo "Nextcloud says that password is wrong."; PASSWORD="" ;;
|
||||
*) echo "Couldn't reach Nextcloud at $NEXTCLOUD; trying the password for email anyway."; break ;;
|
||||
esac
|
||||
done
|
||||
fi
|
||||
|
||||
echo
|
||||
step superpanel "Superpanel in your applications"
|
||||
step tailscale "Tailscale with your Core (headscale.$CORE_DOMAIN)"
|
||||
step nextcloud "Nextcloud in Online Accounts"
|
||||
step email "Email in Online Accounts"
|
||||
step files "Nextcloud files at $MOUNT"
|
||||
unset PASSWORD APP_PASSWORD
|
||||
return $STATUS
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
|
||||
TITLE="Claude Code"
|
||||
DESCRIPTION="Sign in to Anthropic"
|
||||
|
||||
is_installed() {
|
||||
command -v claude
|
||||
}
|
||||
|
||||
is_configured() {
|
||||
claude auth status
|
||||
}
|
||||
|
||||
run_configure() {
|
||||
echo " 1) Claude subscription (Pro, Max, Team or Enterprise)"
|
||||
echo " 2) Anthropic Console (pay for API usage)"
|
||||
local choice
|
||||
read -rp "How do you pay for Claude? [1] " choice
|
||||
case "$choice" in
|
||||
""|1) claude auth login ;;
|
||||
2) claude auth login --console ;;
|
||||
*) echo "Choose 1 or 2."; return 1 ;;
|
||||
esac
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
|
||||
TITLE="opencode"
|
||||
DESCRIPTION="Sign in to AI providers"
|
||||
|
||||
is_installed() {
|
||||
[ -x /usr/bin/opencode ]
|
||||
}
|
||||
|
||||
# Signed in to at least one provider
|
||||
is_configured() {
|
||||
jq -e 'length > 0' "$HOME/.local/share/opencode/auth.json"
|
||||
}
|
||||
|
||||
run_configure() {
|
||||
echo "Choose a provider and sign in or paste its API key. You can pick the model inside opencode with /models."
|
||||
unsandboxed opencode auth login || return 1
|
||||
while confirm "Sign in to another provider?"; do
|
||||
unsandboxed opencode auth login || return 1
|
||||
done
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
|
||||
TITLE="Hermes Agent"
|
||||
DESCRIPTION="Choose a model provider and tools"
|
||||
|
||||
is_installed() {
|
||||
[ -x "$HOME/.local/bin/hermes" ]
|
||||
}
|
||||
|
||||
# Signed in with OAuth (e.g. Nous Portal), or an API key is set
|
||||
is_configured() {
|
||||
[ -s "$HOME/.hermes/auth.json" ] || grep -qE '^[A-Z_]*API_KEY=.+' "$HOME/.hermes/.env"
|
||||
}
|
||||
|
||||
run_configure() {
|
||||
echo " 1) Nous Portal (one sign-in covers the model, web search, images and more)"
|
||||
echo " 2) Another provider (OpenRouter, OpenAI, Anthropic or your own endpoint)"
|
||||
local choice
|
||||
read -rp "Which provider? [1] " choice
|
||||
case "$choice" in
|
||||
""|1) unsandboxed hermes setup --portal ;;
|
||||
2) unsandboxed hermes setup ;;
|
||||
*) echo "Choose 1 or 2."; return 1 ;;
|
||||
esac
|
||||
}
|
||||
Executable
+80
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Add an account to GNOME Online Accounts, the way Settings does.
|
||||
|
||||
Settings adds accounts by calling AddAccount on the Online Accounts service,
|
||||
which saves the account and keeps its password in the GNOME keyring. This
|
||||
calls the same method. The account comes on stdin rather than as arguments,
|
||||
so its password doesn't show up in the process list.
|
||||
|
||||
Usage:
|
||||
goa-account exists <provider> <presentation-identity>
|
||||
goa-account add < account.json
|
||||
|
||||
account.json:
|
||||
{"provider": "owncloud", "identity": "...", "presentation_identity": "...",
|
||||
"credentials": {"password": "..."}, "details": {"Uri": "...", ...}}
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
|
||||
import gi
|
||||
|
||||
gi.require_version("Gio", "2.0")
|
||||
from gi.repository import Gio, GLib # noqa: E402
|
||||
|
||||
BUS_NAME = "org.gnome.OnlineAccounts"
|
||||
|
||||
|
||||
def call(bus, path, interface, method, parameters, reply_type):
|
||||
return bus.call_sync(BUS_NAME, path, interface, method, parameters,
|
||||
GLib.VariantType(reply_type), Gio.DBusCallFlags.NONE,
|
||||
-1, None).unpack()
|
||||
|
||||
|
||||
def accounts(bus):
|
||||
(objects,) = call(bus, "/org/gnome/OnlineAccounts",
|
||||
"org.freedesktop.DBus.ObjectManager", "GetManagedObjects",
|
||||
None, "(a{oa{sa{sv}}})")
|
||||
for interfaces in objects.values():
|
||||
account = interfaces.get("org.gnome.OnlineAccounts.Account")
|
||||
if account:
|
||||
yield account
|
||||
|
||||
|
||||
def exists(bus, provider, presentation_identity):
|
||||
return any(account.get("ProviderType") == provider
|
||||
and account.get("PresentationIdentity") == presentation_identity
|
||||
for account in accounts(bus))
|
||||
|
||||
|
||||
def add(bus, account):
|
||||
credentials = {key: GLib.Variant("s", value)
|
||||
for key, value in account["credentials"].items()}
|
||||
parameters = GLib.Variant("(sssa{sv}a{ss})", (
|
||||
account["provider"], account["identity"],
|
||||
account["presentation_identity"], credentials, account["details"]))
|
||||
call(bus, "/org/gnome/OnlineAccounts/Manager",
|
||||
"org.gnome.OnlineAccounts.Manager", "AddAccount", parameters, "(o)")
|
||||
|
||||
|
||||
def main():
|
||||
bus = Gio.bus_get_sync(Gio.BusType.SESSION, None)
|
||||
if len(sys.argv) == 4 and sys.argv[1] == "exists":
|
||||
return 0 if exists(bus, sys.argv[2], sys.argv[3]) else 1
|
||||
if len(sys.argv) == 2 and sys.argv[1] == "add":
|
||||
account = json.load(sys.stdin)
|
||||
if exists(bus, account["provider"], account["presentation_identity"]):
|
||||
return 0
|
||||
add(bus, account)
|
||||
return 0
|
||||
print(__doc__.strip().split("\n\n")[2], file=sys.stderr)
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
sys.exit(main())
|
||||
except GLib.Error as error:
|
||||
print(f"GNOME Online Accounts: {error.message}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
+8
-1
@@ -53,12 +53,13 @@ cd ~/.local/share/coredesktop
|
||||
# Source utility functions (provides install_packages and other helpers)
|
||||
source utils.sh
|
||||
|
||||
# Install the refresh, update, sibling and version scripts to the user's bin
|
||||
# Install the refresh, update, sibling, version and configure scripts to the user's bin
|
||||
mkdir -p ~/.local/bin
|
||||
install_script ~/.local/share/coredesktop/refresh ~/.local/bin/refresh
|
||||
install_script ~/.local/share/coredesktop/update ~/.local/bin/update
|
||||
install_script ~/.local/share/coredesktop/sibling ~/.local/bin/sibling
|
||||
install_script ~/.local/share/coredesktop/version ~/.local/bin/version
|
||||
install_script ~/.local/share/coredesktop/configure ~/.local/bin/configure
|
||||
|
||||
# Source the package list (defines SYSTEM_UTILS, DEV_TOOLS, DESKTOP, etc. arrays)
|
||||
if [ ! -f "packages.conf" ]; then
|
||||
@@ -150,6 +151,10 @@ echo "Installing Kickstart Neovim..."
|
||||
echo "Installing Claude Code..."
|
||||
bash claude.sh
|
||||
|
||||
# Run AI agents (opencode, hermes) in a sandbox, and turn on Claude Code's own
|
||||
echo "Setting up sandboxed AI agents..."
|
||||
bash ~/.local/share/coredesktop/sandbox/setup.sh
|
||||
|
||||
# Install Migrate Backup System
|
||||
curl -L -o migrate https://github.com/CyphrRiot/Migrate/raw/main/bin/migrate
|
||||
chmod +x migrate
|
||||
@@ -218,3 +223,5 @@ if ! ls /usr/lib/modules/$(uname -r) > /dev/null 2>&1; then
|
||||
else
|
||||
echo "Kernel is up to date, no reboot needed. You may want to consider a reboot if other significant software was updated or installed."
|
||||
fi
|
||||
|
||||
echo "To connect to your Federated Computer Core and sign in to the AI agents, run: configure"
|
||||
|
||||
@@ -4,6 +4,7 @@ SYSTEM_UTILS=(
|
||||
bash-completion
|
||||
bluez
|
||||
bluez-utils
|
||||
bubblewrap
|
||||
borg
|
||||
btop
|
||||
cadaver
|
||||
@@ -79,11 +80,13 @@ DEV_TOOLS=(
|
||||
neovim
|
||||
nodejs
|
||||
npm
|
||||
opencode
|
||||
pandoc
|
||||
python-docutils
|
||||
python-myst-parser
|
||||
python-pillow
|
||||
python-pip
|
||||
python-gobject
|
||||
python-pipx
|
||||
python-pygments
|
||||
rust
|
||||
|
||||
Executable
+119
@@ -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" "$@"
|
||||
}
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../agent
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../agent
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../agent
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../agent
|
||||
@@ -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": []
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"network": {
|
||||
"allowedDomains": []
|
||||
},
|
||||
"filesystem": {
|
||||
"allowRead": [
|
||||
"~/.hermes"
|
||||
],
|
||||
"allowWrite": [
|
||||
"~/.hermes"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -6,6 +6,11 @@
|
||||
|
||||
COREDESKTOP_SHELL="$HOME/.local/share/coredesktop/shell"
|
||||
|
||||
# AI agents such as opencode and hermes run sandboxed (see sandbox/agent), so
|
||||
# sandbox/bin must come before the directories holding their real commands
|
||||
SANDBOX_BIN="$HOME/.local/share/coredesktop/sandbox/bin"
|
||||
PATH="$SANDBOX_BIN:${PATH//$SANDBOX_BIN:/}"
|
||||
|
||||
# History: keep a lot of it, skip duplicates, and don't let shells overwrite
|
||||
# each other's history on exit
|
||||
shopt -s histappend
|
||||
|
||||
@@ -43,6 +43,7 @@ install_script "$COREDESKTOP/update" ~/.local/bin/update
|
||||
install_script "$COREDESKTOP/refresh" ~/.local/bin/refresh
|
||||
install_script "$COREDESKTOP/sibling" ~/.local/bin/sibling
|
||||
install_script "$COREDESKTOP/version" ~/.local/bin/version
|
||||
install_script "$COREDESKTOP/configure" ~/.local/bin/configure
|
||||
|
||||
if [[ -z "$UPDATE_REEXECED" ]]; then
|
||||
export UPDATE_REEXECED=1
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
|
||||
COREDESKTOP="$HOME/.local/share/coredesktop"
|
||||
|
||||
run_update() {
|
||||
local status=0
|
||||
bash "$COREDESKTOP/sandbox/setup.sh" || status=1
|
||||
|
||||
# The real command, not the sandboxed one in sandbox/bin: updating Hermes
|
||||
# writes outside what its sandbox allows
|
||||
if [ -x "$HOME/.local/bin/hermes" ]; then
|
||||
echo "Updating Hermes Agent..."
|
||||
"$HOME/.local/bin/hermes" update < /dev/null || status=1
|
||||
fi
|
||||
return $status
|
||||
}
|
||||
Reference in New Issue
Block a user