docs(hub): align public docs with v0.3 (#3152)

* docs(hub): align public docs with v0.3

Bring the public workflow, API, authority, activity, and self-hosting guidance in line with the released Hub contracts. Clarify the public HTTPS boundary required by provider events while keeping Hub and PostgreSQL private.

* docs(hub): clarify daemon logout scope

* docs(hub): restore Compose path and simplify TLS guidance

* docs(hub): link the single-repo-team-bot example
This commit is contained in:
Mohamed Boudra
2026-08-10 16:13:51 +02:00
committed by GitHub
parent 48e65b407f
commit 42f1d0b289
14 changed files with 149 additions and 81 deletions
+11 -17
View File
@@ -12,34 +12,28 @@ Every event Hub accepts is recorded, whether or not it ran anything. That record
## Where to look
**Project → Activity** lists the events routed to that project: the event, its source, the result, and when it arrived.
**Project → Activity** lists routed trigger runs and their execution state.
The result column says what happened:
**Connections → Known unrouted events** is the organization-level view of accepted provider events that did not start a configured trigger. Hub records one of four bounded reasons:
| Result | Meaning |
| --------------------- | ----------------------------------------------------------------- |
| A trigger name | The event matched that trigger and dispatched |
| A lifecycle state | The execution is in progress or finished |
| `no_matching_trigger` | The event reached the project, but no trigger's filters matched |
| `provider_unrouted` | No project route existed for the resource that produced the event |
**Project → Executions** lists the runs: which trigger, which configuration revision, which daemon, duration, and outcome.
**Connections → Known unrouted events** is the organization-level view. It holds events whose credential belongs to your organization but which no project's configuration claimed. A repository you connected but never named in a trigger lands here.
Those two are different problems. `no_matching_trigger` means your filters are wrong. An unrouted event means no configuration mentions that repository, workspace, or guild at all.
| Reason | Meaning |
| --------------------------- | ------------------------------------------------------------ |
| `no_project_route` | No project route is configured for the event |
| `no_trigger_for_source` | No configured trigger handles that event source |
| `trigger_filters_rejected` | A trigger handles the source, but its filters rejected it |
| `configuration_unavailable` | A relevant configuration or connection could not be resolved |
## Nothing happened when I mentioned the bot
Work down this list.
1. **Is the event in the project's Activity?** If not, check **Connections → Known unrouted events**. If it is there, no trigger names that resource. Add `filters.repo`, `filters.workspace`, or `filters.guild` and push.
1. **Is the event in the project's Activity?** If not, check **Connections → Known unrouted events** and use its reason to distinguish routing, source, filter, and configuration failures.
2. **Is the event anywhere at all?** If not, the event never reached Hub. Check the provider's own delivery log: GitHub's App → Advanced → Recent Deliveries, or Slack's Event Subscriptions page. Then check that the app is subscribed to that event type.
3. **Is your user in `from_users`?** This is the most common cause. GitHub uses your login; Slack and Discord use the user ID, not the display name.
4. **Did the mention match?** On GitHub, `contains` must appear in the comment body. On Slack and Discord the bot must actually be mentioned, and `pattern` matches only at the start of the text after the mention.
4. **Did the invocation match?** On GitHub, `contains` must appear in the comment body. On Slack and Discord the bot must be mentioned, and `pattern` or its legacy `contains` alias must prefix the text after the mention.
5. **Is the configuration you think is active actually active?** The Configuration tab shows the active revision and the last sync attempt. A failed push leaves the old revision serving.
6. **Is the daemon connected?** An offline daemon fails dispatch with `daemon_not_connected`.
7. **Did it run and stop early?** Check the execution. An agent that never reports back is ended by `idle_timeout` after 5 minutes by default, or `timeout` after an hour.
7. **Did it run and stop early?** Compare the execution with the step's authored `idle_timeout` and `max_runtime`, and the workflow's `max_runtime`. All three limits are explicit in the workflow.
8. **Did it run, but deliver nothing?** Check the step's prompt. An agent that is not told to call `hub.reply` and `hub.finish_execution` can answer in its own transcript and never report back. See [Tell the agent which tool to call](/docs/hub/workflows#tell-the-agent-which-tool-to-call).
## Sync failures
+19 -23
View File
@@ -54,10 +54,12 @@ API failures use RFC 9457 problem details. Missing, invalid, or revoked credenti
```json
{
"type": "https://hub.example.com/problems/unauthorized",
"title": "Unauthorized",
"type": "https://paseo.sh/problems/unauthorized",
"title": "Authentication required",
"status": 401,
"detail": "Provide a valid organization API key."
"detail": "Provide an active Paseo organization credential in the Authorization: Bearer header.",
"code": "unauthorized",
"requestId": "5e967c44-fc22-4f6d-8fc5-1bbff33121af"
}
```
@@ -130,7 +132,8 @@ Limits:
- Bundle: at most 100 files.
- File path: at most 512 characters.
- File content: at most 1,000,000 characters; partial byte limits are enforced during compilation.
- File content: at most 1,000,000 characters.
- Prompt partials: at most 1,000,000 bytes each and 5,000,000 bytes in total.
On success, Hub returns `201`:
@@ -163,7 +166,7 @@ The trigger must exist in the active configuration, and `actor` must be listed
in that trigger's `filters.from_users` allowlist.
```http
POST /api/manual-runs
POST /api/v1/manual-runs
```
Request body:
@@ -181,32 +184,27 @@ Request body:
- `expectedVersionId` is optional. When supplied, Hub rejects the dispatch if that revision is no longer active.
- `input` is the same string a provider message uses: leading `key=value` tokens are parsed as declared inputs, and the remainder becomes `${{ paseo.prompt }}`.
- `deliveryKey` should be unique and stable per dispatch. Reusing one resolves to the existing trigger instead of starting a duplicate run.
- `deliveryKey` should be unique and stable per dispatch. Hub uses it for durable deduplication, but does not promise exactly-once dispatch or replay of an earlier response.
On success, Hub returns `200`:
```json
{
"deliveryKey": "deploy-2026-08-04-001",
"triggerId": "00000000-0000-4000-8000-000000000000",
"executionId": "00000000-0000-4000-8000-000000000000",
"status": "spawning",
"daemonId": "00000000-0000-4000-8000-000000000000",
"agentId": "agent-id"
"providerEventReceiptId": "00000000-0000-4000-8000-000000000000",
"triggerRunId": "00000000-0000-4000-8000-000000000000",
"configuredTriggerName": "deploy",
"workflowStatus": "running"
}
```
Common responses are `400 {"error":"invalid_request"}`, `404` for an
unknown project, missing configuration, or missing trigger, `403
{"error":"actor_forbidden"}` when the actor is not allowed by the trigger,
and `409` when the daemon is offline, the expected configuration is no longer
current, or the run could not be dispatched.
Common responses are `400` for an invalid request, `403` when the actor is not allowed by the trigger, `404` for an unknown project, configuration, or trigger, and `409` when the daemon is offline, the expected configuration is no longer current, or dispatch conflicts. Each uses the problem-details shape above.
Example:
```bash
curl --fail-with-body -sS -X POST "$HUB_URL/api/manual-runs" \
-H "Authorization: Bearer $PASEO_API_KEY" \
curl --fail-with-body -sS -X POST "$PASEO_HUB_URL/api/v1/manual-runs" \
-H "Authorization: Bearer $PASEO_HUB_API_KEY" \
-H "Content-Type: application/json" \
--data '{
"projectSlug": "my-project",
@@ -230,7 +228,7 @@ be used as one.
POST /api/v1/daemons/enrollment-tokens
```
Send an empty JSON object as the request body. On success, Hub returns `201`:
No request body is required. On success, Hub returns `201`:
```json
{
@@ -245,10 +243,8 @@ The token expires after 10 minutes and is consumed when the daemon enrolls.
```bash
curl --fail-with-body -sS -X POST \
"$HUB_URL/api/v1/daemons/enrollment-tokens" \
-H "Authorization: Bearer $PASEO_API_KEY" \
-H "Content-Type: application/json" \
--data '{}'
"$PASEO_HUB_URL/api/v1/daemons/enrollment-tokens" \
-H "Authorization: Bearer $PASEO_HUB_API_KEY"
```
Direct API consumers can pass the returned token to the daemon enrollment protocol. The Paseo CLI intentionally does not accept raw enrollment tokens; `connect` owns the authenticated single-flow exchange.
+28 -10
View File
@@ -64,6 +64,18 @@ The only top-level keys are `environments` and `agents`. A `triggers` key is rej
For `worktree`, use `newBranch` and optional `base` with `branch-off`, `branch` with `checkout-branch`, or positive `prNumber` with `checkout-pr`.
```yaml
environments:
review:
kind: daemon
daemon: build-server
cwd: /workspace/project
worktree:
mode: branch-off
newBranch: paseo/review
base: origin/main
```
An environment is a complete named object. A step selects its name; objects are not inherited, merged, or partially overridden.
### Named agents
@@ -80,6 +92,8 @@ Each agent is one complete provider configuration:
A named selection preserves the complete object, including structured options. Named agents have no parent, patch, or per-step override.
Hub passes `model`, `mode`, `thinkingOptionId`, and `options` to the Paseo daemon without renaming or flattening provider fields. The selected daemon validates them against its current provider schema; Hub does not translate provider-native options.
## Workflow files
`.paseo/workflows/review.yml`:
@@ -88,6 +102,8 @@ A named selection preserves the complete object, including structured options. N
name: review
on: manual.run
max_runtime: 2h
filters:
from_users: [automation]
inputs:
repo:
type: string
@@ -103,15 +119,15 @@ steps:
- text: ${{ paseo.prompt }}
```
| Field | Required | Notes |
| ------------- | ------------------ | --------------------------------------------------------- |
| `name` | yes | Workflow name, unique across the bundle. |
| `on` | yes | Provider event such as `manual.run` or `discord.mention`. |
| `max_runtime` | yes | Hard limit for the complete run, up to 24h. |
| `filters` | provider-dependent | Provider resource and sender allowlists. |
| `inputs` | no | Typed invocation headers. |
| `values` | no | Named expressions. |
| `steps` | yes | One or more ordered inline steps. |
| Field | Required | Notes |
| ------------- | -------- | --------------------------------------------------------- |
| `name` | yes | Workflow name, unique across the bundle. |
| `on` | yes | Provider event such as `manual.run` or `discord.mention`. |
| `max_runtime` | yes | Hard limit for the complete run, up to 24h. |
| `filters` | yes | Provider resource filters and the sender allowlist. |
| `inputs` | no | Typed invocation headers. |
| `values` | no | Named expressions. |
| `steps` | yes | One or more ordered inline steps. |
### Inputs and values
@@ -176,7 +192,7 @@ prompt:
Includes resolve relative to `.paseo/workflows/`, so shared partials use `partials/<name>.md`. Missing files, absolute or traversing paths, symlinks, content mismatches, and files outside the partial tree are rejected.
### Output authority
### Output capabilities
Authority stays on the step that uses it:
@@ -189,6 +205,8 @@ allow_outputs:
Slack workflows use `slack.reply`; Discord workflows use `discord.reply`. The declaration grants `hub.reply`, and the prompt must tell the agent to call it. GitHub has no reply output; use an explicit [`github` block](/docs/hub/github).
Every step receives `hub.finish_execution`. The prompt must tell the agent when to call it; Hub does not append completion or reply instructions. If `output.schema` is present, `hub.finish_execution` requires an `output` value that matches the schema. If an `allow_outputs` entry is `required: true`, the agent must emit that output before finishing. `max` defaults to `1`.
## Migrating a monolithic file
Keep `environments` in `hub.yml`, convert the environment list to a named map, and move each former trigger into its own `.paseo/workflows/<name>.yml` file. Move shared prompt files to `.paseo/workflows/partials/`. Define complete named agent configurations under `agents` and replace dynamic provider fields with finite named-agent selection.
+2
View File
@@ -21,6 +21,8 @@ A project configuration is one versioned bundle:
`hub.yml` owns named environments and agents. Each direct-child workflow file owns one trigger and its ordered inline steps. Prompt partials referenced by those workflows live below `workflows/partials/`. Workflow discovery is fixed by convention; there is no manifest or include list.
[single-repo-team-bot](https://github.com/getpaseo/hub/tree/main/examples/single-repo-team-bot) is a complete bundle in this shape: Discord, Slack, and GitHub workflows running a classifier and a worker on shared partials. Copy `.paseo/` into your repository and replace the placeholders its README lists.
## Sources
A configuration comes from one source:
+9 -6
View File
@@ -46,18 +46,21 @@ The agent can use `git` and `gh` within the declared repositories and permission
| `connection` | Project GitHub connection slug. |
| `repositories` | Repositories the token can reach. On a GitHub-triggered run, this defaults to the triggering repository. Required for other providers. |
| `permissions` | Installation-token permissions such as `contents`, `pull_requests`, and `issues`. Defaults to `contents: read`. |
| `duration` | Token lifetime. Defaults to `1h`, GitHub's maximum. |
| `duration` | Positive token lifetime up to `1h`. Defaults to `1h`, GitHub's maximum. |
Requested authority cannot exceed the GitHub App installation. Activation and dispatch fail clearly when the connection, repository, or permissions cannot be resolved.
## Agent environment
Hub supplies `GH_TOKEN` and git configuration through environment variables:
Hub supplies `GH_TOKEN` and process-scoped git configuration through environment variables:
- Commits use the App bot identity.
- `git@github.com` remotes are rewritten to HTTPS.
- Global and system git configuration are ignored.
- The daemon host's git credentials are not used or changed.
- Commits use the App bot login as `user.name` and `<app-id>+<bot-login>@users.noreply.github.com` as `user.email`.
- `git@github.com:` and `ssh://git@github.com/` remotes are rewritten to HTTPS.
- `gh auth git-credential` handles GitHub credentials for the step.
- User-global and system git configuration are ignored, and terminal credential prompts are disabled.
- The daemon host's git identity and credentials are not read or changed.
`GH_TOKEN` and Hub's git configuration variables are reserved when a step has a `github` block; workflow `env` cannot replace them.
## Keep authority on the worker
+3 -3
View File
@@ -1,6 +1,6 @@
---
title: Hosted Hub
description: The managed Paseo Hub, not available yet.
description: Sign in to the managed Paseo Hub or self-host the same service.
nav: Hosted
order: 73
category: Hub
@@ -8,6 +8,6 @@ category: Hub
# Hosted Hub
Not available yet. For now, see [self-hosting](/docs/hub/self-hosting).
[Paseo Hub](https://hub.paseo.sh) is the managed service. Existing accounts can sign in; new account registration is currently closed.
When it ships, Paseo owns the GitHub App, Slack app, and Discord application, so connecting a provider is one click rather than a console walkthrough. Projects, configuration, triggers, daemons, and activity work the same way on both.
Paseo owns the hosted GitHub App, Slack app, and Discord application. Projects, configuration, triggers, daemons, and activity work the same way when you [self-host](/docs/hub/self-hosting).
+21 -10
View File
@@ -8,20 +8,21 @@ category: Hub
# Hub quickstart
## 1. Connect GitHub and a daemon
## 1. Create a project
Open **Connections** in Hub and connect the GitHub account or organization whose repositories you use. On the machine that will run agents:
Open **Connections** in Hub and connect the GitHub account or organization whose repositories you use. Then open **Projects → New project** and create a project.
## 2. Log in and connect a daemon
On the machine that will run agents:
```sh
paseo hub login https://your-hub.example.com
paseo hub projects
paseo hub connect
```
Approve the browser login. See [Daemons](/docs/hub/daemons) for the separation between your CLI login and the daemon relationship.
## 2. Create a project
Open **Projects → New project**. In **Configuration**, choose the repository that will hold the configuration and select **Use for configuration**.
Approve the browser login. `projects` shows the slug needed by `deploy`. See [Daemons](/docs/hub/daemons) for the separation between your CLI login and the daemon relationship.
## 3. Add the bundle
@@ -79,9 +80,7 @@ Workflow files are discovered as direct `.yml` children of `.paseo/workflows/`.
## 4. Validate and deploy
Push the files to the configuration repository's default branch. Hub fetches the complete bundle at that commit, validates it, and activates a revision. A failed sync leaves the previous revision active.
You can validate or deploy the same local bundle from its project root:
From the project root:
```sh
paseo hub deploy -p your-project --dry-run
@@ -101,3 +100,15 @@ Comment from the account in `from_users`:
Open the project's **Activity** tab to see routing and execution. If nothing runs, use the [Activity checklist](/docs/hub/activity).
Before widening the allowlist or granting write authority, read [Hub security](/docs/hub/security).
When you no longer need the local CLI login:
```sh
paseo hub logout
```
Logging out does not disconnect the daemon. When the daemon is connected to the same Hub as the active CLI login, use `paseo hub logout --disconnect-daemon` to remove both relationships.
## Next
[single-repo-team-bot](https://github.com/getpaseo/hub/tree/main/examples/single-repo-team-bot) is a complete bundle covering all three providers, with a classifier, a worker, and shared prompt partials.
@@ -16,6 +16,8 @@ Go to **Settings → Developer settings → GitHub Apps → New GitHub App** on
Replace `hub.example.com` with your `PASEO_HUB_APP_URL`.
The webhook URL must be reachable from GitHub. Keep GitHub's default SSL verification enabled; see [Provider URLs](/docs/hub/self-hosting#provider-urls).
| Setting | Value |
| ------------------ | ---------------------------------------------------------- |
| Homepage URL | `https://hub.example.com` |
+42 -6
View File
@@ -20,11 +20,11 @@ Migrations run automatically at startup. Hub does not start listening when a mig
Hub has one public URL and one persistent application secret:
| Variable | Purpose |
| ----------------------- | ------------------------------------------------------------------ |
| `PASEO_HUB_APP_URL` | Public origin used by the dashboard, authentication, and callbacks |
| `PASEO_HUB_AUTH_SECRET` | Protects browser sessions and derives execution credentials |
| `DATABASE_URL` | PostgreSQL connection string |
| Variable | Purpose |
| ----------------------- | ---------------------------------------------------------------------------- |
| `PASEO_HUB_APP_URL` | Public origin used by the dashboard, authentication, callbacks, and webhooks |
| `PASEO_HUB_AUTH_SECRET` | Protects browser sessions and derives execution credentials |
| `DATABASE_URL` | PostgreSQL connection string |
Generate `PASEO_HUB_AUTH_SECRET` once and keep it across restarts:
@@ -89,7 +89,26 @@ docker compose up -d
The stack publishes Hub on port `3000` and stores PostgreSQL data in a named volume. The Hub image is `ghcr.io/getpaseo/hub:latest`.
When a reverse proxy terminates HTTPS, set `PASEO_HUB_TRUSTED_CLIENT_IP_HEADER` to the header carrying the original client IP.
### HTTPS with Caddy
Compose serves plain HTTP on port `3000`. Run Caddy on the same host to terminate TLS:
```caddyfile
hub.example.com {
reverse_proxy 127.0.0.1:3000
}
```
Point `hub.example.com` at the host and open ports 80 and 443. Caddy [obtains and renews the certificate](https://caddyserver.com/docs/automatic-https).
Then set in `.env`:
```dotenv
PASEO_HUB_APP_URL=https://hub.example.com
PASEO_HUB_TRUSTED_CLIENT_IP_HEADER=x-forwarded-for
```
To keep port `3000` off the public interface, change the `hub` port in `compose.yml` to `"127.0.0.1:3000:3000"`.
## Fly
@@ -122,6 +141,23 @@ fly deploy -a your-hub \
Keep one machine running. Hub holds the Discord gateway connection and dispatches events to daemons, so a stopped machine misses events.
## Provider URLs
Slack and GitHub call Hub at `PASEO_HUB_APP_URL`:
| Provider setting | URL |
| ---------------------------- | ------------------------------------------------------ |
| GitHub webhook | `<PASEO_HUB_APP_URL>/webhook` |
| GitHub OAuth callback | `<PASEO_HUB_APP_URL>/api/integrations/github/callback` |
| Slack Events API request URL | `<PASEO_HUB_APP_URL>/api/integrations/slack/events` |
| Slack OAuth callback | `<PASEO_HUB_APP_URL>/api/integrations/slack/callback` |
Slack requires the Events API request URL to be publicly reachable over HTTPS with a valid certificate, and its [OAuth redirect URL](https://docs.slack.dev/authentication/installing-with-oauth/) must use HTTPS. See Slack's [HTTP request URL](https://docs.slack.dev/apis/events-api/using-http-request-urls/) requirements.
GitHub must reach the webhook URL and [verifies SSL certificates](https://docs.github.com/en/webhooks/using-webhooks/best-practices-for-using-webhooks#use-https-and-ssl-verification) by default.
A Hub on `localhost` or a LAN address still runs manual and daemon workflows, but Slack and GitHub cannot reach it. To test provider setup locally, point `PASEO_HUB_APP_URL` at an HTTPS tunnel before starting Hub. A changed tunnel origin requires updated provider settings and a Hub restart.
## Upgrades
Pull the new image or source and deploy it. Migrations are forward-only. Back up PostgreSQL first; it contains configuration revisions, connections, and execution history.
+1 -1
View File
@@ -8,7 +8,7 @@ category: Hub
# Slack for Hub
Hub receives Slack mentions over the Events API. Socket Mode is not used, so your Hub must be reachable from Slack.
Hub receives Slack mentions over the Events API. Socket Mode is not used, so your Hub needs a publicly reachable HTTPS origin with a valid certificate. See [Provider URLs](/docs/hub/self-hosting#provider-urls).
## Create the app
+1 -1
View File
@@ -34,7 +34,7 @@ steps:
- { type: discord.reply, max: 1, required: true }
```
Turn on Discord Developer Mode to copy IDs. `from_users` matches the author; `guild` and `channels` constrain where the mention arrived. All filters must pass.
Turn on Discord Developer Mode to copy IDs. `from_users` matches the author; `guild` and `channels` constrain where the mention arrived. `pattern` is a required prefix after the mention; `contains` is its legacy alias and has the same prefix behavior. All filters must pass.
The reply posts in the triggering thread or channel. `discord.reply` grants `hub.reply`, but does not rewrite the prompt. A Discord trigger grants no GitHub credential; add a [`github` block](/docs/hub/github) to the step that needs one.
+2 -2
View File
@@ -66,8 +66,8 @@ An allowlist is one layer of defense. It does not make a permitted account trust
| `workspace` | Slack | Team id, `T01234567` |
| `guild` | Discord | Guild id |
| `channels` | Slack, Discord | Channel ids |
| `contains` | all | Substring of the message text |
| `pattern` | all | Prefix of the message text |
| `contains` | all | GitHub substring; Slack and Discord invocation prefix |
| `pattern` | all | Invocation prefix |
| `connection` | all | A connection slug, when the organization has several |
All conditions must pass. There is no `any` mode.
+1 -1
View File
@@ -34,7 +34,7 @@ steps:
- { type: slack.reply, max: 1, required: true }
```
Slack filters use IDs, not display names. `from_users` matches the author, `workspace` the team, and `channels` the channel. `pattern` checks the beginning of text after the mention; `contains` checks a substring. All filters must pass.
Slack filters use IDs, not display names. `from_users` matches the author, `workspace` the team, and `channels` the channel. `pattern` is a required prefix after the mention; `contains` is its legacy alias and has the same prefix behavior. All filters must pass.
The reply posts in the triggering thread. A root message gets a thread; a threaded message stays there. `slack.reply` grants `hub.reply`, but does not add reply instructions to the prompt.
+7 -1
View File
@@ -2,7 +2,7 @@
title: Hub workflows
description: Build ordered Hub workflows with prompts, routing, outputs, and provider authority.
nav: Workflows
order: 65
order: 64
category: Hub
---
@@ -55,6 +55,8 @@ A finite input can select among complete named environments:
name: route-repository
on: manual.run
max_runtime: 1h
filters:
from_users: [automation]
inputs:
repo:
type: string
@@ -94,6 +96,8 @@ For dynamic routing, select complete named agents from a finite expression:
name: route-agent
on: manual.run
max_runtime: 1h
filters:
from_users: [automation]
inputs:
agent:
type: string
@@ -192,6 +196,8 @@ Steps run in file order. `if` may read inputs, values, and prior step output:
name: conditional-review
on: manual.run
max_runtime: 1h
filters:
from_users: [automation]
steps:
- id: inspect
environment: paseo