# Byoa Link

> Onboard a Gradient Bang operator to run a Bring-Your-Own-Agent (BYOA) — logs in with email/password, claims a corp ship as BYOA, generates a per-ship wake secret, and writes `.env.byoa` for the `uv run byoa` CLI. Usage `/byoa-link [env]` (env defaults to `prod`; `live` is an accepted synonym for `prod`).

- Skill: `pipecat-ai/byoa-link` (Agent Skill)
- Install (CLI): `npx skillmds@latest add pipecat-ai/byoa-link`
- Raw SKILL.md: https://api.skillmd.com/api/skills/pipecat-ai/byoa-link/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: pipecat-ai (https://skillmd.com/u/pipecat-ai)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/pipecat-ai/byoa-link

---


# BYOA link

> **env defaults to `prod` when omitted.** `live` is accepted as a user-facing synonym for `prod` (e.g. `/byoa-link live` is equivalent to `/byoa-link prod` and to `/byoa-link` with no arg). `local` is the only other valid value.

Walks an operator through everything they need to run `uv run byoa` against a Gradient Bang corp ship. End state: a populated `.env.byoa` at the **main repo root** (mode 0600), a ship claimed as BYOA in `private` mode with a wake secret registered server-side, and clear next-step instructions for the operator.

**Always write the env file to the main repo root**, never to the current directory. The skill is often invoked from a git worktree (`.claude/worktrees/<name>/`), and a file dropped there is invisible from the operator's normal checkout. Resolve the destination via `git rev-parse --path-format=absolute --git-common-dir` (its parent is always the main worktree root, even when invoked from a linked worktree). Only honor `--out` when explicitly passed.

## Parameters

```
/byoa-link [env] [--force] [--ship-id <uuid>] [--out <path>]
```

- **env**: `prod` (default, also accepts `live` as a synonym) or `local`. Picks the game server endpoint:
  - `prod` / `live` → `https://api.gradient-bang.com/functions/v1` (operator-facing; no env file needed). If the operator types nothing, this is what they get.
  - `local` → sources `SUPABASE_URL` from `.env.supabase` (`http://127.0.0.1:54321` when `npx supabase start` is running)
- **--force**: overwrite an existing `.env.byoa` without prompting
- **--ship-id**: skip the ship picker; use this corp ship directly
- **--out**: write the env file somewhere other than `<repo-root>/.env.byoa`. Without this flag, the file always lands at the main repo root regardless of cwd.

The `dev` env was dropped — it required internal-only env files (`.env.cloud.dev`, `EDGE_API_TOKEN`) that operators don't have. Internal team members testing against dev should run `local` with their dev Supabase URL exported in shell.

## Pre-flight

Refuse to proceed if any of these is true. Surface a clean error and stop:

- For `local`: `${SUPABASE_URL}` is unset in `.env.supabase` (or the file is missing entirely).
- The resolved destination already has a `.env.byoa` and `--force` was not passed. Check the resolved repo-root path (or `--out`), not cwd.
- `SUBAGENT_BUS_DATABASE_URL` is **only** required for the local-dev wake daemon (`byoa --serve`); not for setup. Don't pre-flight it here.

## Auth header cheat-sheet

`/ship_byoa_configure` uses `getAuthenticatedUser()` — same pattern as `verify_token`, `user_character_create`, `reset-password`. JWT-only; no admin token. All BYOA actions (list, claim, clear, set) go through it.

| Endpoint | Scheme | Headers required |
|---|---|---|
| `/login` | public | `Content-Type` only |
| `/ship_byoa_configure` | `getAuthenticatedUser()` | `Authorization: Bearer <user_jwt>` |

## Steps

### 1. Resolve the game server URL

For `prod` or `live` (the default — if the operator types nothing, treat as `prod`; `live` is a user-facing synonym):

```bash
SUPABASE_URL=https://api.gradient-bang.com
```

