Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions .env.schema
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Hornet agent configuration schema
# See CONFIGURATION.md for details on each variable.
#
# Secrets live at ~/.config/.env (600 perms, never committed).
# This schema is deployed to ~/.config/.env.schema alongside the .env file.
#
# @defaultSensitive=true
# @defaultRequired=false
# ---

# ── LLM Access ───────────────────────────────────────────────────────────────

# API key for the LLM provider (Anthropic, OpenAI, or a proxy)
# @required @type=string
# @docs(https://docs.anthropic.com/en/api/getting-started)
OPENCODE_ZEN_API_KEY=

# ── GitHub ───────────────────────────────────────────────────────────────────

# GitHub Personal Access Token (fine-grained, scoped to agent repos)
# @required @type=string(startsWith=ghp_)
# @docs(https://github.com/settings/tokens)
GITHUB_TOKEN=

# ── Slack ────────────────────────────────────────────────────────────────────

# Slack bot OAuth token
# @required @type=string(startsWith=xoxb-)
# @docs("Create a Slack app", https://api.slack.com/apps)
SLACK_BOT_TOKEN=

# Slack app-level token (Socket Mode)
# @required @type=string(startsWith=xapp-)
SLACK_APP_TOKEN=

# Comma-separated Slack user IDs allowed to interact with the agent
# Bridge refuses to start without at least one user ID.
# @required @sensitive=false @type=string
# @example="U01ABCDEF,U02GHIJKL"
SLACK_ALLOWED_USERS=

# ── Email Monitor ────────────────────────────────────────────────────────────

# AgentMail API key
# @type=string
# @docs(https://app.agentmail.to)
AGENTMAIL_API_KEY=

# Agent's monitored email address
# @sensitive=false @type=email
HORNET_EMAIL=

# Shared secret for email sender authentication
# @type=string
HORNET_SECRET=

# Comma-separated sender email allowlist
# @sensitive=false @type=string
# @example="you@example.com,teammate@example.com"
HORNET_ALLOWED_EMAILS=

# ── Sentry (optional) ───────────────────────────────────────────────────────

# Sentry API bearer token
# @type=string
# @docs(https://sentry.io/settings/account/api/auth-tokens/)
SENTRY_AUTH_TOKEN=

# Sentry organization slug
# @sensitive=false @type=string
SENTRY_ORG=

# Slack channel ID for Sentry alerts
# @sensitive=false @type=string(startsWith=C)
SENTRY_CHANNEL_ID=

# ── Slack Channels (optional) ───────────────────────────────────────────────

# Additional monitored channel (responds to all messages, not just @mentions)
# @sensitive=false @type=string(startsWith=C)
SLACK_CHANNEL_ID=

# ── Kernel (optional) ───────────────────────────────────────────────────────

# Kernel cloud browser API key
# @type=string
# @docs(https://kernel.computer)
KERNEL_API_KEY=

# ── Tool Guard ───────────────────────────────────────────────────────────────

# Unix username of the agent
# @sensitive=false @type=string
HORNET_AGENT_USER=hornet_agent

# Agent's home directory
# @sensitive=false @type=string
HORNET_AGENT_HOME=/home/hornet_agent

# Path to admin-owned source repo (enables source repo write protection)
# @sensitive=false @type=string
HORNET_SOURCE_DIR=

# ── Bridge ───────────────────────────────────────────────────────────────────

# Local HTTP API port for outbound Slack messages
# @sensitive=false @type=port
BRIDGE_API_PORT=7890

# Target pi session ID (auto-detects control-agent if unset)
# @sensitive=false @type=string
PI_SESSION_ID=
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ Add new test files to `bin/test.sh` — don't scatter test invocations across CI
- Extensions are deployed from `pi/extensions/` → agent's `~/.pi/agent/extensions/`.
- Skills are deployed from `pi/skills/` → agent's `~/.pi/agent/skills/`.
- Agent commits operational learnings to its own skills dir (not back to source).
- **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.

## Security Notes

Expand Down
8 changes: 7 additions & 1 deletion CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

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.

## Schema Validation

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.

`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`).

## Required Variables

