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"
2020DRY_RUN=0
2121
22+ # Helper: run a command as hornet_agent
23+ as_agent () {
24+ sudo -u " $AGENT_USER " " $@ "
25+ }
26+
2227for arg in " $@ " ; do
2328 case " $arg " in
2429 --dry-run) DRY_RUN=1 ;;
@@ -31,14 +36,49 @@ log() { echo " $1"; }
3136PROTECTED_EXTENSIONS=(tool-guard.ts tool-guard.test.mjs)
3237PROTECTED_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
3676echo " 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
4383for 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
94130echo " 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
99135if [ " $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/"
103138else
104139 log " would copy: skills/"
@@ -108,53 +143,140 @@ fi
108143
109144echo " 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
114149if [ " $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"
133174else
134175 log " would copy: slack-bridge/"
135176fi
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
139202echo " 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
149211fi
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
153275echo " "
154276if [ " $DRY_RUN " -eq 1 ]; then
155277 echo " π Dry run β no changes made."
156278else
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