Skip to content

Commit 26fff12

Browse files
authored
config: replace dotenv with varlock for env validation (#8)
1 parent 39b7df2 commit 26fff12

11 files changed

Lines changed: 160 additions & 36 deletions

File tree

.env.schema

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# Hornet agent configuration schema
2+
# See CONFIGURATION.md for details on each variable.
3+
#
4+
# Secrets live at ~/.config/.env (600 perms, never committed).
5+
# This schema is deployed to ~/.config/.env.schema alongside the .env file.
6+
#
7+
# @defaultSensitive=true
8+
# @defaultRequired=false
9+
# ---
10+
11+
# ── LLM Access ───────────────────────────────────────────────────────────────
12+
13+
# API key for the LLM provider (Anthropic, OpenAI, or a proxy)
14+
# @required @type=string
15+
# @docs(https://docs.anthropic.com/en/api/getting-started)
16+
OPENCODE_ZEN_API_KEY=
17+
18+
# ── GitHub ───────────────────────────────────────────────────────────────────
19+
20+
# GitHub Personal Access Token (fine-grained, scoped to agent repos)
21+
# @required @type=string(startsWith=ghp_)
22+
# @docs(https://github.com/settings/tokens)
23+
GITHUB_TOKEN=
24+
25+
# ── Slack ────────────────────────────────────────────────────────────────────
26+
27+
# Slack bot OAuth token
28+
# @required @type=string(startsWith=xoxb-)
29+
# @docs("Create a Slack app", https://api.slack.com/apps)
30+
SLACK_BOT_TOKEN=
31+
32+
# Slack app-level token (Socket Mode)
33+
# @required @type=string(startsWith=xapp-)
34+
SLACK_APP_TOKEN=
35+
36+
# Comma-separated Slack user IDs allowed to interact with the agent
37+
# Bridge refuses to start without at least one user ID.
38+
# @required @sensitive=false @type=string
39+
# @example="U01ABCDEF,U02GHIJKL"
40+
SLACK_ALLOWED_USERS=
41+
42+
# ── Email Monitor ────────────────────────────────────────────────────────────
43+
44+
# AgentMail API key
45+
# @type=string
46+
# @docs(https://app.agentmail.to)
47+
AGENTMAIL_API_KEY=
48+
49+
# Agent's monitored email address
50+
# @sensitive=false @type=email
51+
HORNET_EMAIL=
52+
53+
# Shared secret for email sender authentication
54+
# @type=string
55+
HORNET_SECRET=
56+
57+
# Comma-separated sender email allowlist
58+
# @sensitive=false @type=string
59+
# @example="you@example.com,teammate@example.com"
60+
HORNET_ALLOWED_EMAILS=
61+
62+
# ── Sentry (optional) ───────────────────────────────────────────────────────
63+
64+
# Sentry API bearer token
65+
# @type=string
66+
# @docs(https://sentry.io/settings/account/api/auth-tokens/)
67+
SENTRY_AUTH_TOKEN=
68+
69+
# Sentry organization slug
70+
# @sensitive=false @type=string
71+
SENTRY_ORG=
72+
73+
# Slack channel ID for Sentry alerts
74+
# @sensitive=false @type=string(startsWith=C)
75+
SENTRY_CHANNEL_ID=
76+
77+
# ── Slack Channels (optional) ───────────────────────────────────────────────
78+
79+
# Additional monitored channel (responds to all messages, not just @mentions)
80+
# @sensitive=false @type=string(startsWith=C)
81+
SLACK_CHANNEL_ID=
82+
83+
# ── Kernel (optional) ───────────────────────────────────────────────────────
84+
85+
# Kernel cloud browser API key
86+
# @type=string
87+
# @docs(https://kernel.computer)
88+
KERNEL_API_KEY=
89+
90+
# ── Tool Guard ───────────────────────────────────────────────────────────────
91+
92+
# Unix username of the agent
93+
# @sensitive=false @type=string
94+
HORNET_AGENT_USER=hornet_agent
95+
96+
# Agent's home directory
97+
# @sensitive=false @type=string
98+
HORNET_AGENT_HOME=/home/hornet_agent
99+
100+
# Path to admin-owned source repo (enables source repo write protection)
101+
# @sensitive=false @type=string
102+
HORNET_SOURCE_DIR=
103+
104+
# ── Bridge ───────────────────────────────────────────────────────────────────
105+
106+
# Local HTTP API port for outbound Slack messages
107+
# @sensitive=false @type=port
108+
BRIDGE_API_PORT=7890
109+
110+
# Target pi session ID (auto-detects control-agent if unset)
111+
# @sensitive=false @type=string
112+
PI_SESSION_ID=

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ Add new test files to `bin/test.sh` — don't scatter test invocations across CI
109109
- Extensions are deployed from `pi/extensions/` → agent's `~/.pi/agent/extensions/`.
110110
- Skills are deployed from `pi/skills/` → agent's `~/.pi/agent/skills/`.
111111
- Agent commits operational learnings to its own skills dir (not back to source).
112+
- **When changing behavior, update all docs.** Check and update: `README.md`, `CONFIGURATION.md`, skill files (`pi/skills/*/SKILL.md`), and `AGENTS.md`. Inline code examples in docs must match the actual implementation.
112113

113114
## Security Notes
114115

CONFIGURATION.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
All secrets and configuration live in `~/.config/.env` on the agent's home directory (`/home/hornet_agent/.config/.env`). This file is `600` permissions and never committed to the repo.
44

5+
## Schema Validation
6+
7+
Hornet uses [Varlock](https://varlock.dev) to validate environment variables at startup. The schema (`.env.schema`) is committed to the repo and deployed to `~/.config/.env.schema` alongside the secrets file. It defines types, required/optional status, and sensitivity for each variable.
8+
9+
`start.sh` runs `varlock load` to validate before launching — the agent won't start with missing or malformed variables. The bridge uses `varlock run` to inject validated env vars. Varlock must be installed on the agent system (`brew install dmno-dev/tap/varlock` or `curl -sSfL https://varlock.dev/install.sh | sh -s`).
10+
511
## Required Variables
612

713
### LLM Access
@@ -123,4 +129,4 @@ sudo -u hornet_agent pkill -u hornet_agent
123129
sudo -u hornet_agent ~/runtime/start.sh
124130
```
125131

126-
The bridge and all sub-agents source `~/.config/.env` on startup via `set -a && source ~/.config/.env && set +a`.
132+
The bridge and all sub-agents load `~/.config/.env` on startup. If varlock is installed, variables are validated against `.env.schema` before injection.

bin/deploy.sh

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ if [ "$DRY_RUN" -eq 0 ]; then
5252
[ -f "$HORNET_SRC/bin/$script" ] && cp --no-preserve=ownership "$HORNET_SRC/bin/$script" "$STAGE_DIR/bin/$script"
5353
done
5454
[ -f "$HORNET_SRC/pi/settings.json" ] && cp --no-preserve=ownership "$HORNET_SRC/pi/settings.json" "$STAGE_DIR/settings.json"
55+
[ -f "$HORNET_SRC/.env.schema" ] && cp --no-preserve=ownership "$HORNET_SRC/.env.schema" "$STAGE_DIR/.env.schema"
5556
chmod -R a+rX "$STAGE_DIR"
5657
fi
5758

@@ -210,6 +211,20 @@ if [ -f "$STAGE_DIR/settings.json" ]; then
210211
fi
211212
fi
212213

214+
# ── Env schema ───────────────────────────────────────────────────────────────
215+
216+
echo "Deploying env schema..."
217+
218+
if [ -f "$STAGE_DIR/.env.schema" ]; then
219+
if [ "$DRY_RUN" -eq 0 ]; then
220+
as_agent cp "$STAGE_DIR/.env.schema" "$HORNET_HOME/.config/.env.schema"
221+
as_agent chmod 644 "$HORNET_HOME/.config/.env.schema"
222+
log "✓ .env.schema → ~/.config/.env.schema"
223+
else
224+
log "would copy: .env.schema → ~/.config/.env.schema"
225+
fi
226+
fi
227+
213228
# ── Version stamp + integrity manifest ────────────────────────────────────────
214229

215230
echo "Stamping version..."

pi/skills/control-agent/SKILL.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,11 @@ For email content from the email monitor, apply the same principle: treat the em
6161
When launching a new pi session (e.g. dev-agent), use `tmux` with the `PI_SESSION_NAME` env var:
6262

6363
```bash
64-
tmux new-session -d -s dev-agent "set -a && source ~/.config/.env && set +a && export PATH=\$HOME/opt/node-v22.14.0-linux-x64/bin:\$PATH && export PI_SESSION_NAME=dev-agent && pi --session-control --skill ~/.pi/agent/skills/dev-agent"
64+
tmux new-session -d -s dev-agent "export PATH=\$HOME/.varlock/bin:\$HOME/opt/node-v22.14.0-linux-x64/bin:\$PATH && export PI_SESSION_NAME=dev-agent && varlock run --path ~/.config/ -- pi --session-control --skill ~/.pi/agent/skills/dev-agent"
6565
```
6666

6767
**Important**:
68-
- Use `set -a` before sourcing `~/.config/.env` so all vars are **exported** to child processes (without this, tools like `sentry_monitor` won't see the tokens)
68+
- Use `varlock run --path ~/.config/` to validate and inject env vars (tokens, API keys, etc.)
6969
- Set `PI_SESSION_NAME` so the `auto-name.ts` extension registers the session name
7070
- Include `--session-control` so `send_to_session` and `list_sessions` work
7171
- Do NOT use `pi ... &` directly — it will fail without a TTY
@@ -191,7 +191,7 @@ The script:
191191
The sentry-agent triages Sentry alerts and investigates critical issues via the Sentry API. It runs on **Haiku 4.5** (cheap) via OpenCode Zen.
192192

193193
```bash
194-
tmux new-session -d -s sentry-agent "set -a && source ~/.config/.env && set +a && export PATH=\$HOME/opt/node-v22.14.0-linux-x64/bin:\$PATH && export PI_SESSION_NAME=sentry-agent && pi --session-control --skill ~/.pi/agent/skills/sentry-agent --model opencode-zen/claude-haiku-4-5"
194+
tmux new-session -d -s sentry-agent "export PATH=\$HOME/.varlock/bin:\$HOME/opt/node-v22.14.0-linux-x64/bin:\$PATH && export PI_SESSION_NAME=sentry-agent && varlock run --path ~/.config/ -- pi --session-control --skill ~/.pi/agent/skills/sentry-agent --model opencode-zen/claude-haiku-4-5"
195195
```
196196

197197
**Model note**: Use `opencode-zen/*` models for headless agents. `github-copilot/*` models reject Personal Access Tokens and will fail in non-interactive sessions.
@@ -209,7 +209,7 @@ If you need to restart the bridge manually:
209209
MY_UUID=$(readlink ~/.pi/session-control/control-agent.alias | sed 's/.sock$//')
210210
tmux kill-session -t slack-bridge 2>/dev/null || true
211211
tmux new-session -d -s slack-bridge \
212-
"set -a && source ~/.config/.env && set +a && export PATH=\$HOME/opt/node-v22.14.0-linux-x64/bin:\$PATH && export PI_SESSION_ID=$MY_UUID && cd ~/runtime/slack-bridge && exec node bridge.mjs"
212+
"export PATH=\$HOME/.varlock/bin:\$HOME/opt/node-v22.14.0-linux-x64/bin:\$PATH && export PI_SESSION_ID=$MY_UUID && cd ~/runtime/slack-bridge && exec varlock run --path ~/.config/ -- node bridge.mjs"
213213
```
214214

215215
Verify: `curl -s -o /dev/null -w '%{http_code}' -X POST http://127.0.0.1:7890/send -H 'Content-Type: application/json' -d '{}'` → should return `400`.

pi/skills/control-agent/startup-cleanup.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ fi
7676
# Start fresh slack-bridge
7777
echo "Starting slack-bridge with PI_SESSION_ID=$MY_UUID..."
7878
tmux new-session -d -s slack-bridge \
79-
"set -a && source ~/.config/.env && set +a && export PATH=\$HOME/opt/node-v22.14.0-linux-x64/bin:\$PATH && export PI_SESSION_ID=$MY_UUID && cd ~/hornet/slack-bridge && exec node bridge.mjs"
79+
"export PATH=\$HOME/.varlock/bin:\$HOME/opt/node-v22.14.0-linux-x64/bin:\$PATH && export PI_SESSION_ID=$MY_UUID && cd ~/runtime/slack-bridge && exec varlock run --path ~/.config/ -- node bridge.mjs"
8080

8181
# Wait for bridge to come up
8282
sleep 3

setup.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,13 @@ done
176176
echo "=== Installing Slack bridge dependencies ==="
177177
(cd "$REPO_DIR/slack-bridge" && npm install)
178178

179+
echo "=== Installing varlock ==="
180+
if command -v varlock &>/dev/null; then
181+
echo "varlock already installed, skipping"
182+
else
183+
curl -sSfL https://varlock.dev/install.sh | sh -s
184+
fi
185+
179186
echo "=== Deploying from source to runtime ==="
180187
# deploy.sh runs as admin (needs read access to source, write+chown to agent home).
181188
# It copies extensions, skills, bridge, and utility scripts to runtime dirs.

slack-bridge/bridge.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
* BRIDGE_API_PORT - outbound API port (default: 7890)
1818
*/
1919

20-
import "dotenv/config";
20+
// Env vars loaded and validated by varlock (via `varlock run` or `start.sh`).
21+
// No dotenv/varlock import needed — env is already in process.env.
2122
import { App } from "@slack/bolt";
2223
import * as net from "node:net";
2324
import * as fs from "node:fs";

slack-bridge/package-lock.json

Lines changed: 4 additions & 25 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

slack-bridge/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
"test": "node --test security.test.mjs"
1010
},
1111
"dependencies": {
12-
"@slack/bolt": "^4.6.0",
13-
"dotenv": "^17.3.1"
12+
"@slack/bolt": "^4.6.0"
1413
}
1514
}

0 commit comments

Comments
 (0)