diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..bc73a71 --- /dev/null +++ b/.github/workflows/docs.yml @@ -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 diff --git a/.gitignore b/.gitignore index 7ad4a14..296721c 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/AGENTS.md b/AGENTS.md index f74b2ee..f973989 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 ``, ``, 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: diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..830650d --- /dev/null +++ b/docs/architecture.md @@ -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-` 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. diff --git a/docs/cli.md b/docs/cli.md new file mode 100644 index 0000000..5e3597f --- /dev/null +++ b/docs/cli.md @@ -0,0 +1,60 @@ +# CLI reference + +`npx @boldblackai/create-dispatch` — generate a repository for your dispatch +agent. + +## Usage + +```bash +npx @boldblackai/create-dispatch +# or equivalently +npm init @boldblackai/dispatch +``` + +If no name is given (and stdin is a TTY), you'll be prompted for one. + +## The 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 (`-exec`, `-task`, `-instance`), +- the ECS cluster and service name, +- the log group, +- the SSM namespace (`//`), +- the KMS alias (`alias/-ssm`), +- and the EBS volume tag (`-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 ` — 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 `/` directory whose contents match the +bundled `template/` snapshot except every lowercase `dispatch` reference — +file contents **and** file/directory names — is renamed to ``. 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. diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..37d26be --- /dev/null +++ b/docs/getting-started.md @@ -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 → **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. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..ff6fa04 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,26 @@ +# dispatch + +dispatch is an opinionated, one-command deployment of +[hermes-agent](https://hermes-agent.nousresearch.com/) — a long-running coding +agent that lives in your Slack workspace and runs on your own AWS account. + +One `npx` command generates a repository for your own dispatch agent. The +generated repo is a set of skills you open in your favorite +[harness](https://github.com/boldblackai/harness); the skills deploy the agent +to AWS ECS and manage it for the rest of its life. + +This documentation covers what a dispatch agent is, how to generate one, and +how to set up, manage, upgrade, and tear it down. + +[Get started](getting-started.md) + +## Where to go next + +- [Getting started](getting-started.md) — generate a repo and deploy your first agent +- [Architecture](architecture.md) — what the CloudFormation stack builds on AWS +- [Concepts: skills](skills.md) — the three skills that run the lifecycle +- [Concepts: security model](security.md) — the deployer/service-role split +- [Concepts: secrets](secrets.md) — SSM SecureStrings under `/dispatch/` +- [CLI reference](cli.md) — `npx @boldblackai/create-dispatch` flags +- [Upgrading](upgrading.md) — rolling the running agent to a new image tag +- [Tearing down](teardown.md) — removing every AWS resource diff --git a/docs/secrets.md b/docs/secrets.md new file mode 100644 index 0000000..d9fd759 --- /dev/null +++ b/docs/secrets.md @@ -0,0 +1,77 @@ +# Concepts: secrets + +Where a dispatch agent's secrets live, how they are encrypted, and how to +rotate them. + +## SSM, not CloudFormation + +Secrets are SSM Parameter Store **SecureStrings** under the `/dispatch/` +namespace (renamed to your agent's name at generation). They are deliberately +**not** CloudFormation resources — they live in SSM so they survive stack +updates and stack deletes, and stay out of template diffs. + +Each parameter must be encrypted with the **agent's own KMS key** (alias +`alias/dispatch-ssm`, created by the setup stack) — **not** the default +`alias/aws/ssm`. The deployer IAM policy pins `kms:Decrypt`/`kms:Encrypt` to +`alias/dispatch-ssm` via `kms:ResourceAliases`, so the agent can only decrypt +parameters this key encrypted. A parameter left under the default SSM key +fails to decrypt — the plugin can't resolve it and the gateway runs without +it. + +## Resolution at startup + +A Hermes secret-source plugin (`aws_ssm`, from +[boldblackai/hermes-aws-ssm-secret-source](https://github.com/boldblackai/hermes-aws-ssm-secret-source), +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. + +## The inventory + +### Slack (required) + +| SSM key | What it is | Where to find it | +|---|---|---| +| `/dispatch/SLACK_BOT_TOKEN` | Slack bot OAuth token (`xoxb-`) | Slack app → OAuth & Permissions → Bot User OAuth Token | +| `/dispatch/SLACK_APP_TOKEN` | Slack app-level token (`xapp-`, enables socket mode) | Slack app → Basic Information → App-Level Tokens | +| `/dispatch/SLACK_ALLOWED_USERS` | Comma-separated Slack user IDs allowed to use the bot | Slack profile → "Copy member ID" | +| `/dispatch/SLACK_HOME_CHANNEL` | Slack channel ID the bot treats as home | Right-click channel → "Copy link", take the trailing ID | + +### Inference-provider key (create at least one) + +| SSM key | What it is | Where to find it | +|---|---|---| +| `/dispatch/OPENROUTER_API_KEY` | OpenRouter API key (recommended) | | +| `/dispatch/ANTHROPIC_API_KEY` | Anthropic (direct Claude API) | | +| `/dispatch/ZAI_API_KEY` | Z.AI / Zhipu (GLM) | | + +The aws_ssm plugin resolves every provider key present in SSM, so you can +create more than one if the gateway uses multiple providers. + +### Optional: GitHub authentication + +| SSM key | What it is | Where to find it | +|---|---|---| +| `/dispatch/GH_TOKEN_VAL` | GitHub PAT for the on-boot `gh auth login` | | + +`/dispatch/GH_TOKEN_VAL` is the ONE secret still injected via CloudFormation +(`secrets[]` + the `EnableGitHubKey` stack parameter), because the on-boot +`gh auth login --with-token` runs before Hermes (and the aws_ssm plugin) start. +Named `*_VAL`, not `GH_TOKEN`, to avoid `gh`'s reserved env var. Enable it only +if the agent should make authenticated `gh`/HTTPS-git calls; when disabled the +login is skipped and no token is injected. + +## Rotation + +Rotate any SSM-resolved secret by overwriting the parameter (with the same +KMS key) and restarting the task: + +```bash +aws ssm put-parameter --name /swe-pal/OPENROUTER_API_KEY \ + --type SecureString --key-id alias/swe-pal-ssm \ + --value "sk-or-..." --overwrite +``` + +then restart the agent (manage-dispatch mode 2, or scale the service +0 → 1). The `GH_TOKEN_VAL` exception is rotated via its stack parameter +instead. diff --git a/docs/security.md b/docs/security.md new file mode 100644 index 0000000..a7ce540 --- /dev/null +++ b/docs/security.md @@ -0,0 +1,71 @@ +# Concepts: security model + +How the two-role IAM model keeps a leaked deployer key from being +root-equivalent. + +## The two roles + +dispatch uses a **two-role model** so the deployer's long-lived access key is +never root-equivalent if it leaks: + +- **`dispatch-deployer`** — the human identity. Its attached policy + (`dispatch-deploy-policy.json`) carries narrow powers: manage the + CloudFormation stack, write/read SSM secrets, shell in, scale the service, + debug the container instance, recover orphans during teardown, and manage + **one** literal role (`dispatch-cfn-exec`). +- **`dispatch-cfn-exec`** — the CloudFormation service role. Its inline policy + (`dispatch-cfn-exec-policy.json`, trust `dispatch-cfn-exec-trust.json`) + carries the broad infrastructure-create lifecycle (EC2/ASG/ECS/IAM/KMS/logs) + that CloudFormation assumes during every deploy and the stack delete. + +The deployer identity only ever passes `dispatch-cfn-exec` to CloudFormation +(`iam:PassRole` conditioned to `cloudformation.amazonaws.com`); it never +touches infrastructure resources directly. + +Because `dispatch-cfn-exec` is assumable **only** by +`cloudformation.amazonaws.com` (its trust policy) and the deployer's only +`iam:PassRole` for it is conditioned to that same service, none of the broad +powers are reachable by the human-held key — closing the privilege-escalation +chains a leaked deployer key otherwise opens. + +## Why the service role exists at all + +The stack cannot create the role it assumes to create itself. It is therefore +created out-of-band in setup Phase 0 and deleted last in teardown (after the +stack is gone). + +## Scoping rules in the deployer policy + +Resources whose ARNs use AWS-assigned IDs cannot be pinned by ARN prefix. The +policy pins what it can and uses tag conditions (ABAC) or read-only +`Resource: "*"` where AWS forces it: + +| Statement type | Examples | +|---|---| +| ARN-pinned | `CloudFormation` (`stack/dispatch/*`), `ManageCfnExecRole` (`role/dispatch-cfn-exec`), `PassRoleToCfn`, `ECSExec` (`cluster/dispatch`, `task/dispatch/*`), `ECSServiceManage`, `LogsRead`, `SSMSecrets` (`parameter/dispatch/*`) | +| Tag-conditioned (ABAC) | `EC2NetworkingManage` (`Name = dispatch*`), `EC2InstanceOps` (`ClawName = dispatch`, ARN-scoped to `instance/*`), `EC2DataVolumeManage` (`Name = dispatch-data`), `KMSUseKey` (`ResourceAliases = alias/dispatch-ssm`) | +| Read-only `*` (AWS-forced) | `ReadOnlyDescribe`, `ECSRead`, `SSMMessages`, `CloudFormationGlobalMeta` | + +Two deliberate omissions: + +- **No `iam:SimulatePrincipalPolicy`** — a leaked key should not be able to + probe its own scope. Permission gaps surface as the exact + `is not authorized to perform` error at deploy time. +- **No `sts:DecodeAuthorizationMessage`** — the deployer key deliberately + cannot decode encoded denial messages; use a separate admin identity. + +## ECS Exec and session logging + +Shell-in (`ecs:ExecuteCommand`) permissions are on the deployer, scoped to the +agent's cluster and tasks. AWS additionally recommends **denying** +`ssm:StartSession` on ECS tasks (`DenyDirectSSMSession`): sessions via +`ecs:ExecuteCommand` are logged; direct SSM sessions bypass ECS Exec logging +and consume the session quota. + +## The secrets boundary + +Secrets are SSM SecureStrings under `/dispatch/`, encrypted with the stack's +own KMS key. The deployer policy pins `kms:Decrypt`/`kms:Encrypt` to +`alias/dispatch-ssm` via `kms:ResourceAliases`, so the deployer can only +decrypt parameters this key encrypted. See +[Concepts: secrets](secrets.md). diff --git a/docs/skills.md b/docs/skills.md new file mode 100644 index 0000000..5f55c01 --- /dev/null +++ b/docs/skills.md @@ -0,0 +1,80 @@ +# Concepts: skills + +The three skills that run a dispatch agent's lifecycle, and what each one +does. + +## The lifecycle + +```text +setup-dispatch → manage-dispatch → teardown-dispatch + (deploy) (day-2 ops) (decommission) +``` + +All three ship inside the generated repo at `.agents/skills/`. Your harness +discovers them automatically; each is invoked by name — `/setup-dispatch`, +`/manage-dispatch`, `/teardown-dispatch` — or simply by asking the agent in +natural language ("roll the image to hermes-1.9.11"). + +## setup-dispatch + +Bootstraps the agent from nothing to a running gateway. Follows a gated +sequence — each phase must succeed before the next begins: + +| Phase | What happens | +|---|---| +| 0 | Create the CloudFormation service role (`dispatch-cfn-exec`) | +| 1 | Collect configuration, probe one ARM64 availability zone | +| 2 | Deploy the CloudFormation stack (first deploy at `DesiredCount 0`) | +| 3 | Write the SSM secrets | +| 4 | Scale the service to 1 and verify boot | +| 5 | Overlay `agent_home/`, install the `aws_ssm` plugin, merge secrets config | +| 6 | Shell in (ECS Exec) | +| 7 | Final report | + +Permissions are not pre-checked: if the deployer principal is missing an +action, CloudFormation surfaces the exact `is not authorized to perform` error +at deploy time — fix the policy and re-run. + +## manage-dispatch + +Five modes covering everything after the first deploy: + +1. **Overlay** (default) — push the repo's `agent_home/` onto the agent's + `~/.hermes` to update skills, memories, system prompt, or personas without + a redeploy. Transferred over ECS Exec; `config.yaml` is excluded (use mode 3). +2. **Run** — execute arbitrary commands on the live agent for inspection, + debugging, or one-off operations, over ECS Exec. +3. **Merge-config** — key-level merge of `agent_home/config.yaml` into the + live `config.yaml`, with conflict resolution. +4. **Upgrade image** — roll the running agent onto a new + `ghcr.io/boldblackai/harness` tag by bumping the `HarnessImageTag` stack + parameter and redeploying. No image rebuild. +5. **Host** — retrieve a stuck container instance's console output or force a + wedged instance to replace itself, scoped to the agent's instances via + `aws:ResourceTag/ClawName`. + +## teardown-dispatch + +Removes everything, in the reverse order of setup so dependencies delete +cleanly without orphans: + +| Phase | What happens | +|---|---| +| 0 | Pre-flight — confirm AWS access | +| 1 | Scale the service to 0 | +| 2 | Delete the CloudFormation stack (EBS volume is retained) | +| 3 | Delete the retained EBS volume | +| 4 | Delete the orphaned VPC and networking | +| 5 | Delete the SSM secrets | +| 6 | Delete the CloudFormation service role | +| 7 | Final verification — no stacks, volumes, params, or service role remain | + +## Where curated state lives + +`agent_home/` in the generated repo is the source of truth for the agent's +skills, memories, system prompt, and personas. Changes to it reach the live +agent through the overlay (manage mode 1) — commit to the repo, push the +overlay, restart. `config.yaml` changes go through merge-config (mode 3). + +The agent's own runtime state (sessions, SQLite databases) lives on the EBS +volume and is never touched by the overlay. diff --git a/docs/teardown.md b/docs/teardown.md new file mode 100644 index 0000000..7234e2e --- /dev/null +++ b/docs/teardown.md @@ -0,0 +1,51 @@ +# Tearing down + +Remove a dispatch agent and every associated AWS resource. + +## Run the teardown skill + +Open the generated repo in your harness and run the `/teardown-dispatch` +skill. It follows the reverse order of setup so dependencies delete cleanly +without orphans: + +| Phase | What happens | +|---|---| +| 0 | Pre-flight — confirm AWS access | +| 1 | Scale the service to 0 | +| 2 | Delete the CloudFormation stack (the EBS volume is **retained**, not deleted) | +| 3 | Delete the retained EBS volume | +| 4 | Delete the orphaned VPC and networking (subnets, route tables, IGW, SG) | +| 5 | Delete the SSM secrets (every parameter under `//`) | +| 6 | Delete the CloudFormation service role (`dispatch-cfn-exec`) | +| 7 | Final verification — no stacks, volumes, params, or service role remain | + +## What is deleted last, and why + +Two resources are deliberately deleted **after** the stack: + +- **The EBS data volume** — `DeletionPolicy: Retain` keeps it alive through the + stack delete so a teardown run aborting mid-way never destroys your data by + accident. Deleting it explicitly (Phase 3) is the point of no return. +- **The CloudFormation service role** — the stack cannot create the role it + assumes to create itself, so it is created out-of-band in setup Phase 0 and + survives `delete-stack`. Phase 6 removes it once the stack is gone. + +## Data warning + +Phase 3 permanently destroys the agent's SQLite databases — sessions, +memories, and any runtime state. If you might want the data back, snapshot the +volume before tearing down: + +```bash +aws ec2 create-snapshot \ + --description "swe-pal data volume backup" \ + --volume-id +``` + +## After teardown + +The generated repository itself (and its git history) is untouched — delete +the local directory when you no longer want it. To bring the agent back, run +`/setup-dispatch` again on a freshly generated (or the same) repo: setup +re-creates everything from scratch, including the service role and SSM +secrets (you'll re-enter them). diff --git a/docs/upgrading.md b/docs/upgrading.md new file mode 100644 index 0000000..476cc85 --- /dev/null +++ b/docs/upgrading.md @@ -0,0 +1,42 @@ +# Upgrading + +How to roll a running dispatch agent onto a new harness image tag, and how to +update its skills without a redeploy. + +## Upgrade image (manage-dispatch mode 4) + +The agent runs `ghcr.io/boldblackai/harness:` — the `HarnessImageTag` +stack parameter selects the tag. Rolling to a new release is a parameter bump +plus redeploy; **no image is rebuilt**: + +1. Pick the tag from the + [harness releases](https://github.com/boldblackai/harness/releases) page + (e.g. `hermes-1.9.11`). +2. Run the `/manage-dispatch` skill in upgrade-image mode (mode 4), naming the + new tag. The skill bumps the `HarnessImageTag` stack parameter and + redeploys the stack. +3. The ECS service replaces the task; state survives on the EBS volume. + +The generator's template tracks the latest released tag by default, so +**freshly generated agents already run the newest image** — upgrading applies +to agents deployed earlier. + +## Update skills, memories, prompts (manage-dispatch mode 1 — overlay) + +Everything in the repo's `agent_home/` — skills, memories, system prompt, +personas — updates **without any redeploy**: + +1. Edit `agent_home/` in the generated repo and commit. +2. Run the `/manage-dispatch` skill in overlay mode (the default). It pushes + the repo's `agent_home/` onto the agent's `~/.hermes` over ECS Exec + (tar + base64 transport), with a dry-run diff first. +3. Restart if the change needs one (the skill tells you when). + +`config.yaml` is excluded from the overlay — use merge-config mode (mode 3) +for it. + +## Merge config (manage-dispatch mode 3) + +`agent_home/config.yaml` merges into the live `config.yaml` at the key level: +fetch live config → merge → flag conflicts → show diff → push. Conflicts +resolve by choosing the repo value, the live value, or a hand-edited blend. diff --git a/rfcs/2026-08-18_docs-site.md b/rfcs/2026-08-18_docs-site.md new file mode 100644 index 0000000..cc07e86 --- /dev/null +++ b/rfcs/2026-08-18_docs-site.md @@ -0,0 +1,64 @@ +# Docs site (Zensical → GitHub Pages, mirrored at dispatch.boldblack.ai/docs/) + +**Date:** 2026-08-18 +**Status:** Proposed + +## Goal + +Give `@boldblackai/create-dispatch` a documentation website — the same shape +the harness project uses: Zensical builds `docs/` in this repo, deploys to +GitHub Pages (`boldblackai.github.io/create-dispatch/`), and the +dispatch-site Amplify build mirrors the published Pages tree into +`static/docs/` so the docs are also served at +`https://dispatch.boldblack.ai/docs/` — byte-identical, no rebuild drift, no +second toolchain in the site's CI. + +## Motivation + +- The product site (dispatch-site) links "docs" to nothing today; README is the + only long-form documentation. +- The harness project established the pattern end to end (harness `docs/` + + `zensical.toml` + `docs.yml`; harness-site `scripts/fetch-docs.py` mirror + + `/docs/` nav entry). Reusing it keeps the two products' docs infrastructure + identical. + +## Technical details + +- `docs/` — 8 pages: index, getting-started, architecture, concepts (skills / + security model / secrets), CLI reference, upgrading, teardown. Content is + grounded in the README, the template README, and the three shipped skills; + present-tense, user-facing. +- `zensical.toml` — pinned to zensical 0.0.43 (same as harness). + `site_url = "https://dispatch.boldblack.ai/docs/"` so canonical tags and + sitemap point at the brand domain, not github.io (the mirror is the + canonical host; Pages remains the origin). +- `.github/workflows/docs.yml` — same as harness's: build on PR (proof only), + deploy to Pages on push to main + workflow_dispatch, paths-filtered to + `docs/**`, `zensical.toml`, the workflow, README.md, AGENTS.md. +- Zensical emits relative links for pages/assets/search, so the built tree is + relocatable under any subpath — the property the mirror depends on. Keep it + that way: no absolute-internal-link builds. +- `site/` is gitignored (build output). +- GitHub Pages must be enabled once (Settings → Pages → Source: GitHub + Actions) — admin console step, or `gh api` with pages-write admin + permission. +- dispatch-site changes (separate PR, dispatch-site repo): `scripts/fetch-docs.py` + (mirrors `https://boldblackai.github.io/create-dispatch/` → `static/docs/`), + a `/docs/` nav entry, `.gitignore` entry, and an `amplify.yml` build step. + The mirror hard-fails on an empty sitemap, so the dispatch-site PR must not + ship before Pages is live. + +## Migration notes + +None — additive. Fresh repo state on main; no template changes, so the golden +test is unaffected. + +## Implementation checklist + +- [ ] `docs/` + `zensical.toml` + `.github/workflows/docs.yml` + `.gitignore` +- [ ] CI green (lint, format, actionlint, tsc, golden test) +- [ ] Local `zensical build --clean` verified: relative links, canonical at + `dispatch.boldblack.ai/docs/` +- [ ] Enable GitHub Pages (build_type=workflow) +- [ ] dispatch-site: mirror script + nav + amplify step +- [ ] First Pages deploy → mirror → `dispatch.boldblack.ai/docs/` serves diff --git a/zensical.toml b/zensical.toml new file mode 100644 index 0000000..286461c --- /dev/null +++ b/zensical.toml @@ -0,0 +1,93 @@ +[project] + +site_name = "dispatch" +site_description = "Deploy your own long-running coding agent — one command, your AWS account, your Slack workspace." +site_author = "BoldBlack" +site_url = "https://dispatch.boldblack.ai/docs/" + +copyright = """ +Copyright © 2026 BoldBlack +""" + +nav = [ + { "Home" = "index.md" }, + { "Getting started" = "getting-started.md" }, + { "Architecture" = "architecture.md" }, + { "Concepts" = [ + { "Skills" = "skills.md" }, + { "Security model" = "security.md" }, + { "Secrets" = "secrets.md" }, + ] }, + { "CLI reference" = "cli.md" }, + { "Upgrading" = "upgrading.md" }, + { "Tearing down" = "teardown.md" }, +] + +[project.theme] + +language = "en" + +features = [ + "announce.dismiss", + "content.code.annotate", + "content.code.copy", + "content.code.select", + "content.footnote.tooltips", + "content.tabs.link", + "content.tooltips", + "navigation.footer", + "navigation.indexes", + "navigation.instant", + "navigation.instant.prefetch", + "navigation.path", + "navigation.sections", + "navigation.top", + "navigation.tracking", + "search.highlight", +] + +[[project.theme.palette]] +scheme = "default" +toggle.icon = "lucide/sun" +toggle.name = "Switch to dark mode" + +[[project.theme.palette]] +scheme = "slate" +toggle.icon = "lucide/moon" +toggle.name = "Switch to light mode" + +[project.markdown_extensions.abbr] +[project.markdown_extensions.admonition] +[project.markdown_extensions.attr_list] +[project.markdown_extensions.def_list] +[project.markdown_extensions.footnotes] +[project.markdown_extensions.md_in_html] +[project.markdown_extensions.toc] +permalink = true +[project.markdown_extensions.pymdownx.arithmatex] +generic = true +[project.markdown_extensions.pymdownx.betterem] +[project.markdown_extensions.pymdownx.caret] +[project.markdown_extensions.pymdownx.details] +[project.markdown_extensions.pymdownx.emoji] +emoji_generator = "zensical.extensions.emoji.to_svg" +emoji_index = "zensical.extensions.emoji.twemoji" +[project.markdown_extensions.pymdownx.highlight] +anchor_linenums = true +line_spans = "__span" +pygments_lang_class = true +[project.markdown_extensions.pymdownx.inlinehilite] +[project.markdown_extensions.pymdownx.keys] +[project.markdown_extensions.pymdownx.magiclink] +[project.markdown_extensions.pymdownx.mark] +[project.markdown_extensions.pymdownx.smartsymbols] +[project.markdown_extensions.pymdownx.superfences] +custom_fences = [ + { name = "mermaid", class = "mermaid", format = "pymdownx.superfences.fence_code_format" }, +] +[project.markdown_extensions.pymdownx.tabbed] +alternate_style = true +combine_header_slug = true +[project.markdown_extensions.pymdownx.tasklist] +custom_checkbox = true +[project.markdown_extensions.pymdownx.tilde]