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
67 changes: 67 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: Deploy Docs

on:
push:
branches:
- main
paths:
- "docs/**"
- "zensical.toml"
- ".github/workflows/docs.yml"
- "README.md"
- "AGENTS.md"
pull_request:
paths:
- "docs/**"
- "zensical.toml"
- ".github/workflows/docs.yml"
- "README.md"
- "AGENTS.md"
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write

# A docs change on a PR only needs to prove the site builds; deploying is gated
# to non-PR events (push to main, or workflow_dispatch), preserving the original
# deploy behavior.
jobs:
build:
name: Build docs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5

- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0

- run: uv tool install zensical==0.0.43

- name: Build site
run: zensical build --clean

# Only package the site for an actual deploy; skip on PR builds.
- name: Configure Pages
if: github.event_name != 'pull_request'
uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6

- name: Upload artifact
if: github.event_name != 'pull_request'
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5
with:
path: site

deploy:
name: Deploy to GitHub Pages
if: github.event_name != 'pull_request'
needs: build
concurrency:
group: pages
cancel-in-progress: false
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5
id: deployment
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ node_modules/
dist/
*.tsbuildinfo

# Zensical docs build output (docs/ + zensical.toml -> site/, deployed to
# GitHub Pages by .github/workflows/docs.yml)
site/

# Test coverage
coverage/

Expand Down
12 changes: 12 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@
- Always redact AWS identifiers (account IDs, resource ARNs, instance IDs, etc.) in RFCs, PRs, PR comments, commit messages, and any other text checked into the repository. Use placeholders like `<account-id>`, `<resource-id>`, or a descriptive label instead.
- Never put links to the corkboard in code or commit messages (or any other text checked into the repository). The corkboard is a low-security, internal resource; reference it by name ("the corkboard") instead of embedding its URL.

## Documentation website

