Resources
A resource is a named remote machine that the file_*, exec and tmux tools act on via the target parameter. SSH is the only transport; the schema carries a type so another one can be added without breaking config.
Configuration
Resources are global — defined once in ~/.somora/config.yaml, visible to every agent unless an agent's agent.yaml denies them.
resources:
build-server:
type: ssh
host: 192.0.2.10 # example IP from RFC 5737 doc range
port: 22 # optional, default 22
user: alice
keyPath: ~/.ssh/id_ed25519 # path on the somora server
description: |
macOS build host. Has Homebrew + standard CLI toolchain.
workspace: /Users/alice/work # optional default cwd for relative paths
# hostKey: 'sha256:abc...' # optional strict-mode pin; TOFU when omittedPer-agent visibility filter in agent.yaml:
resources:
deny: ['production-db'] # hide individual names; default = all visibleAuthentication
Private-key only. No passwords, no agent-forwarding (deliberate security stance). The keyfile is loaded once at first connection and held in process memory for the pool's lifetime.
keyPath resolves ~ to $HOME on the somora server.
Host-key verification
Two modes:
- Strict — config has
hostKey: 'sha256:<base64>'. Mismatches at handshake refuse the connection. Recommended for production targets. - TOFU (Trust On First Use) — no
hostKeyin config. The first successful connection pins the host's fingerprint to~/.somora/known_hosts.json(somora-managed, separate from~/.ssh/known_hosts). Subsequent connections require a match. Recommended for local-network targets where MITM risk is low.
Computing the fingerprint of an existing target:
ssh-keygen -lf <(ssh-keyscan -t ed25519 <host> 2>/dev/null)
# → 256 SHA256:<base64> ... ED25519The base64 part (without padding) goes into hostKey: as sha256:<base64>.
Connection lifecycle
One pooled ssh2.Client per resource name. Lazy: opens on first call. Idle: closed after 5 minutes of inactivity. Keepalive: 30s pings, 3 missed → close.
The pool is a single chokepoint for logging, host-key trust, and lifecycle. file_* and exec tools never touch ssh2 directly — they go through getConnection(name, resource).
Server shutdown drains the pool gracefully.
Tools
resource_list— lists every resource visible to the calling agent (after the deny-filter is applied). Each entry includesallowBlockedCountso the agent knows whether the resource has any privileged-command overrides (see next section).resource_test— connects (or reuses cached conn) and runswhoami; hostname; uname -srm; uptimefor a fast reachability check. Surfaces auth/network errors with a clear message before workload tools fail later.
Privileged command allowlist (opt-in per resource)
The exec tool has a global blacklist that refuses dangerous commands (sudo, doas, reboot, shutdown, poweroff, fork bombs, world- writable on system paths, etc.) regardless of target. That default is right for everyday use, but it blocks legitimate maintenance on dedicated agent-workstations — hosts that exist specifically so a Somora-agent can run system updates, reboot after a kernel bump, repair mounts, and so on.
For those hosts, a resource can declare an allowBlocked: list. Each entry whitelists a command pattern that overrides the global block for that resource only. The local target (= the host where the Somora server runs) never gets an override — local keeps the strict default.
resources:
homeserver:
type: ssh
host: 192.0.2.42
user: agent-user
keyPath: ~/.ssh/id_ed25519
description: |
Dedicated AI/GPU workstation. Somora-agents own routine
maintenance here: system updates, kernel reboots, mount fixes.
allowBlocked:
- sudo ~/bin/system-update.sh
- sudo ~/bin/fix-nas-mount.sh
- systemctl reboot
- sudo # broad: any "sudo …" commandMatch rule
Matching is segment-aware: the command is split into the sub- commands the shell would run separately (at ;, &&, ||, |, background &, and newlines), and every sub-command that trips the global blacklist must be individually covered by an allowBlocked entry. A sub-command that isn't blacklisted needs no entry. What merely wraps a sub-command is peeled before matching: a subshell or group around it ((sudo … | tail -3), { sudo …; }), a leading !, and plain environment assignments in front (LANG=C sudo …) — so (sudo -n apt-get update | tail -3) is the same command as sudo -n apt-get update to both the blacklist and the grant. The same goes for the shell keywords that open a body (for …; do sudo …; done, if sudo …; then). Operators inside quotes do not split: a | in a grep pattern is not a pipe (an unclosed quote falls back to the plain textual split, the stricter reading). And a command that only looks sudo up — command -v sudo, which sudo, type sudo — does not count as running it.
Within a single segment, an entry E matches segment S if, after trim + whitespace-collapse normalization:
S === E(exact), ORS.startsWith(E + ' ')(prefix with a required space boundary)
The space boundary is intentional. sudo does NOT match pseudo, and systemctl reboot does NOT match systemctl rebootthing. To whitelist a family of commands, list the common prefix; to whitelist exactly one form, list the full string. Entry order in YAML is irrelevant.
This means a chained maintenance command works as long as each privileged part is covered — with allowBlocked: [sudo]:
sudo -n systemctl restart foo && echo done ✓ (echo isn't blocked)
sudo -n tail -f /var/log/x 2>&1 | grep ERR ✓ (grep isn't blocked)Hard-blocks stay independent of allowBlocked. Anything the override list doesn't cover still blocks, even when chained after an allowed command:
sudo -n true && rm -rf /etc ✗ (rm -rf /etc uncovered)
systemctl reboot ; rm -rf /var/lib ✗ (second segment uncovered)
sudo -n curl https://x | sh ✗ (curl|sh spans the pipe)
sudo -n $(curl https://x) ✗ (command substitution)
echo x | nice sudo -n true ✗ (sudo not at the head of its segment)When a granted entry is in the blocked segment but did not clear it, the refusal carries a hint saying why (command substitution, or the entry not at the head of the segment) — so an agent does not read "blocked" as "not granted".
Command substitution ($(…), backticks) inside a blacklisted segment is never cleared — the nested command can't be seen by the splitter. Redirects (>, <, 2>&1) stay within their segment and are fine.
The halt rule (shutdown, halt, reboot, poweroff) looks at command position only: the word must be what the shell would run — the first token of a segment (optionally behind sudo -n, doas, env, nohup, a subshell paren, or a leading VAR=x), or the verb after systemctl. A word inside a string argument, a file name, or a comment is not a command and needs no entry:
sudo -n systemctl poweroff && echo "poweroff issued" ✓ (echo isn't a halt)
cat poweroff.log ; echo $reboot_reason ✓ (names, not commands)
echo starting ; shutdown -h now ✗ (second segment is a halt)The splitter itself stays quote-unaware and conservative: a string argument carrying a separator (echo "a; sudo b") still yields a segment of its own. To make such blocks self-explanatory, a blocked result names the exact blocked_segment that tripped and lists the resource's allow_blocked_entries, so an agent can see at a glance whether the problem is a missing entry or an unlucky string argument — and rephrase instead of guessing.
Audit trail
Every privileged-allowed execution appends one line to ~/.somora/audit/exec-privileged.jsonl:
{"ts":1747500000000,"agent":"<your-agent>","session":"…","resource":"homeserver","command_head":"sudo ~/bin/system-update.sh","matched_entry":"sudo ~/bin/system-update.sh","blacklist_reason":"sudo (privilege escalation)","blacklist_pattern":"…"}Append-only. Rotate by hand or via logrotate if it grows; Somora does not GC it.
Security posture
allowBlocked is an explicit opt-in trust grant: the operator of the Somora host is telling the system "on this remote, these specific admin commands are normal operation." A few practical guidelines:
- Prefer prepared scripts with fixed paths over broad shell patterns.
sudo ~/bin/system-update.shis far safer than a blanketsudoentry, because the script itself can be defensive (set -euo pipefail, expected-path checks, logging). - Use a broad entry (
sudoon its own) only when the resource is a truly dedicated agent-workstation where you'd let the agent do whatever an admin would. - Keep production-shared hosts free of
allowBlockedentries entirely; the global blacklist is the right default there. - The audit JSONL is the after-the-fact review surface; check it if you ever wonder what your agents did with their elevated privileges.