Skip to content

Commit 700e959

Browse files
committed
arch: move source to admin home, deploy via staging
Source repo now lives at ~/hornet (admin-owned, agent can't read). Deploy stages to /tmp, installs as hornet_agent via sudo -u. Changes: - deploy.sh: stages source β†’ /tmp, copies as agent, stamps hornet-version.json + hornet-manifest.json for integrity checks - start.sh: references ~/runtime/ (deployed copies only) - security-audit.sh: uses manifest for integrity, gracefully skips checks that need source access - tool-guard.ts: updated source repo path to /home/bentlegen/hornet - setup.sh: REPO_DIR auto-detected from script location - README.md: updated architecture and quick start
1 parent d18c7f9 commit 700e959

7 files changed

Lines changed: 337 additions & 206 deletions

File tree

β€ŽREADME.mdβ€Ž

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,8 @@ Hornet was partly inspired by [OpenClaw](https://github.com/openclaw/openclaw)'s
5959
## Architecture
6060

6161
```
62-
hornet_agent (unprivileged uid)
63-
β”‚
64-
β”œβ”€β”€ ~/hornet/ ← this repo
62+
admin_user
63+
β”œβ”€β”€ ~/hornet/ ← this repo (admin-owned, agent can't write)
6564
β”‚ β”œβ”€β”€ bin/ ← πŸ”’ security scripts (all root-protected)
6665
β”‚ β”‚ β”œβ”€β”€ security-audit.sh 24-check security audit
6766
β”‚ β”‚ β”œβ”€β”€ setup-firewall.sh iptables per-UID lockdown
@@ -80,7 +79,11 @@ hornet_agent (unprivileged uid)
8079
β”‚ β”‚ └── security.mjs ← πŸ”’ content wrapping, rate limiting, auth
8180
β”‚ β”œβ”€β”€ setup.sh ← πŸ”’ system setup (creates user, firewall, etc.)
8281
β”‚ └── SECURITY.md ← πŸ”’ threat model
83-
β”‚
82+
83+
hornet_agent (unprivileged uid)
84+
β”œβ”€β”€ ~/runtime/slack-bridge/ deployed bridge (from source)
85+
β”œβ”€β”€ ~/.pi/agent/extensions/ deployed extensions (from source)
86+
β”œβ”€β”€ ~/.pi/agent/skills/ agent-owned operational knowledge
8487
β”œβ”€β”€ ~/workspace/ project repos + git worktrees
8588
└── ~/.config/.env secrets (600 perms, not in repo)
8689
```
@@ -90,17 +93,17 @@ hornet_agent (unprivileged uid)
9093
## Quick Start
9194

9295
```bash
93-
# Clone
94-
sudo su - hornet_agent -c 'git clone git@github.com:modem-dev/hornet.git ~/hornet'
96+
# Clone (as admin user β€” source repo lives outside hornet_agent's home)
97+
git clone git@github.com:modem-dev/hornet.git ~/hornet
9598

9699
# Setup (creates user, firewall, permissions β€” run as root)
97-
sudo bash /home/hornet_agent/hornet/setup.sh <admin_username>
100+
sudo bash ~/hornet/setup.sh <admin_username>
98101

99102
# Add secrets
100103
sudo su - hornet_agent -c 'vim ~/.config/.env'
101104

102105
# Launch
103-
sudo -u hornet_agent /home/hornet_agent/hornet/start.sh
106+
sudo -u hornet_agent ~/hornet/start.sh
104107
```
105108

106109
## Configuration
@@ -143,17 +146,18 @@ sudo -u hornet_agent tmux attach -t dev-agent # Ctrl+b d to detach
143146
sudo -u hornet_agent pkill -u hornet_agent
144147

145148
# Restart
146-
sudo -u hornet_agent /home/hornet_agent/hornet/start.sh
149+
sudo -u hornet_agent ~/hornet/start.sh
147150
```
148151

149152
## Tests
150153

151154
```bash
152-
# All 202 tests
155+
# All tests (HORNET_SRC points to the admin-owned source repo)
156+
HORNET_SRC=~/hornet
153157
sudo -u hornet_agent bash -c "export PATH=~/opt/node-v22.14.0-linux-x64/bin:\$PATH && \
154-
cd ~/hornet/slack-bridge && node --test security.test.mjs && \
155-
cd ~/hornet/pi/extensions && node --test tool-guard.test.mjs && \
156-
cd ~/hornet/bin && node --test scan-extensions.test.mjs && \
158+
cd $HORNET_SRC/slack-bridge && node --test security.test.mjs && \
159+
cd $HORNET_SRC/pi/extensions && node --test tool-guard.test.mjs && \
160+
cd $HORNET_SRC/bin && node --test scan-extensions.test.mjs && \
157161
bash hornet-safe-bash.test.sh && bash redact-logs.test.sh && bash security-audit.test.sh"
158162
```
159163

β€Žbin/deploy.shβ€Ž

Lines changed: 172 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,29 @@
11
#!/bin/bash
22
# Deploy extensions and bridge from hornet source to agent runtime.
33
#
4-
# Invoked by the admin after editing ~/hornet/ source:
5-
# sudo -u hornet_agent ~/hornet/bin/deploy.sh
6-
# sudo -u hornet_agent ~/hornet/bin/deploy.sh --dry-run
4+
# Run as admin:
5+
# ~/hornet/bin/deploy.sh
6+
# ~/hornet/bin/deploy.sh --dry-run
77
#
8-
# Runs as hornet_agent so it can write to ~/.pi/agent/ and ~/runtime/.
9-
# Protected security files are made read-only (chmod a-w) after copy.
10-
# The agent owns these files but cannot write to them; tool-guard blocks
11-
# chmod at the pi level, and the source repo is always available to
12-
# re-deploy if runtime copies are tampered with.
8+
# The source repo lives in the admin's home (agent can't read it).
9+
# This script stages files to a temp dir, then uses sudo -u hornet_agent
10+
# to install them into the agent's runtime directories. It also stamps
11+
# a version file + hash manifest so the agent can verify integrity
12+
# without needing access to the source.
1313
#
14-
# For stronger protection (root-owned runtime files, bind mount), run
15-
# setup.sh as root β€” it calls this script then applies root-level hardening.
16-
17-
set -euo pipefail
14+
# Protected security files are made read-only (chmod a-w) after copy.
1815

19-
HORNET_SRC="$HOME/hornet"
16+
# Auto-detect source repo from this script's location
17+
HORNET_SRC="${HORNET_SRC:-$(cd "$(dirname "$0")/.." && pwd)}"
18+
HORNET_HOME="${HORNET_HOME:-/home/hornet_agent}"
19+
AGENT_USER="hornet_agent"
2020
DRY_RUN=0
2121

22+
# Helper: run a command as hornet_agent
23+
as_agent() {
24+
sudo -u "$AGENT_USER" "$@"
25+
}
26+
2227
for arg in "$@"; do
2328
case "$arg" in
2429
--dry-run) DRY_RUN=1 ;;
@@ -31,14 +36,49 @@ log() { echo " $1"; }
3136
PROTECTED_EXTENSIONS=(tool-guard.ts tool-guard.test.mjs)
3237
PROTECTED_BRIDGE_FILES=(security.mjs security.test.mjs)
3338

39+
# ── Stage source to temp dir (readable by agent) ────────────────────────────
40+
41+
STAGE_DIR=$(mktemp -d /tmp/hornet-deploy.XXXXXX)
42+
chmod 755 "$STAGE_DIR"
43+
trap 'rm -rf "$STAGE_DIR"' EXIT
44+
45+
if [ "$DRY_RUN" -eq 0 ]; then
46+
cp -r --no-preserve=ownership "$HORNET_SRC/pi/extensions" "$STAGE_DIR/extensions"
47+
cp -r --no-preserve=ownership "$HORNET_SRC/pi/skills" "$STAGE_DIR/skills"
48+
cp -r --no-preserve=ownership "$HORNET_SRC/slack-bridge" "$STAGE_DIR/slack-bridge"
49+
cp --no-preserve=ownership "$HORNET_SRC/start.sh" "$STAGE_DIR/start.sh"
50+
mkdir -p "$STAGE_DIR/bin"
51+
for script in harden-permissions.sh redact-logs.sh; do
52+
[ -f "$HORNET_SRC/bin/$script" ] && cp --no-preserve=ownership "$HORNET_SRC/bin/$script" "$STAGE_DIR/bin/$script"
53+
done
54+
[ -f "$HORNET_SRC/pi/settings.json" ] && cp --no-preserve=ownership "$HORNET_SRC/pi/settings.json" "$STAGE_DIR/settings.json"
55+
chmod -R a+rX "$STAGE_DIR"
56+
fi
57+
58+
# ── Unlock all existing deployed files ────────────────────────────────────────
59+
# Previous deploys may have left files/dirs read-only. Unlock before overwrite.
60+
# Runs before set -e so partial failures don't abort the script.
61+
# Uses chmod -R to handle dirs that lost execute bits.
62+
63+
if [ "$DRY_RUN" -eq 0 ]; then
64+
as_agent chmod -R u+rwX "$HORNET_HOME/.pi/agent/extensions" 2>/dev/null || true
65+
as_agent chmod -R u+rwX "$HORNET_HOME/.pi/agent/skills" 2>/dev/null || true
66+
as_agent chmod -R u+rwX "$HORNET_HOME/runtime" 2>/dev/null || true
67+
as_agent chmod u+w "$HORNET_HOME/.pi/agent/settings.json" 2>/dev/null || true
68+
as_agent chmod u+w "$HORNET_HOME/.pi/agent/hornet-version.json" 2>/dev/null || true
69+
as_agent chmod u+w "$HORNET_HOME/.pi/agent/hornet-manifest.json" 2>/dev/null || true
70+
fi
71+
72+
set -euo pipefail
73+
3474
# ── Extensions ───────────────────────────────────────────────────────────────
3575

3676
echo "Deploying extensions..."
3777

38-
EXT_SRC="$HORNET_SRC/pi/extensions"
39-
EXT_DEST="$HOME/.pi/agent/extensions"
78+
EXT_SRC="$STAGE_DIR/extensions"
79+
EXT_DEST="$HORNET_HOME/.pi/agent/extensions"
4080

41-
[ "$DRY_RUN" -eq 0 ] && mkdir -p "$EXT_DEST"
81+
[ "$DRY_RUN" -eq 0 ] && as_agent mkdir -p "$EXT_DEST"
4282

4383
for ext in "$EXT_SRC"/*; do
4484
base=$(basename "$ext")
@@ -47,15 +87,10 @@ for ext in "$EXT_SRC"/*; do
4787
if [ -d "$ext" ]; then
4888
if [ "$DRY_RUN" -eq 0 ]; then
4989
# Make destination writable first (source files may have been a-w)
50-
if [ -d "$EXT_DEST/$base" ]; then
51-
find "$EXT_DEST/$base" -type d -exec chmod u+w {} + 2>/dev/null || true
52-
find "$EXT_DEST/$base" -type f -exec chmod u+w {} + 2>/dev/null || true
53-
fi
54-
mkdir -p "$EXT_DEST/$base"
55-
cp -a "$ext/." "$EXT_DEST/$base/"
56-
# Ensure everything is writable (cp -a preserves source's a-w perms)
57-
find "$EXT_DEST/$base" -type d -exec chmod u+w {} + 2>/dev/null || true
58-
find "$EXT_DEST/$base" -type f -exec chmod u+w {} + 2>/dev/null || true
90+
as_agent bash -c "
91+
mkdir -p '$EXT_DEST/$base'
92+
cp -r '$ext/.' '$EXT_DEST/$base/'
93+
"
5994
log "βœ“ $base/"
6095
else
6196
log "would copy: $base/"
@@ -70,14 +105,15 @@ for ext in "$EXT_SRC"/*; do
70105
done
71106

72107
if [ "$DRY_RUN" -eq 0 ]; then
73-
# Unlock destination if it exists and is read-only (from previous deploy)
74-
[ -f "$EXT_DEST/$base" ] && chmod u+w "$EXT_DEST/$base" 2>/dev/null || true
75-
cp -a "$ext" "$EXT_DEST/$base"
108+
as_agent bash -c "
109+
[ -f '$EXT_DEST/$base' ] && chmod u+w '$EXT_DEST/$base' 2>/dev/null || true
110+
cp '$ext' '$EXT_DEST/$base'
111+
"
76112
if [ "$is_protected" -eq 1 ]; then
77-
chmod a-w "$EXT_DEST/$base"
113+
as_agent chmod a-w "$EXT_DEST/$base"
78114
log "βœ“ $base (read-only)"
79115
else
80-
chmod u+w "$EXT_DEST/$base"
116+
as_agent chmod u+w "$EXT_DEST/$base"
81117
log "βœ“ $base"
82118
fi
83119
else
@@ -93,12 +129,11 @@ done
93129

94130
echo "Deploying skills..."
95131

96-
SKILLS_SRC="$HORNET_SRC/pi/skills"
97-
SKILLS_DEST="$HOME/.pi/agent/skills"
132+
SKILLS_SRC="$STAGE_DIR/skills"
133+
SKILLS_DEST="$HORNET_HOME/.pi/agent/skills"
98134

99135
if [ "$DRY_RUN" -eq 0 ]; then
100-
mkdir -p "$SKILLS_DEST"
101-
cp -a "$SKILLS_SRC/." "$SKILLS_DEST/"
136+
as_agent bash -c "mkdir -p '$SKILLS_DEST' && cp -r '$SKILLS_SRC/.' '$SKILLS_DEST/'"
102137
log "βœ“ skills/"
103138
else
104139
log "would copy: skills/"
@@ -108,53 +143,140 @@ fi
108143

109144
echo "Deploying slack-bridge..."
110145

111-
BRIDGE_SRC="$HORNET_SRC/slack-bridge"
112-
BRIDGE_DEST="$HOME/runtime/slack-bridge"
146+
BRIDGE_SRC="$STAGE_DIR/slack-bridge"
147+
BRIDGE_DEST="$HORNET_HOME/runtime/slack-bridge"
113148

114149
if [ "$DRY_RUN" -eq 0 ]; then
115-
mkdir -p "$BRIDGE_DEST"
116-
117-
# Unlock protected files before bulk copy (so cp can overwrite them)
118-
for pf in "${PROTECTED_BRIDGE_FILES[@]}"; do
119-
[ -f "$BRIDGE_DEST/$pf" ] && chmod u+w "$BRIDGE_DEST/$pf" 2>/dev/null || true
120-
done
121-
122-
cp -a "$BRIDGE_SRC/." "$BRIDGE_DEST/"
150+
as_agent bash -c "
151+
mkdir -p '$BRIDGE_DEST'
152+
# Unlock protected files before bulk copy
153+
for pf in ${PROTECTED_BRIDGE_FILES[*]}; do
154+
[ -f '$BRIDGE_DEST/\$pf' ] && chmod u+w '$BRIDGE_DEST/\$pf' 2>/dev/null || true
155+
done
156+
cp -r '$BRIDGE_SRC/.' '$BRIDGE_DEST/'
157+
"
123158

124159
# Lock protected files read-only
125160
for pf in "${PROTECTED_BRIDGE_FILES[@]}"; do
126-
[ -f "$BRIDGE_DEST/$pf" ] && chmod a-w "$BRIDGE_DEST/$pf" && log "βœ“ $pf (read-only)"
161+
if as_agent test -f "$BRIDGE_DEST/$pf"; then
162+
as_agent chmod a-w "$BRIDGE_DEST/$pf"
163+
log "βœ“ $pf (read-only)"
164+
fi
127165
done
128166

129167
# Agent-modifiable files stay writable
130-
[ -f "$BRIDGE_DEST/bridge.mjs" ] && chmod u+w "$BRIDGE_DEST/bridge.mjs" && log "βœ“ bridge.mjs"
168+
if as_agent test -f "$BRIDGE_DEST/bridge.mjs"; then
169+
as_agent chmod u+w "$BRIDGE_DEST/bridge.mjs"
170+
log "βœ“ bridge.mjs"
171+
fi
131172

132173
log "βœ“ node_modules/ + package files"
133174
else
134175
log "would copy: slack-bridge/"
135176
fi
136177

178+
# ── Runtime bin (utility scripts + start.sh) ─────────────────────────────────
179+
180+
echo "Deploying runtime scripts..."
181+
182+
if [ "$DRY_RUN" -eq 0 ]; then
183+
as_agent mkdir -p "$HORNET_HOME/runtime/bin"
184+
185+
for script in harden-permissions.sh redact-logs.sh; do
186+
if [ -f "$STAGE_DIR/bin/$script" ]; then
187+
as_agent cp "$STAGE_DIR/bin/$script" "$HORNET_HOME/runtime/bin/$script"
188+
as_agent chmod u+x "$HORNET_HOME/runtime/bin/$script"
189+
log "βœ“ bin/$script"
190+
fi
191+
done
192+
193+
as_agent cp "$STAGE_DIR/start.sh" "$HORNET_HOME/runtime/start.sh"
194+
as_agent chmod u+x "$HORNET_HOME/runtime/start.sh"
195+
log "βœ“ start.sh"
196+
else
197+
log "would copy: runtime scripts"
198+
fi
199+
137200
# ── Settings ─────────────────────────────────────────────────────────────────
138201

139202
echo "Deploying settings..."
140203

141-
if [ -f "$HORNET_SRC/pi/settings.json" ]; then
204+
if [ -f "$STAGE_DIR/settings.json" ]; then
142205
if [ "$DRY_RUN" -eq 0 ]; then
143-
cp "$HORNET_SRC/pi/settings.json" "$HOME/.pi/agent/settings.json"
144-
chmod 600 "$HOME/.pi/agent/settings.json"
206+
as_agent bash -c "cp '$STAGE_DIR/settings.json' '$HORNET_HOME/.pi/agent/settings.json' && chmod 600 '$HORNET_HOME/.pi/agent/settings.json'"
145207
log "βœ“ settings.json"
146208
else
147209
log "would copy: settings.json"
148210
fi
149211
fi
150212

213+
# ── Version stamp + integrity manifest ────────────────────────────────────────
214+
215+
echo "Stamping version..."
216+
217+
VERSION_DIR="$HORNET_HOME/.pi/agent"
218+
VERSION_FILE="$VERSION_DIR/hornet-version.json"
219+
MANIFEST_FILE="$VERSION_DIR/hornet-manifest.json"
220+
221+
if [ "$DRY_RUN" -eq 0 ]; then
222+
# Get git info from source (admin can read it)
223+
GIT_SHA=$(cd "$HORNET_SRC" && git rev-parse HEAD 2>/dev/null || echo "unknown")
224+
GIT_SHA_SHORT=$(cd "$HORNET_SRC" && git rev-parse --short HEAD 2>/dev/null || echo "unknown")
225+
GIT_BRANCH=$(cd "$HORNET_SRC" && git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
226+
DEPLOY_TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
227+
228+
# Write version file via agent
229+
as_agent bash -c "cat > '$VERSION_FILE'" <<VEOF
230+
{
231+
"sha": "$GIT_SHA",
232+
"short": "$GIT_SHA_SHORT",
233+
"branch": "$GIT_BRANCH",
234+
"deployed_at": "$DEPLOY_TS",
235+
"deployed_by": "$(whoami)"
236+
}
237+
VEOF
238+
as_agent chmod 644 "$VERSION_FILE"
239+
log "βœ“ hornet-version.json ($GIT_SHA_SHORT @ $GIT_BRANCH)"
240+
241+
# Generate sha256 manifest of all deployed files (excluding node_modules)
242+
# Agent reads its own files to compute hashes
243+
as_agent bash -c "
244+
cd /tmp
245+
{
246+
echo '{'
247+
echo ' \"generated_at\": \"$DEPLOY_TS\",'
248+
echo ' \"source_sha\": \"$GIT_SHA\",'
249+
echo ' \"files\": {'
250+
first=1
251+
for dir in '$HORNET_HOME/.pi/agent/extensions' '$HORNET_HOME/.pi/agent/skills' '$HORNET_HOME/runtime/slack-bridge' '$HORNET_HOME/runtime/bin'; do
252+
if [ -d \"\$dir\" ]; then
253+
while IFS= read -r f; do
254+
hash=\$(sha256sum \"\$f\" | cut -d' ' -f1)
255+
rel=\"\${f#$HORNET_HOME/}\"
256+
[ \"\$first\" -eq 1 ] && first=0 || echo ','
257+
printf ' \"%s\": \"%s\"' \"\$rel\" \"\$hash\"
258+
done < <(find \"\$dir\" -type f -not -path '*/node_modules/*' -not -name '*.log' | sort)
259+
fi
260+
done
261+
echo ''
262+
echo ' }'
263+
echo '}'
264+
} > '$MANIFEST_FILE'
265+
chmod 644 '$MANIFEST_FILE'
266+
"
267+
manifest_count=$(as_agent grep -c '": "' "$MANIFEST_FILE" 2>/dev/null || echo 0)
268+
log "βœ“ hornet-manifest.json ($manifest_count files)"
269+
else
270+
log "would stamp: hornet-version.json + hornet-manifest.json"
271+
fi
272+
151273
# ── Summary ──────────────────────────────────────────────────────────────────
152274

153275
echo ""
154276
if [ "$DRY_RUN" -eq 1 ]; then
155277
echo "πŸ” Dry run β€” no changes made."
156278
else
157-
echo "βœ… Deployed. Protected files are read-only."
279+
echo "βœ… Deployed $GIT_SHA_SHORT. Protected files are read-only."
158280
echo ""
159281
echo "If the bridge is running, restart it:"
160282
echo " sudo -u hornet_agent bash -c 'cd ~/runtime/slack-bridge && node bridge.mjs'"

0 commit comments

Comments
Β (0)