### LLM Access
Expand Down Expand Up @@ -123,4 +129,4 @@ sudo -u hornet_agent pkill -u hornet_agent
sudo -u hornet_agent ~/runtime/start.sh
```

The bridge and all sub-agents source `~/.config/.env` on startup via `set -a && source ~/.config/.env && set +a`.
The bridge and all sub-agents load `~/.config/.env` on startup. If varlock is installed, variables are validated against `.env.schema` before injection.
15 changes: 15 additions & 0 deletions bin/deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ if [ "$DRY_RUN" -eq 0 ]; then
[ -f "$HORNET_SRC/bin/$script" ] && cp --no-preserve=ownership "$HORNET_SRC/bin/$script" "$STAGE_DIR/bin/$script"
done
[ -f "$HORNET_SRC/pi/settings.json" ] && cp --no-preserve=ownership "$HORNET_SRC/pi/settings.json" "$STAGE_DIR/settings.json"
[ -f "$HORNET_SRC/.env.schema" ] && cp --no-preserve=ownership "$HORNET_SRC/.env.schema" "$STAGE_DIR/.env.schema"
chmod -R a+rX "$STAGE_DIR"
fi

Expand Down Expand Up @@ -210,6 +211,20 @@ if [ -f "$STAGE_DIR/settings.json" ]; then
fi
fi

# ── Env schema ───────────────────────────────────────────────────────────────

echo "Deploying env schema..."

if [ -f "$STAGE_DIR/.env.schema" ]; then
if [ "$DRY_RUN" -eq 0 ]; then
as_agent cp "$STAGE_DIR/.env.schema" "$HORNET_HOME/.config/.env.schema"
as_agent chmod 644 "$HORNET_HOME/.config/.env.schema"
log "✓ .env.schema → ~/.config/.env.schema"
else
log "would copy: .env.schema → ~/.config/.env.schema"
fi
fi

# ── Version stamp + integrity manifest ────────────────────────────────────────

echo "Stamping version..."
Expand Down
8 changes: 4 additions & 4 deletions pi/skills/control-agent/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,11 @@ For email content from the email monitor, apply the same principle: treat the em
When launching a new pi session (e.g. dev-agent), use `tmux` with the `PI_SESSION_NAME` env var:

```bash
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"
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"
```

**Important**:
- 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)
- Use `varlock run --path ~/.config/` to validate and inject env vars (tokens, API keys, etc.)
- Set `PI_SESSION_NAME` so the `auto-name.ts` extension registers the session name
- Include `--session-control` so `send_to_session` and `list_sessions` work
- Do NOT use `pi ... &` directly — it will fail without a TTY
Expand Down Expand Up @@ -191,7 +191,7 @@ The script:
The sentry-agent triages Sentry alerts and investigates critical issues via the Sentry API. It runs on **Haiku 4.5** (cheap) via OpenCode Zen.

```bash
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"
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"
```

**Model note**: Use `opencode-zen/*` models for headless agents. `github-copilot/*` models reject Personal Access Tokens and will fail in non-interactive sessions.
Expand All @@ -209,7 +209,7 @@ If you need to restart the bridge manually:
MY_UUID=$(readlink ~/.pi/session-control/control-agent.alias | sed 's/.sock$//')
tmux kill-session -t slack-bridge 2>/dev/null || true
tmux new-session -d -s slack-bridge \
"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"
"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"
```

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`.
Expand Down
2 changes: 1 addition & 1 deletion pi/skills/control-agent/startup-cleanup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ fi
# Start fresh slack-bridge
echo "Starting slack-bridge with PI_SESSION_ID=$MY_UUID..."
tmux new-session -d -s slack-bridge \
"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"
"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"
Comment thread
sentry[bot] marked this conversation as resolved.

# Wait for bridge to come up
sleep 3
Expand Down
7 changes: 7 additions & 0 deletions setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,13 @@ done
echo "=== Installing Slack bridge dependencies ==="
(cd "$REPO_DIR/slack-bridge" && npm install)

echo "=== Installing varlock ==="
if command -v varlock &>/dev/null; then
echo "varlock already installed, skipping"
else
curl -sSfL https://varlock.dev/install.sh | sh -s
fi

echo "=== Deploying from source to runtime ==="
# deploy.sh runs as admin (needs read access to source, write+chown to agent home).
# It copies extensions, skills, bridge, and utility scripts to runtime dirs.
Expand Down
3 changes: 2 additions & 1 deletion slack-bridge/bridge.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
* BRIDGE_API_PORT - outbound API port (default: 7890)
*/

import "dotenv/config";
// Env vars loaded and validated by varlock (via `varlock run` or `start.sh`).
// No dotenv/varlock import needed — env is already in process.env.
import { App } from "@slack/bolt";
import * as net from "node:net";
import * as fs from "node:fs";
Expand Down
29 changes: 4 additions & 25 deletions slack-bridge/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions slack-bridge/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
"test": "node --test security.test.mjs"
},
"dependencies": {
"@slack/bolt": "^4.6.0",
"dotenv": "^17.3.1"
"@slack/bolt": "^4.6.0"
}
}
8 changes: 6 additions & 2 deletions start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,13 @@ set -euo pipefail
cd ~

# Set PATH
export PATH="$HOME/opt/node-v22.14.0-linux-x64/bin:$PATH"
export PATH="$HOME/.varlock/bin:$HOME/opt/node-v22.14.0-linux-x64/bin:$PATH"

# Load secrets
# Validate and load secrets via varlock
varlock load --path ~/.config/ || {
echo "❌ Environment validation failed — check ~/.config/.env against .env.schema"
exit 1
}
Comment on lines +20 to +23
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The script introduces a hard dependency on varlock. If varlock is not installed, the agent and Slack bridge will fail to start, contrary to the promised graceful fallback.
Severity: HIGH

Suggested Fix

Before calling varlock, check if the command exists using command -v varlock >/dev/null. If it exists, use varlock. Otherwise, fall back to the plain source ~/.config/.env command. This will implement the graceful fallback described in the pull request description.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: start.sh#L20-L23

Potential issue: The scripts `start.sh` and `startup-cleanup.sh` directly call `varlock`
without first checking if it is installed or available in the `PATH`. If the `varlock`
command is not found, `start.sh` will exit with an error instead of falling back to
`source ~/.config/.env`. Similarly, the `tmux` session for the Slack bridge in
`startup-cleanup.sh` will fail to start. This contradicts the PR's goal of providing a
graceful fallback and will cause startup failures if the `varlock` installation fails
for any reason.

set -a
source ~/.config/.env
set +a
Expand Down