Hardcoded — operator never types it. (If the CNAME proxy isn't live, fall back to the direct Supabase project URL; this URL is public.)

Anything other than `prod`, `live`, or `local` should error out with `unknown env: <value>; expected prod | live | local` before any network call.

For `local`:

```bash
set -a && source .env.supabase && set +a
# SUPABASE_URL now points at http://127.0.0.1:54321 (or whatever the file has).
```

All curls in steps 2–6 append `/functions/v1/<endpoint>` to `SUPABASE_URL`, so the resolved value must NOT include `/functions/v1`.

`BYOA_WAKE_SECRET` is the **per-ship** bearer that authenticates `wake_agent` → wake-receiver (local daemon or operator's Vercel Function). Generate a fresh random hex string (`openssl rand -hex 32`) **per ship** — never share across ships. The skill writes the value into `.env.byoa` AND sends the same value to us via `ship_byoa_configure { action: 'set', wake_secret }`. We encrypt at rest and never return it to clients.

The bus **channel** is allocated server-side per voice session and injected into the harness env by `wake_agent`. Operators never see or set a channel. The local-dev wake daemon (`uv run byoa --serve`) loads `.env.byoa` first, then falls back to `.env.bot` for `SUBAGENT_BUS_DATABASE_URL` when running from a bot checkout. Standalone operators set it in shell env or add it to `.env.byoa` by hand.

### 2. Log the operator in

Prompt for email + password (unless supplied as positional args):

```bash
curl -s -X POST "${SUPABASE_URL}/functions/v1/login" \
  -H "Content-Type: application/json" \
  -d '{"email": "<email>", "password": "<password>"}'
```

Extract `session.access_token` and the character list (`characters`). On any non-success response (4xx, `success: false`), surface the error and stop.

### 3. Pick the operator's character

If exactly one character → use it. If multiple → list by `name + character_id` (short prefix) and ask the operator to pick.

### 4. List corp ships

```bash
curl -s -X POST "${SUPABASE_URL}/functions/v1/ship_byoa_configure" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <access_token>" \
  -d '{"character_id": "<character_id>", "action": "list"}'
```

Response shape: `{ success, action: "list", ships: [{ ship_id, name, sector, byoa_owner_character_id_prefix, claimable_by_me }, ...] }`. Filter to `claimable_by_me === true`.

If `--ship-id` was passed, validate it appears in this filtered list. Otherwise present (`name` + 8-char `ship_id` prefix + `sector`) and ask the operator to pick.

If the chosen ship is already claimed by the same operator (`byoa_owner_character_id_prefix` matches the operator's character prefix), the claim in step 5 is idempotent — proceed anyway.

### 5. Claim the ship as BYOA

```bash
curl -s -X POST "${SUPABASE_URL}/functions/v1/ship_byoa_configure" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <access_token>" \
  -d '{
    "character_id": "<character_id>",
    "ship_id": "<ship_id>",
    "action": "claim",
    "mode": "private"
  }'
```

Confirm the response has `byoa_owner_character_id` matching the operator's character_id. A `409 ship_busy` means the ship has an active task — ask the operator to wait or stop it; do not auto-retry.

### 6. Generate + register the per-ship wake secret

Generate a fresh random hex bearer (`openssl rand -hex 32`). The wake secret is a per-ship, operator-side credential. Capture the freshly-generated hex in memory; write it to `.env.byoa` in step 7 AND send the same value to us so wake_agent can sign outbound POSTs to the operator's wake receiver:

```bash
curl -s -X POST "${SUPABASE_URL}/functions/v1/ship_byoa_configure" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <access_token>" \
  -d '{
    "character_id": "<character_id>",
    "ship_id": "<ship_id>",
    "action": "set",
    "wake_secret": "<generated hex>"
  }'
```

Ask the operator whether they want a custom **wake URL** (the URL wake_agent POSTs to):

- **Leave unset** (recommended for local dev): `wake_agent` falls back to `http://host.docker.internal:8765/wake` — the default port for `uv run byoa --serve` running on the host. No further config needed.
- **Set explicitly**: pass `source_url` on the same `set` call (or a follow-up). Required for Vercel/prod deploys (`https://<your-project>.vercel.app/api/wake`) and for local daemons listening on a non-default port.

For production also set `BYOA_WAKE_SECRET=<same hex>` as project env on the operator's Vercel project (where their wake receiver runs).

### 7. Write `.env.byoa`

Path: `--out` value, or `<main-repo-root>/.env.byoa` if not provided. **Never write to cwd** — the skill is regularly invoked from a worktree, and writing there hides the file from the operator's normal checkout. File mode **0600**.

Resolve the main repo root with `git rev-parse --path-format=absolute --git-common-dir` — its parent is the main worktree root whether the caller is in the main checkout or a linked worktree. The `env.byoa.example` template also lives at that root.

```bash
PROJECT_ROOT="$(dirname "$(git rev-parse --path-format=absolute --git-common-dir)")"
OUT_PATH="${OUT_OVERRIDE:-$PROJECT_ROOT/.env.byoa}"
umask 077
cp "$PROJECT_ROOT/env.byoa.example" "$OUT_PATH"
python3 - "$OUT_PATH" "<character_id>" "<ship_id>" "<wake_secret_hex>" <<'PY'
import sys, pathlib
path, char_id, ship_id, secret = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]
text = pathlib.Path(path).read_text()
text = text.replace("BYOA_CHARACTER_ID=\n", f"BYOA_CHARACTER_ID={char_id}\n", 1)
text = text.replace("BYOA_SHIP_ID=\n", f"BYOA_SHIP_ID={ship_id}\n", 1)
text = text.replace("BYOA_WAKE_SECRET=\n", f"BYOA_WAKE_SECRET={secret}\n", 1)
pathlib.Path(path).write_text(text)
PY
chmod 600 "$OUT_PATH"
```

Only the three required values are filled in; every other option (`BYOA_PROMPT_FILE`, `TASK_LLM_*`, `BYOA_TOOL_CALL_TIMEOUT_SECONDS`, …) stays commented out at its default. `SUBAGENT_BUS_DATABASE_URL` is not in the template — `uv run byoa --serve` picks it up from `.env.bot` via fallback when running from the bot checkout. Standalone operator deploys without a bot checkout need to set it in the shell env or add it to `.env.byoa` by hand.

Verify the file is `0600`, sits at the resolved repo root (not a worktree), and all three values are populated after writing; surface a warning if not.

### 8. Print next steps

Echo a copy-pasteable summary the operator can act on:

- Where the env file was written.
- Author `./prompt.md` (≤ 8 KB, appended to the base task-agent prompt) and set `BYOA_PROMPT_FILE=./prompt.md` (or inline `BYOA_PROMPT=...`) in `.env.byoa`.
- **Local dev** (env != `prod`): start the local wake daemon (separate terminal):
  ```bash
  uv run byoa --serve
  ```
  The daemon reads `.env.byoa` and waits for wakes from `wake_agent`. As long as the ship has no per-ship `source_url` set, `wake_agent` defaults to `http://host.docker.internal:8765/wake` and routes to the daemon automatically using the per-ship `BYOA_WAKE_SECRET`. Only set `DEFAULT_BYOA_SOURCE_URL` on the edge env to override the default for *all* unconfigured ships.
- **Production** (env = `prod`): run `/byoa-deploy-vercel prod` next. It deploys the template at [deployment/vercel/](../../../deployment/vercel/) to the operator's Vercel project (production by default — preview is SSO-gated), pushes `BYOA_WAKE_SECRET` / `TASK_LLM_*` / the matching `*_API_KEY` from `.env.byoa`, health-checks, then logs the operator in again to auto-register `source_url` via `ship_byoa_configure`. Pass `--access-token <jwt>` to reuse the JWT you just minted here and skip the second login prompt.
- Point at `docs/byoa.md` for full env / config reference.
- Rotate the wake secret by re-running `/byoa-link <env> --force --ship-id <ship>` — this writes a fresh value and updates `ship_byoa_configure` in one shot.

## Failure modes

- **401 from /login**: bad credentials. Don't retry.
- **No characters**: direct to `/character-create` first.
- **No corp ships**: operator isn't in a corp or their corp has no ships.
- **409 ship_busy on claim**: ship is mid-task. Don't auto-retry.
- **403 on claim or set**: caller's JWT is for a different operator (not the ship owner). Stop and re-check which account was logged in.
- **401 "No authorization token provided" / "Invalid or expired token" from /ship_byoa_configure**: the `Authorization: Bearer <jwt>` header is missing or the JWT is expired. Re-run `/login` to mint a fresh one.

## What this skill does NOT do

- Write the operator's custom prompt — their authored content, out of scope.
- Deploy the operator's BYOA agent.
- Modify the bot or edge env files — only reads them.

