# Overview Clanker Ship is a workspace where people and coding agents share the same product actions. - The **web app** calls the Effect service layer in-process. - The **public API** (`apps/api`) exposes those same services over HTTP and hosts MCP. - The **worker** (`apps/worker`) runs background jobs using the same services. - Permission checks live in services, not in transports. Agents should prefer: 1. `GET /openapi.json` on the API origin 2. This docs site's `/llms.txt` and `/llms-full.txt` 3. MCP tool `read_validation_kit` for test-writing rules # Architecture Deployable units: | Unit | Package | Responsibility | | ------ | ------------- | -------------------------------------------------- | | Web | `apps/web` | Cookie sessions, UI, OAuth consent page | | API | `apps/api` | Public HTTP `/v1`, MCP, token/register, well-known | | Worker | `apps/worker` | Queue consumer and cron | | Admin | `apps/admin` | Internal admin UI against `clanker_test_admin` | | Docs | `apps/docs` | TanStack Start guides and API reference | Shared libraries: - `@clanker/services` — Effect services, worker runtime, public `HttpApi` + `toWebHandler` - `@clanker/db` — product Postgres schema (`clanker_test`) - `@clanker/admin-db` — admin Postgres schema (`clanker_test_admin`) - `@clanker/core` — ids, settings helpers, validation kit The web app must not HTTP-call the public API for product actions. # Authentication ## Browser (web) Better Auth cookies on the website origin. Google OAuth and email/password both work here. Create and revoke public API keys on Security settings. ## Public API Create a user API key or an account API key on the website, then send: ``` Authorization: Bearer ctk_… ``` or ``` X-API-Key: ctk_… ``` User API keys inherit their owner’s current workspace role. Account API keys have an assigned workspace role with the same permissions as a user in that role. Neither can read production secret values; runtime production reads require a scoped vault key. The secret is shown once at creation. Sign-in, sign-up, and password reset stay on the website. They are not `/v1` routes. ## CLI Run `clanker auth login` to approve a device code in your browser. CLI sessions use dedicated `/cli/oauth/*` endpoints and the `{API_URL}/cli` audience. Access tokens expire after 15 minutes; refresh tokens rotate and expire after 30 days. Revoke sessions on Security settings or run `clanker auth logout`. For unattended use, set `CLANKER_API_KEY` to a user or account API key. Management commands never accept vault keys. See [Secrets Vault](./secrets-vault.md) for permissions and commands. ## Agents MCP and `/agent/v1` use agent OAuth, not API keys: - Authorization UI: website `/agent/oauth/authorize` - Token and registration: API origin `/agent/oauth/*` - Resource: `{API_URL}/agent/mcp` # Public API Base URL: - Local: `http://127.0.0.1:3200` - QA: `https://api.qa.clankership.net` - Production: `https://api.clankertest.com` Auth: `Authorization: Bearer ctk_…` or `X-API-Key: ctk_…` from a key created on the website (Security settings). Dedicated CLI OAuth bearer tokens also work. Account API keys use their assigned workspace role. Production pull requires a separate scoped vault key; see [Secrets vault and CLI](/guides/secrets-vault.md). MCP uses a separate agent OAuth access token. The website does not call this API. Agents and other HTTP clients do. Permission checks live in `@clanker/services`. ## Every web action | UI | HTTP | | ----------------------- | ---------------------------------------------------------------------------------------- | | API keys | `GET/POST /v1/api-keys`, `POST /v1/api-keys/{id}/revoke` | | Profile and photo | `GET/PATCH /v1/me`, `POST /v1/me/photo` | | Notifications | `GET/PATCH /v1/me/preferences` | | Home / onboarding | `GET /v1/me/home`, `GET /v1/me/onboarding` | | Workspaces | `GET/POST /v1/workspaces`, `GET/PATCH /v1/workspaces/{slug}` | | Slug check | `GET /v1/slugs/{slug}`, `GET /v1/workspaces/{slug}/slug-availability?slug=` | | Icon | `POST /v1/workspaces/{slug}/icon` | | Members | `GET /v1/workspaces/{slug}/members`, `PATCH/DELETE .../members/{userId}` | | Invites | `GET/POST .../invites`, revoke/resend/role, `GET/POST /v1/invites/{token}` | | Teams | `GET/POST .../teams`, rename/delete/members | | Events / people / seeds | `GET .../events`, `.../people`, `.../seeds`, `POST /v1/seeds/{id}/join` | | Agent grants | `GET .../integrations`, `POST .../integrations/{id}/revoke` | | Products | `GET/POST .../products`, `GET/PATCH .../products/{product}`, archive/restore | | Environments | `GET/POST .../environments`, `GET/PATCH .../environments/{environment}`, archive/restore | | Deployments | `GET/POST .../deployments`, `GET/PATCH/DELETE .../deployments/{product}/{environment}` | Google OAuth, email sign-in, and password reset remain on the website only. Machine-readable: - `GET /openapi.json` - Docs `llms.txt`, `llms-full.txt`, `AGENTS.md`, `index.json` Products, environments and deployments are documented in [Products and environments](/guides/products-and-environments.md). Versioned secrets, bulk assignment, restore, account keys and vault keys are documented in [Secrets vault and CLI](/guides/secrets-vault.md). # Products and environments A workspace describes what it tests and where it runs: - A **product** is a piece of software the workspace tests. Its `type` is `website`, `api`, `cli`, `mobile_app`, `desktop_app` or `agent`, and its optional `repository`, such as `acme/storefront`, is where its code lives. - An **environment** is a place products run, such as `localhost`, `qa` or `production`. Its `class` is `production` or `non-production`, chosen once. - A **deployment** is one product running in one environment. It records the `baseUrl` where the product answers and the `version` that is running. Secrets belong to a product and its deployments; see [Secrets vault and CLI](/guides/secrets-vault.md). ## Rules - Slugs are 2–48 lowercase letters, digits or hyphens, with no leading or trailing hyphen and no reserved Windows folder name. Product names are 1–100 characters, environment names 1–80. Names are unique ignoring case. - Website and API products need an `http://` or `https://` `baseUrl` in every deployment, without a username, password or `#fragment`. - An environment's class never changes. - Archive replaces delete. An archived product or environment is hidden from lists, read-only, and its deployments refuse vault pulls. It keeps its slug and name, and `restore` brings everything back. Archive and restore can be repeated safely. - Removing a deployment keeps its secret values; deploying the pair again shows them again. - Members, admins and owners make changes; viewers read. Account API keys use their role. ## HTTP Paths are relative to `/v1/workspaces/{slug}`. Writes return the changed object; create returns 201 and remove returns 204. `PATCH` changes only the fields you send, and `null` clears an optional field. | Operation | Request | | ------------------ | ----------------------------------------------------------------------- | | List | `GET /products`, `GET /environments`, `GET /deployments` | | Archived objects | `?archived=include` or `?archived=only` on any list | | Filter deployments | `GET /deployments?product={product}&environment={environment}` | | Get | `GET /products/{product}`, `GET /environments/{environment}` | | Create | `POST /products`, `POST /environments`, `POST /deployments` | | Change | `PATCH /products/{product}`, `PATCH /environments/{environment}` | | Archive, restore | `POST /products/{product}/archive`, `…/restore` (same for environments) | | One deployment | `GET`, `PATCH`, `DELETE /deployments/{product}/{environment}` | ```http POST /v1/workspaces/acme/deployments Authorization: Bearer ctk_… { "product": "web-app", "environment": "qa", "baseUrl": "https://qa.shop.example" } ``` Record what is running after each deploy: ```http PATCH /v1/workspaces/acme/deployments/web-app/qa { "version": "a1c9e02" } ``` The response carries `versionUpdatedAt`. Full schemas are in [`/openapi.json`](/openapi.json) as `Product`, `Environment` and `Deployment`. ## CLI Every command prints the API's JSON, takes `--workspace ` or reads `clanker.json`, and exits 2 when it is used wrongly or the request is refused. ```sh clanker environment create qa --name QA --class non-production clanker product create web-app --name 'Web App' --type website clanker deployment create web-app qa --base-url https://qa.shop.example clanker deployment update web-app qa --deployed-version "$GIT_SHA" clanker product list --archived only clanker product archive web-app clanker product restore web-app ``` Pass an empty value to clear a field, such as `--deployed-version ""`. ## Agents Agents use the MCP tools `list_products`, `create_deployment`, `update_deployment` and the rest of the catalog tools, or their `/agent/v1` twins, with an agent grant. See [Agents and MCP](/guides/agents.md). # Planning Planning tracks work as tickets: - A **team** groups workspace members and numbers its tickets with its key, such as `WEB-31`. Open teams are listed for everyone; private teams only for their members. - A **ticket** is any unit of work. It has a `title`, a Markdown `summary`, a Markdown `plan` (the document an agent implements), a `status`, `tags`, an `owner` agent, and at most one `project` and `milestone`. - A **project** gathers tickets from one or more teams toward a goal. It has a `status`, a `lead`, an optional `targetDate`, linked `teams` and `products`, numbered `milestones` (M1, M2) and a Markdown `document`. - Every **status** belongs to a category: `backlog`, `unstarted`, `started`, `completed` or `canceled`. The defaults are Backlog, Todo, In Progress, In Review, Done, Canceled and Duplicate. A new team copies the workspace's statuses and owns its copy. - **Tags** belong to the workspace, or to one team and only its tickets. - **Agent instructions** teach a team's agents to write tickets and plans: a `template` per kind (and optionally per tag), `guidance`, and for tickets the `defaults` an agent's new ticket gets. A team's own replace the workspace's, which replace Clanker Ship's. - A **view** saves `filters`, a `groupBy`, an `orderBy` and a `layout` (`list` or `board`) for one team or one project. Teams start with All tickets, Active and Backlog; projects with Board and All tickets. The default view opens when no view is named. ## Rules - Team keys are 2–5 capital letters and change only while the team has no tickets. Ticket numbers are never reused. - A ticket joins a project only when its team is linked to the project, and a milestone only from its own project. A team stays linked to a project while the project holds its tickets. - Moving a ticket to another team gives it a new ID; the old ID still finds it. - Archived projects are read-only. A team with tickets is archived, not deleted. - Viewers read. Members change tickets, projects and milestones. Owners and admins create teams, change keys and visibility, and change workspace tags; team members change their team's tags and members. - Anyone who can open a project reads all its tickets, a private team's included, but only the team's members change them. - Every category keeps at least one status. The default status for new tickets is a Backlog or Unstarted status and cannot be deleted. Deleting a status that tickets use needs `moveTo`, another status of the same team. - Owners and admins change statuses. Members of a view's team, or of one of a project's teams, change its views; the default view cannot be deleted. - Tickets have one order. Every view ordered by hand (`manual`) shows it, and `reorder` moves a ticket before or after another. ## Views | Field | Values | | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `filters` | `statuses`, `categories`, `tags`, `owners` (agent IDs or `none`), then `projects` for team views, or `milestones` (`M1`, `none`) and `teams` for project views | | `groupBy` | `status`, `category`, `owner`, `tag`, `none`, and `project` for teams or `milestone` and `team` for projects | | `orderBy` | `manual`, `created` (newest first) or `updated` | | `layout` | `list` or `board`; a board's columns are the groups | Filters combine with "and", values within one filter with "or". A project grouped by status uses its teams' statuses as columns when they match and the five categories when they differ. ## HTTP Paths are relative to `/v1/workspaces/{slug}`. Teams are named by key, projects by slug and tickets by ID. `PATCH` changes only the fields you send, and `null` clears an optional field. | Operation | Request | | ---------------- | ------------------------------------------------------------------------------------ | | Teams | `GET`, `POST /teams`; `GET`, `PATCH`, `DELETE /teams/{team}` | | Join, members | `POST /teams/{team}/join`, `POST /teams/{team}/members`, `DELETE …/{person}` | | Archive, restore | `POST /teams/{team}/archive`, `…/restore` (same for projects) | | Projects | `GET /projects?team=&product=`, `POST /projects`, `GET`, `PATCH /projects/{project}` | | Priority order | `POST /projects/{project}/reorder` with `before` or `after` | | Milestones | `POST /projects/{project}/milestones`, `PATCH`, `DELETE …/{milestone}` | | Tickets | `GET /tickets?team=&project=&owner=mine&category=started`, `POST /tickets` | | One ticket | `GET`, `PATCH /tickets/{ticket}`, `POST /tickets/{ticket}/move`, `…/reorder` | | Statuses | `GET`, `POST /ticket-statuses`; `PATCH`, `DELETE …/{status}?moveTo=`; `…/reorder` | | Views | `GET`, `POST /teams/{team}/views`; `GET`, `PATCH`, `DELETE …/views/{view}` | | Project views | The same under `/projects/{project}/views` | | Tags, agents | `GET`, `POST /tags`, `PATCH`, `DELETE /tags/{tag}`, `GET /agents` | | Instructions | `GET /agent-instructions?kind=ticket&team=`, `PUT`, `DELETE …?kind=&team=` | ```http POST /v1/workspaces/acme/tickets Authorization: Bearer ctk_… { "team": "WEB", "title": "Stacked promo codes fail at checkout", "tags": ["bug"] } ``` The response is the ticket with its ID, such as `WEB-31`. Change its status by name: ```http PATCH /v1/workspaces/acme/tickets/WEB-31 { "status": "In Progress" } ``` Save a view, then read it with its tickets: ```http POST /v1/workspaces/acme/teams/WEB/views { "name": "Checkout bugs", "filters": { "tags": ["bug"], "projects": ["checkout-v2"] }, "layout": "board" } GET /v1/workspaces/acme/teams/WEB/views/checkout-bugs ``` ## CLI ```sh clanker team list clanker ticket create --team WEB --title "Stacked promo codes fail at checkout" --tag bug clanker ticket update WEB-31 --status "In Progress" clanker ticket move WEB-31 --team PAY clanker project create checkout-v2 --name "Checkout v2" --team WEB --team PAY clanker milestone create checkout-v2 --name Wallets clanker ticket reorder WEB-31 --before WEB-29 clanker status create "In QA" --category started --team PAY clanker view create "Checkout bugs" --team WEB --tag bug --in-project checkout-v2 --layout board clanker view show checkout-bugs --team WEB clanker instructions show --kind ticket --team WEB clanker instructions set --kind ticket --team WEB --tag-template-file bug=bug.md --owner creator ``` ## Agents The MCP tools mirror the HTTP operations: `list_teams`, `get_team`, `create_team`, `update_team`, `join_team`, `add_team_member`, `remove_team_member`, `list_projects`, `get_project`, `create_project`, `update_project`, `reorder_project`, `archive_project`, `restore_project`, `create_milestone`, `update_milestone`, `delete_milestone`, `list_tickets`, `get_ticket`, `create_ticket`, `update_ticket`, `move_ticket`, `reorder_ticket`, `list_views`, `get_view`, `create_view`, `update_view`, `delete_view`, `list_ticket_statuses`, `list_tags`, `list_agents`, `get_agent_instructions` and `update_agent_instructions`. Agents act with their person's role; an agent of a member cannot create a team. An agent names itself as a ticket's owner with `"owner": "me"`, and works through a view with `get_view`, such as "work through the Checkout bugs view". Before writing a ticket or a plan, an agent calls `get_agent_instructions` with the `kind` and the team. A ticket an agent creates gets the instructions' default status, tags and owner for what it leaves out. The plugin's `create-ticket` and `write-plan` skills do this. `show_work`, `show_ticket` and `show_inbox` answer with My work, a ticket or the agent's inbox. Clients with MCP Apps draw them as screens the person can act on; others get text with the website link. See [Agents and MCP](/guides/agents.md#mcp-apps). # Agent messages People send tickets to coding agents. A **registered agent** is one client, such as Claude Code, working for one person; it owns tickets and has an inbox. The Clanker Ship plugin registers each computer the agent runs on and reports its sessions with their repository and branch. - Send a ticket with an `ask`: `draft`, `refine`, `plan` or `build`, and an optional `note`. - Mention an agent in a ticket comment. `@Claude` reaches your own Claude; pass `mentions` with agent IDs to reach a teammate's. - A message is `queued`, then `delivered` once the plugin shows it to the agent, then `completed`, or `declined` if the agent is revoked. ## Routing Each message goes to one of the agent's computers: the live one; when several are live, the one with a live session in one of the ticket's repositories (the `repository` of the products the ticket's project changes), else the most recently active; when none is live, the one seen most recently. A session seen in the last minute is live. ## HTTP Paths are relative to `/v1/workspaces/{slug}`. | Operation | Request | | -------------------- | ---------------------------------------------------------------- | | Agents and computers | `GET /agents?person=me` | | Register a computer | `POST /agents/register` with `client` and `computer` | | Revoke an agent | `POST /agents/{agent}/revoke` | | Send a ticket | `POST /agent-messages` with `agent`, `ticket`, `ask`, `note` | | A ticket's messages | `GET /tickets/{ticket}/messages` | | Comments | `GET`, `POST /tickets/{ticket}/comments` with `body`, `mentions` | | Notifications | `GET /notifications`, `POST /notifications/read` | ```http POST /v1/workspaces/acme/agent-messages { "agent": "agt_…", "ticket": "WEB-33", "ask": "draft", "note": "Use the Places API." } ``` Sending to a revoked agent returns `409`. ## Agents An agent reads its inbox with `list_messages` and `get_message`, does the work, and calls `complete_message` with an optional `reply`; the sender is told. It replies to a mention with `comment_on_ticket`. The plugin's CLI also calls `/agent/v1/messages:wait`, which answers as soon as a message arrives or with an empty list after up to 25 seconds, `/agent/v1/messages:deliver` and `/agent/v1/sessions:heartbeat`. ## Plugin The plugin's hooks run `clanker hook` at session start, on each prompt and when a turn stops; they add new messages to the agent's context, and the Stop hook keeps the turn going while new messages wait. In Claude Code, start a session with `claude --channels plugin:clanker-ship@clankership` and `clanker channel` pushes each message into it, even while it is idle. In Codex, the plugin starts a helper that hands each message to the thread with `codex queue --thread`. ```sh clanker agent list --mine clanker ticket send WEB-33 --agent agt_… --ask plan --note "Behind the checkout_v2 flag" clanker ticket comment WEB-31 --body "@Claude add an expiry case to the plan" clanker inbox --client claude clanker inbox complete msg_… --client claude --reply "Plan updated" ``` # Test runs A **test run** is one `clanker run` of your Playwright specs, published to your workspace. Every member can open it on the website: each test, every attempt, and its error, screenshots, video and trace. - A run tests one **product** in one **environment**. The CLI reads that deployment's `baseUrl` from your workspace, so you name only the environment. - Each test is **passed**, **flaky**, **failed** or **skipped**. Attempt 1 decides passed, failed or skipped. - A run is **failed** when a test failed, or when Playwright reported an error outside the tests, such as a web server that did not start. The run page shows that error. A run with no tests never passes. - `clanker retry` runs a run's failed tests again on the same commit. A test that passes every retry attempt (two by default) is **flaky** and counts as passed. The run shows an asterisk: `Passed*`, `100%*`. - **Pass rate** is (passed + flaky) ÷ (tests − skipped). The first-attempt pass rate never changes. ## Publish a run Link the repository once, with the product its tests cover: ```bash clanker link acme --product storefront ``` Then run against an environment: ```bash clanker run --environment qa clanker run --environment preview --base-url https://pr-482.storefront.acme.dev clanker run --environment qa --coverage critical ``` `--base-url` replaces the deployment's URL for one run, for a local server on another port or a preview build. `--product` tests another product of the workspace. `--no-publish` runs without publishing; without `--environment` it uses your Playwright config's own `baseURL` and needs no sign-in. The CLI prints the run's URL. It exits 0 when every test passed, 1 when the run failed, and 2 when the run could not start or publish, for example when the product is not deployed to that environment. Each evidence file is tried up to five times when the connection drops or the server has an error; a file that still fails stops the publish, and the CLI names it. ## Retry failures ```bash clanker retry 1289 clanker retry --passes 3 clanker run --environment qa || clanker retry ``` A retry runs only failed tests, against the run's URL, on the commit the run tested; on another commit it is refused, because a pass on changed code is a fix, not flakiness. Without a number it retries your newest run on the current commit. It exits 0 when no test is still failing. ## Rules - Members, admins and owners publish and retry; viewers read. Account API keys use their role, so CI can publish with `CLANKER_API_KEY`. - Only finished runs with failed tests and no error outside them can be retried, one retry at a time. - A run still running after 6 hours is marked interrupted. - Files are at most 100 MB each. ## HTTP Paths are relative to `/v1/workspaces/{slug}`; `{slug}` may also be the workspace id, and `{run}` is the run number. See the [OpenAPI reference](/openapi.json). | Operation | HTTP | | --------------- | --------------------------------------------------------- | | List runs | `GET /test-runs?product=&environment=&result=&q=&cursor=` | | Get a run | `GET /test-runs/{run}` | | Start a run | `POST /test-runs` | | Upload a file | `PUT /test-runs/{run}/files/{path}` | | Finish a run | `POST /test-runs/{run}/finish` | | Start a retry | `POST /test-runs/{run}/retries` | | Finish a retry | `POST /test-runs/{run}/retries/{retry}/finish` | | Download a file | `GET /test-runs/{run}/files/{path}` | # Secrets vault and CLI Install the standalone `clanker` CLI with `curl -fsSL https://clankertest.com/install.sh | sh` (Windows: `irm https://clankertest.com/install.ps1 | iex`). It brings its own Node, Playwright and browsers, about 320 MB, into `~/.local` (Windows: `%LOCALAPPDATA%\Clanker`) and updates itself in the background. The Clanker Ship plugin starts the same install on its first agent session. Run commands from your project root. `clanker run` runs that project's specs with the CLI's own Playwright, so the project doesn't need Playwright installed, and publishes the run to your workspace; see [Test runs](/guides/test-runs.md). ## Sign in `clanker auth login` prints a sign-in URL and code and opens your browser. Approve only the code shown by your own terminal. `--no-browser` prints the URL without opening it. Login uses a dedicated device authorization grant; the CLI refreshes its access token automatically. `clanker auth whoami` shows the active identity, and `clanker auth logout` revokes the saved login. Security settings also lists and revokes CLI logins. For automation, set `CLANKER_API_KEY` to a user or account API key, or pipe it to `clanker auth login --api-key-stdin`. The environment variable takes precedence over saved credentials. Credentials are stored in `$XDG_CONFIG_HOME/clanker` (default `~/.config/clanker`) with owner-only file permissions. User keys inherit their user's current workspace role. Account API keys have an assigned workspace role: viewer, member, admin, or owner. Their permissions match those of a user in that role. Account keys are scoped to one workspace and do not represent a person's profile or personal keys. Admins manage account keys in Security settings or `/account-api-keys`; only owners manage owner keys. CLI OAuth tokens use `ctat_`, ordinary keys `ctk_`, and vault keys `ctvk_`. The API rejects MCP tokens for management and never accepts a vault key as a CLI login. Production plaintext is accessible only through scoped vault-key pull. ## Link a repository Run `clanker link ` once in a repository. It checks that you belong to the workspace and writes `clanker.json` at the repository root (the folder holding `.git`), holding the workspace's id: `{ "workspaces": { "production": "" } }`. The id stays the same when the workspace's slug changes. `clanker link --product ` also records the product the repository's tests cover, which `clanker run` uses. Commit it so teammates and CI use the same workspace. Without a slug, `clanker link` lists your workspaces and asks for a number in a terminal; outside a terminal it prints their slugs and exits 2. Run it again to relink; `clanker unlink` removes the file. Workspace commands read the nearest `clanker.json` from the current folder up to the repository root, so they work from any subfolder. `--workspace ` overrides the link for one command. With neither, a command exits 2 and says how to link. Renaming the workspace does not break the link. If you lose access, commands report that the workspace was not found; run `clanker link` to link another one. ## Products and environments Secrets attach to products and their deployments. Create and manage them as described in [Products and environments](/guides/products-and-environments.md). A pull works only while the product is deployed to the environment and both are active. Removing a deployment or archiving either side keeps every secret value; deploying again or restoring makes the same values available. `GET/POST /account-api-keys`, `PATCH /account-api-keys/{id}` and `POST /account-api-keys/{id}/revoke` manage account keys. ## Manage secrets - `GET /vault` returns active products, the environments they are deployed to, secret names, status and version numbers. Non-production values can be returned; production values never appear in management responses. - `GET/POST /products/{product}/secrets` lists or defines names. Create accepts `name` and optional `values`, an object keyed by environment slug. - `GET/PATCH/DELETE /products/{product}/secrets/{name}` reads, renames, sets or deletes a definition. A read returns `createdAt`, the product, its deployed environments, and `values` keyed by environment slug: each current version's number, `isSet`, `createdAt`, `actor`, `generator`, `publicKey` and `restoredFrom`, plus `value` for non-production. Patch accepts `newName`, `values`, `expectedVersions`, and `requestId`. `expectedVersions` maps changed environment slugs to their current version number; stale writes return HTTP 409. - `POST /vault/assignments` accepts `products`, `environments`, `name`, `value` and optional `requestId`. Every selected pair must be deployed. Up to 200 pairs can be assigned in one transaction. Responses contain `{product, environment, status, version?}` per pair. A member's production targets return `status: "skipped"`; eligible non-production targets are written. - `GET /products/{product}/secrets/{name}/environments/{environment}/versions` returns up to 100 versions, newest first. Use `before={lastVersion}` for the next page. Each version has `version`, `isSet`, `createdAt`, `restoredFrom`, `generator`, `publicKey`, and `actor` (`{ id, type: "user" | "accountApiKey", name }`). `values=true` includes historical non-production plaintext and is forbidden for production. - `POST .../environments/{environment}/restore` accepts `version` and optional `requestId`. It writes a new version and references the restored version. Restoring a generated version brings back its generator and public key. Secret names use letters, digits and underscores, begin with a letter or underscore, and contain at most 255 characters. Values are limited to 64 KiB of UTF-8. `null` and `""` both clear the value; whitespace is preserved. Omitted fields preserve the current value. Clearing retains the definition and records an unset version. Restoring an unset version clears the current value. Members write non-production values; admins/owners write production values. Deleting a definition with any current production value requires admin/owner. The website shows an X for unset values. Bulk assignment is available only in the CLI and API. Each secret's website page shows its versions; members can show old non-production values one at a time and restore any version they may write. Retries of set/clear/restore with the same `requestId` reuse the original result; reusing an ID for a different value or restore target returns HTTP 409. Keep the same ID when a network response is uncertain. IDs are scoped to a secret and environment. CLI writes generate an ID unless `--request-id` is supplied. ```sh clanker vault secret add --product frontend --name MY_SECRET printf %s 'example-value' | clanker vault secret set --product frontend --env qa --name MY_SECRET clanker vault secret ls --product frontend clanker vault secret ls --product frontend --env qa --values clanker vault secret clear --product frontend --env qa --name MY_SECRET clanker vault secret history --product frontend --env qa --name MY_SECRET clanker vault secret restore 1 --product frontend --env qa --name MY_SECRET clanker vault pull --product frontend --env qa clanker vault pull --workspace other-workspace --product frontend --env qa ``` Secret input comes from stdin, never a value argument. Repeat `--product` and `--env` for bulk set. Commands emit JSON; exit 0 means success, exit 2 means a request/usage failure, and exit 3 means a bulk assignment or generation skipped some targets. ## Generate values `POST /vault/generations` creates values on the server. It accepts `products`, `environments`, `name`, `generator`, optional `reveal` and optional `requestId`, with the same targets, limits and roles as `/vault/assignments`: every pair must be deployed, a missing name is defined, and a member's production targets are skipped. Each environment gets its own value, shared by every selected product in that environment, so no value crosses environments. | `generator.type` | Options (default) | Stored value | | ---------------- | ------------------------------------------------------------------------------- | ----------------------------- | | `token` | `bytes` 16–512 (32), `encoding` `hex`, `base64` or `base64url` (`hex`) | Random bytes in that encoding | | `password` | `length` 8–256 (32); `lowercase`, `uppercase`, `digits`, `symbols` (all `true`) | At least one of each class | | `uuid` | none | A random version 4 UUID | | `ed25519` | none | PKCS#8 PEM private key | Password symbols are `-_.~+=`, which need no quoting in shells, dotenv files, JSON or URL user info. Use a 32- or 64-byte `token` for an HMAC key. Unknown options, options for another type, and out-of-range values return 400. Responses contain `{product, environment, status, version?, publicKey?, value?}` per pair. `ed25519` results always include the SPKI PEM `publicKey`; versions store it as non-secret metadata that reads and history return for every environment class. `value` appears only when `reveal` is `true`, only in the response that created it, and for production only to admins and owners. A retry with the same `requestId` returns the original versions and public keys without values; reusing an ID with different options or targets returns 409. Setting a value by hand replaces the generator and public key. PEM values span several lines. Quote them in dotenv files and shell variables. ```sh clanker vault secret generate --product cli --env qa --env production --name SIGNING_KEY --type ed25519 clanker vault secret generate --product frontend --env qa --name DB_PASSWORD --type password --length 24 --no-symbols --show clanker vault secret generate --product frontend --product api --env qa --name AUTH_SECRET --type token --bytes 32 ``` `--bytes` and `--encoding` apply to `token`; `--length` and `--no-lowercase`, `--no-uppercase`, `--no-digits` and `--no-symbols` apply to `password`. The CLI rejects options for another type before sending the request. `--show` sets `reveal`. ## Vault keys and runtime pull `GET/POST /vault/keys`, `PATCH /vault/keys/{id}`, and `POST /vault/keys/{id}/revoke` manage vault keys. Writes require admin/owner. Create/update accepts `name`, `productIds`, and `environmentIds`, with active workspace product IDs and production environment IDs. Creation returns `secret` exactly once. Subsequent responses contain metadata only; storage retains a hash. ```sh clanker vault key create --name 'Web runtime' --product frontend --env production clanker vault key list clanker vault key update KEY_ID --name 'Web runtime' --product frontend --env production clanker vault key revoke KEY_ID ``` `GET /products/{product}/environments/{environment}/secrets/pull` returns the current JSON name-to-value map. Unset names are omitted; an all-unset pair returns `{}`. Production requires a live vault key granting both IDs. Non-production accepts ordinary API keys or dedicated CLI tokens. Vault keys cannot pull non-production values. Revocation, detached links and archival are checked on every pull. Production pull attempts produce redacted audit records. All API responses use `Cache-Control: no-store`. Invalid inputs return 400, authentication failures 401, role/grant refusals 403, missing or inaccessible resources 404, and duplicate names/slugs, archives or stale writes 409. # Agents and MCP The MCP server lives on the API worker at `/agent/mcp`. Local default: `http://127.0.0.1:3200/agent/mcp` QA: `https://api.qa.clankership.net/agent/mcp` Production: `https://api.clankertest.com/agent/mcp` Call `read_validation_kit` before writing or generating tests. Do not cache a packaged copy of the kit. The HTTP equivalent is `POST /agent/v1/validation-kit:read` with the same agent access token. ## Scopes An agent grant carries one or both scopes. The user who grants access chooses the workspace on the consent page, and their current role caps what the agent can do: a viewer can only grant `read`. | Scope | Lets an agent | | ------- | --------------------------------------------------------------- | | `read` | Read the validation kit, products, environments and deployments | | `write` | Create, change, archive, restore and remove them | `tools/list` returns only the tools the grant allows. Changes are recorded as events made by the agent for the user who granted it. ## Products, environments and deployments Each tool has an HTTP twin at `POST /agent/v1/:` with the same JSON body, such as `POST /agent/v1/products:list`. See [Products and environments](/guides/products-and-environments.md) for the rules. | Tools | Scope | | ----------------------------------------------------------------- | --------------------------- | | `list_products`, `get_product` | `read` | | `list_environments`, `get_environment` | `read` | | `list_deployments`, `get_deployment` | `read` | | `create_product`, `update_product`, `restore_product` | `write` | | `create_environment`, `update_environment`, `restore_environment` | `write` | | `create_deployment`, `update_deployment` | `write` | | `archive_product`, `archive_environment`, `remove_deployment` | `write`, marked destructive | Errors come back as `{ code, message }` with `not_found`, `conflict`, `validation_failed` or `permission_denied`. ## MCP Apps Clients that support [MCP Apps](https://github.com/modelcontextprotocol/ext-apps) draw three screens inside the conversation. Each `show_*` tool names its screen in `_meta.ui.resourceUri`, and the screen is a `ui://` resource with MIME type `text/html;profile=mcp-app`. | Tool | Screen | Shows | | ------------- | -------------------------- | --------------------------------------------------- | | `show_work` | `ui://clanker-ship/work` | The open tickets your agents own, grouped by status | | `show_ticket` | `ui://clanker-ship/ticket` | One ticket's status, owner, summary and plan | | `show_inbox` | `ui://clanker-ship/inbox` | Your agent's open messages | The three tools need the `read` scope. Their HTTP twins are `POST /agent/v1/work:show`, `/tickets:show` and `/messages:show`. A screen gets its data from the tool result and loads nothing from the network. Its buttons go through the host: changing a status calls `update_ticket`, Mark done calls `complete_message`, Ask to refine and Work on this post a message into the conversation, and Open on Clanker Ship opens the ticket's page. A screen shows only the buttons the host supports, and takes the host's theme and style variables. A client without MCP Apps gets the same answer as text: a ticket's status, owner, summary and page link, for example. Every result also carries the data as `structuredContent`.