Built with [Zensical](https://zensical.org) from `docs/`. Config in
`zensical.toml`. Deploys to GitHub Pages via `.github/workflows/docs.yml`,
which also runs a build-only check on PRs that touch `docs/`, `zensical.toml`,
or the workflow itself (so doc build failures are caught before merge). The
published Pages tree is mirrored by the dispatch-site Amplify build and served
at `https://dispatch.boldblack.ai/docs/`. Build locally with
`uv tool install zensical==0.0.43 && zensical build --clean`. Zensical emits
relative links — keep it that way (no absolute-internal-link builds); the
mirror's relocatability depends on it.

## RFCs

Significant changes, architectural decisions, and new features should be proposed as RFCs in the `rfcs/` directory. RFCs use the format `rfcs/YYYY-MM-DD_short_title.md` with the following structure:
Expand Down
90 changes: 90 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Architecture

What a deployed dispatch agent looks like on AWS, and why it is shaped that
way.

## One repo, one agent, one Slack app

Each generated repository corresponds to one long-running agent and one Slack
application/user. Generate as many as you like — a `@swe-pal` for code reviews
and PRs, a `@reporter` that posts scheduled reports, a `@docs` that keeps
documentation in sync.

## The stack

The CloudFormation stack (`template.yaml` in the setup skill) owns the full
infrastructure:

| Resource | Details |
|---|---|
| VPC + subnets | Public subnet, internet gateway; inbound-less security group |
| EBS data volume | Standalone gp3 volume, mounted at `/data`, `DeletionPolicy: Retain` |
| Launch template + ASG | Single ARM64 container instance, `min=max=desired=1` |
| IAM roles | Task execution, task, container-instance instance profile |
| KMS key | `alias/dispatch-ssm` — encrypts the SSM secrets |
| Log group | `/ecs/dispatch` |
| ECS service | EC2 launch type, host networking, `DesiredCount` gated by parameter |

**Key decisions:**

- **Socket mode, outbound-only.** The agent is a Slack socket-mode bot. It
makes an outbound WebSocket connection to Slack, so there is no load balancer
and no inbound port — the security group is inbound-less. Public IP on the
instance ENI provides the outbound path (no NAT gateway).
- **EC2 launch type, not Fargate.** The persistent state is SQLite, and
SQLite's WAL mode needs a real local block device (it is unsafe on NFS).
EBS gives that, and lets the data volume survive instance replacement: the
volume is standalone with `DeletionPolicy: Retain`, and the instance's
UserData reattaches it on every boot.
- **Host networking.** The task shares the container instance's ENI. With no
inbound ports there is nothing to map.
- **ARM64.** The instance probes for an ARM64 availability zone at setup and
the harness image ships multi-arch (amd64 + arm64).

## The persistent volume

The EBS volume is surfaced into the container as four host bind-mounts that
mirror the harness CLI bind-mounts:

| Host path (on `/data`) | Container path |
|---|---|
| `hermes/` | `~/.hermes` |
| `config/` | `~/.config` |
| `mise/` | `~/.local/share/mise` |
| `mise-state/` | `~/.local/state/mise` |

Because state lives on EBS, an instance replacement (ASG recycle, instance
failure, manual rebuild) does not lose sessions, memories, or skills.

## The overlay

`agent_home/` in the generated repo is the **curated state** that ships with
the repo: skills, memories, system prompt, personas. It is not baked into the
image.

The `manage-dispatch` skill's overlay mode pushes this directory onto the
running agent's `~/.hermes` over ECS Exec — updating skills, memories, and
prompts **without a CloudFormation redeploy or image rebuild**. This is the
day-2 workflow: edit `agent_home/` in the repo, push the overlay, restart.

`config.yaml` is excluded from the overlay — it is managed by merge-config
mode, which does a key-level merge into the live config.

## The image

The agent runs the hardened `ghcr.io/boldblackai/harness` image
(`hermes-<version>` tags). The `HarnessImageTag` stack parameter selects the
tag; bumping it and redeploying rolls the agent to a new image **without
rebuilding anything**. The
[harness releases](https://github.com/boldblackai/harness/releases) page lists
the tags.

## The secrets path

Secrets live as SSM SecureStrings under `/dispatch/`, encrypted with the
stack's own KMS key (`alias/dispatch-ssm`). A Hermes secret-source plugin
(`aws_ssm`, installed during setup) resolves every `/dispatch/*` parameter into
the gateway's environment at startup. Adding or rotating a key is an SSM write
plus a task restart — no template edit, no redeploy.

See [Concepts: secrets](secrets.md) for the full inventory.
60 changes: 60 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# CLI reference

`npx @boldblackai/create-dispatch` — generate a repository for your dispatch
agent.

## Usage

```bash
npx @boldblackai/create-dispatch <name>
# or equivalently
npm init @boldblackai/dispatch <name>
```

If no name is given (and stdin is a TTY), you'll be prompted for one.

## The name

`<name>` must match `^[a-zA-Z]([a-zA-Z0-9-]*[a-zA-Z0-9])?$` and be 1–59
characters. It becomes:

- the CloudFormation stack name,
- the IAM role prefix (`<name>-exec`, `<name>-task`, `<name>-instance`),
- the ECS cluster and service name,
- the log group,
- the SSM namespace (`/<name>/`),
- the KMS alias (`alias/<name>-ssm`),
- and the EBS volume tag (`<name>-data`).

The 59-char ceiling keeps the `-exec`/`-task`/`-instance` role suffixes under
IAM's 64-char role-name limit. A name containing the literal region token
`us-east-1` is rejected (it would be corrupted by region substitution).

## Options

- `--region <region>` — AWS region to bake into the agent (default
`us-east-1`). Substituted into the deployer IAM policy's `kms:ViaService`
so the agent works in that region.
- `--force` — generate into a non-empty target directory, merging with
existing files (default: refuse).
- `--version`, `-V` — print the version.
- `--help`, `-h` — show help.

`--region` must match `^[a-z]{2}(-gov)?-[a-z]+-[0-9]+$` (any AWS region,
including GovCloud/China). If omitted and stdin is a TTY you'll be prompted;
otherwise the default is used silently.

## What generation does

Running the generator produces a `<name>/` directory whose contents match the
bundled `template/` snapshot except every lowercase `dispatch` reference —
file contents **and** file/directory names — is renamed to `<name>`. A second
literal token, `us-east-1`, is substituted with the chosen AWS region so
region-bearing static files match the deploy region.

## The pointer stub

The unscoped npm name `create-dispatch` is a reserved pointer stub: the CLI is
published as `@boldblackai/create-dispatch`. Reaching the unscoped name — by
habit or by guess — prints a pointer to the real package instead of a dead
end.
125 changes: 125 additions & 0 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Getting started

Generate a repository for your dispatch agent, deploy it to AWS, and connect it
to Slack. End to end this takes about 30 minutes, most of it waiting on
CloudFormation.

## Prerequisites

- **Node.js 18+** — to run the generator (`npx`).
- **An AWS account** — the agent runs on ECS (EC2 launch type) in a region of
your choice (default `us-east-1`). You will create a dedicated least-privilege
deployer IAM user during setup.
- **A Slack workspace where you can install apps** — the agent runs as a
socket-mode bot, so it needs no inbound URLs or load balancer.
- **A harness** — the generated repo is a set of skills, opened in
[Pi](https://github.com/boldblackai/harness), [Hermes](https://github.com/boldblackai/harness),
or [OpenCode](https://github.com/boldblackai/harness) (see
[harness docs](https://boldblackai.github.io/harness/)).

## 1. Generate the repo

```bash
npx @boldblackai/create-dispatch swe-pal
```

This creates a `swe-pal/` directory: a renamed snapshot of the dispatch
template. Every `dispatch` reference — file contents and file/directory names,
including the SSM namespace, IAM scopes, and KMS alias — now reads `swe-pal`.

You can also pass the AWS region the agent will deploy into:

```bash
npx @boldblackai/create-dispatch swe-pal --region us-west-2
```

The region is substituted into the generated claw (notably the deployer IAM
policy's `kms:ViaService`, which is a static JSON that cannot use
CloudFormation's `${AWS::Region}`). All CLI flags are covered in the
[CLI reference](cli.md).

## 2. Create the deployer IAM user

The setup skill cannot run until AWS credentials exist, and credentials need a
principal allowed to create and tear down the agent. Create a dedicated
least-privilege deployer user rather than reusing a broad admin principal.

The generated repo's README walks through this in detail. In short:

1. IAM → Users → Create user (e.g. `dispatch-deployer`) with programmatic
access.
2. Attach the repo's `dispatch-deploy-policy.json` policy to the user.
3. Put the access key in the repo's `.env` (gitignored).

The agent uses a **two-role model** so a leaked deployer key is never
root-equivalent — see [Security model](security.md) for how the split works.

## 3. Create the Slack app

The agent runs as a Slack **socket-mode** bot: it makes an outbound WebSocket
connection to Slack, so there is no inbound URL to host. The manifest
(`slack-manifest.json`) fully defines the app — name, slash commands, OAuth
scopes, event subscriptions, and socket mode.

1. Go to <https://api.slack.com/apps> → **Create New App** → **From an app
manifest**, pick your workspace, and paste the manifest.
2. Generate an app-level token (`xapp-`, scope `connections:write`) — this
becomes the `SLACK_APP_TOKEN` secret.
3. Install the app to the workspace and copy the bot token (`xoxb-`) — this
becomes the `SLACK_BOT_TOKEN` secret.
4. Copy your Slack user ID and a home channel ID for the allow-list and home
channel secrets.

## 4. Gather the secrets

The agent resolves its secrets at startup from SSM Parameter Store
SecureStrings under the `/dispatch/` namespace (renamed to your agent's name).
Secrets are not CloudFormation resources — they survive stack updates and
deletes. [Concepts: secrets](secrets.md) covers the full list and the KMS
key requirement.

At minimum: the four Slack values, plus one inference-provider API key
(OpenRouter, Anthropic, or Z.AI).

## 5. Run the setup skill

Open the generated repo in your harness and run the `/setup-dispatch` skill. It
follows a gated sequence:

1. Create the CloudFormation service role (`dispatch-cfn-exec`).
2. Probe one ARM64 availability zone.
3. Deploy CloudFormation (VPC, persistent EBS volume, single-instance Auto
Scaling Group, ECS service at `DesiredCount 0` on first deploy).
4. Write the SSM secrets.
5. Scale to 1.
6. Overlay `agent_home/`, install the `aws_ssm` secret-source plugin, and
merge its secrets config.
7. Restart and verify.

It will prompt for an inference provider — [OpenRouter](https://openrouter.ai/),
[ZAI](https://z.ai/subscribe), and [Anthropic](https://www.anthropic.com/) are
supported out of the box, and any provider
[hermes-agent already supports](https://hermes-agent.nousresearch.com/docs/integrations/providers/)
works too.

When the skill finishes, your agent is live in Slack. Talk to it the way you
would talk to any colleague: `@swe-pal can you review PR 42?`

## What you get

- [hermes-agent](https://hermes-agent.nousresearch.com/docs) running on AWS ECS
(EC2 launch type) — a single container instance in an Auto Scaling Group with
a persistent EBS data volume — via the hardened
[harness](https://github.com/boldblackai/harness) Docker image.
- GitHub and Slack integration.
- SQLite-backed persistent state on a retained gp3 EBS volume (local block
storage — SQLite WAL is unsafe on NFS).

## Next steps

- [Architecture](architecture.md) — what got built on AWS and why it is shaped
that way.
- [Upgrading](upgrading.md) — roll the running agent onto a new harness image
tag.
- [Concepts: skills](skills.md) — how to change the agent's skills, memories,
and system prompt without a redeploy.
Loading
Loading